如何估算一个Python对象的内存占用


需求

最近接到一个需求:制作一个工具,可以即时查看线上服务器的内存使用状况,不要求精确,但是如果存在爆内存的情况,要能查到有嫌疑的Entity是哪个。

pytracemalloc神器无用武之地

在网上一番搜索,对GuppyPySizerpytracemalloc进行比较之后,貌似大家对pytracemalloc的评价较好。按照官方文档,需要对Python2.7.8源码打个patch,虽然我们项目使用的是2.7.11,不过也问题不大,打Patch时obmalloc.c里面有2个reject,手动将其merge到源码中即可。编译通过,写个小程序进行实现,可行,而且可以定位到源代码的行号,确实神器。于是将情况报告给领导,正以为将大功告成之时,领导说,tracemalloc这个库我们已经集成到引擎中,但是对服务器性能有影响,所以一般就在测试环境用用,不满足即时查看线上服务器内存使用状况的要求。根据我了解的情况,确实有这样的问题,pytracemalloc其实是修改了Python虚拟机分配内存时的代码,所以:

  1. 如果线上服务器内存已经暴涨,这时再去服务器打开tracemalloc,由于暴涨的内存已经分配过,所以什么都看不出来;
  2. 那么如果服务器一启动就打开tracemalloc,那么每次分配内存都要去tracemalloc那里进行记录,性能影响不能忽略。

看来这条路是走不通。

sys.getsizeof的局限与解决方案

之前查看data数据占用内存情况时,用过sys.getsizeof函数,但是这个函数有如下几个问题:

  1. 对内置类型对象可以统计出准确数据,但是无法处理用户自定义的类型;
  2. 对于嵌套的容器类型对象,不统计内层嵌套对象的内存占用,比如[1, 2, range(1000)]一般只占用不到100个字节,显然是错误的。

对于自定义的类型对象,我们可以通过dir(object)遍历其属性,对每一个属性再按照其类型(简单内置类型?容器类型?自定义类型?)进行进一步的递归调用。

对于容器类型,也可以对其每一个元素按照类型进行递归,类似上面对对象属性的递归。

Python脚本层统计内存的困境

由于Python对象是引用语义的,也就意味着[objectA, objectA, objectA]这样的对象所占用的内存,其实只用统计一遍objectA。但是如果两个Entity中都同时引用同一个object,我觉得这个时候,还是应该在每个entity中都统计一遍object的内存占用,原因是如果不这样做的话,整个统计结果将会和遍历Entity的次序有关。这个工具的作用是找出有可能导致爆内存的Entity,并不需要精确的统计出每个Entity所占用的内存(相对大小比绝对大小更重要),而精确的每个进程占用多少内存可以通过操作系统层面的相关工具获得。所以,对每一个Entity,设置一个集合parsed用于保存已经统计过的对象id,对于已经存在于parsed集合中的对象,再次遇见时,直接返回0。

关于遍历次序,还需要特殊处理一下__dict__变量。当我们想统计某个可疑Entity的一级属性分别占用多少内存时,一定要将__dict__挪到遍历列表的最后进行统计。不然由于__dict__中包含大量庞杂的信息,先统计__dict__将会导致后续遍历普通一级属性时,得到的内存数据偏小。

最后,对于那些继承自容器类型(比如list)的类型对象,它们既是容器,又是自定义对象,所以自然需要统计两次,但是两次要共用一个parsed集合。

测试结果

由于涉及到具体的项目代码,所以这里就不放代码啦。放一个自己开发机的统计结果吧:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
>>> import memgo
>>> memgo.get_top_mem_entities(20)
>>> start mem go...
Top 20(KB > 1KB):
('CreditTrader', 231854, 157.5830078125)
('CreditTrader', 231858, 140.0361328125)
('CreditTrader', 231860, 139.953125)
('CreditTrader', 10860, 135.1005859375)
('Space', 10010, 30.1552734375)
('Space', 10004, 14.9208984375)
('Space', 10005, 12.6435546875)
('Space', 10064, 11.2939453125)
('Monster', 32717, 6.6826171875)
('Monster', 22194, 6.2109375)
('Monster', 22264, 6.2109375)
('Monster', 22265, 6.2109375)
('Monster', 22310, 6.2109375)
('Monster', 22311, 6.2109375)
('Monster', 22327, 6.2109375)
('Monster', 22328, 6.2109375)
('Monster', 22329, 6.2109375)
('Monster', 22407, 6.2109375)
('Monster', 22408, 6.2109375)
('Monster', 22409, 6.2109375)
done!

>>> memgo.size_of_ent(231854)
>>> start mem go...
size is 157.583007812KB
Top10 is(Byte):
('store', 158584)
('globalBox', 1911)
('__dict__', 333)
('volatileInfo', 104)
('userList', 72)
('__module__', 49)
('IsAvatar', 24)
('S_COMBAT', 24)
('S_IDLE', 24)
('S_MOVE', 24)
done!

程序耗时比较长,为防止影响线上服务器的逻辑运算,可以使用os.fork()在子进程中进行统计,反正这个工具对内存中的entity是只读的,正好利用fork的Copy on Write特性,基本不会影响到服务器的正常运行。


推荐阅读:
Python协程:从yield/send到async/await/
探究如何给Python程序做hotfix
踩坑记——覆写Python中的__cmp__

转载请注明出处: http://blog.guoyb.com/2017/09/09/python-mem/

欢迎使用微信扫描下方二维码,关注我的微信公众号TechTalking,技术·生活·思考:
后端技术小黑屋

Comments