JAVA线上故障排查完整套路

JAVA 线上故障排查完整套路,从 CPU、磁盘、内存、网络、GC 一条龙!

  • CPU
  • 磁盘
  • 内存
  • GC问题
  • 网络

线上故障主要会包括cpu、磁盘、内存以及网络问题,而大多数故障可能会包含不止一个层面的问题,所以进行排查时候尽量四个方面依次排查一遍。

同时例如jstack、jmap等工具也是不囿于一个方面的问题的,基本上出问题就是df、free、top 三连,然后依次jstack、jmap伺候,具体问题具体分析即可。

CPU

CPU问题检查

uptime查看平均负荷

Linux以及大多数基于 UNIX的操作系统都提供了一条命令来显示系统的平均负荷(loadaverage) 。

[root@ip-172-29-22-176 ~]# uptime
 01:20:39 up 475 days, 16:43,  1 user,  load average: 3.15, 3.03, 3.05

平均负荷值代表了在 1min、 5min和 15min内可以运行的任务平均数量。可运行的任务包括当前正在运行的任务以及虽然可以运行但正在等待某个处理器空闲的任务。本例中的系统只有两个 CPU,它可以通过查看/proc/cpuinfo的内容来确定:

[root@ip-172-29-22-176 ~]# cat /proc/cpuinfo
processor       : 0
vendor_id       : AuthenticAMD
cpu family      : 23
model           : 1
model name      : AMD EPYC 7571
stepping        : 2
microcode       : 0x800126c
cpu MHz         : 2541.334
cache size      : 512 KB
physical id     : 0
siblings        : 8
core id         : 0
cpu cores       : 4
apicid          : 0
initial apicid  : 0
fpu             : yes
fpu_exception   : yes
cpuid level     : 13
wp              : yes
flags           : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good nopl nonstop_tsc cpuid extd_apicid aperfmperf tsc_known_freq pni pclmulqdq ssse3 fma cx16 sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm cmp_legacy cr8_legacy abm sse4a misalignsse 3dnowprefetch topoext perfctr_core vmmcall fsgsbase bmi1 avx2 smep bmi2 rdseed adx smap clflushopt sha_ni xsaveopt xsavec xgetbv1 clzero xsaveerptr arat npt nrip_save
bugs            : sysret_ss_attrs null_seg spectre_v1 spectre_v2 spec_store_bypass
bogomips        : 4399.84
TLB size        : 2560 4K pages
clflush size    : 64
cache_alignment : 64
address sizes   : 48 bits physical, 48 bits virtual
power management:

processor       : 1
vendor_id       : AuthenticAMD
cpu family      : 23
model           : 1
model name      : AMD EPYC 7571
stepping        : 2
microcode       : 0x800126c
cpu MHz         : 2543.705
cache size      : 512 KB
physical id     : 0
siblings        : 8
core id         : 1
cpu cores       : 4
apicid          : 2
initial apicid  : 2
fpu             : yes
fpu_exception   : yes
cpuid level     : 13
wp              : yes
flags           : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good nopl nonstop_tsc cpuid extd_apicid aperfmperf tsc_known_freq pni pclmulqdq ssse3 fma cx16 sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm cmp_legacy cr8_legacy abm sse4a misalignsse 3dnowprefetch topoext perfctr_core vmmcall fsgsbase bmi1 avx2 smep bmi2 rdseed adx smap clflushopt sha_ni xsaveopt xsavec xgetbv1 clzero xsaveerptr arat npt nrip_save
bugs            : sysret_ss_attrs null_seg spectre_v1 spectre_v2 spec_store_bypass
bogomips        : 4399.84
TLB size        : 2560 4K pages
clflush size    : 64
cache_alignment : 64
address sizes   : 48 bits physical, 48 bits virtual
power management:
.........下面5个核的信息省略

我的机器有8个处理器,因此平均情况下,处理器要执行的工作会稍少于它的处理能力。在较高层次上,这意味着机器需要执行的工作少于它的处理能力。注意:若在双 CPU的机器上 uptime命令显示的负荷平均值小于 2.00的话,这表明处理器仍拥有额外的空闲周期。在 8个 CPU的机器上如果负荷平均值小于 8.00的话也表明同样的情况,如此等等。然而,负荷平均值单独并不能说明全部问题。

CPU总核数 = 物理CPU个数 X 每颗物理CPU的核数
总逻辑CPU数 = 物理CPU个数 X 每颗物理CPU的核数 X 超线程数
其他命令

#查看物理CPU个数
cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l
# 查看每个物理CPU中core的个数(即核数)
cat /proc/cpuinfo| grep "cpu cores"| uniq
#查看逻辑CPU的个数
cat /proc/cpuinfo| grep "processor"| wc -l
#查看CPU信息(型号)
cat /proc/cpuinfo | grep name | cut -f2 -d: | uniq -c

vmstat

vmstat是一个实时性能监视工具。提供了有助于发现系统异常活动的数据,例如会降低系统性能的过多页面错误或上下文切换次数。这些数据的显示频率可由用户指定。vmstat监控如下:

[root@ip-172-29-22-176 ~]# vmstat
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 5  0      0 33120512      0 5950340    0    0     0    20    0    0  3  4 93  0  0

vmstat 5 10 命令的含义是每 5s 输出 vmstat 信息, 共执行 10 次。

vmstat 提供以下信息 :
procs部分提供了在生成报告时正在运行的进程数目(r)以及被阻塞的进程数目(b)。可以利用这些信息来检查运行中以及阻塞进程的数目是否与预期值相符。如果与预期不符的话,可以检查:应用和内核的参数、系统调度器和 I/O调度器、进程在可用处理器之间的分布等。
memory部分提供了换出内存(swpd)、空闲内存(free)、 I/O数据结构的缓冲区缓存(buff),以及从磁盘读取文件的内存缓存(cache)的容量,单位为 KB。 swpd的取值反映了kswapd的活动情况 。
s wap 部分提供了从磁盘上换入的内存容量 (si) 以及换出到磁盘上的内存量 (so) , 单位为 KB/s 。 so 反映了当数据被换出至交换区时 kswapd 的活动情况,而 si 则反映了当页面被换回到物理内存时发生页面错误的情况 。

io 部分提供了从设备读入的块数 (bi) 以及写出到设备上的块数 (bo) ,单位为 KB/s 。当运行 I/O 密集的应用时,应特别注意这两个部分的值 。
system 部分提供了每秒的中断数目 (in) 和上下文切换数目 (cs) 。

cpu 部分提供了用户 (us) 、 系统 (sy) 、 真正空闲 (id) 以及等待 I/O 完成 (wa) 在 CPU 总时间中所占的百分比。 CPU 利用率也许是最常用的量度。 若 wa 值过大, 则应该检查 I/O 子系统,例如,可以断定需要更多的 I/O 控制器和磁盘以便减少 I/O 等待时间。
    注意 uptime 提供了可运行进程数目在 3 个时间范围 (1min 、 5min 和 15min) 内的另一种视图。 因此, 如果 uptime 给出的平均负荷值在任何时间范围内都大于 1 ,则 vmstat 报告的可运行任务数量也应该接近 1 。
    vmstat 能够以重复的时间间隔定期提供信息,因此可通过以下命令获得动态的系统视图 。
vmstat 5 10 上述命令的含义是每 5s 输出 vmstat 信息, 共执行 10 次。

sar工具

#安装 
yum install -y sysstat
# 1、CPU利用率
sar -u -P ALL -C 5

top监控具体进程问题

一般来讲我们首先会排查cpu方面的问题。cpu异常往往还是比较好定位的。原因包括业务逻辑问题(死循环)、频繁gc以及上下文切换过多。而最常见的往往是业务逻辑(或者框架逻辑)导致的,可以使用jstack来分析对应的堆栈情况。

我们先用ps命令找到对应进程的pid(如果你有好几个目标进程,可以先用top看一下哪个占用比较高)。

接着用top -H -p pid来找到cpu使用率比较高的一些线程,也可以直接top -p pid 进去之后按大写的H,获取当前进程下的所有线程信息

[root@peiyanbing bing]$ top
top - 13:36:36 up 12 days, 43 min,  2 users,  load average: 0.34, 1.19, 1.11
Tasks: 107 total,   1 running, 106 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.5 us,  0.3 sy,  0.0 ni, 99.0 id,  0.0 wa,  0.0 hi,  0.2 si,  0.0 st
KiB Mem :  3879860 total,  1047036 free,  1331800 used,  1501024 buff/cache
KiB Swap:        0 total,        0 free,        0 used.  2311152 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                                                                         
  703 root      20   0  353032  14684   5440 S   0.7  0.4  45:12.11 agent                                                                                           
 2317 polkitd   20   0 2198808 100800   4896 S   0.7  2.6  62:41.44 beam.smp                                                                                        
 1921 polkitd   20   0 1837668 418980  18436 S   0.3 10.8  85:25.19 mysqld                                                                                          
16380 root      20   0 3536976 478784  13088 S   0.3 12.3   0:15.18 java                                                                                     
[root@peiyanbing bing]$ top -H -p 16380
top - 13:41:02 up 12 days, 47 min,  2 users,  load average: 0.00, 0.49, 0.84
Threads:  38 total,   0 running,  38 sleeping,   0 stopped,   0 zombie
%Cpu(s):  1.0 us,  0.3 sy,  0.0 ni, 98.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  3879860 total,  1046368 free,  1332428 used,  1501064 buff/cache
KiB Swap:        0 total,        0 free,        0 used.  2310516 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND                                                                                          
16380 root      20   0 3536976 478784  13088 S  0.0 12.3   0:00.00 java                                                                                             
16381 root      20   0 3536976 478784  13088 S  0.0 12.3   0:05.74 java                                                                                             
16382 root      20   0 3536976 478784  13088 S  0.0 12.3   0:00.18 java                                                                                             
16383 root      20   0 3536976 478784  13088 S  0.0 12.3   0:00.19 java 

补充知识点:top 视图交互参数

Z:改变颜色;
B:加粗
t:显示和隐藏任务/cpu信息;
m:内存信息
1:监控每个逻辑CPU的状况;
f:进入字段显示配置模式,可增加或者移除显示字段,按相应的字母新增或去除;
o:进入字段顺序设置模式,可配置显示位置顺序,按相应的字母往下移动,按“shift+相应的字母”往上移动    
F:进入字段排序配置模式,可设置排序的字段;
R:正常排序/反向排序;
s:设置刷新的时间
u:输入用户,显示用户的任务
i:忽略闲置和僵死进程。这是一个开关式命令。
r:重新安排一个进程的优先级别。系统提示用户输入需要改变的进程PID以及需要设置的进程优先级值。
#输入一个正值将使优先级降低,反之则可以使该进程拥有更高的优先权。默认值是10。
c:切换显示命令名称和完整命令行。
M:根据驻留内存大小进行排序。
P:根据CPU使用百分比大小进行排序。
H:显示线程

执行 jstack 12895 对当前的进程做 dump,输出所有的线程信息,将线程编号 **16381 转成 16 进制是 3ffd

[root@peiyanbing ~]# jstack 16380
2020-08-26 13:37:30
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.212-b10 mixed mode):

"Attach Listener" #360 daemon prio=9 os_prio=0 tid=0x00007fa8b4001000 nid=0x41a4 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"DestroyJavaVM" #359 prio=5 os_prio=0 tid=0x00007fa8e0009800 nid=0x3ffd waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"pool-1-thread-4" #357 prio=5 os_prio=0 tid=0x00007fa8e1905000 nid=0x4166 waiting on condition [0x00007fa8c56b1000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000fb6c6548> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)

"org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-1" #356 prio=5 os_prio=0 tid=0x00007fa8e1903000 nid=0x4165 waiting on condition [0x00007fa8c57b2000]
   java.lang.Thread.State: TIMED_WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000fcb01a78> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2078)
        at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:467)
        at org.springframework.amqp.rabbit.listener.BlockingQueueConsumer.nextMessage(BlockingQueueConsumer.java:499)
        at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.doReceiveAndExecute(SimpleMessageListenerContainer.java:930)
        at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.receiveAndExecute(SimpleMessageListenerContainer.java:916)
        at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.access$1600(SimpleMessageListenerContainer.java:83)
        at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.mainLoop(SimpleMessageListenerContainer.java:1291)
        at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.run(SimpleMessageListenerContainer.java:1197)
        at java.lang.Thread.run(Thread.java:748)

"pool-1-thread-3" #355 prio=5 os_prio=0 tid=0x00007fa8e1901000 nid=0x4164 waiting on condition [0x00007fa8c58b3000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000fb6c6548> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)

然后将占用最高的pid转换为16进制printf '%x\n' pid得到nid

[bing@peiyanbing ~]$ printf '%x\n' 1638
3ffd

接着直接在jstack中找到相应的堆栈信息jstack pid |grep 'nid' -C5 –color

[bing@peiyanbing ~]$ jstack 16380 |grep '3ffd' -C5

可以看到我们已经找到了 nid=0x3ffd 的堆栈信息,接着只要仔细分析一番即可。

频繁gc

当然我们还是会使用jstack来分析问题,但有时候我们可以先确定下gc是不是太频繁,使用jstat -gc pid 1000命令来对gc分代变化情况进行观察,1000表示采样间隔(ms),S0C/S1C、S0U/S1U、EC/EU、OC/OU、MC/MU分别代表两个Survivor区、Eden区、老年代、元数据区的容量和使用量。YGC/YGT、FGC/FGCT、GCT则代表YoungGc、FullGc的耗时和次数以及总耗时。如果看到gc比较频繁,再针对gc方面做进一步分析。

有时候会发现找是 VM 的线程占用过高,我们发现我开启的参数中,有垃圾回收的日志显示,所以我们要换一个思路,可能是我们的业务线程没问题,而是垃圾回收的导致的。

(代码中有打印 GC参数,生产上可以使用这个** jstat –gc **来统计,达到类似的效果)

是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT 编译等运行数据,在没有 GUI 图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。

假设需要每 250 毫秒查询一次进程 13616 垃圾收集状况,一共查询 10 次,那命令应当是:
jstat -gc 13616 2500 10

[root@peiyanbing ~]# jstat -gc 16380 2500 10
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
11776.0 11776.0  0.0   11773.5 300032.0 295621.5  42496.0    15357.9   43776.0 41147.8 5888.0 5392.8     15    0.222   2      0.162    0.385
11776.0 11776.0  0.0   11773.5 300032.0 295621.5  42496.0    15357.9   43776.0 41147.8 5888.0 5392.8     15    0.222   2      0.162    0.385
11776.0 11776.0  0.0   11773.5 300032.0 295621.5  42496.0    15357.9   43776.0 41147.8 5888.0 5392.8     15    0.222   2      0.162    0.385
11776.0 11776.0  0.0   11773.5 300032.0 295621.5  42496.0    15357.9   43776.0 41147.8 5888.0 5392.8     15    0.222   2      0.162    0.385
11776.0 11776.0  0.0   11773.5 300032.0 295621.5  42496.0    15357.9   43776.0 41147.8 5888.0 5392.8     15    0.222   2      0.162    0.385
11776.0 11776.0  0.0   11773.5 300032.0 295621.5  42496.0    15357.9   43776.0 41147.8 5888.0 5392.8     15    0.222   2      0.162    0.385

# 若只想看在乎的数据   就用: jstat-gc 16380 2500 20 | awk '{print $13,$14,$15,$16,$17}'

参数每一列的意思:

S0C:第一个幸存区的大小 
S1C:第二个幸存区的大小 
S0U:第一个幸存区的使用大小 
S1U:第二个幸存区的使用大小 
EC:伊甸园区的大小 
EU:伊甸园区的使用大小 
OC:老年代大小 
OU:老年代使用大小 
MC:方法区大小 
MU:方法区使用大小 
CCSC:压缩类空间大小 
CCSU:压缩类空间使用大小 
YGC:年轻代垃圾回收次数 
YGCT:年轻代垃圾回收消耗时间 
FGC:老年代垃圾回收次数 
FGCT:老年代垃圾回收消耗时间 
GCT:垃圾回收消耗总时间

内存占用过高内存占用过高思路

用于生成堆转储快照(一般称为 heapdump 或 dump 文件)。jmap 的作用并不仅仅是为了获取 dump 文件,它还可以查询 finalize 执行队列、Java 堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等。和 jinfo 命令一样,jmap 有不少功能在 Windows 平台下都是受限的,除了生成 dump 文件的

-dump 选项和用于查看每个类的实例、

空间占用统计的 -histo 选项在所有操作系统都提供之外。

上下文切换

针对频繁上下文问题,我们可以使用vmstat命令来进行查看

[root@peiyanbing ~]# vmstat 16380
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 1  0      0 1027212 169524 1333272    0    0     1     2    6    1  1  0 99  0  0

cs(context switch)一列则代表了上下文切换的次数。

如果我们希望对特定的pid进行监控那么可以使用 pidstat -w pid命令,cswch和nvcswch表示自愿及非自愿切换

磁盘

磁盘问题和cpu一样是属于比较基础的。首先是磁盘空间方面,我们直接使用df -hl来查看文件系统状态

[root@peiyanbing ~]# df -hl
文件系统        容量  已用  可用 已用% 挂载点
devtmpfs        1.9G     0  1.9G    0% /dev
tmpfs           1.9G     0  1.9G    0% /dev/shm
tmpfs           1.9G  8.8M  1.9G    1% /run
tmpfs           1.9G     0  1.9G    0% /sys/fs/cgroup
/dev/vda1        40G   15G   23G   39% /
overlay          40G   15G   23G   39% /var/lib/docker/overlay2/d1392931fbc59e47925fe2806eaf2f4ab0a22f0145c0f48cd20302322d89667f/merged
overlay          40G   15G   23G   39% /var/lib/docker/overlay2/f4e7278257e7b0709c82fec6413a6e9b7768aa4cf12e673c2fa4992d64f40059/merged
overlay          40G   15G   23G   39% /var/lib/docker/overlay2/dbb4fa8f6307af9a8d8e10376d59a8591d666dec45652d18bbafc1c0417a76c3/merged
tmpfs           379M     0  379M    0% /run/user/1000

更多时候,磁盘问题还是性能上的问题。我们可以通过iostatiostat -d -k -x来进行分析

最后一列%util可以看到每块磁盘写入的程度,而rrqpm/s以及wrqm/s分别表示读写速度,一般就能帮助定位到具体哪块磁盘出现问题了。

另外我们还需要知道是哪个进程在进行读写,一般来说开发自己心里有数,或者用iotop命令来进行定位文件读写的来源。

网络

最后就是网络问题,涉及到网络层面的问题一般都比较复杂,场景多,定位难,成为了大多数开发的噩梦,应该是最复杂的了。这里会举一些例子,并从tcp层、应用层以及工具的使用等方面进行阐述。

超时

超时错误大部分处在应用层面,所以这块着重理解概念。超时大体可以分为连接超时和读写超时,某些使用连接池的客户端框架还会存在获取连接超时和空闲连接清理超时。

  • 读写超时。readTimeout/writeTimeout,有些框架叫做so_timeout或者socketTimeout,均指的是数据读写超时。注意这边的超时大部分是指逻辑上的超时。soa的超时指的也是读超时。读写超时一般都只针对客户端设置。
  • 连接超时。connectionTimeout,客户端通常指与服务端建立连接的最大时间。服务端这边connectionTimeout就有些五花八门了,jetty中表示空闲连接清理时间,tomcat则表示连接维持的最大时间。
  • 其他。包括连接获取超时connectionAcquireTimeout和空闲连接清理超时idleConnectionTimeout。多用于使用连接池或队列的客户端或服务端框架。

我们在设置各种超时时间中,需要确认的是尽量保持客户端的超时小于服务端的超时,以保证连接正常结束。

在实际开发中,我们关心最多的应该是接口的读写超时了。

如何设置合理的接口超时是一个问题。如果接口超时设置的过长,那么有可能会过多地占用服务端的tcp连接。而如果接口设置的过短,那么接口超时就会非常频繁。

服务端接口明明rt降低,但客户端仍然一直超时又是另一个问题。这个问题其实很简单,客户端到服务端的链路包括网络传输、排队以及服务处理等,每一个环节都可能是耗时的原因。

TCP队列溢出

tcp队列溢出是个相对底层的错误,它可能会造成超时、rst等更表层的错误。因此错误也更隐蔽,所以我们单独说一说。


image.png

如上图所示,这里有两个队列:syns queue(半连接队列)、accept queue(全连接队列)。三次握手,在server收到client的syn后,把消息放到syns queue,回复syn+ack给client,server收到client的ack,如果这时accept queue没满,那就从syns queue拿出暂存的信息放入accept queue中,否则按tcp_abort_on_overflow指示的执行。

tcp_abort_on_overflow 0表示如果三次握手第三步的时候accept queue满了那么server扔掉client发过来的ack。tcp_abort_on_overflow 1则表示第三步的时候如果全连接队列满了,server发送一个rst包给client,表示废掉这个握手过程和这个连接,意味着日志里可能会有很多connection reset / connection reset by peer。

那么在实际开发中,我们怎么能快速定位到tcp队列溢出呢?

netstat命令

执行netstat -s | egrep "listen|LISTEN"

overflowed表示全连接队列溢出的次数,sockets dropped表示半连接队列溢出的次数。

ss命令,执行ss -lnt

[root@peiyanbing ~]# ss -lnt
State       Recv-Q Send-Q                                     Local Address:Port                                                    Peer Address:Port              
LISTEN      0      511                                                    *:443                                                                *:*                  
LISTEN      0      511                                                    *:80                                                                 *:*                  
LISTEN      0      128                                                    *:22                                                                 *:*                  
LISTEN      0      1024                                                [::]:15672                                                           [::]:*                  
LISTEN      0      1024                                                [::]:5672                                                            [::]:*                  
LISTEN      0      1024                                                [::]:3306                                                            [::]:*                  
LISTEN      0      1024                                                [::]:6379                                                            [::]:*                  
LISTEN      0      128                                                 [::]:22                                                              [::]:*

上面看到Send-Q 表示第三列的listen端口上的全连接队列最大为511,第一列Recv-Q为全连接队列当前使用了多少。

接着我们看看怎么设置全连接、半连接队列大小吧:

全连接队列的大小取决于min(backlog, somaxconn)。backlog是在socket创建的时候传入的,somaxconn是一个os级别的系统参数。而半连接队列的大小取决于max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog)。

在日常开发中,我们往往使用servlet容器作为服务端,所以我们有时候也需要关注容器的连接队列大小。在tomcat中backlog叫做acceptCount,在jetty里面则是acceptQueueSize。

RST异常

RST包表示连接重置,用于关闭一些无用的连接,通常表示异常关闭,区别于四次挥手。

在实际开发中,我们往往会看到connection reset / connection reset by peer错误,这种情况就是RST包导致的。

端口不存在

如果像不存在的端口发出建立连接SYN请求,那么服务端发现自己并没有这个端口则会直接返回一个RST报文,用于中断连接。

主动代替FIN终止连接

一般来说,正常的连接关闭都是需要通过FIN报文实现,然而我们也可以用RST报文来代替FIN,表示直接终止连接。实际开发中,可设置SO_LINGER数值来控制,这种往往是故意的,来跳过TIMED_WAIT,提供交互效率,不闲就慎用。

客户端或服务端有一边发生了异常,该方向对端发送RST以告知关闭连接

我们上面讲的tcp队列溢出发送RST包其实也是属于这一种。这种往往是由于某些原因,一方无法再能正常处理请求连接了(比如程序崩了,队列满了),从而告知另一方关闭连接。

接收到的TCP报文不在已知的TCP连接内

比如,一方机器由于网络实在太差TCP报文失踪了,另一方关闭了该连接,然后过了许久收到了之前失踪的TCP报文,但由于对应的TCP连接已不存在,那么会直接发一个RST包以便开启新的连接。

一方长期未收到另一方的确认报文,在一定时间或重传次数后发出RST报文

这种大多也和网络环境相关了,网络环境差可能会导致更多的RST报文。

之前说过RST报文多会导致程序报错,在一个已关闭的连接上读操作会报connection reset,而在一个已关闭的连接上写操作则会报connection reset by peer。通常我们可能还会看到broken pipe错误,这是管道层面的错误,表示对已关闭的管道进行读写,往往是在收到RST,报出connection reset错后继续读写数据报的错,这个在glibc源码注释中也有介绍。

我们在排查故障时候怎么确定有RST包的存在呢?当然是使用tcpdump命令进行抓包,并使用wireshark进行简单分析了。tcpdump -i en0 tcp -w xxx.cap,en0表示监听的网卡。

TIME_WAIT和CLOSE_WAIT

TIME_WAIT和CLOSE_WAIT是啥意思相信大家都知道。

在线上时,我们可以直接用命令netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'来查看time-wait和close_wait的数量

用ss命令会更快`ss -ant | awk '{++S[$1]} END {for(a in S) print a, S[a]}'

TIME_WAIT

time_wait的存在一是为了丢失的数据包被后面连接复用,二是为了在2MSL的时间范围内正常关闭连接。它的存在其实会大大减少RST包的出现。

过多的time_wait在短连接频繁的场景比较容易出现。这种情况可以在服务端做一些内核参数调优:

#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
net.ipv4.tcp_tw_reuse = 1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
net.ipv4.tcp_tw_recycle = 1

当然我们不要忘记在NAT环境下因为时间戳错乱导致数据包被拒绝的坑了,另外的办法就是改小tcp_max_tw_buckets,超过这个数的time_wait都会被干掉,不过这也会导致报time wait bucket table overflow的错。

CLOSE_WAIT

close_wait往往都是因为应用程序写的有问题,没有在ACK后再次发起FIN报文。close_wait出现的概率甚至比time_wait要更高,后果也更严重。往往是由于某个地方阻塞住了,没有正常关闭连接,从而渐渐地消耗完所有的线程。

想要定位这类问题,最好是通过jstack来分析线程堆栈来排查问题,具体可参考上述章节。这里仅举一个例子。

开发同学说应用上线后CLOSE_WAIT就一直增多,直到挂掉为止,jstack后找到比较可疑的堆栈是大部分线程都卡在了countdownlatch.await方法,找开发同学了解后得知使用了多线程但是确没有catch异常,修改后发现异常仅仅是最简单的升级sdk后常出现的class not found。

你可能感兴趣的:(JAVA线上故障排查完整套路)