如何排查机器故障?线上故障如何快速排查

人气:326 ℃/2024-01-23 10:45:36

简介: 有哪些常见的线上故障?如何快速定位问题?本文详细总结工作中的经验,从服务器、Java应用、数据库、redis、网络和业务六个层面分享线上故障排查的思路和技巧。较长,同学们可收藏后再看。

前言

线上定位问题是,主要靠监控和日志。一旦超出监控的范围,则排查思路很重要,按照流程化的思路来定位问题,能够让我们在定位问题时从容、淡定,快速的定位到线上的问题。

线上问题定位思维导图

一 服务器层面1.1 磁盘1.1.1 问题现象

当磁盘容量不足的时候,应用时常会抛出如下的异常信息:

java.io.IOException: 磁盘空间不足

或是类似如下告警信息:

1.1.2 排查思路1.1.2.1 利用 df 查询磁盘状态

利用以下指令获取磁盘状态:

df -h

结果是:

可知 / 路径下占用量最大。

1.1.2.2 利用 du 查看文件夹大小

利用以下指令获取目录下文件夹大小:

du -sh *

结果是:

可知root文件夹占用空间最大,然后层层递推找到对应的最大的一个或数个文件夹。

1.1.2.3 利用 ls 查看文件大小

利用以下指令获取目录下文件夹大小:

ls -lh

结果是:

可以找到最大的文件是日志文件,然后使用rm指令进行移除以释放磁盘。

1.1.3 相关命令1.1.3.1 df

主要是用于显示目前在 Linux 系统上的文件系统磁盘使用情况统计。

(1)常用参数

启动参数:

(2)结果参数

1.1.3.2 du

主要是为了显示目录或文件的大小。

(1)常用参数

启动参数:

(2)结果参数

1.1.3.3 ls

主要是用于显示指定工作目录下的内容的信息。

(1)常用参数

启动参数:

(2)结果参数

1.2 CPU过高1.2.1 问题现象

当CPU过高的时候,接口性能会快速下降,同时监控也会开始报警。

1.2.2 排查思路1.2.2.1 利用 top 查询CPU使用率最高的进程

利用以下指令获取系统CPU使用率信息:

top

结果是:

从而可以得知pid为14201的进程使用CPU最高。

1.2.3 相关命令1.2.3.1 top

(1)常用参数

启动参数:

top进程内指令参数:

(2)结果参数

二 应用层面2.1 Tomcat假死案例分析2.1.1 发现问题

监控平台发现某个Tomcat节点已经无法采集到数据,连上服务器查看服务器进程还在,netstat -anop|grep 8001端口也有监听,查看日志打印时断时续。

2.2.2 查询日志

查看NG日志,发现有数据进入到当前服务器(有8001和8002两个Tomcat),NG显示8002节点访问正常,8001节点有404错误打印,说明Tomcat已经处于假死状态,这个Tomcat已经不能正常工作了。

过滤Tomcat节点的日志,发现有OOM的异常,但是重启后,有时候Tomcat挂掉后,又不会打印如下OOM的异常:

TopicNewController.getTopicSoftList() error="Java heap space From class java.lang.OutOfMemoryError"appstore_apitomcat2.2.3 获取内存快照

在一次OOM发生后立刻抓取内存快照,需要执行命令的用户与JAVA进程启动用户是同一个,否则会有异常:

/data/program/jdk/bin/jmap -dump:live,format=b,file=/home/www/jmaplogs/jmap-8001-2.bin 18760ps -ef|grep store.cn.xml|grep -v grep|awk '{print $2}'|xargs /data/program/jdk-1.8.0_11/bin/jmap -dump:live,format=b,file=api.bin

内存dump文件比较大,有1.4G,先压缩,然后拉取到本地用7ZIP解压。

linux压缩dump为.tgz。

在windows下用7zip需要经过2步解压:

.bin.tgz---.bin.tar--.bin2.2.4 分析内存快照文件

使用Memory Analyzer解析dump文件,发现有很明显的内存泄漏提示。

点击查看详情,发现定位到了代码的具体某行,一目了然:

查看shallow heap与retained heap能发现生成了大量的Object(810325个对象),后面分析代码发现是上报softItem对象超过300多万个对象,在循环的时候,所有的数据全部保存在某个方法中无法释放,导致内存堆积到1.5G,从而超过了JVM分配的最大数,从而出现OOM。

java.lang.Object[810325] @ 0xb0e971e0

2.2.5 相关知识2.2.5.1 JVM内存

2.2.5.2 内存分配的流程

如果通过逃逸分析,则会先在TLAB分配,如果不满足条件才在Eden上分配。

2.2.4.3 GC

(1)GC触发的场景

2)GC Roots

GC Roots有4种对象:

(3)GC算法

(4)新生代使用的GC算法

(5)老年代使用的GC算法

Parallel Compacting

Concurrent Mark-Sweep(CMS)

(6)垃圾收集器总结

(7)实际场景中算法使用的组合

(8)GC日志格式

(a)监控内存的OOM场景

不要在线上使用jmap手动抓取内存快照,其一系统OOM时手工触发已经来不及,另外在生成dump文件时会占用系统内存资源,导致系统崩溃。只需要在JVM启动参数中提取设置如下参数,一旦OOM触发会自动生成对应的文件,用MAT分析即可。

# 内存OOM时,自动生成dump文件 -XX: HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/logs/

如果Young GC比较频繁,5S内有打印一条,或者有Old GC的打印,代表内存设置过小或者有内存泄漏,此时需要抓取内存快照进行分享。

(b)Young Gc日志

2020-09-23T01:45:05.487 0800: 126221.918: [GC (Allocation Failure) 2020-09-23T01:45:05.487 0800: 126221.918: [ParNew: 1750755K->2896K(1922432K), 0.0409026 secs] 1867906K->120367K(4019584K), 0.0412358 secs] [Times: user=0.13 sys=0.01, real=0.04 secs]

(c)Old GC日志

2020-10-27T20:27:57.733 0800: 639877.297: [Full GC (Heap Inspection Initiated GC) 2020-10-27T20:27:57.733 0800: 639877.297: [CMS: 165992K->120406K(524288K), 0.7776748 secs] 329034K->120406K(1004928K), [Metaspace: 178787K->178787K(1216512K)], 0.7787158 secs] [Times: user=0.71 sys=0.00, real=0.78 secs]

2.2 应用CPU过高2.2.1 发现问题

一般情况下会有监控告警进行提示:

2.2.2 查找问题进程

利用top查到占用cpu最高的进程pid为14,结果图如下:

2.2.3 查找问题线程

利用 top -H -p 查看进程内占用cpu最高线程,从下图可知,问题线程主要是activeCpu Thread,其pid为417。

2.2.4 查询线程详细信息

2.2.5 分析代码

由上一步可知该问题是由 CpuThread.java 类引发的,故查询项目代码,获得如下信息:

2.2.6 获得结论

根据代码和日志分析,可知是由于限制值max太大,致使线程长时间循环执行,从而导致问题出现。

三 MySQL3.1 死锁3.1.1 问题出现

最近线上随着流量变大,突然开始报如下异常,即发生了死锁问题:

Deadlock found when trying to get lock; try restarting transaction ;3.1.2 问题分析3.1.2.1 查询事务隔离级别

利用 select @@tx_isolation 命令获取到数据库隔离级别信息:

3.1.2.2 查询数据库死锁日志

利用 show engine innodb status 命令获取到如下死锁信息:

由上可知,是由于两个事物对这条记录同时持有S锁(共享锁)的情况下,再次尝试获取该条记录的X锁(排它锁),从而导致互相等待引发死锁。

3.1.2.3 分析代码

根据死锁日志的SQL语句,定位获取到如下伪代码逻辑:

@Transactional(rollbackFor = Exception.class)void saveOrUpdate(MeetingInfo info) { // insert ignore into table values (...) int result = mapper.insertIgnore(info); if (result>0) { return; } // update table set xx=xx where id = xx mapper.update(info);}3.1.2.4 获得结论

分析获得产生问题的加锁时序如下,然后修改代码实现以解决该问题。

3.2 慢SQL3.2.1 问题出现

应用TPS下降,并出现SQL执行超时异常或者出现了类似如下的告警信息,则常常意味着出现了慢SQL。

3.2.2 问题分析

分析执行计划:利用explain指令获得该SQL语句的执行计划,根据该执行计划,可能有两种场景。

SQL不走索引或扫描行数过多等致使执行时长过长。

SQL没问题,只是因为事务并发导致等待锁,致使执行时长过长。

3.2.3 场景一3.2.3.1 优化SQL

通过增加索引,调整SQL语句的方式优化执行时长, 例如下的执行计划:

该SQL的执行计划的type为ALL,同时根据以下type语义,可知无索引的全表查询,故可为其检索列增加索引进而解决。

3.2.4 场景二3.2.4.1 查询当前事务情况

可以通过查看如下3张表做相应的处理:

-- 当前运行的所有事务select * from information_schema.innodb_trx;-- 当前出现的锁SELECT * FROM information_schema.INNODB_LOCKS;-- 锁等待的对应关系select * from information_schema.INNODB_LOCK_WAITS;

(1)查看当前的事务有哪些:

(2)查看事务锁类型索引的详细信息:

lock_table字段能看到被锁的索引的表名,lock_mode可以看到锁类型是X锁,lock_type可以看到是行锁record。

3.2.4.2 分析

根据事务情况,得到表信息,和相关的事务时序信息:

DROP TABLE IF EXISTS `emp`;CREATE TABLE `emp` (`id` int(11) NOT NULL AUTO_INCREMENT,`salary` int(10) DEFAULT NULL,`name` varchar(255) DEFAULT NULL,PRIMARY KEY (`id`),KEY `idx_name` (`name`(191)) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4;

A事物锁住一条记录,不提交,B事物需要更新此条记录,此时会阻塞,如下图是执行顺序:

3.2.4.3 解决方案

(1)修改方案

由前一步的结果,分析事务间加锁时序,例如可以通过tx_query字段得知被阻塞的事务SQL,trx_state得知事务状态等,找到对应代码逻辑,进行优化修改。

(2)临时修改方案

trx_mysql_thread_id是对应的事务sessionId,可以通过以下命令杀死长时间执行的事务,从而避免阻塞其他事务执行。

kill 1058533.3 连接数过多3.3.1 问题出现

常出现too many connections异常,数据库连接到达最大连接数。

3.3.2 解决方案

解决方案:

通过set global max_connections=XXX增大最大连接数。

先利用show processlist获取连接信息,然后利用kill杀死过多的连。

常用脚本如下:

排序数据库连接的数目 mysql -h127.0.0.0.1 -uabc_test -pXXXXX -P3306 -A -e 'show processlist'| awk '{print $4}'|sort|uniq -c|sort -rn|head -103.4 相关知识3.4.1 索引

3.4.1.1 MySql不同的存储引擎

3.4.1.2 InnoDB B Tree索引实现

主键索引(聚集索引):

InnoDB存储引擎中页的大小为16KB,一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),指针类型也一般为4或8个字节,也就是说一个页(B Tree中的一个节点)中大概存储16KB/(8B 8B)=1K个键值(因为是估值,为方便计算,这里的K取值为[10]^3)。

也就是说一个深度为3的B Tree索引可以维护 10^3 10^3 10^3 = 10亿 条记录。

每个索引的左指针都是比自己小的索引/节点,右指针是大于等于自己的索引/节点。

3.4.2 B Tree索引检索3.4.2.1 主键索引检索

select * from table where id = 1

3.4.2.2 辅助索引检索

select * from table where name = 'a'

3.4.3 事物的隔离级别3.4.3.1 如何查看数据库的事务隔离级别

使用如下命令可以查看事务的隔离级别:

show variables like 'tx_isolation';

阿里云上的rds的隔离级别是read committed ,而不是原生mysql的“可重复读(repeatable-read)。

3.4.3.2 多版本并发控制MVCC

MySQL InnoDB存储引擎,实现的是基于多版本的并发控制协议——MVCC (Multi-Version Concurrency Control) (注:与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)。

MVCC最大的好处,相信也是耳熟能详:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能。

在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。

快照读:简单的select操作,属于快照读,不加锁。(当然,也有例外,下面会分析)。

select * from table where ?;

当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。

select * from table where ? lock in share mode; 加S锁 (共享锁)-- 下面的都是X锁 (排它锁)select * from table where ? for update;insert into table values (…);update table set ? where ?;delete from table where ?;3.4.4.3 场景模拟

修改事务隔离级别的语句:

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- READ UNCOMMITTED/READ COMMITTED/REPEATABLE READ/SERIALIZABLE

(1)脏读场景模拟

DROP TABLE IF EXISTS `employee`;CREATE TABLE `employee` ( `id` int(11) NOT NULL, `name` varchar(50) NOT NULL, `salary` int(11) DEFAULT NULL, KEY `IDX_ID` (`id`) USING BTREE) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ------------------------------ Records of employee-- ----------------------------INSERT INTO `employee` VALUES ('10', '1s', '10');INSERT INTO `employee` VALUES ('20', '2s', '20');INSERT INTO `employee` VALUES ('30', '3s', '30');

脏读场景模拟

(2)不可重复读模拟

DROP TABLE IF EXISTS `employee`;CREATE TABLE `employee` ( `id` int(11) NOT NULL, `name` varchar(50) NOT NULL, `salary` int(11) DEFAULT NULL, KEY `IDX_ID` (`id`) USING BTREE) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ------------------------------ Records of employee-- ----------------------------INSERT INTO `employee` VALUES ('10', '1s', '10');INSERT INTO `employee` VALUES ('20', '2s', '20');INSERT INTO `employee` VALUES ('30', '3s', '30');

不可重复读的重点是修改: 同样的条件, 你读取过的数据, 再次读取出来发现值不一样了。

(3)幻读场景模拟

表结构与数据如下:id不是主键,也不是唯一索引,只是一个普通索引,事务隔离级别设置的是RR,可以模拟到GAP锁产生的场景。

DROP TABLE IF EXISTS `emp`;CREATE TABLE `emp` ( `id` int(11) NOT NULL, `salary` int(11) DEFAULT NULL, KEY `IDX_ID` (`id`) USING BTREE) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ------------------------------ Records of emp-- ----------------------------INSERT INTO `emp` VALUES ('10', '10');INSERT INTO `emp` VALUES ('20', '20');INSERT INTO `emp` VALUES ('30', '30');

修改id=20的数据后,会再加多个锁:20会被加X锁,[10-20],[20-30]之间会被加GAP锁。

幻读的重点在于新增或者删除 (数据条数变化)。同样的条件, 第1次和第2次读出来的记录数不一样。

在标准的事务隔离级别定义下,REPEATABLE READ是不能防止幻读产生的。INNODB使用了2种技术手段(MVCC AND GAP LOCK)实现了防止幻读的发生。

3.4.4 Innodb的多种锁3.4.4.1 锁类型

3.4.4.2 行锁

只在Repeatable read和Serializable两种事务隔离级别下才会取得上面的锁。

3.4.4.3 意向锁

(1)场景

在mysql中有表锁,LOCK TABLE my_tabl_name READ; 用读锁锁表,会阻塞其他事务修改表数据。LOCK TABLE my_table_name WRITe; 用写锁锁表,会阻塞其他事务读和写。

Innodb引擎又支持行锁,行锁分为共享锁,一个事务对一行的共享只读锁。排它锁,一个事务对一行的排他读写锁。

这两种类型的锁共存的问题考虑这个例子:

事务A锁住了表中的一行,让这一行只能读,不能写。之后,事务B申请整个表的写锁。如果事务B申请成功,那么理论上它就能修改表中的任意一行,这与A持有的行锁是冲突的。

数据库需要避免这种冲突,就是说要让B的申请被阻塞,直到A释放了行锁。

(2)问题

数据库要怎么判断这个冲突呢?

(3)答案

无意向锁的情况下:

step1:判断表是否已被其他事务用表锁锁表

step2:判断表中的每一行是否已被行锁锁住。

有意向锁的情况下:

(4)总结

在无意向锁的情况下,step2需要遍历整个表,才能确认是否能拿到表锁。而在意向锁存在的情况下,事务A必须先申请表的意向共享锁,成功后再申请一行的行锁,不需要再遍历整个表,提升了效率。因此意向锁主要是为了实现多粒度锁机制(白话:为了表锁和行锁都能用)。

3.4.4.4 X/S锁

3.4.4.5 一条SQL的加锁分析

-- select操作均不加锁,采用的是快照读,因此在下面的讨论中就忽略了SQL1:select * from t1 where id = 10;SQL2:delete from t1 where id = 10;

组合分为如下几种场景:

(1)组合7的GAP锁详解读

Insert操作,如insert [10,aa],首先会定位到[6,c]与[10,b]间,然后在插入前,会检查这个GAP是否已经被锁上,如果被锁上,则Insert不能插入记录。因此,通过第一遍的当前读,不仅将满足条件的记录锁上 (X锁),与组合三类似。同时还是增加3把GAP锁,将可能插入满足条件记录的3个GAP给锁上,保证后续的Insert不能插入新的id=10的记录,也就杜绝了同一事务的第二次当前读,出现幻象的情况。

既然防止幻读,需要靠GAP锁的保护,为什么组合五、组合六,也是RR隔离级别,却不需要加GAP锁呢?

GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。而组合五,id是主键;组合六,id是unique键,都能够保证唯一性。

一个等值查询,最多只能返回一条记录,而且新的相同取值的记录,一定不会在新插入进来,因此也就避免了GAP锁的使用。

(2)结论

3.4.5 线上问题处理3.4.5.1 观察问题的几个常见库表

首先可以通过下属两个命令来查看mysql的相应的系统变量和状态变量。

# status代表当前系统的运行状态,只能查看,不能修改show status like '

百科

More+
首页/电脑版/网名
© 2026 NiBaKu.Com All Rights Reserved.