6.1海恩法则和墨菲定律
海恩法则:每一起严重事故背后,必然有29次轻微事故和300起未遂先兆及1000起事故隐患
- 事故的发生是量的积累的结果
- 再好的技术,再完美的规章,在实际操作层面也无法取代人自身的素质和责任心
墨菲定律:如果有两种或两种以上方式去做某件事情,而选择其中一种方式将导致灾难,则必定有人会做出这种选择。
- 任何事情都没有表面看起来那么简单
- 所有事情的发展都会比你预计的时间长
- 会出错的事总会出错
- 如果你担心某种情况发生,那么它更有可能发生
对问题要彻查,不能因为问题的现象不明显而忽略。
6.2线上应急目标、原则和方法
6.2.1应急目标
在生产环境发生故障时快速恢复服务,避免或减少故障造成的损失,避免或减少故障对客户的影响。
6.2.2应急原则
- 第一时间恢复系统,快速止损
- 有明显的资金损失时,第一时间升级,快速止损
- 应急指挥围绕目标,快速启动应急过程和快速决策止损方案
- 当前应急责任人如果在短时间内不能解决问题,则必须进行升级处理
- 应急过程中在不影响用户体验的前提下,要保留部分现场和数据
6.2.3线上应急的方法和流程
6个阶段:发现问题,定位问题,解决问题,消除问题,回顾问题,避免措施。
总体目标:尽快恢复问题,消除影响。
1.发现问题
通常通过自动化的监控和报警系统来实现,
通常会对系统层面,应用层面和数据库层面进行监控。
- 系统层面监控:对系统CPU利用率,系统负载,内存使用情况,网络I/O负载,磁盘负载,I/O等待,交换区使用,线程数及打开的文件句柄数等进行监控,一旦超出阈值需报警
- 应用层面监控:服务接口的响应时间,吞吐量,调用频次,接口成功率及接口的波动率等进行监控
- 资源层监控:数据库,缓存,消息队列的监控。
- 数据库负载,慢sql,连接数等,
- 缓存连接数,占用的内存,吞吐量,响应时间等进行监控
- 消息队列的响应时间,吞吐量,负载,积压情况等
2.定位问题
考虑问题:
- 问题系统最近是否进行了上线?
- 依赖的基础平台和资源是否进行上线或升级
- 依赖的系统最近是否进行了上线
- 运营是否在系统里面做过运营变更
- 网络是否有波动
- 最近业务是否上量
- 服务使用方是否有促销活动
3.解决问题
每个系统会对各种严重情况设计止损和降级开关,在发生严重问题时先使用止损策略,恢复问题后再定位和解决问题
清晰定位问题根本原因,提出解决问题的有效方案。
4.消除影响
- 技术人员在应急过程中对系统做的临时性改变,后证明无效,尝试恢复到原来的状态
- 技术人员在应急过程中对系统进行降级开关操作,事后需恢复
- 运营人在应急过程中对系统做的特殊设置如某些流量路由的开关,需恢复
- 对使用方或者用户造成的问题,尽量采取补偿的策略进行修复,在极端情况下需一一核实
- 对外由专门的客服团队整理话术统一对外宣布发生故障的原因并安抚用户,话术尽量贴近客观事实,从用户角度出发
5.回顾问题
回顾事故产生原因,应急过程合理性,提出整改措施,聚焦于以下问题
- 类似的问题还有哪些没想到
- 做了哪些事情,事故不会发生
- 做了哪些事情,事故即使发生,也不会产生损失
- 做哪些事情,事故即使发生,也不会产生这么大损失
6.避免措施
建立改进措施和避免措施跟进方案和机制
6.3技术攻关的方法论
技术攻关目标是解决问题
- 最近是否有变更,升级和上线
- 之前是否遇到过相同或者类似的问题
- 是否有相关领域的专家
考虑when 什么时候的问题,what什么问题,who谁发现的,影响了谁,where 哪里出现的问题,哪里没有出现,why为什么出现问题
根据答案判断那个系统的问题,并从这个系统入手,查日志,查数据,并结合代码定位问题根源。
最小化复现指在个人开发环境内通过模拟生产环境来重视生产环境产生的问题。最小化复现环境必须是问题产生时所涉及的组件的最小化集合,不需要包含所有组件。
定位问题后,给出最佳方案,给出选择原因。
方案需要在开发环境和QA环境下进行验证,验证能否能否解决问题,还要避免影响现有功能,需要做回归验证。
6.4环境搭建和示例服务启动
环境:
- OS:Ubuntu 14.04.2.LTS
- 内核:3.16.0-30-generic
- 硬件架构:x86_64
- JDK:jdk1.8.0_20
搭建原创发号器服务Vesta
- 源码地址:https://github.com/robertleepeak/vesta-id-generator.git
- 进入工作目录,运行发布脚本打包发布./make-release.sh
- 启动发号器REST服务
- curl语句测试发号器服务是否正常运行
- 发号器准确生成ID,并反解成JSON字符串格式
- 命令 ps -elf | grep java 查看服务进程
6.5高效的服务化治理脚本
6.5.1 show-busiest-java-threads
查找Java进程内CPU利用率最高的线程,一般适用于服务器负载较高的场景,快速定位负载高的成因。
命令格式
- ./show-busiest-java-threads -p 进程号 -c 显示条数
- ./show-busiest-java-threads -h
6.5.2 find-in-jar
用于在Jar包的包名和类名中查找某一关键字,并高亮显示匹配的包名,类名,和路径,多用于定位java.lang.NoClassDefFoundError 和 java.lang.ClassNotFoundException问题,以及类版本重复或者冲突的问题等。
命令格式
- find-in-jar 关键字 类名根路径
6.5.3 grep-in-jar
在Jar包中进行二进制内容查找,通常会解决线上出现的一些不可思议的问题,例如某些功能上线后没有生效,某些日志没有打印等,通常是上线工具或者上线过程出现的问题,可以把线上的二进制包拉下来并查找特定的关键字来定位问题。
命令格式
- grep-in-jar 关键字 路径
6.5.4 jar-conflict-detect
识别冲突的Jar包,可以在一个根目录下找到包含相同类的所有Jar包,并根据相同类的多少来判断Jar包的相似度,常用于
- 某些功能上线却不可用或者没有按照预期起到作用的情况
- 或者使用此脚本分析是否存在两个版本的类,而老版本的类被Java虚拟机加载
命令格式:
jar-conflict-detect 路径
6.5.5 http-spy
利用Linux命令nc检查HTTP请求参数,请求头和请求体等信息,
- 用于调试HTTP服务调用
命令格式:
http-spy
6.5.6 show-mysql-qps
用于快速查看Mysql实例的负载情况,包括每秒查询数,每秒事物数,提交数,回滚数,连接线程数,执行线程数等
命令格式:
show-mysql-qps 用户名 密码
6.6JVM提供的监控命令
6.6JVM提供的监控命令
6.6.1 jad
jad反编译工具可以将字节码的二进制类反编译为Java源代码,常常用于遇到问题但是无法在源代码中定位的场景,通过反编译字节码,可以分析程序的实际执行流程,从而定位深层次的问题。
界面版jd-gui
命令:jad AbstractIdServiceImpl.class
6.6.2btrace
无法根据已有日志定位问题,需要更多的信息如参数,返回值等。btrace可以动态地跟踪Java运行时程序,将定制化的跟踪字节码切面注入运行类中,对运行代码无侵入,对性能的影响也可忽略不计。
命令格式:
btrace [-p port] [-cp classpath] pid btrace-script
- port:监听的端口号
- classpath:指定依赖的类加载路径
- pid:进程号,可通过jps 或者ps 命令获取
- btrace-script:btrace跟踪切面脚本
运行命令前,需编写btrace的跟踪脚本:
@BTrace
public class Btrace{
@OnMethod(
clazz = "com.cloudate.controller.AdminController",
method = "sayHello",
location = "@Location(Kind.RETURN)"
)
public static void sayHello(@Duration long duration){//单位是ns,要转为ms
println(strcat("duration(ms):",str(duration/1000000)));
}
}
这个跟踪脚本对业务代码的方法进行了拦截,并打印方法的执行时间
package com.cloudate.controller;
public class AdminController{
public String sayHello(String name, int age){
return "hello everyone";
}
}
示例:
btrace -p 2020 -cp ~/servlet-api.jar 1507 ~/BTrace.java
6.6.3
6.6.3 jmap
JDK 自带的监控工具,用来查看Java进程对内存的使用情况
示例1:jmap -histo:live 2743
按照占用空间大小打印程序中类的列表,可以分析哪些类占用了比较多的内存。
示例2:jmap 2743
按照占用空间的大小打印程序中加载的动态链接库的列表。此命令可以帮助定位Java进程占用内存较大或者底层动态链接库占用内存较大的问题,在定位Java进程导致的内存泄露场景中有很重要的作用。
示例3 jmap -heap 38574
Java堆的内存结构很复杂,包括新生代,老年代,持久代,直接内存等。通过jmap命令可以查看堆的概要信息。
示例4:jmap -dump:format=b,file=./heap.hprof 2743
需对Java堆的内部结构进行剖析才能进一步分析产生问题的根本原因,通过jmap命令导出Java堆的快照,然后通过其他工具甚至可视化内存分析工具等详细分析
6.6.4 jstat
JDK自带的监控工具。利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行监控,包括对堆大小和垃圾回收状况的监控等。更倾向于输出累积的信息与打印GC等的统计信息等。
示例:jstat -gcutil 2743 5000 10
6.6.5 jstack
JDK自带命令,用于打印给定的Java进程ID 的线程堆栈快照信息,从而可以看到Java进行内线程的执行状态,正在执行的任务等,可以据此分析线程等待,死锁等,
示例:jstack 2743
6.6.6jinfo
可以输出并修改运行时的Java进程的环境变量和虚拟机参数
示例:jinfo 38574
6.6.7其他命令
- javah:生成Java类中本地方法的C头文件,一般用于开发JNI库
- jps:查找java进程
- jhat:分析内存对的快照文件
- jdb:远程调试,线上定位问题
- jstatd:jstat的服务器版本
Java虚拟机图形界面分析工具
- JConsole:查看Java内存和线程堆栈的工具
- JVisualVM:查看Java内存和线程堆栈的工具
- JMAT:Java性能跟踪,分析和定位工具
- JProfiler:Java性能跟踪,分析和定位工具
6.7重要的 Linux基础命令
6.7.1必不可少的基础命令和工具
1. grep
文本内容查找命令,可以利用它打印匹配的上下几行。在线上查找问题时,可以使用命令查找关键字,显示关键字所在行的前后多行,并给关键字着色。
grep -5 'parttern' INPUT_FILE #打印匹配行的前后5行
grep -C 5 'parttern' INPUT_FILE #打印匹配行的前后5行
grep -A 5 'parttern' INPUT_FILE #打印匹配行的后5行
grep -B 5 'parttern' INPUT_FILE #打印匹配行的前5行
grep -A 15 --color 1010061938 #查找后着色
2. find
通过文件名查找文件的所在位置,文件名查找支持模糊匹配。
find . -name FILE_NAME
3. uptime
查看机器的启动时间,登录用户,平均负载等情况,通常用于在线上应急或者技术攻关中确定操作系统的重启时间
uptime
当前时间,系统已运行的时间,当前在线用户,系统平均负载(最近1min,5min,15min)
系统平均负载指:在特定的时间间隔内队列中运行的平均进程数,如果一个进程满足以下条件,它就会位于运行队列中。
- 没有在等待I/O操作的结果
- 没有主动进入等待状态
- 没有被停止
一般每个CPU内核对应的活动进程数不大于3,系统运行良好,即活动进程数小于CPU核心数的3倍。
eg:如果服务器的CPU有3核,那么uptime输出的一串字符数值小于9,就表示系统负载正常,但超过10,即负载过重,需定位系统执行负载超标原因。
4. lsof
用于列出系统当前打开的文件句柄,在linux文件系统中任何资源都是以文件句柄的形式进行管理的。对系统监控及应急排错提供重要的帮助
#查看某个进程打开的文件句柄
lsof -p 2862
#查看某个端口的使用方式
lsof -i 8080
5. ulimit
显示当前的各种系统对用户使用资源的限制
linux系统对每个登录的用户都限制了最大进程数和打开的最大文件句柄数。
#显示当前的各种系统对用户使用资源的限制
ulimit a
#设置用户最大进程数
ulimit -u 1024
#设置用户可以打开的最大文件句柄数
ulimit -n 65530
6. curl
使用Http调用时,查看返回的结果是否符合预期
curl -i "http://www.sina.com" #打印请求响应头信息
curl -v "http://www.sina.com" #打印更多的调试信息
curl -verbose "http://www.sina.com" #打印更多的调试信息
curl -d 'abc=def' "http://www.sina.com" #使用POST方法提交HTTP请求
curl -I "http://www.sina.com" #仅仅返回HTTP头
curl -sw '%{http_code}' "http://www.sina.com" #打印http响应码
7. scp
文件传输命令,可以实现从本地到远程,从远程到本地的双向文件传输。
scp robert@1xx.1xx.x.x:/home/xx/xxx.txt .
scp ./xxx.txt robert@1xx.1xx.x.x:/home/robert/
8. vi 和 vim
文本编辑工具
9. dos2unix 和 unix2dos
用于转换Windows 和UNIX的换行符
dos2unix xxx.txt
unix2dos xxx.txt
10.awk
强大的文本分析工具,在对数据分析并生成报告,把文件逐行读入,以空格为默认分隔符将每行切片,也可以以任何字符为分隔符,把切开的部分进行各种分析和处理。
11. 其他
- sed:文本编辑和切换
- tr:字符替换
- cut:选取命令,分析一段数据并取出想要的部分
- wc:统计字数和行数等
- sort:排序
- uniq:去重或者分组统计
- zip:压缩成zip格式的压缩包或解压
- tar:创建或解压tar格式的包
6.7.2: 查看活动进程的命令
1. ps
用于显示系统内的所有进程
ps -elf
grep 找到目标进程
2. top
用于查看活动进程的CPU和内存信息,能够实时显示系统中各个进程的资源占用情况。
top
输出中可看到整体的CPU占用率,CPU负载,进程占用CPU和内存等资源情况。
- t:切换显示进程和CPU状态信息
- m:切换显示内存的信息
- r:重新设置一个显示进程的优先级。系统提示用户输入需要改变的进程PID及需要设置的进程优先级,然后输入一个正数值使优先级降级,反之则可以使该进程拥有更高的优先级,默认10
- k:中止一个进程,系统将提示用户输入需要中止的进程PID
- s:改变刷新的时间间隔
- u:查看指定用户的进程
3. pidstat
用于监控全部或指定的进程占用系统资源的情况,包括CPU,内存,磁盘I/O,线程切换,线程数等数据
pidstat -urd -p 进程号
6.7.3窥探内存的命令
1.free
用于显示系统内存的使用情况,包括总体内存,已经使用的内存,
用于显示系统内核使用的缓存区,包括缓存和缓冲等
free
- buffers:一般不太大,一般几十到几百MB字节,用于存储磁盘块设备的元数据,比如哪些块属于哪些文件,文件的权限,目录等信息
- cached:会很大,一般GB字节以上,用于存储读写文件的页
- 当对一个文件进行读时:会取磁盘文件页放到其内存区域,然后从内存中读取
- 当写一个文件时:先写到其缓存,将相关的页面标记为dirty,cached随着读写磁盘的多少自动增加减少,也取决于物理内存是否够用,如果应用使用的物理内存较多,操作系统会适当缩小cached来保证用户进程对内存的需要
2.pmap
用来报告进程中各个模块占用内存的具体情况,显示比较底层的进程模块占用内存的信息,并可以打印内存的起止地址等。用于定位深层次JVM或者操作系统的内存问题
pmap -d 2862
6.7.4 针对CPU使用情况的监控命令
1.vmstat
显示关于内核线程,虚拟内存,磁盘I/O,陷阱和CPU占用率的统计信息
vmstat
- buff:I/O系统存储的磁盘块文件的元数据的统计信息
- cache:操作系统用来缓存磁盘数据的缓冲区,操作系统会自动调节这个参数,在内存紧张时操作系统会减少cahe的占用空间来保证其他进程可用
- cs:表示线程环境的切换次数,次数据太大时表明线程的同步机制有问题
- si和so较大时,说明系统频繁使用交换区,应查看操作系统的内存是否够用。
- bi和bo:代表I/O活动,根据其大小可以知道磁盘I/O的负载情况
2.mpstat
用于实施监控系统CPU统计信息,这些信息放在/proc/stat文件中,在多核CPU系统里,不但能查看所有CPU平均使用信息,还能查看某个特定CPU信息
mpstat -P ALL
可以看到每个CPU核的占用率,I/O等待,软中断,硬中断等。
6.7.5监控磁盘I/O的命令
1. iostat
用于监控CPU占用率,平均负载值及I/O读写速度等.
- r/s 和 w/s :IOPS
- rkB/s 和 wkB/s :指的是每秒的数据存取速度,
- await:平均等待时间,一般10ms左右
命令输出包含
- iowait:包括r_wait 和 w_wait ,这些指标较大则说明I/O负载较大,I/O等待比较严重,磁盘读写遇到瓶颈
- 每秒读写速度的最大峰值
- CPU的占用率情况
2.swapon
查看交换分区的使用情况
/sbin/swapon -s
3. df
用于查看文件系统的硬盘挂载点和空间使用情况
df -h
6.7.6 查看网络信息和网络监控命令
1. ifconfig
用于查看机器挂载的网卡情况
ifconfig -a
2.ping
用于检测网络故障的常用命令,可以测试一台主机到另一台主机的网络是否连通
ping www.baidu.com
3. telnet
是TCP/IP 协议族的一员,是网络远程登录服务的标准协议,帮助用户在本地计算机上连接远程主机
telnet IP PORT
4.nc
NetCat 可通过TCP/UDP传输数据,一款网络应用调试分析器,
5. mtr
Linux系统中的网络连通性测试工具,用来检测丢包率
mtr -r sina.com
6. nslookup
检测网络中DNS服务器能否正确解析域名的工具命令,并且可以输出
nslookup sina.com
7. traceroute
可以提供从用户的主机到互联网另一端的主机的路径,虽然每次数据包由同一出发点到达同一目的地的路径可能会不一样
traceroute sina.com
8.sar
多功能监控工具,可以输出每秒网卡存取速度。
sar -n DEV 1 1
9.netstat
显示网络连接,端口信息等
-
根据进程查找端口
-
根据进程名查找进程ID
-
ps -elf | grep 进程
-
根据进程ID 查找进程开启的端口
-
netstat -nap | grep ID
-
-
根据端口查找进程
-
查找使用端口的进程号
-
netstat -nap | grep 端口号
-
根据进程ID查找进程的详细信息
-
ps -elf | grep 进程ID
-
10.iptraf
实时监控网络流量的交互式的彩色文本屏幕界面,监控数据比较全面,可以输出TCP连接,网络接口,协议,端口,网络包大小等信息,但耗费的系统资源比较多且需要管理员权限
sudo ipstraf
11.tcpdump
网络状况分析和跟踪工具,可以用来抓包的实用命令。
#显示来源IP或者目的IP为xxx.xxx.x.xxx的网络通信
sudo tcpdump -i eth0 hostxxx.xxx.x.xxx
#显示来去往的所有FTP会话信息
sudo tcpdump -i eth1 'dst xxx.xxx.x.xxx and (port xx or xx)'
#显示去往xxx.xxx.x.xxx的所有http会话信息
sudo tcpdump -ni eth0 'dst xxx.xxx.x.xxx and tcp and port xxxx'
12.nmap
扫描某一主机打开的端口及端口提供的服务信息,通常用于查看本机有哪些端口对外提供服务,或者确定服务器有哪些端口对外开放。
nmap -v -A localhost
13.ethtool
用于查看网卡的配置情况
ethtool 网卡名称
6.7.7Linux系统的高级工具
1.pstack
用来显示每个进程的本地调用栈,可以使用pstack来查看进程正在挂起的执行方法,也可以查看进程的本地线程堆栈,与JVM的jstack配合使用可以看到JVM线程运行的全部状况
pstack xxxx
2.strace
系统调用工具,监控一个应用程序所使用的系统调用,通过它可以跟踪系统调用,并了解Linux程序是怎样工作的。
6.7.8/proc文件系统
通过/proc文件系统查看运行时系统内核内的数据结构的能力,也可以改变系统内核的参数设置
#显示CPU信息
cat /proc/cpuinfo
#显示内存信息
cat /proc/meminfo
#显示详细的内存映射信息
cat /proc/zoneinfo
#显示磁盘映射信息
cat /proc/mounts
#显示CPU信息
cat /proc/loadavg
6.7.9 摘要命令
1.md5sum
用于生成md5摘要,用于在文件上传和下载操作中校验内容的正确性,或者通过hmac做对称数据签名
md5sum xxx.txt
2.sha256
通过碰撞的方法被破解,用于文件上传和下载操作中校验正确性,或通过校验的sha256-hmac做对称数据签名
3.base64
用于传输8字节码的编码方式之一,可以保证所输出的编码位全都是可读字符,base64制定了一个编码表,以便进行统一转换,变阿彪共有64个字符,被称为base64编码
小结
6.8现实中的应急和攻关案例
6.8.1一次OOM事故分析和定位
产生OOM原因
- Java heap space:表示Java堆空间不足,当应用程序申请更多的内存时,若Java堆内存已经无法满足应用程序的需要,则抛出这种异常
- PermGen space: 表示Java永久代(方法区)的空间不足。
- unable to create new native thread:创建太多线程
- GC overhead limit exceeded:并行(或并发)垃圾回收器的GC回收时间过长,超过98%的时间用来做GC并且回收了不到2%的堆内存时抛出的异常,用来提取预警,避免内存过小导致应用不能正常工作
两个异常与OOM有关系,但没有绝对关系
- java.lang.StackOverflowError:JVM线程递归方法调用的层次太多,占满了线程堆栈导致的,线程堆栈默认大小1MB
- java.net.SocketException: too many open files,是由于系统对文件句柄的使用有限制,而某个应用程序使用的文件句柄超过了这个限制而导致的
解决过程
1.产生问题
2. 解决问题
规律:晚上零点发生概率较大,偶尔其他时间发生,但都发生在整点
思考:整点是否有定时任务,有但时间不吻合。
问题本身:java.lang.OutOfMemoryError:unable to create new native thread
- 原因是不能创建线程,具体原因
- 由于线程使用的资源(内存)过多操作系统已经达到了允许创建的最大数量
- 操作系统设置了引用创建线程的最大数量,此时已经达到了允许创建的最大数量
(1)内存计算
Java内存
- 操作系统本身使用
- 进程服务
- 堆:JVM自动管理的内存,应用对象创建,销毁,类的装载等
- 本地内存:特殊内存,JVM不直接管理其生命周期
- 栈:每个线程有个栈,存储线程工作过程中产生的方法局部变量,方法参数和返回值,每个线程对于的栈的默认大小1MB
从内存角度看,创建线程需要内存空间,os没有剩余内存分配给JVM进程,抛出OOM异常。
计算允许创建的最大线程数:
最大线程数=(操作系统最大可用内存 - JVM内存 - 操作系统预留内存)/线程栈大小
命令:free 内存,使用内存,剩余内存
eg:剩余内存700MB /1MB = 700 个线程
还有700 个线程可以创建,不是内存问题
(2)线程数对比
评估操作系统是否设置了应用创建线程的最大数量,并且达到了最大允许数量
命令:ulimit -a
显示当前的各种系统对用户使用资源的限制
max user processes (-u) 1024 ----- 机器允许使用的最大用户进程数为1024
命令:jstack 端口号 ---- 打印Java线程情况
命令:grep “Thread” js.log | wc -l
结果:904 ---- JVM一共创建了904个线程,但没达到最大限制1024
JVM本身可能会有一些管理线程存在,os内当前用户下可能也有守护线程在运行。
从操作系统角度统计线程数,
命令:pstack
命令:awk ‘{print $3}’ pthreads.log | grep app | wc -l
结果:1021 ------- 当前用户创建线程的线程数1021
接近了1204最大进程数
使用JVM的jstack命令,通过查看输出得知,每个线程都阻塞在打印日志的语句上,log4j中打印日志有锁,作用是让打印日志可以串行,保证日志在日志文件中的正确性和顺序性。
每日凌晨零点的日志切割导致磁盘I/O被占用,于是堵塞打印日志,日志阻塞时线程池阻塞,进而导致线程池被撑大,线程池线程数接近或超过1024就报错。
日志切割导致I/O增大分析
命令:vmstat查看I/O等待数据
问题发生时CPU I/O等待59%
脚本切割cat命令先把日志文件cat后,通过管道打印到另外一个文件,再清空原文件,一定会导致I/O增加。
平台服务化架构使用了两个服务化框架,一个是基于Dubbo,一个是点对点的RPC,用于在紧急情况下Dubbo出现问题,服务降级使用
每个服务都配置了点对点的RPC服务,并且独享一个线程池。
对Dubbo服务框架进行定制化时,设计了自动降级原则,如果Dubbo服务负载增加或者注册中心宕机,会自动切换到点对点的RPC框架(失效转移)。
一旦一部分服务切换到了点对点的RPC,而另一部分没有切换,会导致两个线程池都被撑满,超过1024
最小化重现问题
通过实践来验证产生问题原因
- Tomcat 线程池的最大设置为1500
- 操作系统允许的最大用户进程数为1024
- 在给服务加压的过程中,需要人工制造频繁I/O操作,I/O等待不得低于50%
解决方法
- 将全部应用改为直接使用Log4j 的日志滚动功能
- Tomcat 线程池的线程数设置与操作系统的线程数设置不合理,适当减少Tomcat线程池的线程数
- 升级Log4j日志,使用Logback或者Log4j2
6.8.2一次CPU100%的线上事故排查
偶尔发生没有规律
开始自建简单的调用链跟踪系统,通过应用层将日志推送到另一个类型运营后台的服务器上,由运营后台的服务器来收集整理日志,然后显示在运营后台的界面上
调用链跟踪系统设计
业务服务的发送逻辑,异步推送
伪代码:
public class AsyncEventSender<T>{
//默认设置的缓存事件的最大允许数量
private static final long MAX_BUFFER_SIZE = 10000000;
//使用并发的队列缓存消息
private ConcurrentLinkedQueue<T> bufferQueue = new ConcurrentLinkedQueue<T>();
//发送事件消息
public void sendEventAsync(T event){
//根据队列是否已满,判断是抛弃还是发送
if (bufferQueue.size() < MAX_BUFFER_SIZE){
bufferQueue.add(event);
}
}
}
问题:ConcurrentLinkedQueue 判断队列占用情况,判断抛弃还是发送
命令:jstack
结果:大部分线程工作在buffer.Queue.size()代码上
Concurrent 集合类使用分桶策略减少集合的线程竞争,在获取其整体大小时需要进行统计,而不是直接返回一个预先存储的值,时间复杂度O(n)
推送服务从缓存中提取事件并发送给运营后台,运营后台机器少,业务量有些峰值的时候,运营后台的机器负载就会攀升,导致瓶颈。另一个场景,运营后台部署时单机房,如果跨机房的网络抖动,导致推送服务延时,推送进程会被阻塞,导致缓存的数据积压过多,缓存数据量越大,size计算时间越长,导致CPU占用率100%
改进方案:
- 使用环形队列,生产者通过环形队列写入指针存储数据,消费者通过队列的读取指针来读取数据,如果生产者的进度快于消费者,生产者可抛弃事件
- 开源Disruptor Ring Buffer来实现
- 可以采用其他有界队列来实现
- 采用专业的日志收集器来实现,