写在前面
本文章为笔者原创,转载需要表明出处,联系作者:luckydreamcatcher@163.com | the.matrix.vvv@gmail.com
QA同学在线上测试重构后的golang模块时发现,会偶现后端响应超时的现象。在之前的压测中,接口监控响应稳定在10ms左右,所以猜测存在长尾请求。
目前问题
监控指标
目前业务监控系统,反应接口耗时的系统指标为——平响,即平均响应时间=单位时间内所有请求耗时总和/请求数。
平均数并不能够反应数据的波动情况,例如:请求a耗时10ms(记为cost(a)=10ms),请求b耗时300ms(记为cost(b)=300ms),请求a与请求b的平均响应时间= cost(a, b) = (cost(a) + cost(b)) / 2 =155ms 。平均耗时155ms(<=200ms)是达标的,但是请求b耗时300ms明显是未达标的。
APP后端研发工程师,都了解对端接口请求耗时200ms是一个临界阈值——请求耗时200ms以下,用户对网络延迟几乎无感,体验较好,请求耗时200ms以上,网络延迟感明显,用户体验较差。因此请求耗时是否<=200ms经常作为接口性能优化的判断条件之一。在业务中,经常会遇到命中缓存与未命中缓存时耗时差距较大的场景,所以平响无法全面的衡量系统的性能。
长尾请求
业界关于延迟有一个常用的P99标准,即99%的请求应该比指定的延迟更小,仅允许1%的请求大于指定的延迟,这1%的请求即为”长尾请求”。打个形象的比喻,班级内99%同学的成绩都非常优秀,但总会有几位同学拖班级平均成绩后腿儿,拉低班级的“平均分,这几位同学就是“长尾请求”。
长尾请求的产生原因是多种多样的且复杂的,包括实现方式、系统因素、硬件因素等等,在分布式中常见原因如下:
- 依赖的下游服务有波动;
- 资源竞争(包括:文件、锁、硬件资源);
- 网络波动;
- 机器负载较大,系统调度,排队;
- fullGC;
- CPU降低功率控制温度;
有关长尾请求更多介绍于技术优化思路,参考Google Jeff Dean大神的论文:http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.732.6087&rep=rep1&type=pdf。
长尾请求在某种意义上来讲是无法消除的,但是我们可以通过技术手段将长尾请求控制在一定的比例之内,因此长尾请求也是很多性能优化工作的关注重点。由于长尾请求的存在,平响指标无法很好的反应绝大多数请求的耗时情况,因此有了分位时的概念,通俗的理解就是xx%的耗时在多少之内。
分位时
概念介绍
分位数,是统计学的一个术语,概念如下:
百分位数又称百分位分数(percentile),是一种相对地位量数,它是次数分布(Frequency Distribution,频数分布)中的一个点。把一个次数分布排序后,分为 100 个单位,百分位数就是次数分布中相对于某个特定百分点的原始分数,它表明在次数分布中特定个案百分比低于该分数。
通俗的讲,将数据按照升序(或降序)排列,等分为100份,在P=0.9(即99%)位置的数是多少。例如:全校800名学生,80分位数,指80%的学生考分在多少分以上,我们可以这样计算:
- 将800名学生成绩,按照从高到低的降序排列;
- 800名同学80%的名次为:800 * 80% = 640;
- 全校成绩排名第640名的学生成绩即我们所需的80分位数;
现实中,存在total(总数) * percent(百分比)
为浮点数的情况,例如9名学生的分数分别为:100,88,89,90,95,70,65,78,79,求90分位数,按照上述思路来计算:
- 将9名学生成绩,按照从高到低的升序排列为:100, 95, 90, 89, 88, 79, 78, 70, 65;
- 9名同学90%的名次为:9 * 90% = 8.1;
问题来了,第8.1名学生的成绩为多少?显然不存在第8.1名学生,假如存在的话,那么第8.1名学生的成绩一定在第8名与第9名之间。拆开来看,第8.1名学生成绩等价于在第8名学生成绩基础上,加上第9名与第8名成绩之差乘以10%=score(8)+(score(9)-score(8))*10% = 70 + (65 - 70)*10% =69.50,即这9名学生的90分位数为69.50分(注意:假设第9名与第8名成绩区间是分布均匀的,实际上样本数量较少时波动比较大,随着样本数量变大趋向于均匀)。
总结分位数计算规则如下:
- 将输入数组升序/降序排列,数组长度为n;
- 求数组[0, n)的P%的下标,m = n*P% - 1 = i + j,i代表整数部分,j代表小数部分;
- 求下标为m的元素值 f(m) = f(i) + (f(j) - f(i)) * j;
参考上述,可得分位时,是将所有请求耗时由小至大升序排列,求得分位数。
计算工具
计算分位时的工具,可参考笔者写的简易Python脚本
1 | curl -L -O https://raw.githubusercontent.com/keepalive555/victorinox/main/src/percentile.py |
求一批请求耗时的99分位时,Linux示例命令如下:
1 | cat service.log|grep -o -P "cost\[\d+(\.|\])?"|grep -o -P "\d+"|./percentile.py |
在笔者的案例中,取生产环境日志约10w条,求得重构后golang
接口,99.9分位时为200ms,平响为10ms,差距是要比想想中的要大的多,所以关注系统性能指标不只需要关注平响,也需要关注分位时。
优化思路
长尾请求的产生原因是多种多样的,分布式系统中最常见的场景是受下游服务拖累,例如:MySQL慢查询、分布式缓存过期、下游服务过载等等,合理设置下游服务超时时间是非常有必要的。
目前许多流行的RPC框架,提供了解决长尾请求的方案——Backup Request
,例如百度内部的BRPC框架。客户端首先向一台下游服务Server发送RPC请求,若在backup_request_ms
(通常小于超时时间)内未取到数据,则在向下游服务另外一台Server发送RPC请求,哪台Server先响应则取哪条。设置合理的backup_request_ms
,大部分情况下只会发一个请求,对下游服务的压力可以不计。
目前了解到,百度小程序C端团队,在做BackupRequest
的改造,准备借鉴一下^_^。