Fork me on GitHub

一些系统调优案例的总结

1. SQL造成大对象JVM频繁full gc

哪些场景会频繁full gc?

  • 内存泄漏(对象没有及时被回收,某些代码问题导致)
  • 死循环一直创建对象
  • 一直创建大对象

主要看下大对象的场景:

  • 可能来源于数据库,查询的结果集太大。
  • 第三方接口传输的对象
  • 消息队列的消息太大

一个排查步骤

  • 是否有吻合时间点的发布?机器的CPU、网络IO、带宽是否正常?如果有先回滚和隔离机器。
  • 观察内存监控,是否有逐渐上升的内存占用,且触发GC之后没办法释放内存(内存泄漏)
  • 如果无内存泄漏(内存在每次fullgc之后能降下来)
    • jmap -histo工具查看存活对象
    • jmap -dump:format=b,file=file [pid]命令dump内存快照。利用mat工具分析
      • dump内存占用很小,考虑堆外内存的占用。
      • dump分析占用最多的对象和相关调用栈,查看可疑代码。
  • 比如:sql查出来的大对象一般伴随着网络IO变大,时间点吻合可以重点欢迎是不是sql的原因。

案例原因

  • 一个in list查找,参数没有判空,导致条件没有带上,查询命中很多记录。
  • sql一般会复用mybatis模板,某个sql模板如果参数没校验,可能产生大量记录的查询。

2. 内存泄漏案例

  • 内存溢出:程序没有足够的内存分配对象,会发生内存溢出。
  • 内存泄漏:对象没有及时释放,导致内存逐渐增加,就是内存泄漏。内存泄漏一般不会造成程序无法运行,但是会不断的累计造成内存不足,明显现象:触发了GC之后内存占用率不下降,且缓慢上升。
    image-20220701103254617

案例及原因

  • 本地内存存放商品数据,如果只存放热点商品,内存占用不会太大。但是如果全量商品加载到内存中,那么内存会不够。
  • 每个缓存记录加了7天的失效时间,保证淘汰掉不是热点访问的商品数据。
  • 经过一次重构之后,过期时间功能失效,没有过期时间淘汰,本地缓存越来越大。
  • 在一定时间之后,报警内存不足。dump内存之后发现缓存了大量的商品数据,造成内存泄漏,且因为本地缓存的引用一直没办法被GC清除。

3. 磁盘IO导致线程阻塞

  • 日常的CPU高的问题可以通过标准步骤:
    • top -H - p pid来查看cpu使用率高的线程id
    • 线程id转换16进制
    • jstack -l pid | grep ‘线程id16进制’
  • 但是有时候cpu突刺可能是在一个很短的时间内发生,此时可以采用shell脚本自动执行jstack,比如5s执行一次stack,每次执行完成之后放到不同的日志文件中。只保留2000个日志文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/bin/bash
num=0
log="/tmp/jstack_thread_log/thread_info"

cd /tmp
if [ ! -d "jstack_thread_log" ]; then
mkdir jstack_thread_log
fi

while ((num <= 10000));

do

ID=`ps -ef | grep java | grep gaea | grep -v "grep" | awk '{print $2}'`

if [ -n "$ID" ]; then
jstack $ID >> ${log}
fi

num=$(( $num + 1 ))

mod=$(( $num%100 ))

if [ $mod -eq 0 ]; then
back=$log$num
mv $log $back
fi

sleep 5

done

下一次响应变慢的时候,找到对应时间点的jstack日志文件,里面有很多线程阻塞在logback输出日志的过程,后来精简了log + 配置日志异步输出,则问题得以解决。

4. 死锁案例

1. update顺序导致的死锁

一个关单定时任务和人工后台关单的死锁案例

背景:
image-20220701103317304

  1. 定时关单的sql及加锁分析。
    1
    update order set status = 'canceled' where created_time > '2020-01-01 08:00:00' and created_time < '2020-01-01 08:00:00' and status = 'UNPAID';

因为created_time是二级索引,会先给二级索引加锁,再给对应的聚簇索引加锁。(加锁步骤如下所示)
image-20220701103343047

  1. 后台关单的sql及加锁分析
1
update t_order set status = 'CANCELLED' where id in (2, 3, 5) and status = 'UNPAID'

直接在聚簇索引的记录上加锁:
image-20220701103353119

可以看到定时任务的sql对主键索引加锁顺序是5,4,3,2。而后台的sql对主键加锁顺序是2,3,5。

比如第一个sql对3加锁之后,尝试对2加锁时发现后台取消的sql事务已经对2加锁,而此时第二个sql又尝试对3加锁,两个sql互相等待对方的锁,也就发生了死锁。

解决办法:

  • sql从语句上0保证加锁顺序一致。也就是都按照id排序加锁防止死锁。
  • 或者让批量操作,变为单个执行,减少锁占用的粒度和时间。
  • 取消订单都加一个分布式锁排队执行。

5. 后台导出数据引发的OOM

问题描述:公司的后台系统,导出功能之后偶发性的发生OOM。

排查步骤

  1. 因为是偶发性的,所以认为是后台系统的内存不足,单方面加大了内存。
  2. 但是没有解决问题,只是降低了OOM的频率。加入参数-XX:+HeapDumpOnOutOfMemoryError参数,来触发OOM时自动dump内存。
  3. dump内存之后进行分析,确认了大量string对象,跟其引用都是导出excel处服务。
  4. 结合Arthas查看,导出执行时间比较长,但是会在短时间内执行导出很多次,且用户session和参数是一致的。
  5. 现象总结就是短时间内多次导出相同的数据,且操作人是一个,产生大量的string对象。未能及时在年轻代清除(比如空间担保机制、比如超过了对象的阈值、比如创建对象速度过快未进行标记就晋升到老年代)。
  6. 最后排查是点击导出按钮没有在点击时置灰,导出交互会引导操作人多次点击,每次点击都会去读数据库的几万数据且导出excel,方法执行比较慢,对象无法回收,导致内存溢出。
  7. 最后解决:前端导出之后置灰,响应了之后才可以继续点击,且减少了查询订单信息的非必字段来瘦身对象,且分批次查询数据库改为了多线程。之后没有出现OOM。

6. 网站流量暴增后,网站反应出现卡顿

问题描述:在测试环境的页面速度很快,但到生产会变慢。推测访问量增大之后,对象创建变多,频繁的GC导致STW。

  1. 定位:使用jstat -gc pid指令观察JVM的GC次数,且观察Eden区增长的速度,来观察到触发MinorGC频率特别高,FullGC的触发频率次数和MinorGC几乎一致,甚至更多,这样导致STW业务线程,造成网页出现卡顿。
  2. 猜想是触发了老年代的空间担保机制,新生代在minorGC之前发现老年代可用连续空间比新生代所有对象小(或者是历次晋升的平均大小),就会触发空间担保机制,进而触发一次fullGC。
  3. 调大内存,老年代内存变大,且新生代的内存也变大,对象触发MinorGC的频次减少。
  4. 但是还是会发生卡顿,且卡顿时间比之前更长,发生卡顿频率降低。
  5. 继续jstat -gc pid观察,发现GC的次数不多,但是每次GC的时间变大,推测是因为内存调整变大,GC时间被拉长了。
  6. jinfo看到使用的是parallel垃圾回收器的组合,想到特点是多线程进行回收,但是整个回收过程会STW,造成GC期间不能正常使用。
  7. 这里替换为parNew + CMS垃圾回收器,CMS在垃圾回收阶段可以和用户线程并发执行,能有效的减少STW时间。
  8. 替换之后要设置合理的触发CMSGC的阈值,如果太大,比如在垃圾回收过程因为内存不足再次因为Concurrent mode failure触发full gc,会导致CMS退化为Serial Old单线程回收,整个回收过程都会STW。 总之使用了CMS要注意GC日志中是否有Concurrent mode failure关键字。

7. 未设置元空间大小应用启动很慢

MetaSpace会存放静态变量和很多类的信息,在启动时如果不设置元空间大小,则默认21M。

  1. 启动时间很长,且GC日志显示频发触发FullGC metaSpace不足
  2. 查看启动参数,未设置初始化的metaSpace大小。默认是21m
  3. 启动时会加载很多类,大量使用MetaSpace,21m很快被用完触发FullGC,然后MetaSpace动态扩容,然后再full gc再扩容的过程,使得启动变慢。
  4. -XX:MetaspaceSize 参数意思是触发元空间fullgc的阈值。max的参数代表元空间最大值,设置为-1时也受制于直接内存的大小。
-------------本文结束感谢您的阅读-------------

本文标题:一些系统调优案例的总结

文章作者:夸克

发布时间:2021年11月25日 - 10:11

最后更新:2022年07月01日 - 10:07

原始链接:https://zhanglijun1217.github.io/2021/11/25/一些系统调优案例的总结/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。