Python服务热更新
Date: 2019/05/29 Categories: 工作 总结 Tags: 热更新 滚动升级
需要解决的问题
希望不要重启服务就可以升级数据, 一般是静态的词表或tag等资源数据,
- 不停服务
- 不影响正在处理的请求
- 时间不要太长
- 对服务的改造尽量小
- 对多进程服务来说, 需要在进程间共享使用的内存
方法1: 利用基于mmap的磁盘数据库
比如lmdb, 利用mmap系统调用来实现多进程共享内存.
好处:
- 使用方便, 使用广泛的python绑定
- 是一个数据库, 无需开发各种自定义的工具
- 可以加载大于内存的数据
- 没有background thread, fork-safe
缺点:
- 作为kv数据库, 只能存放bytes, 存放struct类型的结构需要额外的decoding, 速度上会有损失
- 性能不如内存中的hash表
- mmap的文件是惰性加载的, 初次访问需要从磁盘中加载, 延迟很高, 需要利用mlock等方法提前加载到内存.
- 如果没有用mlock且系统page cache压力比较大的时候, 比如传输大文件等, 会把已经加载的页换出, 导致性能下降, 这一点可以用对传输限速的方法规避
方法2: 同时启动两个服务
python web开发中常用的方法, 目的在于避免停止旧服务和启动新服务之间的downtime.当框架不支持时也可以用进程池来模拟.
步骤如下
- 启动新进程
- 当新进程可以正常工作时, 切换流量, 即新的进程开始监听的端口上accept
- 关闭旧进程
好处:
- 某些framework内置了此功能, 比如[1]的
- 能用于code reload和data reload两种场景
- 不使用mmap, 减少了惰性加载带来的延迟不稳定
坏处:
- 需要同时加载两个进程, 需要服务器有额外的内存, 或者说服务最多只能利用一半的内存 在服务压力大的时候可能会OOM
- 如果框架没有内置, 需要自己写代码实现, 很可能有bug
- 相当于重启了整个服务, 而不是只加载需要加载的数据
例子: uwsgi master refork
配置文件srv.ini
如下
[uwsgi]
procname = qrw_srv
procname-master = qrw_srv.master
master = true
processes = 6
wsgi-file = srv.py
callable = application
http-socket = qrw.sock
vacuum = false
thunder-lock = true
master-fifo = 0.fifo
master-fifo = 1.fifo
if-exists = 1.fifo
hook-accepting1-once = exec:echo q > 1.fifo && rm -f 1.fifo
endif =
# srv.py
# coding: utf-8
from flask import Flask
import time
application = Flask(__name__)
time.sleep(30) # 模拟资源加载的耗时
@application.route('/', methods=['GET'])
def route_root():
return 'hello world'
uwsgi --ini srv.ini # 启动服务
$ echo -e "GET / HTTP/1.1\r\n\r\n" | socat unix-connect:`pwd`/qrw.sock STDIO # 测试服务
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 11
hello world
这里我们设置了两个控制fifo, 当初次启动的时候会绑定到0.fifo, 要reload服务,只需要执行
echo 1f > 0.fifo
即可, 这个指令让老得master先切换到1.fifo, 然后fork出新的master,
当新master的第一个worker开始accept请求时 我们用hook执行命令去关闭老得master并删除对应的fifo文件.
注意到当初次启动时, hook-accepting1-once不会生效, 因为此时没有1.fifo文件.
方法3: 封装c++模块
使用c++时, 可以启动一个background thread来加载数据, 然后用一个mutex保护实际的数据指针, 加载完毕后更改数据指针并释放旧内存即可.
Python语言可以把需要访问数据的部分封装为python module, 暴露数据加载接口即可.
改进1: 使用mmap
为避免每个进程加载一份数据浪费内存, 可以用mmap将数据文件映射到进程地址空间.
如果需要避免从磁盘加载, 可以把文件放入/dev/shm
改进2: 结构化的内存映射
一般的mmap只能映射平坦内存, 如果需要在内存中保存结构化数据需要一些额外的工作.
这里介绍我用过的两种方法
- Boost IPC: 使用了offset_ptr, 将指针从绝对地址变为相对于地址块的偏移量, 兼容boost container(stl 类似的容器库),boost multi_index(同一份数据, 多个索引)等. 可以和mmap或共享内存配合保存结构化数据.
- FlatBuffers: Google开源的编解码库, 卖点是zero-copy的解码, 实现方法类似offset_ptr, 编码效率低解码效率高. 类似的库有Capn Proto
优点:
- 灵活,可以实现自定义复杂数据类型的热加载
- 速度快
缺点:
- 实现复杂
- 依赖c/c++