Django 是一个 Python 的富 web 框架,其功能丰富,但是由于其是基于 python 的,所以内存使用方面是比一般的静态语言要高,请参见 python 内存模型

关于 Django 和 uwsgi 配套使用的时候,为何内存会越来越大,可以参考下面的两篇文章:

其中第一篇的参考价值比较大。

回归倒本文,我最近遇到的问题是发生在 Django 和 uwsgi 之间的,问题主要发生在高内存占用,基本上都是些单一的接口,没有大量批量的接口,由于有部分阻塞的接口使用的 gevent,但是并没有很消耗内存,唯一一个坑爹的库就是 geoip2,他的启动有三种模式,默认会先加载到内存,其次才会尝试从文件直接读取,默认肯定都是把几十M的文件加载到内存里面了。

总结一下这个 web 应用的特点如下:

  • Django uwsgi rest-framework
  • 200w+ requests
  • 2C 4G
  • geoip2 support
  • uwsgi gevent (cores: 50)
  • blocking api (30s wait)

我首先尝试了把 geoip2 的模式调整到 从文件读写,一定程度上缓解了大内存的问题,但是发现请求时间是以前的2倍多,很显然这个请求本来就很慢,虽然频率不大,但是并不完美。

下面是我开始诊断这个问题的一些经历和思路,如果需要直接获取解决方案,可以跳过下面的思路,直接看结论。

Djnago 内存问题该如何诊断

简单的工具,适合用来发现明显的内存泄漏:

  • htop
  • uwsgitop

上面的两个工具都是系统工具,通过简单的命令和配置就可以使用,但是只能从宏观上面来看这个问题,不能定位到具体的问题,也就没办法解决。

从 uwsgi 来探测内存问题

配置优化

如果要诊断内存问题,需要把 uwsgi 的配置单进程模式,这样可以避免多进程的干扰,方便观察。

日志配置

uwsgi 提供了丰富的日志格式,可以通过日志打印来定位具体的请求,从请求的内存使用来判断问题的所在。

下面的配置仅供参考:

daemonize=/var/log/uwsgi/$(MODULE_NAME).log
memory-report=true
log-master = true
log-x-forwarded-for = true
logformat = [CORE: %(core)] [RES/%(rssM)MB VIRT/%(vszM)MB] [pid: %(pid)] %(addr) [%(ltime)] %(method) %(uri) => generated %(size) bytes in %(msecs) msecs (%(proto) %(status))

由于我使用了 gevent 所以这个地方我开启了 CORE 的记录。

该日志会记录当前请求的 url 的实际内存使用和虚拟内存使用情况,通过前后的内存对比就可以发现内存突变点在哪里,进而就进入了 django 内部的排查了。

从 Django 来探测内存问题

Django 就是一个 python 的 web 框架,除了框架自身的问题外,业务上的问题,基本上都是 python 代码的问题了。

python 内存诊断方法

Python 是一门动态,垃圾自动回收的语言,大内存占用,只能说明是代码需要优话,但是发生内存泄漏的化,那只能使自引用问题,阻碍了垃圾回收器的收集。

objgraph 是该作者为了解决对象自引用问题的一个诊断工具,相比guppy更加的简洁,支持的 python 版本较多,目前 guppy 只支持 python 2.7 而且长期不维护了,踩坑了,如果你要折腾的话,请自便。

至于objgraph的话,文档请戳,也很简单,通过调用相应的 python 方法,对比 python 对象的增量,来找出问题。

Django 内存诊断方法

Django 的内存诊断与 python 并没有太多的区别,最终都回归到 python 问题上了,但是 Django 诊断起来很麻烦,基本上要靠日志,而且生产环境的一些中间件依赖,认证依赖很难在本地环境配起来,也更是没得必要都配置起来,我们只需要发现问题的本质,然后解决,经过探索后还是有办法解决的。

这里我提前发现了大内存的 url 请求,通过url conf 找到了对应的 view 方法。

  1. 利用 Django shell
    python manage.py shell
    
  2. 导入 restframe work view
    from device.views import DeviceIpListView
    from django.test import RequestFactory
    import objgraph
    
  3. 巧设断点
    factory = RequestFactory()
    req = factory.get("api/nuri/etau/device/device_ip_list/")
    objgraph.show_growth(limit=3)
    DeviceIpListView.as_view()(req).render()
    objgraph.show_growth(limit=3)
    DeviceIpListView.as_view()(req).render()
    objgraph.show_growth(limit=3)
    

并没有发现有对象残留,说明这次并不是一个内存泄漏问题,纯属于一个内存占用问题。如果我们启用四个 uwsgi 进程的话,那么一共有几百M的重复内存滞留在内存中,这个时候其实我们只希望有一份即可,我们可以考虑把共享内存拷贝到 redis 中,然后通过网络共享。

上面的方法中,我们直接构造了 uwsgi 的 request 然后通过调用 view 方法,得到我们需要的结果,这是一种很好的测试方法。

针对 geoip2 的大内存占用问题优化

geoip2 会在我开四个 uwsgi 进程的时候,加载4分数据到内存中,其中四分的内容是相同的,那有没有办法让这四份内存成为一份共享的勒?linux 上面其实有,vmware 这些公司在很多年前都想过这个问题,多个虚拟机共享内存问题,况且我们这个地方是多个进程共享,linux 内核大于 2.6 后,引入了 KSM 机制。uwsgi 内部对该机制做了支持,我们只需要在 uwsgi 配置即可:

echo 1 > /sys/kernel/mm/ksm/run

uwsgi 配置中增加:

ksm=10

重启 uwsgi 进程后,可以功过命令观察共享内存情况:

grep -H '' /sys/kernel/mm/ksm/pages_*

/sys/kernel/mm/ksm/pages_shared:1872
/sys/kernel/mm/ksm/pages_sharing:3420
/sys/kernel/mm/ksm/pages_to_scan:100
/sys/kernel/mm/ksm/pages_unshared:15443
/sys/kernel/mm/ksm/pages_volatile:4431

另外通过 uwsgi.log 文件 或者通过 htop 可以发现内存使用量减少了,但是偶尔还会有彪涨的情况,暂未发现原因,也没有错误发生,后续留作观察。