记录一次接口性能优化

记录一次接口性能优化
逐暗者背景
系统简介
公司有个权限系统,管理着员工的所有权限。其中有个数据权限的模块,管理着员工可以看到哪些数据,不能看到哪些数据。这次出现问题的是数据鉴权接口,业务系统调用该接口来鉴定用户能否操作某个数据。
B端系统特性就是并发量不是很大,但其业务复杂度非常高。以数据鉴权接口为例,需要计算当前登录用户的组织和父组织、职务等信息,同时也要计算权限的继承关系、优先级等信息。
举个例子:授权员工张三所在的组织拥有A订单的读写权限,即便没有直接授权张三,张三也能操作这个A订单。
再举个例子:授权员工张三拥有销售1组下所有订单的读写权限,即便没有直接授权订单B(挂在销售1组下),张三也能操作这个B订单。
问题背景
由于坐席接入了权限系统,导致原本并发量不大的系统需要额外承接15倍左右的流量。至此,我们需要对接口进行压测,用来评估当流量翻15倍之后是否能够通过横向扩展pod来保证服务可用,以及需要横向扩展多少pod来支撑这些流量。
测试
软硬件环境
pod服务:2C4G 4pod
数据库:4C16G (和其他数据库共用一个实例)
软件:JDK8
压测并发数:30 -> 120
压测结果
性能指标 | 优化前 | 优化1 | 优化1+2 | 优化1 vs 优化前 | 优化1+2 vs 优化前 | 优化1+2vs优化1 |
---|---|---|---|---|---|---|
平均响应时间 | 1.21s | 692.25ms | 57.92ms | ↓ 42.8% | ↓ 95.2% | ↓ 91.6% |
最大一次响应时间 | 10.38s | 6.19s | 569ms | ↓ 40.4% | ↓ 94.5% | ↓ 90.8% |
中位数响应时间 | 77ms | 49.00ms | 34ms | ↓ 36.4% | ↓ 55.8% | ↓ 30.6% |
P90响应时间 | 5.8s | 3.25s | 142.11ms | ↓ 44.0% | ↓ 97.5% | ↓ 95.6% |
P95响应时间 | 6.61s | 3.80s | 171ms | ↓ 42.5% | ↓ 97.4% | ↓ 95.5% |
P99响应时间 | 7.91s | 4.58s | 201.88ms | ↓ 42.1% | ↓ 97.4% | ↓ 95.6% |
TPS最大 | 83.4req/s | —— | 503req/s | —— | ↑ 503% | —— |
TPS中位 | 35.9req/s | —— | 453req/s | —— | ↑ 1162% | —— |
TPS最小 | 8.77req/s | —— | 270req/s | —— | ↑ 2979% | —— |
优化前
优化后
遇到的问题
- 接口响应性能并不好,在优化前并发30的情况下很快响应时间就变成秒级了。
- 压测4个pod,其中1个pod响应时间在5~10秒,CPU打满200%,而剩下3个pod的响应时间却在30ms,CPU只有50% 。
- 单pod问题
排查及优化
由于问题2的干扰,一开始一直以为是pod存在问题,于是先排查了pod问题。
- 重建pod
为了避免是单个pod出现了奇怪的问题导致测试结果不准,先进行了重建,并使新的pod在新节点上,这样也同时排除了节点问题。但结果依旧,1个pod响应时间很长,但剩下3个pod响应时间很短。
- 压测中切流量
为了彻底排除某一个pod的问题,进行了压测中把有问题的pod直接流量切0 。 结果当流量切0之后,剩下的3个pod中会出现1个响应时间很长,而另外2个pod响应时间很短。
- 排查数据库
由于实在找不到pod的问题,并且出现问题的pod是随机的,所以只能先把排查方向转到其他地方,首先排查的是数据库的慢sql。结果测试数据库并没有相关慢sql 。
- 第一次优化
实在没找到原因,又纠结某一个pod现象很奇怪,于是只能先进行优化。在鉴权比较复杂的情况下,针对部分中间计算结果做了缓存。例如计算登录账号所属组织和父组织、职务的结果进行缓存,对权限集成的父子结构做了缓存。主要目的在于减少sql查询次数,缩短接口响应时间。
结果是耗时下降了40%左右,但依旧有1个pod的cpu打满,响应时间较长,剩下3个pod响应时间较短。
- 再次排查cpu占用
这一次在压测中,将jvm的线程dump处理,进行了排查。发现没有单个线程占用大量cpu的现象,说明没有死循环或者大量占用cpu的问题代码。但是有不少线程占用4%cpu,发现主要是解析sql。因为项目中使用了mybatis的多租户插件和软删除插件,插件会解析sql,并追加一些条件(例如租户条件用于租户隔离,软删除条件用于过滤已删除数据)。于是代码:CCJSqlParserUtil.parse(sql) 中消耗了不少cpu资源。
知道了问题之后就比较好处理了,对sql解析的结果进行本地缓存下来,如此相同模板的sql则不需要再次消耗cpu去解析。
最终响应时间缩短了90%
单pod的问题
虽然优化解决了问题1,但问题2并未被解决。由于解决了sql解析需要大量cpu的问题,在30并发时不再出现cpu打满的情况。于是逐步将压测并发数增加到120线程,接口响应时间也从几十毫秒增加到一百多毫秒,cpu也出现了单个pod占用比较高(虽然没打满),但其他3个pod的cpu占用较低的现象。
为什么会出现这个现象呢?
其实是因为其中某一个pod(非固定)存在一些扰动(可以是网络、或者其他请求)导致4个pod负载不均衡,从而导致这个pod的响应时间稍微变长(不用很明显)。对于压测发起线程来说(例如30个并发线程),其中有四分之一请求还没响应(分配到这个pod上的),其余四分之三的请求已经响应(分配到其他3个pod上的)。接着四分之三的线程继续发起下一轮请求,经过流量均分,又有一部分请求分配到了这个pod上,造成了负载更加不均衡。一直恶性循环,直至所有请求都停留在了这个pod上等待响应,最终这个pod变成了一个类似限流器的作用,限制了其他3个pod的请求量,于是表现出了其他3个pod的cpu占用较少,响应时间较短的现象。