故事背景

这段时间在做一个nginx + uwsgi + python的我的项目,有个需要是须要在服务运行过程中能够扭转配置并失效,能够了解为热重载. 之前这些配置都是写死在我的项目的配置文件中的根底配置,个别就是python我的项目中的config.py文件. 当初配置变更应用了开源的apollo作为治理端,须要python应用client对接apollo.

先看一份常见的python后盾应用uwsgi的配置:

test@python:~/app$ cat uwsgi.ini[uwsgi]module = appwsgi-file = app.pymaster = trueprocesses = 4           # 多个work过程enable-threads = true   # 容许启动多线程#lazy-apps = true       # 前面再说http = :3000die-on-term = truepidfile = ./uwsgi.pidchdir = /home/test/appdisable-logging = truelog-maxsize = 5000000daemonize = /home/test/app/log.log

这里给出python代码的demo app.py:

from flask import Flask, jsonify, requestfrom apollo import Configcf = Config("test", "application")print("----------key-----------")print(cf.SQLALCHEMY_TRACK_MODIFICATIONS)    # 尝试获取一些配置print(cf.LOG_NAME)print("----------key-----------")app = Flask(__name__)@app.route('/')def hello_world():    key = request.values.get('key')    new = getattr(cf, key)    # 尝试实时获取配置    return jsonify({'data': new, 'apo': cf.apo.get_value(key), "my": cf.SQLALCHEMY_POOL_SIZE})application = app  # for uwsgi.iniif __name__ == "__main__":    app.run(port=5000)

再看看这个配置启动后的成果:

test@python:~/app$ ps -ef|grep uwsgi.initest      16224     1  0 14:36 ?        00:00:00 uwsgi --ini uwsgi.initest      16225 16224  0 14:36 ?        00:00:00 uwsgi --ini uwsgi.initest      16226 16224  0 14:36 ?        00:00:00 uwsgi --ini uwsgi.initest      16227 16224  0 14:36 ?        00:00:00 uwsgi --ini uwsgi.initest      16228 16224  0 14:36 ?        00:00:00 uwsgi --ini uwsgi.initest      16229 16224  0 14:36 ?        00:00:00 uwsgi --ini uwsgi.initest      16378 15998  0 14:39 pts/48   00:00:00 grep --color=auto uwsgi.ini

而后问题来了

每次在apollo后盾变更配置时明明配置的localfile本地文件曾经变更然而过程中的cache就是没变...查看了apollo开源阐明中举荐的三种python client,发现实现形式都是大同小异,次要就是启动守护线程长链接pull服务端的接口,服务端有变更时接口就能拜访通,进而触发这个守护线程的动作去更新cache和localfile,下面说了localfile曾经有了更新的动作为啥cache没被更新呢? 带着疑难去看了这三个开源库的issues,而后发现uwsgi+django我的项目中配置的apollo, 不能获取最新apollo数据 嗯,看来是通病了...

验证猜测

翻了下其余语言上没啥相似问题,那会不会是python的特色,先来个手动多过程试试:

1. 执行python app.py2. 批改app.py中的端口号3. 执行python app.py4. 反复2,35. 留神看打印的日志6. 试着拜访下设置的端口 curl "127.0.0.1:3000"7. 批改apollo的配置8. 看看日志,再执行curl "127.0.0.1:3000",看看获取的配置是不是最新的.

而后发现没啥问题啊,每个实例都能拜访到最新的,日志中都打印了更新cache和localfile的日志.那么就排除了python的问题,聚焦到uwsgi的配置上看看吧,网上搜的话比拟凌乱,个别搜官网文档好了,如这里Python/WSGI利用疾速入门,而后就会看到右边有个对于Python线程的注意事项嗯,难道是我没加enable-threads = true导致的? 立马加上试试,成果还是不行,那持续看文档吧,翻看目录直到看到这句 优雅重载的艺术,上面摘抄文档中的一些要害语句:

Preforking VS lazy-apps VS lazy

这是uWSGI我的项目具备争议的抉择之一。默认状况下,uWSGI在第一个过程中加载整个利用,而后在加载完利用之后,会屡次 fork() 本人。这是常见的Unix模式,它可能会大大减少利用的内存应用,容许很多好玩的技巧,而在一些语言上,可能会让带给你很多懊恼。只管它的名声如此,然而uWSGI是作为一个Perl应用服务器 (它不叫做 uWSGI,并且它也并不开源) 诞生的,而在Perl的世界里,preforking个别是一种受到祝愿的形式。然而,对于许多其余的语言、平台和框架来说,这并不是真的,因而,在开始解决uWSGI之前,你应该抉择在你的栈中如何治理 fork() 。而从“优雅重载”的角度来看,preforking极大的进步了速度:只加载你的利用一次,而生成额定的worker将会十分快。防止栈中的每个worker都拜访磁盘会升高启动工夫,特地是对于那些破费大量工夫拜访磁盘以查找模块的框架或者语言。可怜的是,每当你的批改代码时,preforking办法迫使你重载整个栈,而不是只重载worker。除此之外,你的利用可能须要preforking,或者因为其开发的形式,可能齐全因其解体。取而代之的是,lazy-apps模式会每个worker加载你的利用一次。它将须要大概O(n)次加载 (其中,n是worker数),十分有可能会耗费更多内存,但会运行在一个更加统一洁净的环境中。记住:lazy-apps与lazy不同,前者只是批示 uWSGI对于每个worker加载利用一次,而后者更具侵略性些 (个别不提倡),因为它扭转了大量的外部默认行为。

看来是默认配置导致了多过程多线程状况下,uwsgi加载完后第一个残缺的work后,剩下processes中配置的work都是通过fork来的,看看uwsgi的启动日志也会发现确实只加载了一个app,每次操作也只有一个守护线程在监听和打印日志,那为啥fork来就不是残缺的服务了呢,这就要说到unix fork的原理和实现了.

在unix/linux操作系统中,提供了一个fork()零碎函数,它有这些个性:

0. fork()函数用于从一个曾经存在的过程内创立一个新的过程,新的过程称为“子过程”,相应地称创立子过程的过程为“父过程”。应用fork()函数失去的子过程是父过程的复制品,子过程齐全复制了父过程的资源,包含过程上下文、代码区、数据区、堆区、栈区、内存信息、关上文件的文件描述符、信号处理函数、过程优先级、过程组号、当前工作目录、根目录、资源限度和管制终端等信息,而子过程与父过程的区别有过程号、资源应用状况和计时器等。1. 一般的函数调用,调用一次,返回一次,然而fork()调用一次,返回两次。因为操作系统主动把以后过程(父过程)复制了一份(子过程),而后别离在父过程和子过程内返回。2. 子过程永远返回0,父过程返回子过程的ID。3. 一个父过程能够fork()出很多个子过程。因而,父过程要记下每个子过程的ID,而子过程只须要调用getppid()就能够拿到父过程的id。getpid()能够拿到以后过程id4. 父过程、子过程执行程序没有法则,齐全取决于操作系统的调度算法。5. 如果父过程有多个线程会不会复制父过程的多个线程呢?其实子过程创立进去时只有一个线程,就是调用fork()函数的那个线程。

也就是说 uwsgi fork过程(不辨别过程和线程)的时候只会把以后正在执行的app线程复制一份,而不会把随app线程初始化过程中产生的守护线程apollo-client也fork一份,那么解决起来就简略了,配置下lazy-apps = true就能够了,每次fork都是一个真正残缺的app过程蕴含了app线程和apollo-client线程.如果我还没说分明的话,能够参考这里
审慎应用多线程中的forkfork多线程过程时的坑(转)

那么天然就想到既然cache是每个过程独立的,那就罗唆去掉cache应用localfile,也很简略粗犷是能够实现多过程共享配置的性能,每次拜访配置都做下文件IO操作,这里不是什么访问量大的服务的话能够这么操作,上面再说说其余计划.

应用缓存

重构apollo client中线程中的cache缓存的存储形式,比方切换为redis,同样是IO操作比每次都http间接查问apollo配置接口要好些,要是是近程redis-server那网络延时也不可疏忽,进而思考本地redis或者应用uWSGI缓存框架

应用缓存API,在利用中拜访缓存你能够通过应用缓存API,拜访你的实例或者近程实例中的各种缓存。目前,公开了以下函数 (每个语言对其的命名可能与规范有点不同):cache_get(key[,cache])cache_set(key,value[,expires,cache])cache_update(key,value[,expires,cache])cache_exists(key[,cache])cache_del(key[,cache])cache_clear([cache])如果调用该缓存API的语言/平台辨别了字符串和字节 (例如Python 3和Java),那么你必须假如键是字符串,而值是字节 (或者在java之下,是字节数组)。否则,键和值都是无特定编码的字符串,因为在外部,缓存值和缓存键都是简略的二进制blob。expires 参数 (默认为0,示意禁用) 是对象生效的秒数 (并当未设置 purge_lru 的时候,由缓存清道夫移除,见下)cache 参数是所谓的“魔法标识符”,它的语法是

好了,到这里这个问题到此解决了一半. 为什么说一半呢,因为这些配置都是一般配置并不是相似mysql,redis的配置信息,这些配置不会再批改配置后从新生成实例,也就没法应用最新的mysql或redis配置,那么怎么办呢? 上面说说重载服务.

重载服务

如何优化的重启服务?

命令重启uwsgi服务

再守护线程的监听函数最初建加上回调,回调命令函数的实现如下,pid_path是uwsgi启动后生成的pid文件地址.简略粗犷但无效.

# 重载uwsgidef relaod_uwsgi(pid_path):    """选用计划1"""    print("------------relaod_uwsgi---------------")    val = os.system('uwsgi --reload {}'.format(pid_path))    print(val)    if val:        print("重启可能遇到了问题...")

另辟蹊径

py-auto-reloadargument: 必须参数parser: uwsgi_opt_set_intflags: UWSGI_OPT_THREADS|UWSGI_OPT_MASTERhelp: 监控python模块mtime来触发重载 (只在开发时应用)py-autoreloadargument: 必须参数parser: uwsgi_opt_set_intflags: UWSGI_OPT_THREADS|UWSGI_OPT_MASTERhelp: 监控python模块mtime来触发重载 (只在开发时应用)python-auto-reloadargument: 必须参数parser: uwsgi_opt_set_intflags: UWSGI_OPT_THREADS|UWSGI_OPT_MASTERhelp: 监控python模块mtime来触发重载 (只在开发时应用)python-autoreloadargument: 必须参数parser: uwsgi_opt_set_intflags: UWSGI_OPT_THREADS|UWSGI_OPT_MASTERhelp: 监控python模块mtime来触发重载 (只在开发时应用)py-auto-reload-ignoreargument: 必须参数parser: uwsgi_opt_add_string_listflags: UWSGI_OPT_THREADS|UWSGI_OPT_MASTERhelp: 主动重载扫描期间,疏忽指定的模块 (能够屡次指定)

这些配置是监控特定文件来重载uwsgi服务的,那么咱们只有改下localfile的名字为py结尾,那差不多也是没问题的.

留下点货色

最初想说点私货,人类不可能设想出超过意识范畴内的货色,比方做梦,梦中的货色必定都是平时生存中鸡零狗碎的拼凑和假装,代码也是.翻新也是.

这里整顿了一个采坑后奉献进去的python client demo,次要代码是apollo-client-python中的,我在改了外面的http申请应用requests,而后做了点浅浅的封装.欢送大家star!
这篇随记也归档到了这里python-mini,也欢送欢送大家star!

最初:不要自觉地复制粘贴!

请用脑子想想,试着将显示的配置调整以适应你的需要,或者创立新的配置。每个利用和零碎都是彼此之间不同的。作出抉择之前请进行试验。

下面那句不是我说的,是uwsgi文档说的.