我们在开发中,可能会遇到千千万万的异常分析和排查,这是开发人员的必备技能,除了业务性的异常,还有一些比较棘手非业务性的问题,不常用到,但是用到的时候我们可能就比较慌,不知道怎么解决,所以今天我就把我平时遇到的一些比较棘手的,同时也是大家都可能会遇到的异常做一下分享,希望能够帮助到大家,对我自己来说也是一个经验总结和手册积累。
OOM问题
首当其冲的就是OOM问题,这个问题是严重而且比较常见的,OOM问题在生产环境中,一旦出现,一般会是非常严重的问题,服务可能会挂掉。
分析问题,我们需要追溯到问题的本质。OOM报错提示的本质是当JVM无法分配更多的内存来满足程序的需求时就会发生,通常是因为程序使用的内存超过了JVM所允许的最大限制。
咱们就根据JVM和内存的结构,可以将常见的OOM问题类型分为这么几种。
1. 堆内存OOM
表现形式为,服务器的日志一般会打印:java.lang.OutOfMemoryError: Java heap space
,这是我们平时见到出现最多的OOM异常,通常我们需要在配置Java启动参数的时候添加如下:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heapdump.hprof
这样在发生OOM时,程序会自动把当时的内存使用情况,dump保存到指定的文件。
然后我们需要使用MAT或者使用JDK自带的 Java visualvm来分析dump 文件,找出导致OOM 的代码 。当时我们也需要注意真的是服务器内存不够的缘故。
2. 栈内存OOM
出现栈内存OOM问题的异常信息通常是这样的:java.lang.OutOfMemoryError: unable to create new native thread
,如果实际工作中,出现这个问题,一般是由于创建的线程太多,或者设置的单个线程占用内存空间太大导致的。这个时候需要排查服务的线程数量。
推荐使用线程池,可以减少线程的创建,有效控制服务中的线程数量。
3. 栈内存溢出
出现栈内存溢出问题的异常信息是:java.lang.StackOverflowError,这个就太熟悉了,很多时候是因为我们业务代码中写的一些递归调用,递归的深度超过了JVM允许的最大深度,可能会出现栈内存溢出问题。
其实递归的写法是有可以替代的方式的,推荐大家采用Map+for循环的方式来代替递归,这种方式我在开发中用过,感觉很不错,可以给大家推荐,大家可以看下这篇文章:Map+for循环代替递归。
4. GC OOM
出现GC OOM问题时异常信息为:java.lang.OutOfMemoryError: GC overhead limit exceeded
。GC OOM一般是由于JVM在GC时,对象过多,导致内存溢出,建议调整GC的策略。
在老代80%时就是开始GC,并且将-XX:SurvivorRatio(-XX:SurvivorRatio=8)和-XX:NewRatio(-XX:NewRatio=4)
设置的更合理。
5. 元空间OOM
出现元空间OOM问题时异常信息为:java.lang.OutOfMemoryError: Metaspace
,JDK8之后使用Metaspace来代替永久代,Metaspace是方法区在HotSpot中的实现。
这个问题一般是由于加载到内存中的类太多,或者类的体积太大导致的。如果生产环境中出现了这个问题,可以通过下面的命令修改元空间大小:
-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
CPU100%问题
线上服务出现CPU100%问题,出现这个问题,是由于服务长时间占用CPU资源导致的。主要原因有下面这几种:
- 内存消耗过大,导致Full GC次数过多
- 代码中有大量消耗CPU的操作,导致CPU过高,系统运行缓慢;
- 由于锁使用不当,导致死锁。
- 随机出现大量线程访问接口缓慢。
不管什么问题,既然是CPU飙升,核心思路就是先得查下是服务器的哪个进程导致的问题。
步骤:
1. 使用 top -c 命令定位当前进程
```csharp
top -c,可以显示当前进程的运行列表,然后输入 P,按照 CPU 的使用率进行排序。
```
假如 显示进程 PID 为 11111的 Java进程负载消耗最大。
如果是其他组件服务导致的,那我们就针对具体组件服务进行分析,如果是业务Java服务系统,可以根据下面方式继续分析是哪里的代码出现的问题。
2. 定位负载最大进程对应的线程
```csharp
使用命令 top -Hp 11111找到这个进程对应的线程,然后输入 P ,按照 CPU 的使用率进行排序。
```
假设2479508的线程耗费 CPU 最大。
3. 定位哪段代码导致 CPU 过高
因线程 PID 2479508 是十进制,我们需要将其转换为 十六进制 ,为什么要将十进制转换为十六进制 ?因为计算机需要二进制,十六是二的四次方,而十进制不是二的整数次幂,十六进制方便我们可读。比如,其转换结果是:25d594
接着,我们需要使用 jstack 打印进程的堆栈信息,再通过 grep 查看对应线程相关的东西。
jstack 2479501 | grep ‘25d594’ -C5 --color
这个时候就可以打印出代码,基本上帮助我们定位到出问题的位置,可以看到是哪个类中的哪个方法导致此次 CPU 100% 的原因了。
接口超时问题
有时候我们会在生产上遇到这样的一个场景:我们提供的某个API接口,响应时间原本一直都很快,但在某个不经意的时间点,突然出现了接口超时。可能会有很多原因,但是突然的异常有时候会让我们不知所措,但是我们需要有一个排查问题的思路,就是先外后内,先简单后复杂。
- 首先我们需要排除网络抖动或者服务器或者服务网络不通导致的原因,可以看看其他接口或者服务是否正常。
- 网络没有问题,我们再来分析日志,首先看请求是否转发到服务端,没有的话就得分析网关服务或者Nginx的问题。
- 如果在日志中能够看到请求到了服务端,我们再继续往下分析日志,看看到底是SQL查询问题,还是代码逻辑问题。
这就是解决问题的一个思路,大家可以参考下,我这边也列举了几种常见的原因。
1. 网络异常
- 网络抖动导致丢包现象。
- 带宽被占满,带宽指的是在一定时间内传输数据的大小,比如:1秒传输了10M的数据。如果用户请求量突然增多,超出了1秒10M的上限,比如:1秒100M,而服务器带宽本身1秒就只能传输10M,这样会导致在这1秒内,90M数据就会延迟传输的情况,从而导致接口超时的发生。
2. 线程池线程被打满
线程池线程被打满,意味着线程池里面的所有线程都在执行任务,导致本次请求被根据线程池的任务拒绝策略,可能被放在队列中等待,或者丢弃,导致请求迟迟得不到响应,从而导致接口超时。
所以我们得需要分析为什么线程池中的线程数都被占用了,有可能是:
- 线程池的核心线程数和最大线程数设置不合理。
- 或者需要思考为什么所有的线程一直在被卡着执行任务,线程得不到释放,我们需要分析其原因,是否因为死锁导致,或者数据库查询太久或者三方请求得不到响应。
- 或者我们需要思考针对不同的业务拆分不同的线程池来去使用。
3. 数据库死锁
当我们的日志中如果出现如下:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
那就说明数据库可能发生了死锁,在MySQL中,死锁通常发生在并发访问数据库时,MySQL死锁的原因可以归结为以下几种情况:
-
互斥资源的竞争
MySQL使用行级锁,这意味着在一个事务中,某些行可能会被锁定,使得其他事务无法访问这些行。如果多个事务竞争相同的资源并且请求锁的顺序不同,就可能导致死锁。例如:- 事务A锁定行1,尝试锁定行2。
- 事务B锁定行2,尝试锁定行1。
-
事务执行时间过长
长时间运行的事务会持有锁更长时间,从而增加死锁的可能性。尤其是在高并发环境中,长时间持有锁的事务更容易与其他事务发生冲突。 -
不一致的锁定顺序
如果不同事务以不同的顺序请求相同的资源,就可能导致循环等待。例如,事务A先锁定资源R1,再锁定资源R2;而事务B先锁定资源R2,再锁定资源R1,这就可能导致死锁。 -
缺乏索引
缺乏适当的索引会导致表扫描,增加锁定的行数,从而增加发生死锁的概率。例如,在一个没有索引的表上执行更新操作时,MySQL可能会锁定整个表或大量行。 -
行锁升级
InnoDB存储引擎在某些情况下会将行锁升级为表锁。如果一个事务持有大量的行锁,并且其他事务尝试锁定同一张表中的其他行,这可能会导致死锁。 -
锁范围问题
当一个查询涉及范围条件(如BETWEEN、LIKE等)时,MySQL可能会锁定比预期更多的行,从而增加死锁的可能性。例如:UPDATE users SET age = age + 1 WHERE age BETWEEN 20 AND 30;
这个查询可能会锁定所有age在20到30之间的行,如果其他事务也尝试访问这些行,可能会导致死锁。
4. 超时时间设置过短
通常情况下,建议我们在调用远程API接口时,要设置连接超时时间和读超时时间这两个参数,并且可以动态配置。
这样做的好处是,可以防止调用远程API接口万一出现了性能问题,响应时间很长,把我们自己的服务拖挂的情况发生。
比如:你调用的远程API接口,要100秒才返回数据,而你设置的超时时间是100秒。这时1000个请求过来,去请求该API接口,这样会导致tomcat线程池很快被占满,导致整个服务暂时不可用,至少新的请求过来,是没法即使响应的。
5. 无限递归
有些无限递归隐藏的比较深,但如果某次有人误操作,把某个分类的parentId指向了它自己,这样就会出现无限递归的情况。导致接口一直不能返回数据,最终会发生堆栈溢出。
6. sql语句没走索引
mysql在执行某条sql语句之前,会通过抽样统计来估算扫描行数,根据影响行数、区分度、基数、数据页等信息,最后综合评估走哪个索引。如果数据量非常大,查询时间过长,就会导致API接口出现超时问题。
7. 服务OOM
服务堆栈溢出导致接口异常。
索引失效问题
我们可以通过explain关键字,查看sql的执行计划,可以确认索引是否失效。导致索引失效的常见原因可能如下:
- 不满足最左匹配原则。
- 使用了select *
- 索引列上有计算
- 索引列上用了函数
- 字段类型不同,比如字符串类型字段,你传入int类型,就会导致索引失效。
- like左边包含%
- 列对比,比如检索两个字段值一致的数据(name=realName,假设name有索引,此时索引会失效)。
- 使用Or关键字。
索引失效情况比较多,需要涉及到底层的原理,一两句说不清,后续我会给大家进行详细说明,这里先简单列举出来几种情况。
磁盘问题
查看磁盘命令
df -Hl
最快的解决办法是,将/tmp文件夹中的文件删除,可以释放一些磁盘空间。然后找到日志文件,删除7天以前的日志。
为了防止这种情况的发生,我们就需要找到根本原因,比如日志没有设置指定时间段内的日志自动清理机制,以及日志打印机制需要合理设置,同时需要对磁盘内存等服务器情况进行监控。
MQ消息积压问题
这个问题我到时候会专门写一篇文章进行说明,因为这个问题比较常见,也比较重要,这里先略过。
调用接口报错
我这里主要给大家对各种报错码进行详细说明。
1. 返回401
一般生产环境出现这个问题,是由于没有通过接口的登录认证。
出现这种情况,一般用户在尝试访问受保护的资源前,需要通过某种形式的身份验证(如登录),但如果未能正确提供必要的认证信息,如Token、用户名和密码等。就会出现返回码是401的情况。
2. 返回403
如果生产环境请求某个接口,返回码是403,则说明目前没有访问资源的权限。
要注意和401的区别:
- 401着重于认证问题,即用户没有提供正确的身份验证信息。
- 而403则是在认证成功的基础上,用户没有足够的权限去访问请求的资源。
要解决这个问题,我们需要给接口的调用方,分配相应的访问权限。
3. 返回404
请求的接口地址,现在已经不存在了,才会报404。
比如接口名称修改,或者删除等等导致接口不存在了。还有一种情况就是网关或者Nginx配置有修改。
4. 返回405
如果请求的接口,返回码为405,一般是请求方式错误导致的。
最常见的是:接口只支持post方式,但发送的却是get请求。或者接口只支持get方式,但发送的却是post请求。
5. 返回500
如果请求的接口,返回码为500,一般是出现了服务的内部错误。
我们只能查看接口的错误日志,来定位和排查问题。建议出现异常时,把接口请求参数打印出来,方便后面复现问题。
6. 返回502
如果请求的接口,返回码为502,一般是出现了服务不可用的情况。
有两种情况:
- 服务器正在重启中。
- 服务挂掉了。
7. 返回504
如果请求的接口,返回码为504,一般由于网关或者接口超时导致的。接口返回数据的耗时,大于网关设置的超时时间,就会出现这个问题。
出现这种情况,一般需要优化接口相关的代码。