前言:近日我司进行云服务商更换,恰逢由我负责新上线的三方调用 api 维护管理,在将服务由阿里云部署到腾讯云过程中,我们压测发现在腾讯云调用京东接口时 TP999 抖动十分剧烈,尽管业务层有重试操作但是超时依然较多,并不满足业务要求…… 接下来针对过程中发现的种种问题我们便踏上了优化之路。
开端
那还是普通的一夜,突然群里蹦出一些报警消息,多条业务线调用外部 api 超时,dubbo 线程池数量不足,还好很快服务恢复了正常,不过这也为我提了个大醒,我维护的服务作为业务调用的聚合出口,一旦服务发生异常将会导致很多业务方失败。尽管在压测过程中我们便知道了迁移云后的 api 调用可用性会有明显下降,但在 C 端业务还未迁移时便出了这么大影响确实是前期准备不足。
基于表面问题分析
出现问题先从最容易下手的地方开始解决,先分析问题,线程池飙高报警,结合日志、APM(skywalking) 工具进行排查,发现报警时刻线程数量和 cpu 均出现飙升情况,由于服务主要提供的功能就是三方调用,很多线程卡在调用三方 api 等待响应结果步骤上,并且有响应结果的调用消耗在 http 的时间也特别长,这就很容易理解了,是由于三方 api 抖动加之服务创建 http 连接过多先是导致应用响应缓慢,再之服务超时触发重试瞬间导致应用负载翻倍,进而加剧了服务缓慢问题。在不具备限流降级能力时服务一旦出现这种情况很容易导致雪崩,万幸没有太多业务损失,作为一个成熟的程序员总不能给自己留坑啊,赶紧来填坑!
多方位填坑
问题找到了,方案自然不缺了
修改 http 调用方式,引入 http 连接池
这个方案其实在之前压测过程中便考虑采用了,由于使用的三方 sdk 在 http 调用中均使用 java 原生的 HttpURLConnection ,这在接口高并发情况下不停创建连接,性能实在太差了。考虑团队内大家熟悉程度直接用 HttpClinet 覆盖三方 SDK 中的 HttpUtil 实现,稍加修改即可。修改好重新进行压测,TP999 指标、cpu 使用率均有明显下降,这波漂亮,投入产出 ++!
第一个方案就万事大吉了吗?支持请求超时分级,线程隔离
在第一个方案开发上线后我们又发现了新问题,业务侧超时情况依然明显!这个原因很容易想到,针对不同业务方我们只配置了一份超时配置,尽管部分业务方(主要是 C 端业务,对接口超时十分敏感)会提前超时然后触发重试,但是我的应用线程依然在阻塞请求,这是资源的浪费啊,仔细想想,之前的问题还是没有解决,在客户端不停重试下服务的 dubbo 线程岂不是还得爆了啊。再优化,这次我想到了请求分级,由于不同业务方对接口的响应时效要求不同,我们进行了连接池隔离,针对不同的业务方调用可以在调用时设置请求分级,我们大致分类下创建了三种分级 ‘FAST’,'STANDARD','SLOW',不同分级由服务提供者去配置使用不同超时方案。服务稳定性又进一步,问题再一次得到了解决。
这里还要提醒下大家 HttpClinet 使用过程中一般会配置连接池使用,切记搭配连接池使用时超时参数要配置三个,分别为
ConnectionRequestTimeout 获取连接池连接的超时时间(这个大家最容易忽略了) ConnectTimeout 连接超时时间 SocketTimeout socket 读取超时时间
合理应用重试(消费者重试)
尽管在提供者服务上已进行代码优化,但是为了提升业务成功率,合理的使用重试也是很有必要的。此处要注意如果服务响应较慢千万避免消费者的多级重试,如果我们的整个业务调用链每一层都做了重试那么就会导致链路中响应慢的服务压力愈发增长,严重的引发重试风暴,直接压垮服务,所以合理设置重试也是很关键的一环,这里我们后续也要考虑引入熔断降级方案,避免意外发生。
在上述三招连环出击后,问题基本告一段落,2C4G 200 dubbo 线程,30 http 线程,吞吐量固定 1k,服务的 TP999 在压测过程中基本可以稳定在 500 毫秒以下,满足各方要求。
问题再起
伴随着应用接入量越来越大,我们的 C 端业务方又给提出了一个新问题,服务的超时情况怎么和阿里的服务差那么大啊(此处仅是指京东接口),由于是新业务直接上线到腾讯云,怀疑可能是我们服务的性能问题,这个在之前只有一些 B 端业务我确实没有过多注意,必须把这个问题好好查一查,还是从 APM 工具侧查看,果然,接口的 TP999 抖动十分剧烈,很多达到超时阈值,而 C 端业务十分重视这些指标,这也值得我们去学习,往往这些指标上线很容易暴露出根本问题,在高并发下看 TP90、TP99 时指标看起来还是挺正常的,那是因为超高请求量将响应时间给平均下去了。
纠结求解
这次的问题不再简单,面对这些 TP999 的超时响应,我们在观测了应用的整体流量、JVM 情况等等,愣是没有找到瓶颈,有之前阿里云的应用对比我们坚信问题是存在的,可能是在网络请求层面。接下来我们一步步出击。
1、查看 APM 工具发现部分 HTTP 请求确实超时,还有一部分是 HTTP 请求耗时不算长,但是从 APM 统计的调用链路来看耗费在提供者服务的时间比较长导致超时了。上述为两个问题,分别来看,请求可以确定是超时的我怀疑是不是由于使用了 http 连接池,池中的链接是不是超时了,从 httpclient 的日志中我分析了一下并没有这个问题,因为三方响应的 header 只返回了 keepalived,并没有返回其有超时时间。第二个问题我反复查找当时并没有找到原因,服务压力不大,也没有发生长耗时的 GC,实在不好解释这个问题,但是后来问题找到了,继续看结果在下文。
2、为了观察应用的请求响应信息,我们对 http 出口进行了抓包,通过对大量请求的抓包分析,我们找到了在响应比较高的时候抓包的 ip 中竟然有香港的 ip,为了验证这个问题,我们去百度了 dns 解析,发现该 ip 确实为该域名所有。
图上部分为此域名解析出的香港 ip,下半部分为该域名解析出的北京 ip,同一台机器上响应时长差距明显
3、我们的服务器和出口 ip 都是北京的为什么 dns 解析出来的 ip 会返回香港的呢?带着问题我们求助了腾讯云官方,在官方问题排查过程中,我们又做了其他测试,在服务器上写了一个 python 小脚本定时进行指定域名的 dns 域名解析,果然在跑了一段时间后出现了几次解析结果为香港的 ip,狂喜。此时腾讯云官方也有了回应,由于京东自己搭建的 DNS 解析服务器,但是无法准确识别腾讯云的 ip 地域,导致偶尔会解析到香港,解决方案是他们推动京东去识别腾讯云的 ip。
该图为腾讯云默认 dns 统计脚本的日志信息,第三个 ip 地址为香港,虽然较少出现,但是访问耗时长,且不定时会丢包
4、等官方解决进度迟缓啊,我们能有什么临时的解决办法吗?纠结之时我们也接入了架构组为我们搭建的 kong 出口网关,有专门的 net 出口,更高的带宽,完善的 granfa 监控,结合监控请求抖时展现更明显了,于是我们想到了固定 host,更改 dns 方案,由于固定 host 风险太大了,我们采用了更改 dns 的方案尝试。原来机器上默认是腾讯云的 dns,我们分别更换为阿里的 dns 和 114 的 dns 进行了测试,测试结果是差距不大最终我们采用了阿里的 dns,在更换阿里的 dns 后效果明显,原来 TP999 的尖刺明显减少,观察 kong 日志也不存在香港的 ip 了,这里就有点诧异腾讯不是说和 dns 没关系吗,为什么换了别人家的 dns 情况就好了很多(这个问题的结果是明确的,但是原因目前还是停留在猜测阶段,猜测是 114 和阿里 dns 对此有优化,所以在该场景下优于腾讯 dns,若大家有方案可以验证此问题欢迎提出来哦)。
dns 切换后依据 kong 统计计算的 SLA 信息,会比较明显看出效果
小插曲,kong 网关的 dns 解析器是写在 kong.conf 配置文件中的,如果未配置该项则会读取系统的 /etc/resolv.conf 文件,每次 kong 启动后会读取配置并缓存到内存中,如果启动 kong 后再修改系统的 nameserver 对 kong 是不管用的!还好我们之前有在专门的机器中测试 dns 更换效果,所以才敢肯定是 kong 配置问题,但是来回也折腾了不少时间才找到问题。
在这里我也想告诉大家,和别人协作,如果可以请一定准备好对比数据,避免他人因质疑而不配合,或者合作结果并不如预期时可以有个对照。
5、记得最开始有一个问题当时没找到原因吗,「一部分 HTTP 请求耗时不算长,但是从 APM 统计的调用链路来看耗费在提供者服务的时间比较长导致超时了」这个的问题最后还是 http 请求超时的原因,我对照了 kong 的日志和服务的 httpClient 日志,发现出现这种问题的请求有个共同点就是请求头很快接收到了,但是 body 迟迟未读取到然后触发超时了。skyWaling 中展示的 HTTP 耗时很短的原因是因为只要开始有响应就算请求结束了,然后为什么会有只返回 header 的情况呢?我 ping 了一下出现问题的 ip 是香港的,响应时间明显要比北京的慢,而且偶尔会丢包。这个问题解释通了。
终是踏平
经过又一轮的问题解决,测试加验证效果耗费了近两周时间,服务的可用性也提升了一个层次,算是一个小坑,不过这也让我们对 http 的整个请求流程做了很多梳理和二次认知,以上便是我在解决这个超时问题的整个解决路径和心路历程,感谢看我的叨叨叨,希望我们可以一起共同进步。
有道无术,术可成;有术无道,止于术
欢迎大家关注Java之道公众号
好文章,我在看❤️