开篇,从一次500说起

这是一遍技术文。

{0000} 背景

接到报警,某个api在使用几次后会直接500错误。

生产环境Mesos,多实例。单实例配置: 1 Core, 1G RAM, no swap; API Server: gunicorn + django

翻了好几个实例的日志,除了发现一条Booting worker的INFO,什么也没有,初步猜测worker进程OOM。

{0001} 观察

借助docker, 在本地快速搭建相同配置的服务器环境:

1
2
3
4
5
6
7
8
9
version: '2'

services:
    api:
        build: ./api/
        mem_limit: 1024m
        environment:
            DEBUG: 0
        container_name: api

实例跑起来后,开始压这个api:

1
wrk -c 1000 -t 4 -d 10s -H "Authorization:token b5e783..." http://localhost:17000/api/v1/...

top观察,如下图:

Gunicorn

很明显,worker在多次处理后OOMs很高,内存占用很高(eg: PID=16)。从PID也可看出已经有worker被kill。

uwsgi

上图是替换成uwsgi来跑,同样的现象。

{0010} 跟踪调试

1: memory_profiler

memory_profiler1 memory_profiler2 memory_profiler3

如上图所示:主动GC后,首次运行增加了约6M的内存占用,以后每次调用都有增量内存占用,但增量呈递减趋势。

2: gc & objgraph

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import gc
import objgraph

def get(self, request):
    print '[1]: ', sum((getsizeof(o) for o in objgraph.get_leaking_objects())), \
        sum((getsizeof(o) for o in gc.get_objects())), \
        sum(getsizeof(o) for o in gc.garbage)

    ...

    print '[2]: ', sum((getsizeof(o) for o in objgraph.get_leaking_objects())), \
        sum((getsizeof(o) for o in gc.get_objects())), \
        sum(getsizeof(o) for o in gc.garbage)

    del paper_refs

    print '[3]: ', sum((getsizeof(o) for o in objgraph.get_leaking_objects())), \
        sum((getsizeof(o) for o in gc.get_objects())), \
        sum(getsizeof(o) for o in gc.garbage)

    gc.collect()

    print '[4]: ', sum((getsizeof(o) for o in objgraph.get_leaking_objects())), \
        sum((getsizeof(o) for o in gc.get_objects())), \
        sum(getsizeof(o) for o in gc.garbage)

输出结果

如上图所示,首次运行后增加了6456B的leaking_objects,而后的每次请求都有少量增加,每次请求均没有garbage.

3: Dozer

_dozer

上图显示了,这个api一次性弄出了3404个Candidate、PaperPackage、PaperRef以及6808个PaperExtend。过了一遍代码,这个接口在做 同步 全量 导出。

{0011} 复盘

  1. 这是一个Memory bound function

  2. Python memory architecture很复杂,它占用的内存不一定会及时释放。详细请参考obmalloc.c

  3. worker在OOM后被kill,gunicorn spawn new worker

  4. 网关(nginx) 与 gunicorn worker 失联,给client返回了500

{0100} 总结

  1. gunicorn的max_requestsmax_requests_jitter 提供了简单的帮助,其实就是争取在OOM前重启worker。

  2. 尽量多给点内存。

  3. 所有的同步接口,需要pagesize限制。

  4. 全量导出请走异步任务。

  5. 合理使用 .iterator()

  6. 尽快迁移到python3

Comments