别急着加内存!从一次OOM到MySQL锁表,我如何用jstack和jvisualvm揪出真凶
从OOM到MySQL锁表一套完整的问题排查与性能优化实战指南当线上系统突然崩溃屏幕上跳出Memory cgroup out of memory的红色告警时大多数开发者的第一反应往往是赶紧加内存。但真正的问题往往隐藏在这表面现象之下。本文将带你经历一次真实的生产环境故障排查全过程从内存溢出告警开始逐步揭开MySQL锁表、线程阻塞、资源竞争等一系列连环问题的真相。1. 告警初现系统为何被Kill那天下午3点监控系统突然发出刺耳的警报声。登录服务器后发现Java进程神秘消失只留下一条线索dmesg -T | grep -i kill输出结果显示[Mon Jul 12 15:23:45 2021] Memory cgroup out of memory: Kill process 7187 (java) score 1007 or sacrifice childLinux的OOM Killer机制在内存不足时会自动终止得分最高的进程。但这个得分是怎么计算的主要有三个因素内存占用比例进程使用的内存占系统总内存的百分比CPU使用时间进程最近消耗的CPU时间进程优先级nice值较低的进程更容易被选中提示在生产环境可以通过调整/proc/ /oom_score_adj来保护关键进程不被OOM Killer终止2. 初步分析GC日志与负载均衡查看GC日志发现一分钟内发生了3次Full GC这是一个明显的危险信号。正常的GC行为应该是GC类型频率阈值可能问题Young GC5次/分钟新生代设置过小Full GC1次/小时内存泄漏或老年代不足同时检查Nginx日志发现所有流量都被导向了这台故障机器。这解释了为何其他机器闲置而这一台崩溃。但简单的负载均衡调整真的解决问题了吗3. 深入线程分析jstack揭示的阻塞链两周后问题再次出现这次虽然没有进程被Kill但用户开始抱怨接口超时。使用jstack抓取线程快照jstack -l pid thread_dump.log分析日志发现两个关键现象Redisson客户端大量WAITINGredisson-netty-1-5 #31 prio5 os_prio0 tid0x00007f8b9406e800 nid0x1e03 waiting on condition [0x00007f8b7a7f7000]MySQL TIMED_WAITINGhttp-nio-8080-exec-5 #20 daemon prio5 os_prio0 tid0x00007f8b8c00a800 nid0x1d0f waiting on condition [0x00007f8b7b3f8000]这两者看似无关实则暗藏联系。通过线程状态转换图可以更清晰理解用户请求 → 获取Redis连接 → 执行MySQL更新 → 等待锁超时 → Redis连接未释放 → 连接池耗尽4. MySQL锁表罪魁祸首浮出水面错误日志中频繁出现Lock wait timeout exceeded; try restarting transaction检查相关表结构CREATE TABLE t_sys_session_rec ( sessionId VARCHAR(32) PRIMARY KEY, createIp VARCHAR(16), updateTime DATETIME, -- 其他字段... );问题出在一个定时任务上它每60秒执行一次全表扫描// 问题代码示例 Scheduled(fixedRate 60000) public void cleanExpiredSessions() { ListSessionRecord records sessionRecordMapper.selectAllWillExpireSession(); records.forEach(record - { record.setStatus(INVALID); sessionRecordMapper.updateByPrimaryKey(record); }); }这个任务有两个致命缺陷全表扫描where条件中的createIp和updateTime字段无索引长事务逐条更新导致锁持有时间过长5. 内存分析jvisualvm揭示的对象洪流生成堆转储文件jmap -dump:live,formatb,fileheap.hprof pid使用jvisualvm分析发现内存中充斥着HashMap$Node (32% of total objects)char[] (25% of total objects)Hashtable$Entry (18% of total objects)代码审查发现在核心接口中频繁创建临时Map对象public MapString, String getParams(HttpServletRequest request) { MapString, String paramMap new HashMap(); // 问题点1 EnumerationString names request.getParameterNames(); while (names.hasMoreElements()) { String name names.nextElement(); paramMap.put(name.trim(), request.getParameter(name).trim()); // 问题点2 } return paramMap; }每次请求创建多个Map和String对象在QPS 50的情况下每天产生超过150万个临时对象。6. 系统性解决方案6.1 MySQL优化方案方案优点缺点适用场景添加索引快速见效对updateTime无效静态字段查询改用Redis彻底解决锁问题改造量大高并发场景分区表减少单表大小维护复杂大数据量表最终选择将session数据迁移到Redis利用其原子操作和过期特性// 改进后的校验逻辑 public boolean validateTicket(String ticket) { String key session: ticket; return redisTemplate.execute(new RedisCallbackBoolean() { Override public Boolean doInRedis(RedisConnection connection) { byte[] value connection.get(key.getBytes()); if (value ! null) { connection.expire(key.getBytes(), SESSION_TIMEOUT); return true; } return false; } }); }6.2 内存优化关键点对象复用对于频繁创建的Map使用ThreadLocal缓存减少临时对象直接操作String而非创建新对象合理使用连接池spring.redis.pool.max-active100 spring.redis.pool.max-wait10007. 监控与预防体系建立完善的监控应该包括实时指标# GC监控 jstat -gcutil pid 1000 # 线程状态监控 jstack pid | grep -c BLOCKED预警规则Full GC频率 1次/小时线程阻塞率 10%MySQL锁等待 500ms压测验证# 模拟并发请求 ab -n 10000 -c 100 http://localhost:8080/api/ticket这次故障教会我们线上问题从来不是孤立的。从内存溢出到MySQL锁表再到Redis连接池耗尽各个环节环环相扣。真正的解决方案不在于增加硬件资源而在于发现并修复这些隐藏的连锁反应。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2601028.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!