Python服务热更新

Date: 2019/05/29 Categories: 工作 总结 Tags: 热更新 滚动升级



需要解决的问题

希望不要重启服务就可以升级数据, 一般是静态的词表或tag等资源数据,

  1. 不停服务
  2. 不影响正在处理的请求
  3. 时间不要太长
  4. 对服务的改造尽量小
  5. 对多进程服务来说, 需要在进程间共享使用的内存

方法1: 利用基于mmap的磁盘数据库

比如lmdb, 利用mmap系统调用来实现多进程共享内存.

好处:

  1. 使用方便, 使用广泛的python绑定
  2. 是一个数据库, 无需开发各种自定义的工具
  3. 可以加载大于内存的数据
  4. 没有background thread, fork-safe

缺点:

  1. 作为kv数据库, 只能存放bytes, 存放struct类型的结构需要额外的decoding, 速度上会有损失
  2. 性能不如内存中的hash表
  3. mmap的文件是惰性加载的, 初次访问需要从磁盘中加载, 延迟很高, 需要利用mlock等方法提前加载到内存.
  4. 如果没有用mlock且系统page cache压力比较大的时候, 比如传输大文件等, 会把已经加载的页换出, 导致性能下降, 这一点可以用对传输限速的方法规避

方法2: 同时启动两个服务

python web开发中常用的方法, 目的在于避免停止旧服务和启动新服务之间的downtime.当框架不支持时也可以用进程池来模拟.

步骤如下

  1. 启动新进程
  2. 当新进程可以正常工作时, 切换流量, 即新的进程开始监听的端口上accept
  3. 关闭旧进程

好处:

  1. 某些framework内置了此功能, 比如[1]
  2. 能用于code reload和data reload两种场景
  3. 不使用mmap, 减少了惰性加载带来的延迟不稳定

坏处:

  1. 需要同时加载两个进程, 需要服务器有额外的内存, 或者说服务最多只能利用一半的内存 在服务压力大的时候可能会OOM
  2. 如果框架没有内置, 需要自己写代码实现, 很可能有bug
  3. 相当于重启了整个服务, 而不是只加载需要加载的数据

例子: 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只能映射平坦内存, 如果需要在内存中保存结构化数据需要一些额外的工作.

这里介绍我用过的两种方法

  1. Boost IPC: 使用了offset_ptr, 将指针从绝对地址变为相对于地址块的偏移量, 兼容boost container(stl 类似的容器库),boost multi_index(同一份数据, 多个索引)等. 可以和mmap或共享内存配合保存结构化数据.
  2. FlatBuffers: Google开源的编解码库, 卖点是zero-copy的解码, 实现方法类似offset_ptr, 编码效率低解码效率高. 类似的库有Capn Proto

优点:

  1. 灵活,可以实现自定义复杂数据类型的热加载
  2. 速度快

缺点:

  1. 实现复杂
  2. 依赖c/c++

Reference

  1. The Art of Graceful Reloading
  2. uWSGI graceful Python code deploy