机器内存爆满问题排查

1 背景

两周前广告开屏服务突然503报警不断,先查看了各种业务监控没发现流量等有什么大变化,因为不久之前出过一次机器出问题的情况,马上去查看了机器是不是正常,果然内存几乎涨满了。大概十来分钟内存就会达到90%多,然后进程就重启了,但是从日志来看并没有什么异常情况,好在并没有将机器拖死。当时第一件事就是回滚代码,各种代码回滚到几天前,没什么效果。后来追查当时事发的一些修改改动,定位到是因为一个广告的上线导致,屏蔽该广告之后问题恢复了,但是当天还是没有查出最终问题。当时机器内存情况得截图:

2 复现问题

虽然当时定位到了由于一广告导致,但是看代码逻辑并没有发现有使用大内存的逻辑,线下用这广告尝试重现都没有出现类似情况,于是就尝试列出以下的可能: 1.线下环境跟线上不一致无法复现,用线上机器搭建复制服务环境 2.跟请求参数有关,有一些信息引发了这个问题,尝试用nginx的日志复放复现 3.请求Session信息有关,需要用线上流量复现

2.1 Nginx日志复放

因为大多数请求参数都在nginx日志里边有,除了一些用户信息外,所以就用线上nginx日志的在线下环境进行复现,但是并没有复现那出问题。 为了排除问题1的可能性,找了台线上机器环境基本一模一样,刚开始为了能快速复现问题,将uwsgi的work数调少了一些,发现并没有出现,后来调到一摸一样,还是没有复现。 复放流量用的代码:

def online_request(mypid):  
    print "I am child %d ID :%d" % (os.getpid(),mypid)
    url_file = open('splash.url', 'r')
    for line in url_file:
        url_info = line.split(' ')
        if len(url_info) < 2:
            continue
        method = url_info[0]
        url = splash_domain + url_info[1]
        # print method,url
        if method == 'GET':
            rsp = requests.get(url, cookies=get_cookie())
        elif method == 'POST':
            rsp = requests.post(url, cookies=get_cookie())
        # print rsp.content
    url_file.close()

def multi_process():  
    for i in range(100):
        mypid=os.getpid()
        p = Process(target=online_request, args=(mypid,))
        p.start()

线上环境也没法复现只好到正式服务改代码来进行,在代码中强制加入了出问题广告的处理,返回时再去掉,内存果然涨上来了。因为线上没法直接调试,没法定位具体问题,,所以得想办法在可随便整的环境上复现,把该代码原封不动的拷贝到复制好环境的线上机器上还是没法复现。 因为流量是用nginx日志进行重放,跟线上流量不一样的就是 请求没有带用户信息,所以需要尝试带上用户信息进行。

2.2 复制线上流量

想要尝试带上用户信息,刚开始先复制了一份cookie信息放到请求参数里,发现没法复现,设置cookie代码:

def get_cookie():  
    cookie_dict = {'login_flag': '2b6f49d1c08adcd9df37493771b08592', 'sid_guard': 'a3f4027dfe4889db67641f51e481e682|1480857181|2591988|Tue, 03-Jan-2017 13:12:
49 GMT', 'sid_tt': 'a3f4027dfe4889db67641f51e481e682', 'install_id': '6516548479', '_ga': 'GA1.2.571656648.1479953898', 'sessionid': 'a3f4027dfe4889db67641f51  
e481e682', 'qh[360]': '1', '_ba': 'BA0.2-20161201-51e32-BEhFm05HUdmuYonrlIYz', 'ttreq': '1$c761574f35fbc3e335af742f53a663bc9b4f30be'}  
    cookies = requests.utils.cookiejar_from_dict(cookie_dict)
    return cookies
....
rsp = requests.post(url, cookies=get_cookie())  

尝试无果后决定直接复制线上流量来进行复现,想到之前看过有人用gor试过,查了下资料可行,先说下gor的两种尝试: 1.保存出问题是的现场请求文件,gor可以原封不动的保存请求的所有信息 2.直接转发流量 先用方法1去录制了复现问题时的请求信息,然后在用gor进行复放,使用命令如下:

./gor --input-raw ":81" --output-file requests.gor
./gor --input-file requests_0.gor --output-http "192.168.1.95:1230"

没有复现问题,直接在正式服务上启动了一个进程(为了方便调试)来进行,尝试方法2:

./gor --input-raw ":8063|1%" --output-http "192.168.1.95:1230" --output-http "192.168.1.160:1230" --output-file requests.gor

由于复制了1%的流量,虽然问题复现慢了些还是出现了,同时发现一个当时忽略的问题,并不是一个进程打满了所有内存,而是一个进程占用了1%左右内存(正常情况下只占用0.3%左右),由于线上机器进程数比较多才将内存打满了。

3 问题定位

问题复现了,可是无法用复放请求稳定复现,有随机性这个尴尬的场面使得无法调试问题,那只能尝试抓取问题复现时的现场信息了,找到了一个叫meliae的包,可以dump出python的内存使用信息,但是每次dump使用的时间太长,无法在每次请求的时候都dump一次,所以只能在问题复现后,快速dump一次,于是在代码中加了如下逻辑:

if request.GET.get('dump_mem') is not None:  
    from meliae import scanner
    scanner.dump_all_objects('/home/test/log/dump_pack_%s.txt'%request.GET.get('dump_mem'))

顺利拿到了问题出现时以及正常情况下的内存信息,然后用内存信息进行分析,发现了一个异常的dict对象占用50多兆内存,而它引用的字符串等对象占用也很大,有30多万条信息,用一下命令来解析内存dump文件:

from meliae import loader  
#加载dump文件
om = loader.load('/opt/log/dump.txt')  
#计算各Objects的引用关系
om.compute_parents()  
#去掉各对象Instance的_dict_属性
om.collapse_instance_dicts()  
#分析内存占用情况
om.summarize()  

正常
大于1m的dict没有

异常
大于1m的dict占用一个50多兆内存,item信息说明:

dict(140214267288384 50331928B 1461046refs 1par)  
对象ID   占用字节数  引用数   父对象数

用脚本解析出该对象的一些字符串信息发现是一些pgc作者信息,感觉一头雾水想不出来哪里用加载这些信息。 虽然找到了对象信息,但是还是服务定位代码问题出在哪?用meliae能拿到对象ID以及它的父子对象信息,所以尝试用这些信息去定位: 1.对象ID查找变量信息,尝试未果行不通 2.用对象父信息查找相关信息,work 通过该对象的父父父..信息找到一个对象,定位到了相关代码:

# 获取所有dict对象
epdict = om_error.get_all('dict')  
# 获取其父对象
epdict[0].p  
# 获取指定ID对象
par = om_error.objs.get(140214267232160)  

找到了相关对象,然后搜索相关代码定义及使用的地方,顺藤摸瓜在相关调用出打印出堆栈信息,最终定位出了代码的调用逻辑及调用栈情况。

4 最终结论

最终问题是因为某对象的属性信息代码里加载了大量PGC作者相关信息,存储到本地的一个LRUCache占用了很多内存,由于使用python多进程的模式,每个进程都有一份自己的缓存,每个进程多占用了400M内存,100多个进程多占了四五十G内存,撑爆了机器内存。

5 解决方案

找了问题的原因,解决方案很简单,将需要的信息通过服务获取,避免子自身加载过多数据;数据服务已经是现成的,我们只需要接入新服务解决问题了。

6 总结

6.1 排查过程中的问题

问题复现走了很多弯路,说下复现时的一些疑问: 1.线上机器复制环境为什么复现不稳定:
没有group数据库访问权限(囧),中间复现过几次出现内存上涨是因为获取到了缓存数据 2.正式服务环境机器 重放请求时为什么不稳定复现(请求重放时这个请求一直是503也是见鬼了)

6.2 内存问题总结

1.如果能稳定复现的问题要好查很多,用memory_profile或者单步调试就能定位问题
2.对于这种不好复现的问题,最好能取到内存dump信息,分析内存信息来查找问题,过程就类似此文所述,线上环境都可以留这么一个用meliae来dump内存的接口或者零时添加来分析内存问题,这样面对内存保证问题就可以非常直接的分析了

6.3 涉及工具资料

gor:流量复制工具
使用Gor进行测试

meliae:内存分析工具,分析当前内存使用情况
https://pypi.python.org/pypi/meliae
http://blog.chinaunix.net/uid-16480950-id-103655.html

memoryprofile: 内存分析工具,分析内存增量情况,没法直观看出当前内存使用状况,有时候分析有些不准
https://pypi.python.org/pypi/memory
profiler