protocol buffers[1]是google提供的一种将结构化数据进行序列化和反序列化的方法,其优势是语言中立,平台中立,可扩展性好,目前在google内部大量用于数据存储,通信协议等方面。PB在功能上相似XML,可是序列化后的数据更小,解析更快,使用上更简单。用户只要按照proto语法在.proto文件中定义好数据的结构,就可使用PB提供的工具(protoc)自动生成处理数据的代码,使用这些代码就能在程序中方便的经过各类数据流读写数据。PB目前支持Java, C++和Python3种语言。另外,PB还提供了很好的向后兼容,即旧版本的程序能够正常处理新版本的数据,新版本的程序也能正常处理旧版本的数据。数据结构
笔者在项目的测试过程当中,遇到了一个protocal buffer使用不当却是的模块内存不断上涨的问题。这里和你们分享一下问题的定位、分析以及解决过程。框架
5月,出现问题的模块(如下成为模块)内存有泄露的嫌疑,表现为程序在启动后内存一直在缓慢的上涨。因为该模块天天都存在重启的操做,所以没有带来较大的影响。函数
8月,发现线上模块的内存上涨速度加快。工具
9月,模块线上出现内存报警。内存使用量从启动时的40G,在70小时左右上涨到50G,因为会出现OOM的风险,模块不得不频繁重启。测试
9月底,模块的某个版本上线后,因为内存使用量稍有增长,致使程序在启动后不到24小时内就出现内存报警,线上程序的稳定受到很是大的影响。线上程序回滚,而且中止该模块的全部功能迭代,直到内存问题解决为止。大数据
模块是整个系统最核心的模块,业务的中止迭代对产品的研发效率影响巨大。问题亟需解决!优化
出现这种问题后,首先要作的就是在线下复现问题,这样才能更好的定位问题,而且可以快速的验证问题修复的效果。可是通过多天的尝试,在QA的测试环境中,模块的内存表现状况均与线上不一致。具体表现为:google
1)线上模块的内存一直在上涨,直到机器内存耗尽,模块重启;线下模块的内存在压力持续若干小时后就趋于稳定,再也不上涨。线程
2)线下环境中,模块的内存上涨速度没有线上快。指针
出现这两种状况的缘由后面再解释。线上线下表现的不一致给问题的复现和效果验证带来了必定的困难。但好在在线下环境中内存使用量依然是上涨的,能够用来定位问题。
小版本间升级点排查。对于这个内存上涨已存在数月的模块来讲,要直接定位问题的难度是很是大的,并且投入会十分巨大。为了使模块的功能迭代尽快开始,最初咱们将定位的焦点聚焦于近期模块上线的功能排查。寄但愿于经过排查这些数量较少的升级,发现对内存的影响。通过2天的排查,没有任何的发现。
结合该模块内存的历史表现和近期升级功能的排查结果,咱们认为模块的内存增加极可能不是泄露,而是某些数据在不断的调用过程当中不断的增大,从而致使内存不断的上涨。理论上,通过足够长的时间后程序的内存使用是能够稳定的。可是受限于程序的物理内存,咱们没法观察到内存稳定的那一刻。
排除数据热加载致使的内存泄露。在线下环境中,全部的数据文件都没有更新,所以排除了数据热加载致使的内存泄露。
各模块逐步排查。小版本间的升级点排查无果后,咱们将排查的方法调整为对程序内的各个子模块(简称module)逐个排除的方法。模块的module共有13个,若是逐个查,那么消耗的时间会特别多。在实施的过程当中采用了二分法进行分析。具体的是某个module为中间点,将该module及之后的模块去掉,来观察模块的内存变化状况。在去掉中间module(含)以后的模块后,发现内存的上涨速度降低了30%,说明该module以前的模块存在70%的泄露。经过分析这些模块,发现某个module (简称module A) 的嫌疑最大。
经过UT验证内存上涨状况。在以前肯定主要泄露module的过程当中,咱们采用在真实环境中进行验证的方法。这个方法的缺点是时间消耗巨大。启动程序,观察都须要消耗很长的时间,一天只能验证一个版本。为了加快问题的验证速度,并结合模块的特色,咱们采用了写UT调用module的方法进行验证。每次验证的时间只须要30分钟,使得问题验证速度大大加快。
部署监控,定位问题。经过写UT,咱们排除了module A中的两个子module。而且,咱们发现module A单线程的内存上涨速度占线上单线程上涨量的30%,这个地方极可能存在着严重的问题。在UT中,咱们对这个module中最主要的数据结构merged_data(存储其包含的子module的特征数据)进行了监控。咱们发现,merged_data这个数据结构的内存一直上涨,上涨量与module A总体的量一致。到此,咱们确认了merged_data这种类型的结构存在内存上涨。而这种类型的数据结构在模块中还有不少,咱们合理的怀疑整个模块的内存上涨都是这种状况致使的。
咱们先看下module A中merged_data字段的用法。其主要的使用过程以下:
经过上面的代码,咱们能够看到_merged_data字段,在run函数中会向里面插入数据,在reset函数中会调用Clear方法对数据进行清理。结果监控中发现的_merged_data占用的内存空间不断的变大。经过查阅protobuf clear函数的介绍,咱们发现:protobuf的message在执行clear操做时,是不会对其用到的空间进行回收的,只会对数据进行清理。这就致使线程占用的数据愈来愈大,直到出现理论上的最大数据后,其内存使用量才会保持稳定。
咱们能够获得这样一个结论:protobuf的clear操做适合于清理那些数据量变化不大的数据,对于大小变化较大的数据是不适合的,须要按期(或每次)进行delete操做。
图1反映出模块中一些主要protobuf message的变化状况。baseline-old是程序启动后的内存状况。baseline-new是程序启动6小时后的内存状况,能够看到全部的数据结构内存占用量都有增长。而且大部分的数据都有大幅的增长。
在了解了问题的缘由后,解决方案就比较简单了。代码以下:
优化的代码中,在每次reset的时候,都会调用scoped_ptr的reset操做,reset会delete指针指向的对象,而后用新的地址进行赋值。优化后的效果如图2所示。newversion-old是优化版本启动1小时候的数据,newversion-latest是优化版本启动6小时后的数据。能够看到从绝对值和上涨量上,优化效果都很是明显。
这个优化方法可能存在一个问题:那就是每次进行reset时,都会对数据进行析构,并从新申请内存,这个操做理论上是很是耗时的。内存优化后,可能会致使程序的CPU消耗增长。具体CPU的变化状况还须要在测试环境中验证。
优化版本的表现状况如图3。
图4显示的是优化版本与基线版本的CPU IDLE对比状况。能够看到优化版本的CPU IDLE反而更高,CPU占用变少了。一个合理的解释是:当protobuf的messge数据量很是大时,其clear操做消耗的CPU比小message的析构和构造消耗的总的CPU还要多。
下面是Clear操做的代码。
经过上面的代码及图5能够看出,Clear操做采用了递归的方式对Message中的逐个字段都进行了处理。对于基础类型字段,代码会对每一个字段都设置默认值。对于一个很是长大的Message来讲,消耗的CPU会很是多。相对于这种状况,释放Message的内存并从新申请小的空间,所占用CPU资源反而更少一些。在这个Case中,常常出现Clear操做清理六、7M内存的状况。这样数据量的Clear操做与释放Message,再申请200K Message空间比起来,显然更消耗CPU资源。
protobuf的cache机制
protobuf message的clear()操做是存在cache机制的,它并不会释放申请的空间,这致使占用的空间愈来愈大。若是程序中protobuf message占用的空间变化很大,那么最好每次或按期进行清理。这样能够避免内存不断的上涨。这也是模块内存一直上涨的核心问题。
内存监控机制
须要对程序的各个模块添加合适的监控机制,这样当某个module的内存占用增长时,咱们能够及时发现细节的问题,而不用从头排查。根据此次的排查经验,后面会主导在产品代码中添加线程/module级内存和cpu处理时间的监控,将监控再往”下”作一层。
UT在内存问题定位中的做用
在逐个对module进行排查时,UT验证比在测试环境中更高效,固然前提是这些module的UT可以比较容易的写出来。这也是使用先进框架的一个缘由。对于验证环境代价高昂的模块,UT验证的效果更加明显。
百度MTC是业界领先的移动应用测试服务平台,为广大开发者在移动应用测试中面临的成本、技术和效率问题提供解决方案。同时分享行业领先的百度技术,做者来自百度员工和业界领袖等。