这周遇到了一个有意思的问题:一个运行了大半年的 Docker 容器突然 CPU 占用飙升到 90% 以上。本以为是个简单的问题,结果排查下来却发现了一些有趣的细节,在这里和大家分享下。
问题现象
事情是这样的:周三早上收到监控报警,说是测试环境的一个 Docker 容器 CPU 使用率异常。登上去一看,好家伙,CPU 稳定在 95% 左右。这个容器跑的是我们的 Java 后端服务,平时 CPU 也就 20%-30%,这次直接起飞了。
排查过程
首先用 top 命令看了下进程状态,发现是 Java 进程占用 CPU 特别高。第一反应是不是发生了内存泄漏,导致频繁 GC。
通过这个命令发现,CPU 主要被一个线程占用。接着用 jstack 导出了线程栈:
jstack <pid> > thread_dump.txt
查看线程栈时发现了问题所在:有一个处理缓存的线程在疯狂执行本地缓存的过期检查,而且是在死循环里。
深入代码一看,原来是前段时间为了优化缓存,加了个本地缓存的二级缓存机制。但是在设置缓存过期时间的地方,单位用错了 - 把分钟写成了毫秒。导致缓存项在创建后立即就过期了,然后触发重新加载,形成了恶性循环。
解决方案
- 紧急修复:先把时间单位改对
- 增加了缓存相关的监控指标
- 在代码里加了单位转换的工具类,避免手写时间单位转换
教训总结
- 时间单位这种细节真的很容易出问题,特别是在多人协作的项目中
- 本地缓存虽然能提升性能,但如果处理不当反而会造成更大的问题
- 监控报警体系很重要,要不是有监控,可能到用户反馈系统慢了才会发现
后续改进
这次事件后,我们还做了一些改进:
- 在代码规范中明确要求使用
TimeUnit 来处理时间单位
- 为所有缓存模块添加了更细粒度的监控
- 写了一个缓存性能测试套件,确保缓存模块在各种场景下的表现
说实话这个问题虽然看起来很低级,但确实让我对缓存的实现细节有了更深的认识。也再次证明了,在性能优化时,我们不能只关注优化本身,还要考虑可能带来的副作用。
另外推荐一个命令行工具 arthas,它对排查 Java 应用问题特别有帮助,比普通的 jstack、jmap 要强大得多。有兴趣的同学可以去看看。
每次遇到这种问题都是既烦恼又庆幸:烦恼是debug花了不少时间,庆幸是每次都能学到新东西。