机器内存爆满问题排查
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.htmlprofile: 内存分析工具,分析内存增量情况,没法直观看出当前内存使用状况,有时候分析有些不准
https://pypi.python.org/pypi/memoryprofiler