推广 热搜: 行业  机械  设备    系统  教师    参数  经纪  蒸汽 

B站崩了!线上故障如何快速排查?且不被微博发现……

   日期:2025-01-01     移动:http://sjzytwl.xhstdz.com/mobile/quote/86587.html

导读:前天晚上B站突然就崩了,到最后来了个404,直接被送上了热搜,也不知道微博小哥有没有被吓着……事情虽小,但影响巨大,线上故障快速排查始终是运维小哥们逃不过的宿命。

B站崩了!线上故障如何快速排查?且不被微博发现……

 

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

 

前言

 

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

 

线上问题定位思维导图

 

一、服务器层面

 

1、 磁盘

 

1)问题现象

 

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

 

 

 

或是类似如下告警信息:

 

 

2)排查思路

 

 ① 利用 df 查询磁盘状态

 

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

 

 

结果是:

 

 

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

 

 ②利用 du 查看文件夹大小

 

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

 

 

结果是:

 

 

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

 

 ③利用 ls 查看文件大小

 

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

 

 

结果是:

 

 

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

 

3)相关命令

 

 ① df

 

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

 

 

启动参数:

 

 

 

 

 

 ②du

 

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

 

 

启动参数:

 

 

 

 

 ③ls

 

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

 

 

启动参数:

 

 

 

 

2、 CPU过高

 

1)问题现象

 

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

 

2)排查思路

 

 ① 利用 top 查询CPU使用率最高的进程

 

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

 

 

结果是:

 

 

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

 

3)相关命令

 

 ① top

 

 

启动参数:

 

 

top进程内指令参数:

 

 

 

 


二、应用层面

 

1、 Tomcat假死案例分析

 

1) 发现问题

 

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

 

 

2)查询日志

 

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

 

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

 

 

3)获取内存快照

 

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

 

 

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

 

linux压缩dump为.tgz。

 

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

 

 

4)分析内存快照文件

 

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

 

 

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

 

 

 

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

 

 

 

 

5)相关知识

 

① JVM内存

 

 

②内存分配的流程

 

 

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

 

③GC

 

 

 

 

 

GC Roots有4种对象:

 

虚拟机栈(栈桢中的本地变量表)中的引用的对象,就是平时所指的java对象,存放在堆中。

 

方法区中的类静态属性引用的对象,一般指被static修饰引用的对象,加载类的时候就加载到内存中。

 

方法区中的常量引用的对象。

 

本地方法栈中JNI(native方法)引用的对象。

 

 

 

 

串行只使用单条GC线程进行处理,而并行则使用多条。

 

多核情况下,并行一般更有执行效率,但是单核情况下,并行未必比串行更有效率。

 

 

STW会暂停所有应用线程的执行,等待GC线程完成后再继续执行应用线程,从而会导致短时间内应用无响应。

 

 

Concurrent会导致GC线程和应用线程并发执行,因此应用线程和GC线程互相抢用CPU,从而会导致出现浮动垃圾,同时GC时间不可控。

 

 

 

新生代算法都是基于Coping的,速度快。

 

Parallel Scavenge:吞吐量优先。

 

吞吐量=运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)

 

 

 

Parallel Compacting

 

Concurrent Mark-Sweep(CMS)

 

 

 

 

 

 

 

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

 

 

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

 

 

 

 

 

 

 

2、 应用CPU过高

 

1)发现问题

 

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

 

 

2)查找问题进程

 

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

 

 

3)查找问题线程

 

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

 

 

4)查询线程详细信息

 

首先利用 printf "%x " 将tid换为十六进制:xid。

 

再利用 jstack | grep nid=0x -A 10 查询线程信息(若进程无响应,则使用 jstack -f ),信息如下:

 

 

5)分析代码

 

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

 

 

6) 获得结论

 

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

 

三、MySQL

 

1、死锁

 

1) 问题出现

 

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

 

 

2) 问题分析

 

①查询事务隔离级别

 

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

 

 

②查询数据库死锁日志

 

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

 

 

 

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

 

③分析代码

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

 

 

④获得结论

 

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

 

 

2、 慢SQL

 

1) 问题出现

 

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

 

 

2)问题分析

 

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

 

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

 

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

 

3) 场景一

 

①优化SQL

 

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

 

 

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

 

 

4) 场景二

 

①查询当前事务情况

 

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

 

 

 

 

 

 

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

 

②分析

 

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

 

 

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

 

 

③解决方案

 

 

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

 

 

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

 

 

3、 连接数过多

 

1) 问题出现

 

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

 

2) 解决方案

 

解决方案:

 

 

常用脚本如下:

 

 

4、 相关知识

 

1) 索引

 

①MySQL不同的存储引擎

 

 

②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亿 条记录。

 

 

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

 

2) B+ Tree索引检索

 

①主键索引检索

 

 

 

②辅助索引检索

 

 

 

3) 事物的隔离级别

 

①如何查看数据库的事务隔离级别

 

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

 

 

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

 

 

 

②多版本并发控制MVCC

 

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

 

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

 

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

 

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

 

 

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

 

 

 

③场景模拟

 

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

 

 

 

 

脏读场景模拟

 

 

 

 

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

 

 

 

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

 

 

 

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

 

 

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

 

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

 

4) Innodb的多种锁

 

①锁类型

 

 

表锁的优势:开销小;加锁快;无死锁。

 

表锁的劣势:锁粒度大,发生锁冲突的概率高,并发处理能力低。

加锁的方式:自动加锁。查询操作(SELECT),会自动给涉及的所有表加读锁,更新操作(UPDATE、DELETE、INSERT),会自动给涉及的表加写锁。也可以显示加锁。

 

共享读锁:lock table tableName read

 

独占写锁:lock table tableName write

 

批量解锁:unlock tables

 

②行锁

 

 

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

 

③意向锁

 

 

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

 

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

 

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

 

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

 

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

 

 

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

 

 

无意向锁的情况下:

 

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

 

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

 

有意向锁的情况下:

 

step1:不变

 

step2:发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的写锁会被阻塞。

 

 

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

 

④X/S锁

 

 

⑤一条SQL的加锁分析

 

 

 

 

组合分为如下几种场景:

 

 

 

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锁的使用。

 

 

Repeatable Read隔离级别下,id列上有一个非唯一索引,对应SQL:delete from t1 where id = 10; 首先,通过id索引定位到第一条满足查询条件的记录,加记录上的X锁,加GAP上的GAP锁,然后加主键聚簇索引上的记录X锁,然后返回;然后读取下一条,重复进行。直至进行到第一条不满足条件的记录[11,f],此时,不需要加记录X锁,但是仍旧需要加GAP锁,最后返回结束。

 

什么时候会取得gap lock或nextkey lock  这和隔离级别有关,只在REPEATABLE READ或以上的隔离级别下的特定操作才会取得gap lock或nextkey lock。

 

5) 线上问题处理

 

①观察问题的几个常见库表

 

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

 

 

MySQL 5.7.6开始后改成了从如下表获取:

 

 

之前是从如下表获取:

 

 

比较常用的系统变量和状态变量有:

 

 

 

5、 一些建议

 

1) 小表驱动大表

 

nb_soft_nature:小表

 

nb_soft:大表

 

package_name:都是索引

 

MySQL 表关联的算法是Nest Loop Join(嵌套循环连接),是通过驱动表的结果集作为循环基础数据,然后一条一条地通过该结果集中的数据作为过滤条件到下一个表中查询数据,然后合并结果。

 

 

 

nb_soft_nature 中只有24条数据,每条数据的package_name连接到nb_soft表中做查询,由于package_name在nb_soft表中有索引,因此一共只需要24次扫描即可。

 

 

 

同上,需要100多万次扫描才能返回结果

 

2) 使用自增长主键

 

结合B+Tree的特点,自增主键是连续的,在插入过程中尽量减少页分裂,即使要进行页分裂,也只会分裂很少一部分。并且能减少数据的移动,每次插入都是插入到最后。总之就是减少分裂和移动的频率。

 

四、Redis 

 

1、 问题处理思路

 

 

2、 内存告警

 

时常会出现下述异常提示信息:

 

 

1) 设置合理的内存大小

 

设置maxmemory和相对应的回收策略算法,设置最好为物理内存的3/4,或者比例更小,因为redis复制数据等其他服务时,也是需要缓存的。以防缓存数据过大致使redis崩溃,造成系统出错不可用。

 

 

 

 

 

2) 设置合理的内存淘汰策略

 

 

 

 

3) 查看大key

 

 

安装工具dbatools redisTools,列出最大的前N个key

 

 

 

得到如下结果:

 

 

 

原生命令为:

 

 

分析rdb文件中的全部key/某种类型的占用量:

 

 

查看某个key的内存占用量:

 

 

 

 

3、 Redis的慢命令

 

1) 设置Redis的慢命令的时间阈值(单位:微妙)

 

 

 

 

 

 

 

2) 查看Redis的慢命令

 

 

4、 连接过多

 

 

 

 

 

5、 线上Redis节点挂掉一个之后的处理流程

 

1) 查看节点状态

 

执行 cluster nodes 后发现会有一个节点dead:

 

 

2) 移除错误节点

 

 

ip:port> cluster forget 61c70a61ad91bbac231e33352f5bdb9eb0be6289

CLUSTER FORGET <node_id> 从集群中移除 node_id 指定的节点

 

 

 

 

 

3) 恢复节点

 

 

 

 

 

 

s3:本次待添加的从节点ip:port

m3:主节点的ip:port

6147bf416ef216b6a1ef2f100d15de4f439b7352:主节点编号

 

五、网络

 

1、 排查流程

 

1) 现象出现

 

在非压测或者高峰期的情况下,突然出现大量的503等错误码,页面无法打开。

 

2) 查看是否遭受了DOS攻击

 

当Server上有大量半连接状态且源IP地址是随机的,则可以断定遭到SYN攻击了,使用如下命令可以让之现行。

 

 

3) 查看TCP连接状态

 

首先利用以下查看tcp总连接数,判断连接数是否正常:

 

 

然后利用如下命令判断各个状态的连接数是否正常:

 

 

根据上述信息,如果TIME_WAIT 状态数量过多,可利用如下命令查看连接CLOSE_WAIT最多的IP地址,再结合业务分析问题:

 

 

2、 相关知识

 

1) TCP连接

 

TCP三次握手四次挥手

 

 

为什么在第3步中客户端还要再进行一次确认呢?这主要是为了防止已经失效的连接请求报文段突然又传回到服务端而产生错误的场景:

 

所谓"已失效的连接请求报文段"是这样产生的。正常来说,客户端发出连接请求,但因为连接请求报文丢失而未收到确认。于是客户端再次发出一次连接请求,后来收到了确认,建立了连接。数据传输完毕后,释放了连接,客户端一共发送了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,没有"已失效的连接请求报文段"。

 

现在假定一种异常情况,即客户端发出的第一个连接请求报文段并没有丢失,只是在某些网络节点长时间滞留了,以至于延误到连接释放以后的某个时间点才到达服务端。本来这个连接请求已经失效了,但是服务端收到此失效的连接请求报文段后,就误认为这是客户端又发出了一次新的连接请求。于是服务端又向客户端发出请求报文段,同意建立连接。假定不采用三次握手,那么只要服务端发出确认,连接就建立了。

 

由于现在客户端并没有发出连接建立的请求,因此不会理会服务端的确认,也不会向服务端发送数据,但是服务端却以为新的传输连接已经建立了,并一直等待客户端发来数据,这样服务端的许多资源就这样白白浪费了。

 

采用三次握手的办法可以防止上述现象的发生。比如在上述的场景下,客户端不向服务端的发出确认请求,服务端由于收不到确认,就知道客户端并没有要求建立连接。

 

SYN攻击时一种典型的DDOS攻击,检测SYN攻击的方式非常简单,即当Server上有大量半连接状态且源IP地址是随机的,则可以断定遭到SYN攻击了,使用如下命令可以让之现行:

 

 

 

2) 一些常见问题

 

 

因为服务端在LISTEN状态下,收到建立请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而连接关闭时,当收到对方的FIN报文时,仅仅表示对方没有需要发送的数据了,但是还能接收数据,己方未必数据已经全部发送给对方了,所以己方可以立即关闭,也可以将应该发送的数据全部发送完毕后再发送FIN报文给客户端来表示同意现在关闭连接。

 

从这个角度而言,服务端的ACK和FIN一般都会分开发送。

 

 

TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

 

 

虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。

 

在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。

 

MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。

 

六、业务异常日志

 

1、 问题出现

 

主要是通过业务日志监控主动报警或者是查看错误日志被动发现:

 

 

2、 日志分析

 

1) 确认日志格式

 

日志格式如下:

 

 

2) 在日志文件中检索异常

 

利用如下命令可获得异常的详细信息:

 

 

 

根据日志格式和日志信息,可获得traceId为489d71fe-67db-4f59-a916-33f25d35cab8,然后利用以下指令获取整个流程的日志信息:

 

 

 

3) 代码分析

 

然后根据上述流程日志找到对应的代码实现,然后进行具体的业务分析。

 

作者丨小峯来源丨公众号:阿里技术(ID:ali_tech)dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn

本文地址:http://sjzytwl.xhstdz.com/quote/86587.html    物流园资讯网 http://sjzytwl.xhstdz.com/ , 查看更多

特别提示:本信息由相关用户自行提供,真实性未证实,仅供参考。请谨慎采用,风险自负。


0相关评论
相关最新动态
推荐最新动态
点击排行
网站首页  |  关于我们  |  联系方式  |  使用协议  |  版权隐私  |  网站地图  |  排名推广  |  广告服务  |  积分换礼  |  网站留言  |  RSS订阅  |  违规举报  |  鄂ICP备2020018471号