<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>微服务 on Zero Day Notes</title>
        <link>https://blog.5772447.xyz/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/</link>
        <description>Recent content in 微服务 on Zero Day Notes</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh</language>
        <lastBuildDate>Mon, 15 Jan 2024 02:45:00 +0000</lastBuildDate><atom:link href="https://blog.5772447.xyz/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>记一次 Docker 容器反复重启引发的微服务链路雪崩</title>
            <link>https://blog.5772447.xyz/posts/%E4%B8%80%E6%AC%A1docker%E5%AE%B9%E5%99%A8%E5%8F%8D%E5%A4%8D%E9%87%8D%E5%90%AF%E5%AF%BC%E8%87%B4%E5%BE%AE%E6%9C%8D%E5%8A%A1%E9%93%BE%E8%B7%AF%E9%9B%AA%E5%B4%A9%E7%9A%84%E6%8E%92%E6%9F%A5%E5%AE%9E%E5%BD%95/</link>
            <pubDate>Mon, 15 Jan 2024 02:45:00 +0000</pubDate>
            <guid>https://blog.5772447.xyz/posts/%E4%B8%80%E6%AC%A1docker%E5%AE%B9%E5%99%A8%E5%8F%8D%E5%A4%8D%E9%87%8D%E5%90%AF%E5%AF%BC%E8%87%B4%E5%BE%AE%E6%9C%8D%E5%8A%A1%E9%93%BE%E8%B7%AF%E9%9B%AA%E5%B4%A9%E7%9A%84%E6%8E%92%E6%9F%A5%E5%AE%9E%E5%BD%95/</guid>
            <description>&lt;h2 id=&#34;问题背景&#34;&gt;&lt;a href=&#34;#%e9%97%ae%e9%a2%98%e8%83%8c%e6%99%af&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;问题背景&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;公司有一组基于 Spring Boot 构建的订单服务，以 Docker 容器化方式部署在三台物理机上，每个节点运行 4 个实例，由 Nginx 做反向代理负载均衡。该服务是整个订单处理链路的核心上游，日均 QPS 稳定在 3000 左右。&lt;/p&gt;&#xA;&lt;h2 id=&#34;故障现象&#34;&gt;&lt;a href=&#34;#%e6%95%85%e9%9a%9c%e7%8e%b0%e8%b1%a1&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;故障现象&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;凌晨 2:00 左右，运维监控平台突然收到大量告警：订单服务 P99 延迟从正常的 200ms 飙升至 30s，5xx 错误率在 2 分钟内从 0% 上升到 45%。登录服务器后发现该服务的容器正处于 CrashLoopBackOff 状态：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&#xA;&lt;table style=&#34;border-spacing:0;padding:0;margin:0;border:0;&#34;&gt;&lt;tr&gt;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;1&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;2&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;3&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;4&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;;width:100%&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ docker ps -a | grep order-svc&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;f3a2b1c4d5e6   order-svc:v2.1.3   &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;java -jar app.jar&amp;#34;&lt;/span&gt;   Exited &lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;137&lt;span style=&#34;color:#f92672&#34;&gt;)&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt; seconds ago&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;a1b2c3d4e5f6   order-svc:v2.1.3   &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;java -jar app.jar&amp;#34;&lt;/span&gt;   Exited &lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;137&lt;span style=&#34;color:#f92672&#34;&gt;)&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;15&lt;/span&gt; seconds ago&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;...&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;所有实例均在启动后几十秒内被 Kill，退出码 137（SIGKILL），容器日志在退出前无任何异常堆栈，只有 Spring Boot 的标准启动日志。与此同时，上游的支付服务和库存服务也开始出现连接超时，业务链路出现雪崩式故障。&lt;/p&gt;&#xA;&lt;h2 id=&#34;排查过程&#34;&gt;&lt;a href=&#34;#%e6%8e%92%e6%9f%a5%e8%bf%87%e7%a8%8b&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;排查过程&#xD;&#xA;&lt;/h2&gt;&lt;h3 id=&#34;第一步排查容器退出原因&#34;&gt;&lt;a href=&#34;#%e7%ac%ac%e4%b8%80%e6%ad%a5%e6%8e%92%e6%9f%a5%e5%ae%b9%e5%99%a8%e9%80%80%e5%87%ba%e5%8e%9f%e5%9b%a0&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;第一步：排查容器退出原因&#xD;&#xA;&lt;/h3&gt;&lt;p&gt;退出码 137 = 128 + 9（SIGKILL），容器被强制终止。Docker 发送 SIGKILL 最常见的原因是 &lt;strong&gt;OOM（Out of Memory）&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;查看系统日志确认：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&#xA;&lt;table style=&#34;border-spacing:0;padding:0;margin:0;border:0;&#34;&gt;&lt;tr&gt;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;1&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;2&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;3&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;;width:100%&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ dmesg -T | grep -i &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;out of memory&amp;#34;&lt;/span&gt; | tail -20&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;Thu Jan &lt;span style=&#34;color:#ae81ff&#34;&gt;15&lt;/span&gt; 02:03:12 2024&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt; Memory cgroup out of memory: Kill process &lt;span style=&#34;color:#ae81ff&#34;&gt;29183&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;java&lt;span style=&#34;color:#f92672&#34;&gt;)&lt;/span&gt; score &lt;span style=&#34;color:#ae81ff&#34;&gt;1087&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;Thu Jan &lt;span style=&#34;color:#ae81ff&#34;&gt;15&lt;/span&gt; 02:04:15 2024&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt; Memory cgroup out of memory: Kill process &lt;span style=&#34;color:#ae81ff&#34;&gt;30241&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;java&lt;span style=&#34;color:#f92672&#34;&gt;)&lt;/span&gt; score &lt;span style=&#34;color:#ae81ff&#34;&gt;1112&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;确认容器是 OOM Killer 杀死的。&lt;/p&gt;&#xA;&lt;h3 id=&#34;第二步检查容器资源配置&#34;&gt;&lt;a href=&#34;#%e7%ac%ac%e4%ba%8c%e6%ad%a5%e6%a3%80%e6%9f%a5%e5%ae%b9%e5%99%a8%e8%b5%84%e6%ba%90%e9%85%8d%e7%bd%ae&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;第二步：检查容器资源配置&#xD;&#xA;&lt;/h3&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;div style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&#xA;&lt;table style=&#34;border-spacing:0;padding:0;margin:0;border:0;&#34;&gt;&lt;tr&gt;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;1&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;;width:100%&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ docker inspect order-svc | grep -A5 &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;HostConfig&amp;#34;&lt;/span&gt; | grep -E &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Memory|CpuPeriod|CpuQuota&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;发现容器的 &lt;code&gt;--memory&lt;/code&gt; 限制为 512MB，但 JVM 的 &lt;code&gt;-Xmx&lt;/code&gt; 参数设置为 384MB。理论上剩余 128MB 加上 JVM 的堆外内存应该足够，为什么还会 OOM？&lt;/p&gt;&#xA;&lt;h3 id=&#34;第三步分析-jvm-内存分布&#34;&gt;&lt;a href=&#34;#%e7%ac%ac%e4%b8%89%e6%ad%a5%e5%88%86%e6%9e%90-jvm-%e5%86%85%e5%ad%98%e5%88%86%e5%b8%83&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;第三步：分析 JVM 内存分布&#xD;&#xA;&lt;/h3&gt;&lt;p&gt;使用 &lt;code&gt;jcmd&lt;/code&gt; 在容器存活期间抓取 NMT（Native Memory Tracking）报告：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&#xA;&lt;table style=&#34;border-spacing:0;padding:0;margin:0;border:0;&#34;&gt;&lt;tr&gt;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;1&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;;width:100%&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;-XX:NativeMemoryTracking=summary&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;NMT 输出显示 Internal（Direct Buffer + JNI 等）内存持续增长，堆外内存从 60MB 涨到 180MB，已经接近容器总内存上限。进一步分析发现是一个改造后的 HTTP 客户端库&lt;strong&gt;没有释放 DirectByteBuffer&lt;/strong&gt;，每次建立新连接都会分配堆外内存，连接关闭后并未归还。&lt;/p&gt;&#xA;&lt;h3 id=&#34;第四步确认雪崩根因&#34;&gt;&lt;a href=&#34;#%e7%ac%ac%e5%9b%9b%e6%ad%a5%e7%a1%ae%e8%ae%a4%e9%9b%aa%e5%b4%a9%e6%a0%b9%e5%9b%a0&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;第四步：确认雪崩根因&#xD;&#xA;&lt;/h3&gt;&lt;p&gt;上游调用方使用 Feign + Ribbon 做客户端负载均衡。当所有 order-svc 实例不断重启时，Ribbon 的重试机制在短时间内发出大量重试请求，加上连接池未及时关闭，最终耗尽上游服务的线程池，导致整体链路雪崩。&lt;/p&gt;&#xA;&lt;h2 id=&#34;解决方案&#34;&gt;&lt;a href=&#34;#%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;解决方案&#xD;&#xA;&lt;/h2&gt;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;紧急修复&lt;/strong&gt;：将 &lt;code&gt;--memory&lt;/code&gt; 上限临时提升到 1GB，并重启所有实例。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;修复内存泄漏&lt;/strong&gt;：定位 HTTP 客户端库中未释放 DirectByteBuffer 的代码，显式调用 &lt;code&gt;((DirectBuffer) buffer).cleaner().clean()&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;添加 JVM 参数&lt;/strong&gt;：&lt;code&gt;-XX:MaxDirectMemorySize=128m&lt;/code&gt;，限制堆外内存上限，防止无限增长。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;配置 Hystrix 熔断&lt;/strong&gt;：在 Feign 客户端面添加熔断降级逻辑，避免上游被拖垮。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Docker healthcheck 优化&lt;/strong&gt;：增加健康检查，在容器被 Kill 前主动介入。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;根因分析&#34;&gt;&lt;a href=&#34;#%e6%a0%b9%e5%9b%a0%e5%88%86%e6%9e%90&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;根因分析&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;直接原因：HTTP 客户端库的 DirectByteBuffer 泄漏 → 堆外内存持续增长 → 容器内存超限 → OOM Kill → 容器重启 → 泄漏重新开始 → 恶性循环。&lt;/p&gt;&#xA;&lt;p&gt;深层原因：Docker 内存限制（512MB）过于紧凑，对 JVM 堆外内存的预留不足。JVM 的 &lt;code&gt;-Xmx&lt;/code&gt; 只约束了堆内存，堆外内存（Metaspace、Direct Buffer、线程栈）完全不受限制，很容易超出容器限制。&lt;/p&gt;&#xA;&lt;h2 id=&#34;预防措施&#34;&gt;&lt;a href=&#34;#%e9%a2%84%e9%98%b2%e6%8e%aa%e6%96%bd&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;预防措施&#xD;&#xA;&lt;/h2&gt;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;容器内存分配公式&lt;/strong&gt;：总内存 ≥ Xmx + MaxDirectMemorySize + MetaspaceSize + (线程数 × 栈大小) + 系统预留 100MB&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;使用 -XX:+UseContainerSupport&lt;/strong&gt;：让 JVM 感知容器内存限制&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;压测验证&lt;/strong&gt;：上线前对每个服务做内存压力测试，确认内存稳定区间&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;监控堆外内存&lt;/strong&gt;：接入 Prometheus JMX Exporter，监控堆外内存增长趋势&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;总结&#34;&gt;&lt;a href=&#34;#%e6%80%bb%e7%bb%93&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;总结&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;JVM 应用容器化需要特别注意内存规划的完整性。&lt;code&gt;-Xmx&lt;/code&gt; 只控制堆内存，加上 Metaspace、Direct Buffer、线程栈等堆外开销，实际内存远大于堆内存。推荐使用 &lt;code&gt;-XX:MaxRAMPercentage=75.0&lt;/code&gt; 替代硬编码的 &lt;code&gt;-Xmx&lt;/code&gt;，让 JVM 自适应容器内存限制。同时，任何直接操作 DirectByteBuffer 的代码都需要严格的内存生命周期管理，否则泄漏问题会在容器环境下被 OOM Killer 无情放大。&lt;/p&gt;&#xA;</description>
        </item></channel>
</rss>
