本文记录了我在一个真实业务场景中踩过的一个坑:Logstash 同步 MySQL 到 Elasticsearch 越跑越卡,最终发现是 JVM GC 造成的问题。
起因是为了优化查询,我们引入了 ES;结果同步慢了,又引入了 Logstash;结果 Logstash 也慢了......从此踏上了排查之路。希望这篇文章能帮到在类似场景挣扎的你。
背景:分库分表 + 搜索优化 = ES 出场
我们业务用的是 MySQL + ShardingJDBC 做的分库分表,一开始挺好用的,读写都能扛。
但随着业务发展,查询变得越来越复杂,尤其是那种跨库模糊查、聚合查、排序查,ShardingJDBC 就开始吃力了。
内心OS:这个shardingjdbc真的越来越多坑~
于是我们决定:上 Elasticsearch,搞个同步方案,用它来支撑复杂的查询场景。
引入 Logstash:同步 MySQL 到 ES
要把 MySQL 的数据实时同步到 ES,我们一开始调研了几个方案:
- Canal:对 binlog 做解析,但改动大;
- Debezium:需要 Kafka 支撑;
- DataX:适合离线批量;
- Logstash:简单直接,用 JDBC 插件就能搞定。
于是我们选了 Logstash + JDBC + Elasticsearch output 的方案:
bash
input {
jdbc {
jdbc_connection_string => "jdbc:mysql://xxx"
jdbc_user => "root"
statement => "SELECT * FROM table WHERE updated_at > :sql_last_value"
schedule => "* * * * *"
}
}
output {
elasticsearch {
hosts => ["http://localhost:9200"]
index => "my_index"
}
}
一开始运行非常顺利,基本上延迟控制在秒级,业务方反馈也很满意。
问题来了:Logstash 越跑越卡
Logstash 跑了几天后,同步开始变得 越来越慢。
起初是几分钟的延迟,后来变成十几分钟,最后接近实时不同步,导致 ES 中数据滞后,严重影响业务查询。
我们检查了:
- MySQL 正常,没什么大查询;
- ES 也没瓶颈;
- 机器资源还有富余;
- Logstash 本身没报错,但 CPU 一直很高。
于是开始怀疑是 JVM 内存问题。
提高内存?没用!
我们试着把 Logstash 的 jvm.options
中的堆内存调大:
bash
-Xms12g
-Xmx12g
想着是不是内存不够、老 GC 导致抖动。
结果并没有什么用,该卡还是卡。于是,我们转向更底层的方向:GC 分析。
jstat 一查:Full GC 爆炸!
我们使用 jstat -gc <pid> 1000
采样,看到非常惊人的 GC 数据:
bash
YGC YGCT FGC FGCT
4047 234s 19406 16351s
- Young GC 只发生了 4000 多次,总共 4 分钟;
- Full GC 居然有 1.9 万次,耗时超过 4.5 小时!
这时候我们意识到:卡顿的根源在于 JVM 的 GC,准确说是 Full GC 太频繁了。
看看 GC 策略:居然还在用 CMS?
翻看了 Logstash 默认的 JVM 配置:
ruby
bash
复制编辑
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly
也就是说,它用的是 CMS 垃圾回收器。
而 CMS 的特点是:
- 老年代不压缩;
- 遇到碎片化或内存不足时就会频繁 Full GC;
- 一旦 Full GC 多,性能直接雪崩。
难怪跑几天后就崩了:随着数据增多、对象生命周期拉长、老年代对象堆积,GC 开始崩坏式触发。
小结一下当前发现的问题
项 | 说明 |
---|---|
问题现象 | Logstash 越跑越慢,延迟越来越大 |
表面现象 | CPU 高、内存不满、无报错 |
本质原因 | Full GC 次数远远超过 YGC,消耗大量时间 |
根本诱因 | CMS GC 不适合长期运行、大内存场景,内存碎片无法整理 |
解决方案:切换 G1 GC,效果立竿见影!
经过分析,我们决定抛弃 CMS,改用 G1 GC ,它专门为 大堆内存、低延迟场景 优化,非常适合 Logstash 这种长时间运行的服务。
最终 JVM 参数如下:
bash
# 设置堆大小(固定,避免动态扩展)
-Xms20g
-Xmx20g
# 使用 G1 GC(替代 CMS,适合大内存)
-XX:+UseG1GC
# 控制 GC 暂停时间(单位:毫秒,可根据业务调节)
-XX:MaxGCPauseMillis=200
# 更早触发 GC,避免老年代满
-XX:InitiatingHeapOccupancyPercent=45
# 启用并行处理引用,提升 GC 效率
-XX:+ParallelRefProcEnabled
# 增加 GC 日志(可用于后期调优)
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution
-Xloggc:/var/log/logstash/gc.log
🔥 调整后效果非常明显:
- 已连续运行 48 小时,无 Full GC;
- CPU 降至 30% 左右;
- 延迟回到秒级;
- GC 日志正常,Young GC 占主导。
总结
以前背了这么多JVM的八股文,没想到终于派上用场了,不过之前看一些更细的还要分新生代跟老年代的比例之类的。我们是大力出奇迹型
这次问题排查过程中,我有几个感悟:
✅ CMS 已经老了,G1 是更好的替代
Logstash 默认用 CMS 已不适合现代高负载场景,建议直接切换到 G1 GC。
✅ GC 日志要打开!
开启 -Xloggc
日志,从一开始就观察内存与回收行为,可以极大提升排查效率。
✅ 日志中没有报错 ≠ 没有问题
GC 卡顿不会报错,但会拖慢整体吞吐,这是"隐性"的大杀器。