现网问题排查手册

现网问题排查手册

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频繁,一般由两种原因导致:

  1. 内存分配不足,这种情况需要调整服务的内存大小
  2. 服务存在内存泄漏,或代码逻辑问题缓存过多数据在内存中

我们需要通过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线程全部卡在了HttpClientleaseConnection()这里,也就是所有的线程都在等待获取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锁时失败

    死锁产生

解决方案:

  1. Map<Long, ConcurrentHashMap<Long, MeetingInfo>> MEETING_INFO_MAP的values改为有序的容器,如ConcurrentLinkedQueue。
  2. 将processEndSession发送消息异步处理,同一场会议使用同一个线程处理。
  3. 全部使用同一个线程处理

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进行优化

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章