3、应用层监控:JVM、Go Runtime 与 Node.js Event Loop
应用层监控,说白了就是盯着你的程序运行时到底在干嘛。我见过太多团队,基础设施监控做得漂漂亮亮,一到应用层就抓瞎——CPU 跑满了不知道是 GC 引起的,内存飙了不知道是哪个线程在作妖。这一章,咱们就把 JVM、Go Runtime 和 Node.js Event Loop 这三个主流运行时监控讲透。
3.1 JVM 监控:堆内存、GC 与线程
Java 应用的内存管理,核心就是堆内存和 GC。我个人习惯先看堆内存使用曲线,再看 GC 频率和停顿时间,最后才去排查线程问题。这个顺序能帮你快速定位 80% 的性能问题。
3.1.1 堆内存监控
堆内存分新生代和老年代。新生代满了触发 Minor GC,老年代满了触发 Full GC。监控时重点关注这几个指标:
- 堆内存使用率:当前已用堆 / 最大堆。超过 80% 就要警惕。
- 老年代使用率:老年代持续增长,说明对象无法被回收,可能有内存泄漏。
- Metaspace 使用率:元空间满了会触发 Full GC,常见于动态类加载过多的场景。
核心指标:老年代使用率 + Full GC 频率,这两个指标组合能判断 90% 的 JVM 内存问题。
我在项目中遇到过一个问题:堆内存使用率一直稳定在 70%,但每隔 2 小时就会有一次长时间的 Full GC。排查后发现是某个定时任务创建了大量临时对象,这些对象因为引用链问题被晋升到了老年代。嗯,这里要注意——堆内存使用率平稳不代表没有 GC 问题。
3.1.2 GC 监控
GC 监控的核心是停顿时间和频率。我建议至少监控以下指标:
| 指标 | 正常范围 | 告警阈值 |
|---|---|---|
| Young GC 频率 | 每秒 1-5 次 | > 10 次/秒 |
| Young GC 停顿 | < 50ms | > 200ms |
| Full GC 频率 | 每小时 < 1 次 | > 5 次/小时 |
| Full GC 停顿 | < 1s | > 3s |
我曾经遇到过一个线上事故:Full GC 停顿时间达到了 12 秒,导致所有请求超时。查下来是因为堆内存设置了 32GB,但使用了 CMS 收集器,老年代碎片化严重。后来换成 G1,问题就解决了。所以,GC 收集器的选择也很关键。
小技巧:使用 -XX:+PrintGCDetails -XX:+PrintGCDateStamps 开启 GC 日志,配合 gcviewer 或 gceasy 分析,能直观看到 GC 停顿分布。
3.1.3 线程监控
线程监控主要看线程数、线程状态和死锁。我常用的命令是 jstack 和 jvisualvm。重点监控:
- 线程总数:超过 1000 就要注意,可能线程泄漏。
- BLOCKED 线程数:持续大于 0,说明有锁竞争。
- WAITING 线程数:过多可能是有线程池配置问题。
注意:线程 dump 是快照,只能看到瞬间状态。要分析线程趋势,建议用 Prometheus + JMX Exporter 采集线程指标。
3.2 Go Runtime 监控
Go 的运行时监控和 JVM 不太一样。Go 没有传统的堆内存分代,它的 GC 是并发三色标记清除。我个人习惯先看 Goroutine 数量,再看 GC 和内存分配。
3.2.1 Goroutine 监控
Goroutine 是 Go 的轻量级线程,但轻量不代表无限。我见过一个项目,Goroutine 数量飙到 50 万,直接把调度器压垮了。监控指标:
- Goroutine 总数:正常服务几千到几万,超过 10 万要警惕。
- Goroutine 创建速率:每秒创建过多,说明可能有 Goroutine 泄漏。
- Goroutine 状态分布:Running、Runnable、Waiting 的比例。
核心指标:Goroutine 总数 + 创建速率。如果总数持续增长且不下降,基本可以判定有 Goroutine 泄漏。
我曾经排查过一个 Go 服务内存持续增长的问题。用 pprof 一看,发现某个 HTTP 请求处理函数里,每次都会启动一个 Goroutine 去做日志写入,但忘了加 sync.WaitGroup 控制生命周期。结果这些 Goroutine 全卡在 channel 发送上,越积越多。修复后内存直接降了 40%。
3.2.2 GC 与内存监控
Go 的 GC 是并发标记清除,停顿时间通常很短。但如果你分配了大量小对象,GC 压力也会很大。监控指标:
| 指标 | 说明 | 告警阈值 |
|---|---|---|
| GC 次数 | 每分钟 GC 执行次数 | > 10 次/分钟 |
| GC 停顿时间 | STW 时间 | > 100ms |
| 堆内存分配速率 | 每秒分配字节数 | > 1GB/s |
| 堆内存使用率 | 当前堆 / 最大堆 | > 80% |
小技巧:使用 runtime.ReadMemStats 可以获取内存统计信息。生产环境建议用 net/http/pprof 暴露监控端点,配合 Prometheus 采集。
3.3 Node.js Event Loop 监控
Node.js 是单线程事件循环模型。说白了,所有 JavaScript 代码都在一个线程上跑。如果某个任务阻塞了事件循环,整个服务就卡住了。我见过一个 Node 服务,因为某个 API 里做了大量 CPU 密集型计算,导致事件循环延迟从 2ms 飙升到 5 秒。
3.3.1 Event Loop 延迟监控
Event Loop 延迟是衡量 Node 服务健康度的核心指标。它表示事件循环被阻塞的时间。监控方法:
- 使用
process.hrtime:在事件循环开始和结束时记录时间差。 - 使用
monitor-event-loop-delay:npm 包,专门采集延迟指标。 - 使用 Node.js 内置的
perf_hooks:可以获取事件循环的详细统计。
核心指标:Event Loop 延迟的 P99 值。如果 P99 超过 100ms,说明有任务阻塞了事件循环。
我曾经用 clinic.js 分析过一个 Node 服务。发现某个路由处理函数里,用 JSON.parse 解析了一个 10MB 的 JSON 字符串。这个操作阻塞了事件循环 800ms,导致其他请求全部排队。后来改成流式解析,问题就解决了。
3.3.2 内存与 GC 监控
Node.js 的 GC 和 V8 引擎紧密相关。监控指标:
| 指标 | 说明 | 告警阈值 |
|---|---|---|
| 堆内存使用量 | V8 堆内存 | > 1.4GB(默认最大堆) |
| GC 暂停时间 | 每次 GC 的停顿 | > 200ms |
| GC 频率 | 每分钟 GC 次数 | > 20 次/分钟 |
| 外部内存 | Buffer、ArrayBuffer 等 | > 500MB |
注意:Node.js 的默认最大堆内存是 1.4GB(64 位系统)。如果堆内存持续接近这个值,考虑用 --max-old-space-size 调整,或者排查内存泄漏。
3.4 知识体系总览
下面这张图总结了三个运行时监控的核心要点。你想想看,不管是 JVM、Go 还是 Node,最终都要落到内存、GC 和并发模型这三个维度上。
3.5 监控工具选型建议
工具选型上,我个人习惯按场景来分:
| 运行时 | 快速排查 | 持续监控 | 深度分析 |
|---|---|---|---|
| JVM | jstat, jstack, jmap | Prometheus + JMX Exporter | GCeasy, JProfiler |
| Go | pprof, trace | Prometheus + Go client | pprof 火焰图 |
| Node.js | clinic.js, 0x | Prometheus + node_exporter | Chrome DevTools |
小技巧:不管用什么工具,先把基础指标采集起来。我见过太多团队,出了问题才想起来加监控,那时候已经晚了。先把数据拿到手,后面分析才有依据。