背景
麦序列表,是我们进直播间后在直播间顶部的一行在线观众列表。可以右滑翻页。
为了保证麦序列表的实时性,目前客户端每3秒调一个接口刷新,给服务端带来了极大的压力和资源消耗。
因而需要在不丢失用户体验的情况下对其进行性能优化。
彼得·德鲁克(Peter F. Drucker)说过,"你如果无法度量它, 就无法有效改进它"。首先,选取一个指标衡量效果,这里选择机器数量,优化前用了多少机器,优化后用了多少机器。减少的机器是实实在在的减少了成本。
性能优化的手段丰富多样,但大部分围绕着2点:减少计算、减少IO。
在优化之前,先进行分析,分析当前现状,瓶颈在哪里。没进行分析而直接动手干,往往一顿操作猛如虎,回头一看效果有限,代码还复杂了,系统脆弱了。
麦序列表的变动有可能是:
用户进出房,导致排名变动
用户送礼、发公聊、分享,导致排名变动
用户特权变动,导致头像展示的特权信息变动。特权信息包括:头像框、认证、勋章、等级标、真爱团标、守护标、贵族标、VIP、榜1、榜2、榜3 等等。
总之一句话,很多情况都可能导致麦序列表变动。所以没法用推的方式,因为要监听的数据源太多了。也没法缓存太久,因为这些东西变动频率比较快,比如某个大R佩戴了一个豪华头像框,就希望别人能及时在麦序列表上看到。
我们先梳理一下读核心路径:
1.客户端每3秒获取麦序列表
2.从zset获取某页用户
3.获取这一页用户的特权信息(调特权聚合接口)
3.1→特权聚合接口调十几个下游获取特权信息.
4.序列化返回JSON
ok,我们现在来分析这个路径,看看哪个地方可以优化。
路径1:客户端每3秒获取麦序列表
特点:IO多
分析:客户端每3秒轮询,也就是拉的方式,这样单个房间在线1w人,单个房间QPS就3k多了。能不能改成推的方式呢?根据上面的分析,引起麦序列表变动的变量太多了,不现实。那每3秒能不能改成5秒、10秒呢?能是能,就是更新不及时,用户体验差了,大R不开心
动作:动不了
路径2:从zset获取某页用户
特点:1次IO
分析:无需优化
动作:动不了
路径3:获取这一页用户的特权信息(调特权聚合接口)
特点:IO多
分析:特权聚合接口存在流量扩散问题。请求一次特权聚合接口,会调十几个下游,而这十几个下游还有下游。也就是说,少调一次特权聚合接口,就少了很多IO!那怎么少调呢?基于一点:”同一房间不管多少人,同一时刻看到的麦序列表都是一样的“。既然看到的都一样,就非常适合用缓存了。Cache Is King! 这里引入了我们的大杀器:singlefight
动作:用缓存,用singlefight
路径4:序列化返回JSON
特点:计算多
分析:序列化很耗CPU。尤其是一个用户的特权聚合信息特别多,有可能达到几十k。通过火焰图,看到大部分时间消耗在序列化身上了。这个地方,2个思路:
换性能更高的JSON库,比如json-iterator
减少序列化的规模。比如我们一页20个,但是观众在app上可看区域有限,不翻页也就看到4个。所以可以10个一页,这样就减少一半的数据量了。
动作:减少规模
这里重点说下singleflight,它的特点是同一时间最多只有1个请求落到下游,其它请求会等待结果并直接从本地内存中拿。
同一时刻不管请求是1k, 还是1w,都只有1个请求落到下游,不随着请求量的增加而增加。对下游极其友好。
下面看项目中怎么用的
就这么简单?没错,就这简单,调Do方法包一下就行了
最明显的就是,优化之后,大大减少了特权聚合服务的请求量,干掉一半的机器后,CPU还比优化之前的少。最终是减少了60%的特权聚合机器。因为优化是级联的,特权聚合的下游机器也减少了,相关的依赖都减少了。这里不具体细说。
singleflight是golang自带的库。为什么这么神奇呢?源码是不是很复杂。实际不到50行代码(旧版本)!
简单点说,就是用Mutex控制并发,用WaitGroup实现等待通知拿结果,而map存储结果。
本次实践是20/80法则的典型运用。使用简单的方式,取得了有力的效果。而这种简单,是建立在对系统的充分理解,以及性能优化手段的熟练掌握之上。
只有充分理解,才能庖丁解牛,如外科医生手术搬精准。
当我们在优化的时候,先静下心来,分析瓶颈在哪里,而不是一顿胡乱操作。