写在前面
在上一篇文章中,我们讲解了ab实验(A/B testing)。
通常来说,当我们做小流量ab实验并得出正向结论之后,我们将会对实验组进行100%的流量推全,这篇文章就聊一下我所遇到的推全导致的流量激增的问题及解决思路。
提示:本文的所有数据和架构图都是虚构,只是为了展示核心问题和分享类似问题的解决思路,与真实的数据和架构图相差很大,大家能理解意思就行。
背景
先给大家看看架构图:
假设整个系统DAU是1000w,服务A的QPS为50k,服务A有100台实例。做实验时候,申请的A/B流量组是 10%。得出正向结论,便准备将服务A的实验流量推全。
在之前的文章中我们也提到分级发布,这次就结合分级发布来讲讲这次流量推全的完整过程。
为什么小流量验证功能没问题之后还要分级发布呢?为什么不能一下子全部推全呢?
虽然说小流量验证了服务逻辑是没有问题的,但只是小流量而不是全流量。
小流量对于全局来说,比较难观察到性能瓶颈以及对下游的压力。因为对下游压力过大的时候,下游的耗时可能会增加,从而导致自身服务的耗时增加。
⚠️ 前提注意:服务B是服务A的下游,这个服务B的资源是有限的,不能承受过多的QPS,所以在服务A中,存在一层缓存,如果请求过了下游服务B,会将服务B的结果缓存一段时间,比如15分钟,这段时间不再请求下游,从而减少请求服务B的QPS。
服务A的QPS是50k,而当做AB实验的时候,申请的流量组是10%,所以打到下游服务B的应该是5k,但由于有缓存的情况,在监控打点下,统计出打到下游某服务的QPS实际是2k。
分级推全
对服务稳定性的考虑,我们在推全的时候,分了几次推全,一次是50%,第二次是90%,最后是100%。
怎么控制流量呢?通过abtest,因为abtest的一层就是完整的流量,我们申请了整整一层10组流量组,每组流量组占10%的流量。伪代码如下:
abTestList = (id1,id2,id3...)
if req.abTestID in abTestList:
if haveCache:
xxxxx
else:
request xxServer
setCache
根据 abTestList 从而限制打到下游的QPS:
-
从10%流量,abTestList = [id1] -
50% 流量 abTestList = [id1,id2,id3,id4,id5] -
90%流量 abTestList = [id1,id2,id3,id4,id5…. id9] -
100%流量不限制abtest id
这一过程都非常顺利,请求下游的QPS也增长到了25k,本应该是 50k * 90% = 45k的QPS,由于有一层缓存,所以只有25k (⚠️注意:这个25k是监控指标观测出来的),这也符合预期,但问题发生在从90%到100%的过程。
我们估算增量QPS与之前一样:
-
既然90%流量的QPS打到服务B是25k。 -
10%就是 25k/9 约等于 2.7k,每10%的流量有2.7k打到下游。 -
这次从90%流量推到100%,增加10%流量,增量QPS为2.7k。
将这个推全的QPS增量再一次同步到下游,下游说ok没问题,推全吧,这一次我们就去掉了abTest的限制。伪代码如下:
if haveCache:
xxxxx
else:
request xxServer
setCache
结果刚到单台机子,发现单台机子请求下游的QPS的增量不符合预期!! 观察监控发现单台请求下游的增量是180QPS。
单台机子本身请求下游的QPS应该等于本次新增的总QPS除以总实例个数,服务B有100个服务,也就是 2.7k/100 = 27 QPS,但却增长了 180 QPS。
比预期增多的QPS多了好几倍,如果推全,则本次新增的QPS将会是180 * 100,也就是 18k。这对于下游来说,是不可接受的,于是开始排查问题。
排查问题
这时候大家可能会问不应该先
回滚止损吗?但其实目前上到单台实例,单台请求下游的QPS会LB(负载均衡)到下游的各个实例,一平均其实单台对下游的增量很小,这种情况如果没有继续推流量,是没有任何损失的,并且保持出这种情况还可以帮我们加速排查速度。
排查思路如下:
-
计算过程: 流量到90%的时候通过打点监控请求下游的QPS确实是 25k,那么自然而然剩下的10%就是,25k/9 = 2.7k,计算过程没有问题。 -
监控打点: 检查了一下监控打点的代码, 在本地试验了一次,确实打点计数是正确的,并且到实例上捞日志,根据时间统计请求数,发现和打点统计的QPS也对的上。而且监控打点是公司的基础架构,如果有问题,其他组早就爆出来了,理论上应该没有问题。 -
abtest平台: 或者说是不是最后的这10%流量是不均衡的,abtest平台的问题? 于是也是捞日志去看请求参数中,这10个组的统计,每个组之间只有两位数的差别,差别不大,而且abtest平台也是公司的基础架构,如果有问题,其他组早就爆出来了,理论上应该也是没问题的。 -
代码Bug: 排除外部因素外,查看内因。再次CR本次的代码。留意到这次上线放量和之前的代码,唯一区别就是去掉了根据请求参数的abtest进行流量限制,难道是上游传的abtest有问题? 于是捞日志进行统计,发现上游传过来的abtest居然存在空的情况,而这种情况占了大约30%,那么进行反推,30%也就是差不多是总量的 50k * 30% = 15k,15k/100台实例,就是150的QPS,刚好就是单台多出来的增量。
这时候可能大家有问题:居然有30%的abtest为空,这是怎么做实验的?是这样的,一开始整个服务都没有传abtest的,是我接手后,一条一条链路往上加的,C端链路很长也很多上游,第一轮补充参数后,当时统计到了abtest覆盖率是到了
90%,我们才开始做实验的。在做实验的过程中,不断有新的上游新增,从而稀释了abtest的覆盖率,平时光顾着做实验没太留意这个指标,所以就遗漏了!!
解决问题
定位到问题后,解决方案就简单,在下游无法扩容的情况,只能做限流和降级。 已经有了一层缓存,还能怎么限呢?
一开始的想法是限制某一些流量大但转化低的上游的流量,让这部分上游的流量,不走到下游服务,走降级服务。
但会引起另一个问题,这部分上游的转化会几乎降到0,这在老板层面是不可接受的,于是放弃。
但我们突然意识到,没登陆的用户占了20%,能填上大部分的空白,并且没登陆的用户也不会进行转化,要转化就必须登录,所以我们限制了这部分没登陆的用户,直接走降级服务。于是代码变成:
if unSignIn:
return fallback
if haveCache:
xxxxx
else:
request xxServer
setCache
去除了20%未登录的流量之后,通过监控观察到单台打到下游的增量QPS为 77,全量就是 77*100 = 7.7k,7.7k的增量进行推送 (其实20%未登陆的流量按照正常计算不会那么减少那么多的,从180到77,但是捞日志发现abtest为空70~80%都是未登陆的用户)
于是和下游沟通好了之后,开始新一轮的放量,这一次的放量不出意外,下游的耗时增加了,导致服务A的P99从50ms增长到80ms~88ms,虽然很危险,但还是在规定的极限时间100ms返回到服务A的上游了。但只要有一点抖动,那么就会超时。
刚扩完量准备松一口气的时候,突然来了oncall说线上某个入口有大量超时。原来是某个入口的耗时需要在70ms返回,于是火速回滚先不放量。 和上游对齐后,给耗时要求比较高的入口上了ban位,都降级处理。
于是又开始了新一轮的扩量。由于ban掉的是比较大的流量入口,所以上到单台后发现比上一次的减少了大概20的QPS,最终打到下游的QPS增量为 57,也就是 57*100,5.7k的流量增量。当然耗时也增加了,P99从50ms又到了75~85ms之间。
一到高峰期就会爆很多的超时,可用性也跌破了99.95%,那就只能从其他地方扣点东西来优化:
-
用本地缓存+sync.Pool 将一些环节的耗时降低。 -
接着发现超时的是多个请求的情况,一次请求拿多个结果。于是拆分了多次少个进行请求,多次请求,但是拿少量结果。虽然非常的扣,但也有一点效果。毕竟服务B是计算密集型,一次太多的话,计算比较慢。
这两个优化后:P99从75~85将降到65~75。后面是资源到位了,下游终于扩容了,也总算是把耗时从峰值75ms回落到峰值40ms了。
看到这里,大家可能会有一些疑问:
1. 比如为什么不对流量大且转化低的入口做百分比的限制?
我也想过,但是会引起另一个问题,这个问题就不细说了。
2. 为什么不对缓存进行打点,缓存+打到下游的量如果和自身承担的qps有较大的gap,不就能发现了吗?
其实我们有缓存的打点,因为我们有多个入参的场景,比如一次请求中,有10个参数进来,其中有一个命中了缓存,有9个打到了下游,那么一个QPS就会有两次重复的打点。这对于我们统计来说也有干扰。 本质还是因为我们参数监控没有得到重视导致的。
3. 既然会多出这么多的流量,为什么还要冒着风险推全?
首先本次实验得出的是 显著提升,所以老板要求推全。其次之前也《推全》过,但以流量组的形式推全,就是abtest在这10个流量组中,而这一次的推全是不带任何实验组的推全,没有意料到这个abtest的大缺失,所以我和下游的对齐的是无风险的推全,为了不延期以及考虑到资源不足是长期存在的问题,所以只能冒着风险推全了。流量激增的耗时增加,只能从其他地方优化硬抠时间出来补上。
这次推全花了两三天的时间,被PM的老板投诉了,因为PM的老板急着要推全后的数据,不过我老板比较注重服务稳定性,只是说了两句,让我下次注意一点就过去了。
