<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Pprof on Zampo Blog</title><link>https://blog.cpdd.fyi/tags/pprof/</link><description>Recent content in Pprof on Zampo Blog</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Fri, 22 May 2026 00:01:00 +0800</lastBuildDate><atom:link href="https://blog.cpdd.fyi/tags/pprof/index.xml" rel="self" type="application/rss+xml"/><item><title>Go 服务被 OOMKilled，别先怪 GC</title><link>https://blog.cpdd.fyi/posts/go-gc-debugging/</link><pubDate>Fri, 22 May 2026 00:01:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-gc-debugging/</guid><description>&lt;p&gt;服务又被 OOMKilled 了。&lt;/p&gt;
&lt;p&gt;Pod 刚重启，群里已经有人开始翻 GC 日志。有人说 &lt;code&gt;GC CPU&lt;/code&gt; 到了 20%，有人说把 &lt;code&gt;GOGC&lt;/code&gt; 调低一点，还有人建议直接上 &lt;code&gt;GOMEMLIMIT&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这些动作不一定错，但顺序错了。&lt;/p&gt;
&lt;p&gt;OOMKilled 是结果，不是根因。它只说明容器被内核杀掉了，不说明是谁把内存吃掉了。可能是 Go heap 真的在涨，可能是 goroutine 泄漏把栈和引用一起拖大，可能是 &lt;code&gt;alloc_space&lt;/code&gt; 很高导致 GC 忙，可能是 RSS 里有 cgo、mmap、tmpfs，也可能只是容器 limit 给得太紧。&lt;/p&gt;
&lt;p&gt;Go 服务的内存问题，最怕一句话定性。&lt;/p&gt;
&lt;p&gt;一旦你把所有问题都叫“GC 问题”，排查就会变成调参赌博：今天调 &lt;code&gt;GOGC&lt;/code&gt;，明天加 limit，后天又开始怀疑 pprof 没抓准。真正稳定的做法，是先把几条线拆开：Go 堆、Go runtime 管理的内存、RSS、容器 limit，以及业务生命周期。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-gc-debugging/debug-flow.png" alt="Go GC 排查流程"&gt;&lt;/p&gt;
&lt;h2 id="第一步不是调参数是确认谁杀了你"&gt;第一步不是调参数，是确认谁杀了你&lt;/h2&gt;
&lt;p&gt;线上看到内存上涨，第一件事不是打开 pprof，也不是改 &lt;code&gt;GOGC&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;先确认事故事实。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl describe pod &amp;lt;pod&amp;gt; -n &amp;lt;ns&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl logs &amp;lt;pod&amp;gt; -n &amp;lt;ns&amp;gt; --previous
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl top pod &amp;lt;pod&amp;gt; -n &amp;lt;ns&amp;gt; --containers
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl get pod &amp;lt;pod&amp;gt; -n &amp;lt;ns&amp;gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{.status.containerStatuses[*].lastState.terminated.reason}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;你要先回答几个问题：&lt;/p&gt;</description></item><item><title>内存一直涨，不一定是泄漏：Go GC 真正在做什么</title><link>https://blog.cpdd.fyi/posts/go-gc-three-color-marking/</link><pubDate>Thu, 21 May 2026 20:25:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-gc-three-color-marking/</guid><description>&lt;p&gt;服务内存从 800MB 慢慢涨到 2.6GB。&lt;/p&gt;
&lt;p&gt;曲线不陡，不像雪崩。它只是每天往上爬一点，重启以后掉下来，过几天又回到老地方。告警响了，群里第一句话通常是：是不是内存泄漏？&lt;/p&gt;
&lt;p&gt;这个判断不算错，但太急。&lt;/p&gt;
&lt;p&gt;在 Go 服务里，内存持续上涨可能是真泄漏，也可能是 live heap 变大了，可能是 &lt;code&gt;GOGC&lt;/code&gt; 目标让下一轮回收来得更晚，也可能是短命对象太多，把 GC 拖进了频繁工作。还有一种更容易误判：heap 已经回收了，但 RSS 没有马上按你想象的方式降下来。&lt;/p&gt;
&lt;p&gt;内存涨，不等于内存泄漏。&lt;/p&gt;
&lt;p&gt;要把这件事查清楚，不能只盯 RSS。你得知道 Go GC 什么时候启动、怎么判断对象还活着、为什么需要写屏障、哪里会停顿，以及哪些参数是真的能调。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-gc-three-color-marking/gc-flow.svg" alt="Go GC 流程图"&gt;&lt;/p&gt;
&lt;h2 id="gc-不是保洁员是成本调度器"&gt;GC 不是保洁员，是成本调度器&lt;/h2&gt;
&lt;p&gt;很多人对 GC 的直觉是“内存不用了，运行时帮我扫掉”。这个说法只对了一半。&lt;/p&gt;
&lt;p&gt;真正重要的是另一半：GC 每扫一次，都要花 CPU；扫得太勤，CPU 被回收器吃掉，服务吞吐和延迟会受影响。扫得太晚，内存峰值又会涨上去，容器可能先把你杀掉。&lt;/p&gt;
&lt;p&gt;所以 Go GC 不是等地上脏了才来的保洁员。它更像一个拿着预算表的人：上一轮还有多少对象活着？再往后拖一点，内存能不能接受？现在开始扫，CPU 能不能接受？&lt;/p&gt;
&lt;p&gt;Go 官方 GC Guide 讲得很清楚：GC 主要消耗 CPU 和物理内存。想省 CPU，就得允许堆多长一会儿；想省内存，就得让 GC 更频繁地工作。&lt;/p&gt;
&lt;p&gt;这就是 &lt;code&gt;GOGC&lt;/code&gt; 的本质。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GOGC=100&lt;/code&gt; 不是“内存达到 100MB 触发 GC”。它的意思更接近：在上一轮存活堆的基础上，允许新分配对象按约 100% 的比例增长，再触发下一轮 GC。Go 1.19 之后，目标堆计算还会把 GC roots 的成本纳入进来，可以简化理解为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;下一轮目标 ≈ live heap + (live heap + GC roots) × GOGC / 100
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;GOGC&lt;/code&gt; 调的是成本，不是情绪。&lt;/p&gt;</description></item><item><title>加 buffer 不是修 bug：Go channel 阻塞排查实战</title><link>https://blog.cpdd.fyi/posts/go-channel-practice-debugging/</link><pubDate>Thu, 21 May 2026 18:30:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-channel-practice-debugging/</guid><description>&lt;p&gt;goroutine 数量从 200 慢慢涨到 2 万。&lt;/p&gt;
&lt;p&gt;监控图上那条线，不陡，但一直在爬。就像水龙头没拧紧——不喷，但也不会停。&lt;/p&gt;
&lt;p&gt;你的第一反应是什么？&lt;/p&gt;
&lt;p&gt;加大 buffer。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;make(chan int, 100)&lt;/code&gt; 改成 &lt;code&gt;make(chan int, 1000)&lt;/code&gt;。重启。观察。好像好了。&lt;/p&gt;
&lt;p&gt;三天后，又涨回来了。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-channel-practice-debugging/cover.svg" alt="Go channel 阻塞排查封面"&gt;&lt;/p&gt;
&lt;p&gt;这不是运气差。这是 Go 服务最常见的线上问题之一：channel 阻塞导致 goroutine 泄漏。而&amp;quot;加 buffer&amp;quot;是社区里最流行、也最没用的解法。&lt;/p&gt;
&lt;p&gt;buffer 只是把崩溃推迟了。问题的根因——谁在等谁、谁该退出、谁该 close——一个都没解决。&lt;/p&gt;
&lt;p&gt;这篇文章做一件事：从&amp;quot;goroutine 涨了&amp;quot;这个信号出发，走一遍完整的排查路径，把 channel 的六个最常见陷阱逐个拆开。&lt;/p&gt;
&lt;p&gt;不是背 API。&lt;/p&gt;
&lt;p&gt;是下次线上出问题的时候，你知道先看什么。&lt;/p&gt;
&lt;h2 id="发现怎么知道-channel-卡住了"&gt;发现：怎么知道 channel 卡住了&lt;/h2&gt;
&lt;p&gt;排查 channel 阻塞的第一步不是看代码，是看数字。&lt;/p&gt;
&lt;h3 id="runtimenumgoroutine最简单的基线"&gt;runtime.NumGoroutine()：最简单的基线&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;runtime&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ticker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewTicker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;C&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;goroutines: %d&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NumGoroutine&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这段代码放到任何 Go 服务的 &lt;code&gt;init()&lt;/code&gt; 里，每 30 秒打一行日志。goroutine 数量只升不降，大概率有泄漏。&lt;/p&gt;</description></item><item><title>Go 服务 goroutine 涨了，别先猜：按这条流程查 context 泄漏</title><link>https://blog.cpdd.fyi/posts/go-context-goroutine-leak-debugging/</link><pubDate>Thu, 21 May 2026 13:45:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-context-goroutine-leak-debugging/</guid><description>&lt;p&gt;线上 goroutine 数开始往上爬，最怕的不是它涨。&lt;/p&gt;
&lt;p&gt;最怕的是你盯着监控看了十分钟，然后开始猜：是不是 &lt;code&gt;context&lt;/code&gt; 泄漏了？是不是超时没生效？是不是某个 channel 卡住了？&lt;/p&gt;
&lt;p&gt;这些猜法都有可能对，也都有可能错。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;runtime.NumGoroutine()&lt;/code&gt; 只能告诉你“现在有多少 goroutine”。它不会告诉你这些 goroutine 停在哪里，也不会告诉你是谁创建的，更不会告诉你为什么没有退出。&lt;/p&gt;
&lt;p&gt;排查这类问题，第一步不是解释 &lt;code&gt;context&lt;/code&gt; 原理。&lt;/p&gt;
&lt;p&gt;第一步是采样。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-context-goroutine-leak-debugging/cover.png" alt="Go context goroutine 泄漏排查封面"&gt;&lt;/p&gt;
&lt;p&gt;这篇是 Go &lt;code&gt;context&lt;/code&gt; 系列第三篇。前两篇讲过 &lt;code&gt;context&lt;/code&gt; 的生命周期、&lt;code&gt;cancel&lt;/code&gt; 的边界，以及 Go 为什么坚持显式传 &lt;code&gt;ctx&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这一篇换一个角度：不从源码开始，从排查现场开始。&lt;/p&gt;
&lt;p&gt;一套最小流程就够了：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;goroutine 涨了
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;先采样，确认趋势
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;用 pprof 找等待点
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;用 go vet 查静态路径
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;按创建路径、取消路径、响应路径回到代码
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;修复，再验证
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-context-goroutine-leak-debugging/inline-01.png" alt="Go context 泄漏最小排查流程"&gt;&lt;/p&gt;</description></item></channel></rss>