说实话,刚入行接这家公司业务的时候,碰到这种事我直接就跳进去查Tomcat业务日志,翻到下班眼睛都花了,还是后来开发主管打电话问是不是锁太粗才发现——去年第一次搞鸡蛋促销就是这个问题,最后单量只有预期的30%,老板脸黑了整整一周才愿意给我们批技术优化的预算。这次提前学乖了踩坑踩出来的逻辑,15分钟就定位到根源,和开发一起稳住了场面,这次活动单量甚至比预期多了22%,老板下午还给团队点了奈雪和山姆的蛋糕卷当下午茶。
先从当时的操作顺一遍吧,群里用户侧的现象只是“结果”,不能直接盯着结果找原因,得先抓核心入口的宏观指标。这家公司虽然是中小团队,但基础的Prometheus+Grafana还是有的,监控内容也留了我整理的“救命三板斧”模块:Nginx的Active连接、等待请求占比,应用服务器Tomcat的Active Threads、负载,MySQL的连接数、慢查询数、TPS/QPS。那天我第一反应是先登跳板机看Grafana大屏,扫一眼就发现问题出在Tomcat——4核8G的主应用服务器Active Threads直接冲到了配置上限200,负载也飙升到6.8(4核负载超过1.2就已经有明显的性能瓶颈了),Nginx那边等待请求占比更是达到了92%,MySQL这边反而一切正常。
那时候开发还在对着代码找哪里有问题,我赶紧先看了下Nginx的upstream配置,幸好上周刚把闲置的一台同配置云服务器改成了应用备用机(192.168.1.11:8080)放着吃灰没用,赶紧ssh登录Nginx机器,修改/usr/local/nginx/conf/vhosts/goods.conf里的upstream goods_backend,加了一行server 192.168.1.11:8080 weight=1;,然后必须用nginx -t先检查配置语法有没有问题,没问题再nginx -s reload做优雅重启,别直接nginx -s restart,不然会把当前正在处理的连接全掐断,用户体验会更差。大概过了2分钟,备用机就顶上去了120多个Active Threads,群里运营反馈说有一小部分住在市中心信号好的用户能正常打开商品页了,暂时稳住了军心。
接下来就是真正的根源排查,Tomcat Active Threads满了,要么是GC频繁导致STW(Stop The World),要么是慢SQL把连接池占满了,要么就是有死锁或者锁太粗导致线程大量阻塞。我先查了下GC的情况,新手可以直接用jstat -gcutil 1000 10看,1秒输出一次,连续输出10次,那天查的结果显示YGC次数只有7次,FGC一次都没有,堆内存使用率也只有55%左右,完全排除了GC的问题。
那接下来看线程栈,刚开始我刚入行的时候看jstack的几十万行dump会头大,根本抓不住重点,后来教自己还有新来的实习生先做个过滤:jstack -l | grep -i "BLOCKED",加-i是忽略大小写,避免漏看。那天执行完这个命令,屏幕直接刷了满屏的BLOCKED,仔细看锁的位置,全是com.communityfresh.goods.service.impl.GoodsServiceImpl.updateStock方法里的一个synchronized同步块!原来促销的时候运营同事有5个人同时在后台批量改榴莲、芒果、车厘子这几个爆品的库存和上下架状态,而这个方法是加在Spring单例的GoodsService实例上的,批量操作的时候所有线程都抢这一把锁,全堵死了。

后来还留了个完整的线程栈方便后续复盘:jstack -l 12345 > /tmp/tomcat_thread_20260523_1647.dump,12345是Tomcat的进程号,用ps -ef | grep java | grep goods就能找到,临时文件名带了年月日时分秒,多次dump也不会混淆,加-l是输出锁的具体信息,生产环境排查锁相关问题必须加这个。
这里一定要敲黑板给新手提两个避坑提醒:
第一个是绝对别一开始就扎进业务日志大海捞针,尤其是中小公司没有ELK/EFK这类日志聚合平台的时候,去年第一次鸡蛋促销我就是犯了这个错,翻了3个G的滚动日志,才找到零星的“获取GoodsService锁超时”的WARN日志,要是先看监控的话,5分钟就能锁定方向。
第二个是新手排查线上故障,别乱重启核心应用服务! 上个月刚接这家公司的时候,有个新来的后端实习生帮我看监控,碰到CPU飙到80%就直接重启了Tomcat,结果重启后JVM要重新加载几十万个类,爆品的库存预热数据也要重新从MySQL拉到本地缓存,QPS直接掉到了0,还掉了几十个正在下单的用户,后来被我和开发主管一起说了半小时。重启服务只有在两种情况下考虑:一种是服务本身已经僵死,用ps -ef找不到进程,或者kill -15优雅杀不死只能用kill -9硬杀;另一种是已经有备用机器可以先顶上去所有流量的情况,而且不管哪种情况,重启前必须留现场:线程栈、内存堆、系统日志、应用日志,方便后续复盘找到真正的原因,避免下次再踩同样的坑。
其实刚才讲的整个排查过程,用的就是我这6年踩坑踩出来的一点点实用小逻辑:先从用户侧反馈的结果现象,切换到核心入口的监控指标先宏观找环节,再深入环节微观定位原因,最后和开发一起商量临时解决方案稳住场面,再出永久优化方案避免下次再犯。比如这次生鲜的问题,临时方案是加备用机顶流量、运营同事暂停批量操作只改最核心的榴莲库存,永久方案是把爆品的库存预加载到Redis,秒杀的时候用Redis的decrby原子操作扣减库存,批量上下架改成用Spring Batch的分批异步处理,不用抢同一个同步锁。
你们在1-3年的运维生涯或者兼做运维的后端开发经历中,有没有碰到过因为没思路或者乱重启服务导致的线上小插曲?欢迎在评论区分享你的踩坑故事,我们一起避坑一起进步~

评论列表 (0条):
加载更多评论 Loading...