春节初一~初三期间,红包活动页左上角会有春节排行榜的入口,前期预估峰值:15w/s。
排行榜server优化前后数据对比如下(备注:以下数据,均假设从CKV中拿到数据,忽略掉oidb相关逻辑):
测试条件:CPU占比不超过85%,超时时间不超过900ms
优化前单机QPS | 优化后单机QPS | |
---|---|---|
获取排行榜首页数据(实时拉取好友步数并进行排序) | 2200 | 5200 ( +3000) |
获取排行榜分页数据(从Redis快照中直接获取数据) | 3000 | 5500 ( +2500) |
用户点赞 | 12000 | 12000 |
发送C2C消息 | 12000 | 12000 |
针对春节排行榜运动侧准备了120台V8机器。以排行榜首页为例,优化前,系统QPS(极限值) = 2200*120 = 26.4w/s,优化后,系统QPS(极限值) = 5200*120 = 62.4w/s,提升136.4%。
以CPU占比75%平稳运行而言,系统QPS = 4700*120 = 56.4w/s,此时平均每个请求从发包到收包耗时约40ms。
所有接口中,由于排行榜首页,做的逻辑更多更复杂(包括拉取关系链、取所有好友步数、排序、取用户信息、取会员标记、取点赞数据等),因此,不出意外,首页QPS是最低的。另外,春节期间排行榜所有请求中,首页的请求应该是最多的,毕竟,用户进入页面首先就会请求首页数据。
综上所述,优化排行榜首页性能成为了整个系统的关键所在。这次优化,也是根据这条思路进行的,下面简要介绍下优化的思路和过程。
一、排行榜逻辑架构图
要点如下:
- 存储:主要采用CKV,
- 外部接口:能异步就异步(除oidb查会员标记位外)
- 框架:SPP微线程,相关网络操作均采用异步。
- 备注:SSO寻址走hash一致性寻址,server本地采用Redis做快照,防止排名错乱的问题。
二、压测数据
工欲善其事,必先利其器。这里不得不提到test.server.com这个压测利器,文中所有数据和结果均来自test.server.com提供的压测客户端。
优化的步骤无外乎:
A. 压测得到当前Server性能(CPU不超过80%,延迟不超过900ms)
B. Perf查看CPU消耗点在哪里(因为排行榜这里是CPU Bound型)
C. 针对B的结果相应优化,再重复进行步骤A。
1. 原始压测数据(未做优化前):压测到2200/s时,CPU占比已经飙升至80%。
分析后得知,Server中的Json和map的相关操作吃了很大一部分CPU,约占用30%左右。
2. 优化map操作,将代码中所有涉及到map的逻辑全部替换为hash_map和vector。思路:map底层采用RB_Tree的方式实现,查找复杂度为OLog(n);hash_map底层采用Hash_table实现,查找复杂度为O(1)。
优化前QPS | 优化后QPS | 优化措施 |
---|---|---|
2200 | 3200 (+1000) | hash_map和vector替换map操作 |
备注:C99里面的hash_map,不是标准库,是gcc实现的:__gnu_cxx::hash_map。这里性能提升明显,主要是获取步数数据、用户关系链数据及相互匹配时,查找操作较多。
优化后Perf图如下(搜索关键字<map>如下图,CPU占比11%左右):右图中,之所以map匹配CPU占比超过11%,因为Json相关的操作中大量的使用了map,而Json还没有进行优化。搜索hash_map,CPU占比只有5%左右。
3. 优化Json操作。思路:将Json库替换为简单字符串拼接(这里由于是和前端交互,限定了只能使用json交互,因此优化的思路还是从组装json方面考虑,而不是替换为二进制协议,例如pb等)。
优化前QPS | 优化后QPS | 优化措施 |
---|---|---|
3200 | 4000 (** +800**) | json组装,改为自己拼接字符串 |
右图中,可以看到,优化完成后,占用CPU最多的已经变为ssdasn::的相关操作,这些操作是CKV存储的编解码封装,也就是说,后续的性能优化已经和业务无关了。
此时CPU使用如下,占比约80%左右:4. 减少日志流水操作。思路:忽略正常处理的请求,只打印出错请求处理日志。
优化前QPS | 优化后QPS | 优化措施 |
---|---|---|
4000 | 4500 (+500) | 减少日志打印,只打印出错日志 |
5. gcc编译增加O2选项。思路:利用编译器自身的编译优化选项,从编译执行的底层尝试优化性能(业务无关)
优化前QPS | 优化后QPS | 优化措施 |
---|---|---|
4500 | 5200 (** +700**) | 使用gcc O2优化选项 |
三、走过的弯路
在初步压测出单机QPS=2200后,由于缺乏经验,没有从火焰图去分析CPU到底消耗在哪里。因此只能尝试各种经验上的方法进行优化,例如:
- 去掉同步快照操作(伤害用户体验)
- 去掉关系链CKV同步操作(更换成本高)
- 调整批量获取CKV的数量(经验值)
- 调大进程数
- 关闭系统日志
- 关闭排序(排行榜基础功能)
等...
这些效果都不明显,提升作用有效,最多的时候,有损体验的前提下,QPS才提升到2500左右。
后面在查阅相关资料后,系统化的使用perf、火焰图等工具进行分析,抓到性能瓶颈后,有的放矢,才能在后面的优化过程中,有效的提升系统QPS。
这次优化,从接触学习压测工具开始,到昨天优化告一段落,断断续续持续了有3、4天左右。
参考资料
gcc 编译优化选项O2相关知识,可以参考这篇文章GCC中-O1 -O2 -O3 优化的原理
另外,O2选项优化打开时,注意另外一个选项:-fno-strict-aliasing,如果没有配合使用的话,程序可能产生未定义的行为,大意是说O2选项默认认为不同类型的指针不能指向同一片内存区,如果业务代码中,有强制类型转换,需要注意下这里。具体参考:gcc 编译参数 -fno-strict-aliasing
官方说明在此:Options That Control Optimization
我的理解:O2性能优化选项打开前后代码语义必然相同,最主要的性能优化点,可能还是:未打开前,Gcc编译生成的代码是独立的,每一行代码都可以打断,方便调试;打开后,Gcc编译生成的代码是相关的,并根据一些相关性进行了优化,当然这时候,调试的难度就很大了。