从应用层到网络层排查 Dubbo 接口超时全记录
大家好,我是坤哥
我们常说面试造火箭,很多人对此提出质疑,相信大家看了这篇文章会明白面试造火箭的道理,这篇排查问题的技巧涉及到索引,GC,容器,网络抓包,全链路追踪等基本技能,没有这些造火箭的本事,排查这类问题往往会无从下手,本篇也能回答不少朋友的问题:为什么学 Java 却要掌握网络,MySQL等其他知识体系,这会让你成为更出色的工程师哦。
一. 问题现象
商品团队反馈,会员部分 dubbo 接口偶现超时异常,而且时间不规律,几乎每天都有,商品服务超时报错如下图:

超时的接口平时耗时极短,平均耗时 4-5 毫秒。查看 dubbo 的接口配置,商品调用会员接口超时时间是一秒,失败策略是failfast,快速失败不会重试。
会员共部署了8台机器,都是 Java 应用,Java 版本使用的是 JDK 8,都跑在 docker 容器中 。
二. 问题分析
开始以为只是简单的接口超时,着手排查。首先查看接口逻辑,只有简单的数据库调用,封装参数返回。SQL 走了索引查询,理应返回很快才是。
于是搜索 dubbo 的拦截器 ElapsedFilter 打印的耗时日志(ElapsedFilter 是 dubbo SPI 机制的扩展点之一,超过 300 毫秒的接口耗时都会打印),这个接口的部分时间耗时确实很长。

再查询数据库的慢 SQL 平台,未发现慢 SQL。
1. 数据库超时?
怀疑是获取数据库连接耗时。代码中调用数据库查询可以分为两步,获取数据库连接和 SQL 查询,慢SQL 平台监测的 SQL 执行时间,既然慢 SQL 平台没有发现,那可能是建立连接耗时较长。
查看应用中使用了 Druid 连接池,数据库连接会在初始化使用时建立,而不是每次 SQL 查询再建立连接。Druid 配置的 initial-size(初始化链接数)是 5,max-active(最大连接池数量)是 50,但 min-idle(最小连接池数量)只有 1,猜测可能是连接长时间不被使用后都回收了,有新的请求进来后触发建立连接,耗时较长。
因此调整 min-idle 为 10,并加上了 Druid 连接池的使用情况日志,每隔 3 秒打印连接池的活跃数等信息,重新观察。
改动上线后,还是有超时发生,超时发生时查看连接池日志,连接数正常,连接池连接数也没什么波动。
2. STW?
因此重新回到报错日志观察,ElapsedFilter 打印的耗时日志已经消失。由于在业务方法的入口和出口,以及数据库操作的前后打印了日志,发现整个业务操作耗时极短,都在几毫秒内完成。
但是调用端隔一段时间开始超时报错,且并不是调用一个接口超时,而是调用好几个接口都同时超时。另外,调用端的几台机器都会同时报超时(可以在 elk 平台筛选 hostname 查看),而提供服务超时的机器是同一台。
这样问题就比较清晰了,应该是某一时刻提供服务的某台机器出现比较全局性的问题导致几乎所有接口都超时。
继续观察日志,抓了其中一个超时的请求从调用端到服务端的所有日志(理应有分布式 ID 可以追踪,context id 只能追踪单应用内的一个请求,跨应用就失效了,所以只能自己想办法,此处是根据调用IP+时间+接口+参数在 elk 定位)。
这次有了新的发现,服务端收到请求的时间比调用端发起调用的时间晚了一秒,比较严谨的说法是调用端的超时日志中 dubbo 有打印 startTime(记为T1)和 endTime,同时在服务端的接口方法入口加了日志,可以定位请求进来的时间(记为T2),比较这两个时间,发现 T2 比 T1 晚了一秒多,更长一点的有两到三秒。
内网调用网络时间几乎可以忽略不计,这个延迟时间极其不正常。很容易想到 Java 应用的 STW(Stop The World),其中,特别是垃圾回收会导致短暂的应用停顿,无法响应请求。
排查垃圾回收问题第一件事自然是打开垃圾回收日志(GC log),GC log 打印了 GC 发生的时间,GC 的类型,以及 GC 耗费的时间等。增加 JVM 启动参数
-XX:+PrintGCDetails -Xloggc:${APPLICATION_LOG_DIR}/gc.log -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime。
为了方便排查,同时打印了所有应用的停顿时间。(除了垃圾回收会导致应用停顿,还有很多操作也会导致停顿,比如取消偏向锁等操作)。由于应用是在 docker 环境,因此每次应用发布都会导致 GC 日志被清除,写了个上报程序,定时把 GC log 上报到 elk 平台,方便观察。
以下是接口超时时 gc 的情况:

可以看到,有次 GC 耗费的时间接近两秒,应用的停顿时间也接近两秒,而且此次的垃圾回收是ParNew 算法,也就是发生在新生代。所以基本可以确定,是垃圾回收的停顿导致应用不可用,进而导致接口超时。
2.1 安全点及 FinalReferecne
以下开始排查新生代垃圾回收耗时过长的问题。
首先,应用的 JVM 参数配置是
-Xmx5g -Xms5g -Xmn1g -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m -Xss256k -XX:SurvivorRatio=8 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70
通过观察 marvin 平台(我们的监控平台),发现堆的利用率一直很低,GC 日志甚至没出现过一次 CMS GC,由此可以排除是堆大小不够用的问题。
ParNew 回收算法较为简单,不像老年代使用的 CMS 那么复杂。ParNew 采用标记-复制算法,把新生代分为 Eden 和 Survivor 区,Survivor 区分为 S0 和 S1,新的对象先被分配到 Eden 和 S0 区,都满了无法分配的时候把存活的对象复制到 S1 区,清除剩下的对象,腾出空间。比较耗时的操作有三个地方,找到并标记存活对象,回收对象,复制存活对象(此处涉及到比较多垃圾回收的知识,详细部分就不展开说了)。
找到并标记存活对象前 JVM 需要暂停所有线程,这一步并不是一蹴而就的,需要等所有的线程都跑到安全点。部分线程可能由于执行较长的循环等操作无法马上响应安全点请求,JVM 有个参数可以打印所有安全点操作的耗时,加上参数
-XX:+PrintSafepointStatistics -XX: PrintSafepointStatisticsCount=1 -XX:+UnlockDiagnosticVMOptions -XX: -DisplayVMOutput -XX:+LogVMOutput -XX:LogFile=/opt/apps/logs/safepoint.log。

安全点日志中 spin+block 时间是线程跑到安全点并响应的时间,从日志可以看出,此处耗时极短,大部分时间都消耗在 vmop 操作,也就是到达安全点之后的操作时间,排除线程等待进入安全点耗时过长的可能。
继续分析回收对象的耗时,根据引用强弱程度不通,Java 的对象类型可分为各类 Reference,其中FinalReference 较为特殊,对象有个 finalize 方法,可在对象被回收之前调用,给对象一个垂死挣扎的机会。有些框架会在对象 finalize 方法中做一些资源回收,关闭连接的操作,导致垃圾回收耗时增加。因此通过增加JVM参数 -XX:+PrintReferenceGC,打印各类 Reference 回收的时间。

可以看到,FinalReference 的回收时间也极短。
最后看复制存活对象的耗时,复制的时间主要由存活下来的对象大小决定,从 GC log 可以看到,每次新生代回收基本可以回收百分之九十以上的对象,存活对象极少。因此基本可以排除这种可能。
问题的排查陷入僵局,ParNew 算法较为简单,因此 JVM 并没有更多的日志记录,所以排查更多是通过经验。
2.2 垃圾回收线程
不得已,开始求助网友,上 stackoverflow 发帖(链接见文末),有大神从 GC log 发现,有两次 GC 回收的对象大小几乎一样,但是耗时却相差十倍,因此建议确认下系统的 CPU 情况,是否是虚拟环境,可能存在激烈的 CPU 资源竞争。
其实之前已经怀疑是 CPU 资源问题,通过 marvin 监控发现,垃圾回收时 CPU 波动并不大。但运维发现我们应用的垃圾回收线程数有些问题(GC 线程数可以通过 jstack 命令打印线程堆栈查看),JVM 的 GC 线程数量是根据 CPU 的核数确定的,如果是八个核心以下,GC 线程数等于CPU 核心数,我们应用跑在 docker 容器,分配的核心是六核,但是新生代 GC 线程数却达到了 53个,这明显不正常。
最后发现,这个问题是 JVM 在 docker 环境中,获取到的 CPU 信息是宿主机的(容器中 /proc目录并未做隔离,各容器共享,CPU信息保存在该目录下),并不是指定的六核心,宿主机是八十核心,因此创建的垃圾回收线程数远大于 docker 环境的 CPU 资源,导致每次新生代回收线程间竞争激烈,耗时增加。
通过指定 JVM 垃圾回收线程数为 CPU 核心数,限制新生代垃圾回收线程,增加JVM参数
-XX:ParallelGCThreads=6 -XX:ConcGCThreads=6

效果立竿见影!新生代垃圾回收时间基本降到 50 毫秒以下,成功解决垃圾回收耗时较长问题。
三. 问题重现
本以为超时问题应该彻底解决了,但还是收到了接口超时的报警。现象完全一样,同一时间多个接口同时超时。查看 GC 日志,发现已经没有超过一秒的停顿时间,甚至上百毫秒的都已经没有。
1. Arthas 和 Dubbo
回到代码,重新分析。开始对整个 Dubbo 接口接收请求过程进行拆解,打算借助阿里巴巴开源的Arthas 对请求进行 trace,打印全链路各个步骤的时间。(Arthas 和 dubbo)
服务端接收 dubbo 请求,从网络层开始再到应用层。具体是从 netty 的 worker 线程接收到请求,再投递到 dubbo 的业务线程池(应用使用的是 ALL 类型线程池),由于涉及到多个线程,只能分两段进行 trace,netty 接收请求的一段,dubbo 线程处理的一段。
Arthas 的 trace 功能可以在后台运行,同时只打印耗时超过某个阈值的链路。(trace 采用的是instrument+ASM,会导致短暂的应用暂停,暂停时间取决于被植入的类的数量,需注意)
由于没有打印参数,无法准确定位超时的请求,而且 trace 只能看到调用第一层的耗时时间,结果都是业务处理时间过长,最后放弃了该trace方法。
受 GC 线程数思路的启发,由于应用运行基本不涉及刷盘的操作,都是 CPU 运算+内存读取,耗时仍旧应该是线程竞争或者锁引起的。重新回到 jstack 的堆栈进行排查,发现总线程数仍有 900+,处于一个较高的水平,其中 forkjoin 线程和 netty 的 worker 的线程数仍较多。于是重新搜索代码中线程数是通过CPU核心数设置(Runtime.getRuntime().availableProcessors())的地方,发现还是有不少框架使用这个参数,这部分无法通过设置JVM参数解决。
因此和容器组商量,是否能够给容器内应用传递正确的 CPU 核心数。容器组确认可以通过升级 JDK 版本和开启 CPU SET 技术限制容器使用的 CPU 数,从 Java 8u131 和 Java 9 开始,JVM 可以理解和利用 cpusets 来确定可用处理器的大小(Java 和 cpu set 见文末参考链接)。现在使用的也是JDK 8,小版本升级风险较小,因此测试没问题后,推动应用内8台容器升级到了 Java 8u221,并且开启了 cpu set,指定容器可使用的 CPU 数。
重新修改上线后,还是有超时情况发生。
2. 网络抓包
观察具体的超时机器和时间,发现并不是多台机器超时,而是一段时间有某台机器比较密集的超时,而且频率比之前密集了许多。以前的超时在各机器之间较为随机,而且频率低很多。比较奇怪的是,wukong 经过发布之后,原来超时的机器不超时了,而是其他的机器开始超时,看起来像是接力赛。(docker 应用的宿主机随着每次发布都可能变化)
面对如此奇怪的现象,只能怀疑是宿主机或者是网络问题了,因此开始网络抓包。
由于超时是发生在一台机器上,而且频率较为密集,因此直接进入该机器,使用 tcpdump 命令抓取host 是调用方进来的包并保存抓包记录,通过 wireshark 分析(wireshark 是抓包分析神器),抓到了一些异常包。

注意抓包中出现的 「TCP Out_of_Order」,「TCP Dup ACK」,「TCP Retransmission」,三者的释义如下:
TCP Out_of_Order: 一般来说是网络拥塞,导致顺序包抵达时间不同,延时太长,或者包丢失,需要重新组合数据单元,因为他们可能是由不同的路径到达你的电脑上面。
TCP dup ack XXX#X: 重复应答,#前的表示报文到哪个序号丢失,#后面的是表示第几次丢失
TCP Retransmission: 超时引发的数据重传
终于,通过在 elk 中超时日志打印的 contextid,在 wireshark 过滤定位超时的 TCP 包,发现超时的时候有丢包和重传。抓了多个超时的日志都有丢包发生,确认是网络问题引起。
问题至此基本告一段落,接下来容器组继续排查网络偶发丢包问题。
巨人的肩膀
- stackoverflow 提问帖子: http://aakn.cn/YbFsl
- jstack 可视化分析工具:https://fastthread.io/
- 2018年的 Docker 和 JVM:`https://www.jdon.com/51214
更多精品文章,欢迎大家扫码关注「码海」
从应用层到网络层排查 Dubbo 接口超时全记录的更多相关文章
- 排查dubbo接口重复注销问题,我发现了一个巧妙的设计
背景 我在公司内负责自研的dubbo注册中心相关工作,群里经常接到业务方反馈dubbo接口注销报错.经排查,确定是同一个接口调用了两次注销接口导致,由于我们的注册中心注销接口不能重复调用,调用第二次会 ...
- java反射调用dubbo接口
需求:项目增加幂等 场景:1.三个项目:a .b.c2.a项目加幂等3.b项目dubbo调用项目a的时候超时没有获取返回结果,增加重试机制(非立即重试,3min or 5min 后重试)4.c项目是一 ...
- dubbo 接口初入门
最近公司开发新的一套系统,开发出来的方案会基于dubbo分布式服务框架开发的,那么什么是dubbo,身为测试的我,第一眼看到这个,我得去了解了解dubbo是啥玩意,为开展的测试工作做准备,提前先学 d ...
- 『动善时』JMeter基础 — 52、使用JMeter测试Dubbo接口
目录 1.Dubbo介绍 2.准备测试Dubbo接口的环境 3.Dubbo Sample界面详解 4.Dubbo Sample组件的使用 (1)测试计划内包含的元件 (2)使用zookeeper协议请 ...
- jmeter测试dubbo接口
本文讲解jmeter测试dubbo接口的实现方式,文章以一个dubbo的接口为例子进行讲解,该dubbo接口实现的功能为: 一:首先我们看服务端代码 代码架构为: 1:新建一个maven工程,pom文 ...
- 利用jmeter+JAVA对RPC的单接口(dubbo接口等)进行性能测试
建立JAVA项目 建立maven项目,加入Jmeter所需要的JAR包依赖. POM.xml 加入如下: <dependency> <groupId>org.apache.j ...
- Jmeter实现dubbo接口压测案例
当前项目中重构了消息服务,需要对消息服务接口做性能压测,评估消息服务的性能情况 通过和开发对接,目前消息服务是通过dubbo接口对内提供服务,所以才有了这边文章的记录 最初的压测这个dubbo接口有三 ...
- dubbo接口demo开发
接口需求 客户端输入uncleyong(当然,也可以输入其它字符串),服务端返回hello uncleyong 开发环境 jdk + idea + maven + zookeeper jdk安装 id ...
- jmeter5.1测试dubbo接口
dubbo接口功能介绍 客户端输入uncleyong(当然,也可以是其他字符串),服务端返回hello uncleyong 开发dubbo服务jmeter客户端 idea中创建模块dubbo_jmet ...
随机推荐
- Java中的CPU占用高和内存占用高的问题排查
下面通过模拟实例分析排查Java应用程序CPU和内存占用过高的过程.如果是Java面试,这2个问题在面试过程中出现的概率很高,所以我打算在这里好好总结一下. 1.Java CPU过高的问题排查 举个例 ...
- tomcat部署项目问题
tomcat部署项目的时候,报内存溢出,一种解决方案是直接添加内存,网上都有教程,如下: Windows下,在文件/bin/catalina.bat,Linux下,在文件/bin/catalina.s ...
- VMware 安装 CentOS7 后的简单配置
1.连网 如果能连网,跳过此步 试着ping一下百度 ping baidu.com 动态分配 IP sudo vim /etc/sysconfig/network-scripts/ifcfg-ens3 ...
- 微信支付 V3 的 Java 实现 Payment Spring Boot-1.0.7.RELEASE 发布
Payment Spring Boot 是微信支付V3的Java实现,仅仅依赖Spring内置的一些类库.配置简单方便,可以让开发者快速为Spring Boot应用接入微信支付. 功能特性 实现微信支 ...
- 一个 java 文件的执行过程详解
平时我们都使用 idea.eclipse 等软件来编写代码,在编写完之后直接点击运行就可以启动程序了,那么这个过程是怎么样的? 总体过程 我们编写的 java 文件在由编译器编译后会生成对应的 cla ...
- MySQL深入研究--学习总结(2)
前言 接上文,继续学习后续章节. 第四章&第五章<深入浅出索引> 这两章节主要介绍的索引结构及其如何合理建立索引,但是我觉得讲的比较简单. 总结回顾下吧,其实在我之前的文章< ...
- mysql从一个表提取数据更新另外一个表(修复表数据的不一致)
目前碰到一个数据不一致的情况,有两张表,一张项目表,一张项目成员表,项目表有个字段是项目工作时间,是项目成员的工作时间汇总.是由于该了逻辑,所以要把数据改成一致. 项目表的大致结构如下. 表名:pro ...
- mysql查询较长的执行进程及创建权限账号
A:对于死锁,进程的操作 1.查找当前活跃事务 SELECT * from information_schema.INNODB_TRX 根据trx_started等判断事务是否异常锁定 2.杀死线程 ...
- Python2021哔哩哔哩视频爬取
一.找到想要爬取的视频,进入网页源代码 在网页源代码里面可以很容易的找到视频各种清晰度的源地址 二.对地址发送请求 如果对视频源地址发送get请求会返回403 通过按F12进入开发者工具分析 发现并不 ...
- 几大BSD 区别
OpenBSD 侧重于安全,软件包较少,较陈旧,比如 KDE 才 3.5,为了安全舍弃了 sudo 和 linux 兼容层: FreeBSD 是开发者最多用户最多软件包最多的,有 ZFS 和 Linu ...