关于python:python多进程多线程时使用uwsi与fork的坑

48次阅读

共计 5809 个字符,预计需要花费 15 分钟才能阅读完成。

故事背景

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

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

test@python:~/app$ cat uwsgi.ini
[uwsgi]
module = app
wsgi-file = app.py
master = true
processes = 4           # 多个 work 过程
enable-threads = true   # 容许启动多线程
#lazy-apps = true       # 前面再说
http = :3000
die-on-term = true
pidfile = ./uwsgi.pid
chdir = /home/test/app
disable-logging = true
log-maxsize = 5000000
daemonize = /home/test/app/log.log

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

from flask import Flask, jsonify, request
from apollo import Config

cf = 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.ini
if __name__ == "__main__":
    app.run(port=5000)

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

test@python:~/app$ ps -ef|grep uwsgi.ini
test      16224     1  0 14:36 ?        00:00:00 uwsgi --ini uwsgi.ini
test      16225 16224  0 14:36 ?        00:00:00 uwsgi --ini uwsgi.ini
test      16226 16224  0 14:36 ?        00:00:00 uwsgi --ini uwsgi.ini
test      16227 16224  0 14:36 ?        00:00:00 uwsgi --ini uwsgi.ini
test      16228 16224  0 14:36 ?        00:00:00 uwsgi --ini uwsgi.ini
test      16229 16224  0 14:36 ?        00:00:00 uwsgi --ini uwsgi.ini
test      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.py
2. 批改 app.py 中的端口号
3. 执行 python app.py
4. 反复 2,3
5. 留神看打印的日志
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()能够拿到以后过程 id

4. 父过程、子过程执行程序没有法则,齐全取决于操作系统的调度算法。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 文件地址. 简略粗犷但无效.

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

另辟蹊径

py-auto-reload
argument: 必须参数

parser: uwsgi_opt_set_int

flags: UWSGI_OPT_THREADS|UWSGI_OPT_MASTER

help: 监控 python 模块 mtime 来触发重载 (只在开发时应用)

py-autoreload
argument: 必须参数

parser: uwsgi_opt_set_int

flags: UWSGI_OPT_THREADS|UWSGI_OPT_MASTER

help: 监控 python 模块 mtime 来触发重载 (只在开发时应用)

python-auto-reload
argument: 必须参数

parser: uwsgi_opt_set_int

flags: UWSGI_OPT_THREADS|UWSGI_OPT_MASTER

help: 监控 python 模块 mtime 来触发重载 (只在开发时应用)

python-autoreload
argument: 必须参数

parser: uwsgi_opt_set_int

flags: UWSGI_OPT_THREADS|UWSGI_OPT_MASTER

help: 监控 python 模块 mtime 来触发重载 (只在开发时应用)

py-auto-reload-ignore
argument: 必须参数

parser: uwsgi_opt_add_string_list

flags: UWSGI_OPT_THREADS|UWSGI_OPT_MASTER

help: 主动重载扫描期间,疏忽指定的模块 (能够屡次指定)

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

留下点货色

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

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

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

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

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

正文完
 0