<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>记一次线上CPU 100%的排查与解决过程 on SelfTechHub</title>
        <link>https://blog.irudder.me/tags/%E8%AE%B0%E4%B8%80%E6%AC%A1%E7%BA%BF%E4%B8%8ACPU-100%E7%9A%84%E6%8E%92%E6%9F%A5%E4%B8%8E%E8%A7%A3%E5%86%B3%E8%BF%87%E7%A8%8B.html</link>
        <description>Recent content in 记一次线上CPU 100%的排查与解决过程 on SelfTechHub</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-cn</language>
        <lastBuildDate>Sun, 26 Mar 2023 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.irudder.me/tags/%E8%AE%B0%E4%B8%80%E6%AC%A1%E7%BA%BF%E4%B8%8ACPU-100%E7%9A%84%E6%8E%92%E6%9F%A5%E4%B8%8E%E8%A7%A3%E5%86%B3%E8%BF%87%E7%A8%8B/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>凌晨3点CPU飙升100%：排查过程与血泪教训</title>
        <link>https://blog.irudder.me/tech/jiagouyouhuashizhananlijiexi/jiyicixianshangcpu-100%depaichayujiejueguocheng.html</link>
        <pubDate>Sun, 26 Mar 2023 00:00:00 +0000</pubDate>
        
        <guid>https://blog.irudder.me/tech/jiagouyouhuashizhananlijiexi/jiyicixianshangcpu-100%depaichayujiejueguocheng.html</guid>
        <description>&lt;p&gt;不知你是否经历过这种时刻：手机在凌晨疯狂震动，告警群里全是红色的&lt;code&gt;[CRITICAL]&lt;/code&gt;，服务器CPU监控线笔直地拉成一条横线，且居高不下。&lt;/p&gt;
&lt;p&gt;我一直信奉&amp;quot;周五不发布&amp;quot;的铁律，但两年前的那次破例，让我结结实实地踩了一个大坑。当时为了赶一个看似简单的&amp;quot;数据同步优化&amp;quot;功能，我在周五傍晚匆忙上了一个补丁。结果当天凌晨3点，负责核心结算的微服务集群CPU全线打满，业务响应时间从20ms飙升到超时。&lt;/p&gt;
&lt;p&gt;这不仅是一次事故复盘，更是我对&amp;quot;防御性编程&amp;quot;认知的转折点。在那之前，我以为代码逻辑跑通就行；在那之后，我明白了什么是&lt;strong&gt;敬畏生产环境&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id=&#34;拒绝重启现场保留作案证据&#34;&gt;拒绝重启，现场保留&amp;quot;作案证据&amp;quot;
&lt;/h2&gt;&lt;p&gt;面对线上CPU爆表，很多运维或新手的本能反应是——&lt;strong&gt;重启试试&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;重启确实能暂时止血，但它同时也销毁了&amp;quot;犯罪现场&amp;quot;。如果不知道病因，重启只是把定时炸弹的时间重置了，下一次爆炸会来得更猛烈。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我当时强行按住了想去点重启按钮的手，坚持要先抓取堆栈信息。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://picsum.photos/800/450?random=1768457134097&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;配图&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;这就好比破案，你得先留存指纹。在Linux环境下，定位Java应用CPU高的标准动作其实就那几步，但我发现很多人在紧张时容易乱。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;定位进程&lt;/strong&gt;：&lt;code&gt;top&lt;/code&gt; 命令一敲，PID为 &lt;code&gt;18920&lt;/code&gt; 的Java进程稳居榜首，CPU占用率达到了惊人的 780%（8核机器）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定位线程&lt;/strong&gt;：光知道进程没用，得知道是哪个线程在捣乱。执行 &lt;code&gt;top -Hp 18920&lt;/code&gt;，我看到 PID 为 &lt;code&gt;19011&lt;/code&gt; 的线程一直占用着单核的99%。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;转换进制&lt;/strong&gt;：将线程ID &lt;code&gt;19011&lt;/code&gt; 转换为16进制 &lt;code&gt;4a43&lt;/code&gt;（因为jstack里用的是16进制）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;导出堆栈&lt;/strong&gt;：执行 &lt;code&gt;jstack 18920 | grep -A 20 4a43&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;思考题&lt;/strong&gt;：如果你的生产环境没有权限执行 jstack 或者 top 命令被阉割了，你们现有的监控系统（如Prometheus, SkyWalking）能直接定位到代码行吗？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这一套组合拳打完，仅仅耗时2分钟。屏幕上清晰地打印出了罪魁祸首的代码行数。看到那个类名的时候，我后背一阵发凉——正是我下午提交的那个&amp;quot;优化&amp;quot;代码。&lt;/p&gt;
&lt;h2 id=&#34;死循环隐藏在异常处理里的杀手&#34;&gt;死循环：隐藏在异常处理里的杀手
&lt;/h2&gt;&lt;p&gt;&lt;img src=&#34;https://picsum.photos/800/450?random=1768457132484&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;配图&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;堆栈指向了一行看似人畜无害的代码：一个处理Redis消息队列的 &lt;code&gt;while&lt;/code&gt; 循环。&lt;/p&gt;
&lt;p&gt;在这个案例中，我的初衷是好的。为了保证数据不丢失，我写了一个消费者线程，不断从Redis中拉取数据进行处理。代码逻辑大致如下（简化版）：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 错误示范：造成CPU 100%的逻辑&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; (&lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        String msg &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; redisService.&lt;span style=&#34;color:#a6e22e&#34;&gt;pop&lt;/span&gt;(key);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (msg &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            process(msg);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    } &lt;span style=&#34;color:#66d9ef&#34;&gt;catch&lt;/span&gt; (Exception e) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        log.&lt;span style=&#34;color:#a6e22e&#34;&gt;error&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Redis error&amp;#34;&lt;/span&gt;, e);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// 致命疏忽：这里没有做任何休眠或退避&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&#34;https://picsum.photos/800/450?random=1768457131032&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;配图&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;问题出在哪里？&lt;/p&gt;
&lt;p&gt;当晚凌晨，云服务商的Redis实例发生了一次短暂的内网抖动。&lt;code&gt;redisService.pop(key)&lt;/code&gt; 抛出了连接异常。&lt;/p&gt;
&lt;p&gt;按照正常的逻辑，异常被捕获，打印日志。但是，&lt;strong&gt;由于catch块中没有哪怕1毫秒的 sleep，也没有跳出循环&lt;/strong&gt;，这个 &lt;code&gt;while(true)&lt;/code&gt; 瞬间变成了一个极其紧密的死循环。&lt;/p&gt;
&lt;p&gt;线程疯狂地 抛出异常 -&amp;gt; 捕获异常 -&amp;gt; 打印日志 -&amp;gt; 再次重试。&lt;/p&gt;
&lt;p&gt;JVM在极短时间内进行了数百万次的方法调用和对象创建，日志文件以每秒几百MB的速度疯狂膨胀（导致磁盘IO也跟着报警），CPU直接被吃干抹净。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这是一个典型的&amp;quot;假死循环&amp;quot;。它不是逻辑上的死锁，而是由于缺乏退避机制（Backoff）导致的资源耗尽。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我曾在Code Review中多次强调NPE（空指针）的检查，却忽略了这种在异常态下的资源保护。你是否也曾觉得：&amp;ldquo;加了try-catch就万事大吉了&amp;rdquo;？&lt;/p&gt;
&lt;h2 id=&#34;加上熔断与退避才是成熟的代码&#34;&gt;加上&amp;quot;熔断&amp;quot;与&amp;quot;退避&amp;quot;，才是成熟的代码
&lt;/h2&gt;&lt;p&gt;定位到问题后，修复方案其实很简单。紧急发布修复包，加上了指数退避策略，服务恢复了平静。&lt;/p&gt;
&lt;p&gt;但这件事让我反思了很久：&lt;strong&gt;为什么一个中间件的抖动，能搞垮我的核心服务？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这暴露出架构设计上的脆弱性。我们在设计系统时，往往假设依赖服务是永远可靠的，网络是永远通畅的。&lt;/p&gt;
&lt;p&gt;针对这次事故，我后续落地了三层防护体系，这套方法我沿用至今：&lt;/p&gt;
&lt;h3 id=&#34;1-强制性的退避策略&#34;&gt;1. 强制性的退避策略
&lt;/h3&gt;&lt;p&gt;在任何 &lt;code&gt;while&lt;/code&gt; 循环或重试逻辑中，必须包含休眠机制。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 改进后的逻辑&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt; retryCount &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; 0;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; (&lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        String msg &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; redisService.&lt;span style=&#34;color:#a6e22e&#34;&gt;pop&lt;/span&gt;(key);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// ...处理逻辑&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        retryCount &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; 0; &lt;span style=&#34;color:#75715e&#34;&gt;// 成功则重置&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    } &lt;span style=&#34;color:#66d9ef&#34;&gt;catch&lt;/span&gt; (Exception e) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        log.&lt;span style=&#34;color:#a6e22e&#34;&gt;error&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Redis error&amp;#34;&lt;/span&gt;, e);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        retryCount&lt;span style=&#34;color:#f92672&#34;&gt;++&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// 指数退避：发生错误时，休息时间越来越长，避免把下游打死&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;long&lt;/span&gt; sleepTime &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Math.&lt;span style=&#34;color:#a6e22e&#34;&gt;min&lt;/span&gt;(1000 &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; (1 &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&amp;lt;&lt;/span&gt; retryCount), 60000); 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Thread.&lt;span style=&#34;color:#a6e22e&#34;&gt;sleep&lt;/span&gt;(sleepTime); 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;2-引入轻量级熔断&#34;&gt;2. 引入轻量级熔断
&lt;/h3&gt;&lt;p&gt;不要盲目自信地一直重试。如果Redis真的挂了，你的应用应该&amp;quot;认怂&amp;quot;。我们引入了 Resilience4j 这样的轻量级库，当异常比例达到阈值（比如50%），直接熔断该功能，降级为记录本地文件或内存队列，等待人工介入或自动恢复。&lt;/p&gt;
&lt;h3 id=&#34;3-资源隔离bulkhead&#34;&gt;3. 资源隔离（Bulkhead）
&lt;/h3&gt;&lt;p&gt;之前那个处理线程直接使用了默认的线程池。一旦死循环，虽然只是单线程，但也可能因为疯狂GC影响整个JVM。后来我们将关键任务分配到独立的线程池中，并限制了最大线程数和队列长度。即使这个池子炸了，也不会拖垮处理HTTP请求的主线程池。&lt;/p&gt;
&lt;h2 id=&#34;总结与行动指南&#34;&gt;总结与行动指南
&lt;/h2&gt;&lt;p&gt;那次事故后，我养成了一个习惯：&lt;strong&gt;写任何循环（Loop）的时候，先问自己三个问题：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;它什么时候结束？&lt;/li&gt;
&lt;li&gt;如果内部报错了，它会狂转吗？&lt;/li&gt;
&lt;li&gt;它会不会把CPU或内存吃光？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你正在经历或想要预防类似的问题，以下是3个可落地的建议：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;实战演练定位命令&lt;/strong&gt;：不要等到故障发生时再去百度&amp;quot;Linux CPU 100% 怎么查&amp;quot;。找一台测试机，写一个死循环程序，亲自用 &lt;code&gt;top&lt;/code&gt; 和 &lt;code&gt;jstack&lt;/code&gt; 抓一次，形成肌肉记忆。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;审查所有 &lt;code&gt;while(true)&lt;/code&gt;&lt;/strong&gt;：利用IDE的全局搜索，检查项目中所有的无限循环和递归调用，确保在 &lt;code&gt;catch&lt;/code&gt; 块中有 &lt;code&gt;Thread.sleep&lt;/code&gt; 或退出机制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置JVM监控告警&lt;/strong&gt;：不要只看系统级CPU。确保你的Prometheus/Grafana监控了 JVM 的 GC 频率和线程数。很多时候，CPU 100% 并不是死循环，而是因为内存泄漏导致的频繁 Full GC。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;最后，留一个问题给各位同行：&lt;/strong&gt;
在你的项目中，如果数据库或Redis突然断连1分钟，你的服务是会优雅降级，还是会像无头苍蝇一样疯狂报错直到崩溃？&lt;/p&gt;
&lt;p&gt;愿你的CPU常年保持在 20% 以下，愿你所有的报警都发生在工作日上班时间。&lt;/p&gt;
</description>
        </item>
        
    </channel>
</rss>
