当前位置: 首页 > news >正文

从一次线上OOM到MySQL锁表:我是如何用dmesg、jstack和jvisualvm揪出连环故障的

从一次线上OOM到MySQL锁表:我是如何用dmesg、jstack和jvisualvm揪出连环故障的

凌晨3点17分,企业微信的告警消息突然炸开了锅——生产环境核心登录服务响应时间突破15秒阈值。当我紧急登录服务器时,看到的却是Memory cgroup out of memory: Kill process的残酷日志。这场持续72小时的故障排查之旅,最终揭露出从Nginx配置失衡、Redis连接耗尽、MySQL锁表到内存泄漏的连环问题链。本文将还原如何用dmesg定位系统层异常、用jstack捕捉线程死锁、用jvisualvm剖析堆内存的完整破案过程。

1. 第一现场:OOM Killer的死亡日志

凌晨的告警最先显示的是服务超时,但登录服务器后很快发现更严重的问题——Java进程消失了。在Linux系统中,这种"人间蒸发"式崩溃往往与OOM Killer有关。执行dmesg -T后,关键线索浮出水面:

[Fri May 5 03:17:42 2023] Memory cgroup out of memory: Kill process 7187 (java) score 1007 or sacrifice child

关键排查步骤

  1. 通过free -h确认物理内存和Swap使用情况
  2. 检查/sys/fs/cgroup/memory/memory.limit_in_bytes确认cgroup限制
  3. 分析/var/log/messages中的oom_score_adj记录

注意:在容器化环境中,memory cgroup的限制值可能远小于物理内存,这是现代云原生架构中常见的"内存陷阱"

GC日志分析显示更异常的情况——在崩溃前一分钟发生了3次Full GC,但内存回收效果极差。这暗示着要么存在内存泄漏,要么有无法回收的大对象。此时我做了两个关键动作:

  • jstat -gcutil <pid> 1000实时监控GC情况
  • 保留现场:gcore <pid>生成核心转储文件

2. 负载失衡:被忽视的Nginx权重配置

初步恢复服务后,发现一个诡异现象:集群中仅有个别节点持续高负载。检查Nginx的upstream配置发现:

upstream auth_server { server 10.0.0.1 weight=5; # 故障节点 server 10.0.0.2 weight=1; server 10.0.0.3 weight=1; }

这个看似合理的权重分配,在实际业务中却导致登录请求80%都集中在单个节点。更严重的是,该节点配置的JVM堆内存(-Xmx)比其他节点低2GB。临时解决方案:

  1. 立即调整权重为均衡分配
  2. 统一集群节点规格
  3. 增加max_failsfail_timeout熔断配置

负载均衡策略对比

策略类型优点缺点适用场景
轮询绝对均衡忽略服务器性能差异同构集群
权重考虑服务器性能配置静态不灵活已知性能差异
最少连接动态适应需要额外监控开销长连接服务
IP Hash会话保持可能造成热点有状态服务

3. Redis连接池的雪崩效应

本以为问题就此解决,但一周后类似故障再次出现。这次首先映入眼帘的是Redis报错:

Could not get a resource from the pool

检查连接池配置发现两个致命问题:

  1. max-active=8的极小配置
  2. 代码中存在连接泄漏:
// 错误示例:未使用try-with-resources Jedis jedis = pool.getResource(); try { jedis.get("key"); } catch (Exception e) { // 发生异常时连接未返还 throw e; } // 正确写法应使用try-finally或try-with-resources

通过jstack抓取的线程堆栈更触目惊心:

"http-nio-8080-exec-5" #30 daemon prio=5 os_prio=0 tid=0x00007f48740f6800 nid=0x1e03 waiting on condition [0x00007f486b7e7000] java.lang.Thread.State: TIMED_WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000000f8508f50> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at redis.clients.util.Pool.getResource(Pool.java:50)

连接池优化方案

  1. 根据QPS调整max-active(建议值 = 最大并发数 * 1.2)
  2. 添加testWhileIdletimeBetweenEvictionRunsMillis参数
  3. 引入Hystrix实现熔断降级

4. MySQL锁表:隐藏的真正元凶

深入分析jstack日志后,发现了更底层的危机——大量MySQL线程处于TIMED_WAITING状态:

"http-nio-8080-exec-12" #42 daemon prio=5 os_prio=0 tid=0x00007f48740fa800 nid=0x1e1f waiting for monitor entry [0x00007f486b3e3000] java.lang.Thread.State: BLOCKED (on object monitor) at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:4876) - locked <0x00000000f8a2b1e8> (a com.mysql.jdbc.JDBC4Connection)

对应的业务日志显示:

MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction

问题最终定位到一个定时任务:

SELECT * FROM t_sys_session_rec WHERE UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(update_time) > 21600

这个查询存在三个致命缺陷:

  1. update_time字段使用函数计算导致索引失效
  2. 没有分批处理机制,全表扫描大表
  3. 与业务更新操作产生锁竞争

终极解决方案

  1. 将会话数据迁移到Redis
  2. 对必须保留的MySQL表添加合理索引
  3. 重写定时任务为分批处理模式
// 分批处理示例 int batchSize = 100; List<Session> sessions; do { sessions = sessionDao.findExpiredSessions(batchSize); sessions.forEach(this::invalidateSession); } while (!sessions.isEmpty());

5. 内存泄漏:最后一公里排查

即使解决了所有外部依赖问题,JVM内存使用率仍居高不下。使用jmap -dump:format=b,file=heap.hprof导出堆内存后,通过jvisualvm分析发现:

内存占用Top 3

  1. char[]- 占总堆45%
  2. HashMap$Node- 占总堆23%
  3. String- 占总堆18%

代码审查暴露了三个典型问题:

// 问题1:频繁创建HashMap public Map<String,String> parseParams(HttpServletRequest request) { Map<String,String> params = new HashMap<>(); // 每次调用新建 Enumeration<String> names = request.getParameterNames(); while (names.hasMoreElements()) { String name = names.nextElement(); params.put(name, request.getParameter(name).trim()); } return params; } // 问题2:滥用JSONObject public User parseUser(String json) { JSONObject obj = JSON.parseObject(json); // 内部使用Map存储 return new User(obj.getString("name"),...); } // 问题3:字符串拼接陷阱 public String buildWelcomeMsg(User user) { String msg = "欢迎" + user.getName(); // 产生临时char[] msg += ",您是第" + visitCount + "位访客"; return msg; }

优化方案

  1. 对高频调用的参数解析方法使用对象池
  2. Jackson替代Fastjson的JSON处理
  3. 对于字符串拼接使用StringBuilder
  4. 对常量字符串使用intern()方法
// 优化后的参数解析 private static final Map<String,String> EMPTY_MAP = Collections.emptyMap(); public Map<String,String> parseParams(HttpServletRequest request) { if (!request.getParameterNames().hasMoreElements()) { return EMPTY_MAP; // 复用空集合 } Map<String,String> params = new HashMap<>(16); // 指定初始容量 // ...其余逻辑不变 }

这场连环故障最终促使我们建立了更完善的监控体系:

  • 在Prometheus中配置process_resident_memory_bytes监控
  • 对Redis连接池添加numActivenumIdle指标采集
  • 用Grafana展示MySQL锁等待时间趋势
  • 定期用Arthas进行线上诊断
http://www.jsqmd.com/news/542976/

相关文章:

  • Miro收购Reforge,助力企业顺利迈向人工智能时代转型
  • FireRed-OCR保姆级教程:一键部署,精准提取表格公式转Markdown
  • Qwen3-VL历史文物识别:博物馆数字化管理部署解决方案
  • 77.基于matlab-GUI的图像分割分别包括超像素 (superpixels)分割 SLIC算法
  • 2026年最佳SaaS联盟营销平台:启动SaaS联盟计划
  • GLM-4-9B-Chat-1M保姆级部署指南:vLLM+Chainlit前端一键调用
  • NaViL-9B实战手册:从零部署到生产环境监控的全流程技术文档
  • 硬件知识总结梳理-4(磁珠)
  • NaViL-9B实战手册:健康检查API与服务异常定位全流程
  • AI资讯速递 - 2026-03-27
  • 循环神经网络 (七)双向 RNN 与深层 RNN
  • Wan2.1-umt5与STM32CubeMX:嵌入式AI项目初始化配置联想
  • 智能协作:Krita AI图像生成插件的创作革命
  • 算法认知战:用垃圾信息污染AI训练数据
  • vLLM-v0.17.1入门必看:vLLM Serving API参数详解与最佳实践
  • NaViL-9B图文理解入门:支持中英文混合提问的实测案例
  • SOONet与Transformer架构深度解析:提升长视频理解精度的核心技术
  • CSC荣获全球信息安全奖“尖端证书生命周期管理”奖
  • SenseVoice-small-onnx REST API开发手册:curl+Python调用+健康检查全解析
  • 番茄小说下载器终极指南:打造你的私人离线阅读库
  • RWKV7-1.5B-G1A集成Python爬虫:自动化数据采集与智能分析实战
  • 【图像加密解密】阶跃函数的脉冲控制复杂网络的同步及图像加密解密【含Matlab源码 15219期】含参考文献
  • AutoGLM-Phone-9B快速部署指南:Docker一键启动,5分钟体验多模态AI
  • 亲测好用!圣女司幼幽-造相Z-Turbo镜像,生成古风人物图效果惊艳
  • Qwen2.5-VL-7B-Instruct镜像免配置教程:开箱即用的视觉语言推理平台
  • 在Windows上用Visual Studio 2022封装PCL点云库为C++ DLL,供C#项目调用的完整流程
  • MariaDB完成对GridGain的收购,助力新一代Agentic AI加速发展
  • 600 万奖池 + 不限身份 + KDD 顶会:腾讯广告算法大赛该上车了
  • 镜像免配置优势实测:PyTorch 2.8相比手动安装节省90%环境调试时间
  • 如何通过SMUDebugTool精细化调控AMD锐龙CPU性能?从零掌握专业级超频与调优