现网问题排查手册
1. 服务CPU占用高
CPU 使用率是衡量系统繁忙程度的重要指标,一般情况下单纯的 CPU 高并没有问题,它代表系统正在不断的处理我们的任务,但是如果 CPU 过高,导致任务处理不过来,从而引起 load 高,这个是非常危险需要关注的。 CPU 使用率的安全值没有一个标准值,取决于你的系统是计算密集型还是 IO 密集型,一般计算密集型应用 CPU 使用率偏高 load 偏低,IO 密集型相反。
当出现了服务CPU使用率高,并且任务处理不过来时,我们需要快速定位服务具体在处理什么任务导致CPU使用率很高,具体的操作步骤如下:
-
ps -ef | grep 服务名
: 找到 Java 进程 id -
top -Hp pid
: 找到使用 CPU 最高的线程 -
printf '0x%x\n' tid
: 线程 id 转化 16 进制 -
jstack pid | grep tid
找到线程堆栈
这里我们以网关为作为事例举例:
-
查找网关的进程id,执行:
ps -ef|grep GatewayService
其中13905为网关的进程id。
-
找到使用 CPU 最高的线程,执行:
top -Hp 13905
由于只是演示,这里的CPU使用率并不高,实际场景可能是90以上。上图昨天我们能看到PID列,就是线程的Id,我们拿第一个13935为例。
-
将线程号转换为16进制:
转换成16进制后的线程号为0x374d
-
查询线程的线程栈:
我们可以看到线程名称为RxComputationScheduler-1,通过这个名称我们可以推测出是哪个逻辑导致的CPU占用高,假如不知道这个线程的具体逻辑,可以把完整的线程堆栈打印出来,一般为代码业务逻辑线程,或者是GC线程导致。
完整的线程栈信息通过
jstack -l pid
,一般保存到文件,由运维发送给开发查看,此处我们可以看到上述线程的线程栈:
2. 服务GC频繁/内存泄漏
一般情况下,服务GC频繁,也可以通过经验1中,服务CPU占用高
排查得出,由于GC频繁已经影响到服务的正常使用,此时GC线程的CPU占用率一般都会很高,所以通过方式1,一般可以定位到CPU占用高的线程为如下所示:
除了上述方法还可以通过如下方式定位GC是否频繁: 使用jstat命令查看GC统计情况:jstat -gcutil pid
各个字段的说明如下:
- **S0:**幸存1区当前使用比例
- **S1:**幸存2区当前使用比例
- **E:**伊甸园区使用比例
- **O:**老年代使用比例
- **M:**元数据区使用比例
- **CCS:**压缩使用比例
- **YGC:**年轻代垃圾回收次数
- **FGC:**老年代垃圾回收次数
- **FGCT:**老年代垃圾回收消耗时间
- **GCT:**垃圾回收消耗总时间
这里我们主要关注FGC相关的信息,一般情况下web应用FGC都应该为0,这个数值比较大的话,需要重点关注。
FGC频繁,一般由两种原因导致:
- 内存分配不足,这种情况需要调整服务的内存大小
- 服务存在内存泄漏,或代码逻辑问题缓存过多数据在内存中
我们需要通过dump出堆信息来分析具体占用过多的对象是什么,这时命令为:
jmap -dump:format=b,file=heap.bin pid
此命令是把指定pid服务的堆导出来,这是一个二进制的文件,需要使用专门的工具Eclipse Memory Analyzer。工具的详细使用这里不做过多赘述,有需要的可以到网上搜一下,通过内存泄漏检查,可以看到如下信息:
上图可以看到有三个嫌疑问题,这里我们随机截取一个:
从上面的截图可以看到,线程http-nio-8601-exec-100占用了整个堆内存的48.41%,大小为2个G左右byte[],这显然是不正常的。我们点击See stacktrace,可以看到当前线程的调用栈信息(截图中为一部分栈信息):
根据线程栈信息可以看到该线程正在进行数据流的读操作,并且此线程是tomcat的io线程,可以推测是在处理上传/下载请求。带着这些信息针对性的走查代码,查找问题的根源。
3. 服务请求无响应
服务请求无响应,首先查看服务日志,有没有什么异常日志,但是一般遇到的情况都是线程卡死导致,线程卡死服务是没有日志的。导致的原因是服务在运行的过程中,某些异常场景导致资源未释放,从而导致tomcat的io线程一直被占用,新的请求进来时,没有可用的工作线程处理,导致服务假死的情况发生,这种情况一般可以通过重启服务,立刻就可以恢复服务,但是过一段时间肯定还会复现,所以找到最终原因才能保证后续不会发生类似问题。我们第一步要做的是打印服务的线程栈信息:jstack -l pid > dump.log
,我们可以分析所有的tomcat io线程的线程栈,看一下线程都卡在哪个地方。下图线程栈是网关上次故障线程卡死的信息:
从线程栈可以看到Tomcat
的IO线程全部卡在了HttpClient
的leaseConnection()
这里,也就是所有的线程都在等待获取HttpClient
的连接,但是始终获取不到。分析Zuul网关
和HttpClient
的源码得知,是在异常场景,连接没有释放到HttpClient
的连接池导致。
4. 网络连接数/线程数飙升
一般一个服务的网络连接/线程数飙升是由于当前服务或当前服务的上游服务有问题导致,经验3
中服务请求无响应
也会导致线程数飙升,一直达到配置的最大值,网络连接会随着线程数的增加而增加,一直达到配置的最大值,当前使用的SpringBoot
版本默认是10000个连接。所以排查思路类似于经验3
,但是引起这个问题的原因不一定是当前服务有问题,有可能是上游服务异常导致,所以当查寻当前服务线程栈无异常时,需要同步查一下上游服务的线程栈。如何快速确定是哪个服务有问题呢?我们可以通过Grafana,看下接口性能,假如问题出现在网关和账户两个服务,我们可以先看一下账户服务的接口响应是否异常,假如账户服务的接口响应都很快,那么基本可以断定是网关出了问题。
5. 一次请求调用,代码逻辑走了一半没了踪迹,后续日志也没打
这种问题引起的原因有多种,下面列举一下可能引起的原因:
-
**原因一:**日志打印有问题,如包或类库引用错误导致代码运行了但是日志没有正常打印
-
**原因二:**代码逻辑缺陷
-
**原因三:**系统异常如爆内存导致线程异常退出
-
**原因四:**存在三方调用,但是调用未设置超时时间
问题出现时,前三条原因都可以很容易的确认,原因一和原因二一般可以通过分析代码逻辑确定,原因三,也可以通过日志中搜索OutOfMemory关键字来确定是否发生了内存溢出的异常。原因四排查相对较为繁琐,下面具体讲一下原因四的排查流程:
找到当次调用方法入口的任何一条日志,提取线程名称,假如线程名为:http-nio-8659-exec-9
,线程名拿到以后,在kibana上,指定服务、线程名、机器搜索关键词为线程名,看一下异常发生后,此线程是否还在处理其他请求,结果搜索的结果如下图,发现从4月29号开始,就没有线程http-nio-8659-exec-9
的踪迹了。
这是有点可疑的,但是仍然不能确定,假如这个线程因为空闲被回收了呢?所以此时我们导出服务的线程栈,查看是否还有该线程,结果如下(这个线程栈导出日期是在5月9号):
从线程栈中发现这个线程还在进行流的读操作,到5月9号已经读了10天了,还在读。最终发现是HttpClient
调用三方服务未设置超时时间导致,线程一直卡死,不重启服务这个线程将永远卡在那。
6. 服务CLOSE_WAIT连接数过多排查思路
CLOSE_WAIT状态出现在被动关闭方,收到关闭信号后调用close方法前。是一种「等待关闭」的状态。如下图所示。主动关闭的一方发出 FIN 包,被动关闭的一方响应 ACK 包,此时,被动关闭的一方就进入了 CLOSE_WAIT 状态。
通常出现CLOSE_WAIT分为几种可能:
- 程序问题:如果代码层面忘记了 close 相应的 socket 连接,那么自然不会发出 FIN 包,从而导致 CLOSE_WAIT 累积;或者代码不严谨,出现死循环之类的问题,导致即便后面写了 close 也永远执行不到。
- 响应太慢或者超时设置过小:如果连接双方不和谐,一方不耐烦直接 timeout,另一方却还在忙于耗时逻辑,就会导致 close 被延后。响应太慢是首要问题,不过换个角度看,也可能是 timeout 设置过小。
通常排查路径为
1、netstat -alpn | grep {port}查询连接状态,是否存在过多CLOSE_WAIT
2、根据CLOSE_WAIT的Address地址,推测问题出现方
3、jstack -l pid > dump.log 查询异常
4、dump.log一般主要排查三种状态 1、BLOCKED 2、WAITING 3、RUNNABLE
5、BLOCKED表示一个阻塞线程在等待monitor锁,可能由以下几种原因引起
1>线程通过调用sleep方法进入睡眠状态; 2>线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者; 3>线程试图得到一个锁,而该锁正被其他线程持有; 4>线程在等待某个触发条件;
6、WAITING表示一个线程在等待另一个线程执行一个动作时在这个状态,可能由以下几种原因引起
1>Object#wait()而且不加超时参数
2>Thread#join() 而且不加超时参数
3>LockSupport#park()
7、 RUNNABLE表示线程获得了cpu 时间片(timeslice)
8、先后深入代码逻辑查询该三种状态是否存在问题
以ZuulGateway为例
某次生产事故中,ZuulGateway请求无法处理,导致应用不正常。查看进程,Zuul进程仍在,且仍会打印Eureka更新的日志,但请求的日志不再打印了。查询连接数后发现存在很多CLOSE_WAIT,同时根据堆栈信息判断出问题出现在服务转发的连接上。查询源码逻辑后发现,如果在转发的过程中,代码抛出异常,则httpClient
不会主动将连接释放或关闭,导致httpClient
的连接被占用。一旦连接被占用完,则httpClient
不再能够向下游发送请求。然而,此时用户仍能够向Zuul发送请求。请求数逐渐增加,最终,用户最大请求数也被占满,Zuul服务僵死,无法再处理任何用户请求。
最终解决方法
增加ReleaseConnOnErrorFilter,用于异常时的连接关闭。
7. 线程死锁问题排查思路
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
- jstack -l {pid} 首先找到线程死锁位置
- 深入代码逻辑,找到死锁产生原因
下例为L1(Spring WebSocket)的一次线程死锁经历
- 根据jstack线程栈后发现
- 找到代码位置
-
分析原因后得知:
代码中消息发送(需要获取锁)失败会触发关闭session,同时触发消息发送(需要获取session锁)
由于ConcurrentHashMap无序,[A] -> [A、B] 或 [A] -> [B、A]
[A] -> [A、B]
线程1获取sessionA的锁,发送消息失败后关闭session触发消息发送,A成功,获取sessionB锁
线程2获取sessionA的锁,等待直到线程1释放
死锁产生
[A] -> [B、A]
线程1获取sessionA的锁,发送消息失败后关闭session触发消息发送,获取sessionB锁时失败
线程2获取sessionB的锁,发送消息失败后关闭session触发消息发送,sessionB成功获取,获取sessionA锁时失败
死锁产生
解决方案:
- 将
Map<Long, ConcurrentHashMap<Long, MeetingInfo>> MEETING_INFO_MAP
的values改为有序的容器,如ConcurrentLinkedQueue。 - 将processEndSession发送消息异步处理,同一场会议使用同一个线程处理。
- 全部使用同一个线程处理
8. 锁重入导致的并发修改异常
9. 一次请求,Tomcat和Nginx都没有accessLog就是客户端网络问题吗?
10.数据库死锁问题排查
11. mysql长事务问题排查
- mysql中查询连接的客户端端口: show PROCESSLIST; Host:该进程程序连接mysql的ip:port
- 服务器上查询该连接端口的进程:netstat -anp | grep 36884 查询到该进程
- ps -ef | grep 查询该进程详情属于哪个服务
- jstack -l pid > dump.log 查询异常
以管理系统问题为例
DBA告警,查询到备库有一些长事务
运维根据端口号查询到服务属于哪个数据库
jstack后发现99个BLOCKED,并且都是数据库查询相关,都在等待获取数据库连接
同时发现某条SQL在执行时查询不到结果
KILL该进程,临时性关掉该查询页面
对SQL进行优化