如何用Python实现配置热加载?
背景
由于最近工作需求,需要在已有项目添加一个新功能,实现配置热加载的功能。所谓的配置热加载,也就是说当服务收到配置更新消息之后,我们不用重启服务就可以使用最新的配置去执行任务。
如何实现
下面我分别采用多进程、多线程、协程的方式去实现配置热加载。
使用多进程实现配置热加载
如果我们代码实现上使用多进程, 主进程1来更新配置并发送指令,任务的调用是进程2,如何实现配置热加载呢?
使用signal信号量来实现热加载

当主进程收到配置更新的消息之后(配置读取是如何收到配置更新的消息的? 这里我们暂不讨论), 主进程就向进子程1发送kill信号,子进程1收到kill的信号就退出,之后由信号处理函数来启动一个新的进程,使用最新的配置文件来继续执行任务。
main 函数
def main():
    # 启动一个进程执行任务
    p1 = Process(target=run, args=("p1",))
    p1.start()
    monitor(p1, run) # 注册信号
    processes["case100"] = p1 #将进程pid保存
    num = 0
    while True: # 模拟获取配置更新
        print(
            f"{multiprocessing.active_children()=}, count={len(multiprocessing.active_children())}\n")
        print(f"{processes=}\n")
        sleep(2)
        if num == 4:
            kill_process(processes["case100"]) # kill 当前进程
        if num == 8:
            kill_process(processes["case100"]) # kill 当前进程
        if num == 12:
            kill_process(processes["case100"]) # kill 当前进程
        num += 1
signal_handler 函数
def signal_handler(process: Process, func, signum, frame):
    # print(f"{signum=}")
    global counts
    if signum == 17:  # 17 is SIGCHILD
        # 这个循环是为了忽略SIGTERM发出的信号,避免抢占了主进程发出的SIGCHILD
        for signame in [SIGTERM, SIGCHLD, SIGQUIT]:
            signal.signal(signame, SIG_DFL)
        print("Launch a new process")
        p = multiprocessing.Process(target=func, args=(f"p{counts}",))
        p.start()
        monitor(p, run)
        processes["case100"] = p
        counts += 1
    if signum == 2:
        if process.is_alive():
            print(f"Kill {process} process")
            process.terminate()
        signal.signal(SIGCHLD, SIG_IGN)
        sys.exit("kill parent process")
完整代码如下:
#! /usr/local/bin/python3.8
from multiprocessing import Process
from typing import Dict
import signal
from signal import SIGCHLD, SIGTERM, SIGINT, SIGQUIT, SIG_DFL, SIG_IGN
import multiprocessing
from multiprocessing import Process
from typing import Callable
from data import processes
import sys
from functools import partial
import time
processes: Dict[str, Process] = {}
counts = 2
def run(process: Process):
    while True:
        print(f"{process} running...")
        time.sleep(1)
def kill_process(process: Process):
    print(f"kill {process}")
    process.terminate()
def monitor(process: Process, func: Callable):
    for signame in [SIGTERM, SIGCHLD, SIGINT, SIGQUIT]:
        # SIGTERM is kill signal.
        # No SIGCHILD is not trigger singnal_handler,
        # No SIGINT is not handler ctrl+c,
        # No SIGQUIT is RuntimeError: reentrant call inside <_io.BufferedWriter name='<stdout>'>
        signal.signal(signame, partial(signal_handler, process, func))
def signal_handler(process: Process, func, signum, frame):
    print(f"{signum=}")
    global counts
    if signum == 17:  # 17 is SIGTERM
        for signame in [SIGTERM, SIGCHLD, SIGQUIT]:
            signal.signal(signame, SIG_DFL)
        print("Launch a new process")
        p = multiprocessing.Process(target=func, args=(f"p{counts}",))
        p.start()
        monitor(p, run)
        processes["case100"] = p
        counts += 1
    if signum == 2:
        if process.is_alive():
            print(f"Kill {process} process")
            process.terminate()
        signal.signal(SIGCHLD, SIG_IGN)
        sys.exit("kill parent process")
def main():
    p1 = Process(target=run, args=("p1",))
    p1.start()
    monitor(p1, run)
    processes["case100"] = p1
    num = 0
    while True:
        print(
            f"{multiprocessing.active_children()=}, count={len(multiprocessing.active_children())}\n")
        print(f"{processes=}\n")
        time.sleep(2)
        if num == 4:
            kill_process(processes["case100"])
        if num == 8:
            kill_process(processes["case100"])
        if num == 12:
            kill_process(processes["case100"])
        num += 1
if __name__ == '__main__':
    main()
执行结果如下:
multiprocessing.active_children()=[<Process name='Process-1' pid=2533 parent=2532 started>], count=1
processes={'case100': <Process name='Process-1' pid=2533 parent=2532 started>}
p1 running...
p1 running...
kill <Process name='Process-1' pid=2533 parent=2532 started>
multiprocessing.active_children()=[<Process name='Process-1' pid=2533 parent=2532 started>], count=1
processes={'case100': <Process name='Process-1' pid=2533 parent=2532 started>}
signum=17
Launch a new process
p2 running...
p2 running...
multiprocessing.active_children()=[<Process name='Process-2' pid=2577 parent=2532 started>], count=1
processes={'case100': <Process name='Process-2' pid=2577 parent=2532 started>}
p2 running...
p2 running...
multiprocessing.active_children()=[<Process name='Process-2' pid=2577 parent=2532 started>], count=1
processes={'case100': <Process name='Process-2' pid=2577 parent=2532 started>}
p2 running...
p2 running...
multiprocessing.active_children()=[<Process name='Process-2' pid=2577 parent=2532 started>], count=1
processes={'case100': <Process name='Process-2' pid=2577 parent=2532 started>}
p2 running...
p2 running...
kill <Process name='Process-2' pid=2577 parent=2532 started>
signum=17
Launch a new process
multiprocessing.active_children()=[<Process name='Process-2' pid=2577 parent=2532 stopped exitcode=-SIGTERM>], count=1
processes={'case100': <Process name='Process-3' pid=2675 parent=2532 started>}
p3 running...
p3 running...
multiprocessing.active_children()=[<Process name='Process-3' pid=2675 parent=2532 started>], count=1
总结:
好处:使用信号量可以处理多进程之间通信的问题。
坏处:代码不好写,写出来代码不好理解。信号量使用必须要很熟悉,不然很容易自己给自己写了一个bug.(所有初学者慎用,老司机除外。)
还有一点不是特别理解的就是process.terminate() 发送出信号是SIGTERM number是15,但是第一次signal_handler收到信号却是number=17,如果我要去处理15的信号,就会导致前一个进程不能kill掉的问题。欢迎有对信号量比较熟悉的大佬,前来指点迷津,不甚感谢。
采用multiprocessing.Event 来实现配置热加载
实现逻辑是主进程1 更新配置并发送指令。进程2启动调度任务。
这时候当主进程1更新好配置之后,发送指令给进程2,这时候的指令就是用Event一个异步事件通知。
直接上代码
scheduler 函数
def scheduler():
    while True:
        print('wait message...')
        case_configurations = scheduler_notify_queue.get()
        print(f"Got case configurations {case_configurations=}...")
        task_schedule_event.set() # 设置set之后, is_set 为True
        print(f"Schedule will start ...")
        while task_schedule_event.is_set(): # is_set 为True的话,那么任务就会一直执行
            run(case_configurations)
        print("Clearing all scheduling job ...")
event_scheduler 函数
def event_scheduler(case_config):
    scheduler_notify_queue.put(case_config)
    print(f"Put cases config to the Queue ...")
    task_schedule_event.clear() # clear之后,is_set 为False
    print(f"Clear scheduler jobs ...")
    print(f"Schedule job ...")
完成代码如下:
import multiprocessing
import time
scheduler_notify_queue = multiprocessing.Queue()
task_schedule_event = multiprocessing.Event()
def run(case_configurations: str):
    print(f'{case_configurations} running...')
    time.sleep(3)
def scheduler():
    while True:
        print('wait message...')
        case_configurations = scheduler_notify_queue.get()
        print(f"Got case configurations {case_configurations=}...")
        task_schedule_event.set()
        print(f"Schedule will start ...")
        while task_schedule_event.is_set():
            run(case_configurations)
        print("Clearing all scheduling job ...")
def event_scheduler(case_config: str):
    scheduler_notify_queue.put(case_config)
    print(f"Put cases config to the Queue ...")
    task_schedule_event.clear()
    print(f"Clear scheduler jobs ...")
    print(f"Schedule job ...")
def main():
    scheduler_notify_queue.put('1')
    p = multiprocessing.Process(target=scheduler)
    p.start()
    count = 1
    print(f'{count=}')
    while True:
        if count == 5:
            event_scheduler('100')
        if count == 10:
            event_scheduler('200')
        count += 1
        time.sleep(1)
if __name__ == '__main__':
    main()
执行结果如下:
wait message...
Got case configurations case_configurations='1'...
Schedule will start ...
1 running...
1 running...
Put cases config to the Queue ...
Clear scheduler jobs ...
Schedule job ...
Clearing all scheduling job ...
wait message...
Got case configurations case_configurations='100'...
Schedule will start ...
100 running...
Put cases config to the Queue ...
Clear scheduler jobs ...
Schedule job ...
Clearing all scheduling job ...
wait message...
Got case configurations case_configurations='200'...
Schedule will start ...
200 running...
200 running...
总结:
使用Event事件通知,代码不易出错,代码编写少,易读。相比之前信号量的方法,推荐大家多使用这种方式。
使用多线程或携程的方式,其实和上述实现方式一致。唯一区别就是调用了不同库中,queue 和 event.
# threading
scheduler_notify_queue = queue.Queue()
task_schedule_event = threading.Event()
# async
scheduler_notify_queue = asyncio.Queue()
task_schedule_event = asyncio.Event()
结语:
具体的实现的方式有很多,也各自有各自的优劣势。我们需要去深刻理解到需求本身,才去做技术选型。
如何用Python实现配置热加载?的更多相关文章
- Aspnetcore下面服务器热更新与配置热加载
		原文:Aspnetcore下面服务器热更新与配置热加载 Asp.net的热更新方案Appdomain在aspnetcore中不被支持了 新的方案如下: 配置文件更新选项 reloadOnChange ... 
- 在线配置热加载配置 go-kratos.dev 监听key
		paladin https://v1.go-kratos.dev/#/config-paladin example Service(在线配置热加载配置) # service.go type Servi ... 
- nginx多进程模型之配置热加载---转
		http://blog.csdn.net/brainkick/article/details/7176405 前言: 服务器程序通常都会通过相应的配置文件来控制服务器的工作.很多情况下,配置文件会经常 ... 
- (译文)开始学习Webpack-应用TypeScript,配置热加载和Source Map
		项目初始化:采用TypeScript 我们的版本是: $ node --version v8.5.0 $ npm --version 5.5.1 npm版本升级了,因为npm最近带来了新特性,本地会生 ... 
- springboot idea 配置热加载
		在idea 配置springboot的热加载,只需要三步: 第一步.引用jar包 <dependency> <groupId>org.springframework.boot& ... 
- 关于在Intellij IDEA工具中配置热加载问题
		第一步,创建一个maven项目,然后在pom.xml文件中添加依赖(上图内容). 第二步:来到intellij idea主页面,点击File->Settings->Build->co ... 
- idea配置热加载
		第一步:添加依赖 spring-boot项目中引入如下依赖 <dependency> <groupId>org.springframework.boot</groupId ... 
- SpringBoot入门笔记(三)、热加载
		1.配置热加载环境,在pom.xml添加如下代码 <build> <!--springloader plugin --> <plugins> <plugin& ... 
- spring-boot-devtools热加载不起作用
		在开发过程中,希望修改时能够及时更新修改,即热加载,但是spring-boot-devtools不起作用.这主要是两个原因导致. 一.spring-boot-maven-plugin插件没有配置,如下 ... 
随机推荐
- redis的zset数据结构:跳表
			点赞再看,养成习惯,微信搜索「小大白日志」关注这个搬砖人. 文章不定期同步公众号,还有各种一线大厂面试原题.我的学习系列笔记. 广州这边封闭式管理好久了,今天终于周末可以出去溜溜了 什么是zset z ... 
- Vue图片浏览组件v-viewer使用
			简单介绍v-viewer的两种使用方法: Demo 安装依赖: npm install v-viewer --save 全局引入 import Viewer from 'v-viewer' impor ... 
- [还不会搭建博客吗?]centos7系统部署hexo博客新手入门-进阶,看这一篇就够了
			@ 目录 *本文说明 请大家务必查看 前言 首先介绍一下主角:Hexo 什么是 Hexo? 环境准备 详细版 入门:搭建步骤 安装git: 安装node: 安装Hexo: 进阶:hexo基本操作 发布 ... 
- .net core JWT验证,HttpContext.User为空的问题
			这几天在学习张老师.net core教程JWT部分,链接 https://mp.weixin.qq.com/s/7135y3MkUlPIp-flfwscig 教程使用的.net core 2.2, 在 ... 
- 文本框字符限制、focus光标定位
			一.为一个元素的所有子元素设置统一样式:.className * { color: #6666 } 二.正则表达式: 1.去除所有HTML标签只保留文字: /<\/?.+?\/?>/2.去 ... 
- 在字节跳动,一个更好的企业级SparkSQL Server这么做
			SparkSQL是Spark生态系统中非常重要的组件.面向企业级服务时,SparkSQL存在易用性较差的问题,导致难满足日常的业务开发需求.本文将详细解读,如何通过构建SparkSQL服务器实现使用效 ... 
- unity---脚本代码报错
			同版本编辑器在不同VS版本下会报错问题解决 解决方法 报错情况 解决方法 打开Unity --->Preferences--->External Tools然后点击:Regenerate ... 
- 142_Power BI之同比预测
			博客:www.jiaopengzi.com 焦棚子的文章目录 请点击下载附件 一.背景 最近刚好在做一个简单同比预测的模型,预测方法很简单,就是累计同比预测,把MTD展示出来. [video widt ... 
- 爬取豆瓣喜剧类热门TOP60的电影
			学习任务:爬取豆瓣喜剧类热门TOP60的电影并保存在douban.txt文件中. 代码示例: import requests url="https://movie.douban.com/j/ ... 
- 个人冲刺(六)——体温上报app(一阶段)
			任务:完成了自动获取定位信息以及自动获取时间功能 自动获取定位信息 public void onReceiveLocation(BDLocation location){ //此处的BDLocatio ... 
