第一章:Go生产环境救火手册:从火焰图到根因定位的实战哲学
线上服务突然 CPU 暴涨、GC 频繁、请求超时飙升?别急着重启——真正的救火不是掩盖火焰,而是读懂火焰的语言。Go 的 pprof 工具链是你的第一双红外夜视仪,它不只告诉你“哪里热”,更揭示“为什么热”。
快速捕获实时火焰图
在问题复现窗口期内,立即执行:
# 采集30秒CPU火焰图(需服务已启用pprof HTTP端点,如:6060/debug/pprof/profile)
curl -s "http://localhost:6060/debug/pprof/profile?seconds=30" -o cpu.pprof
# 生成可交互SVG火焰图
go tool pprof -http=":8080" cpu.pprof
⚠️ 注意:-http 启动后访问 http://localhost:8080,点击「Flame Graph」即可直观定位热点函数栈——宽度代表采样占比,纵向深度代表调用层级。
区分真实瓶颈与伪信号
并非所有高亮函数都是根因。重点关注以下三类模式:
- 持续宽底座:如
runtime.mallocgc占比超25%,大概率存在内存泄漏或高频小对象分配; - 深栈+窄峰:如
encoding/json.(*decodeState).object→reflect.Value.Call→(*MyStruct).UnmarshalJSON,提示反射解码开销过大,应改用easyjson或预编译go-json; - goroutine 堆积:用
curl http://localhost:6060/debug/pprof/goroutine?debug=2查看阻塞栈,若大量 goroutine 停留在select或chan send,检查 channel 容量与消费者速率是否失衡。
关键诊断组合拳
| 工具 | 触发命令 | 核心价值 |
|---|---|---|
| 内存分配热点 | go tool pprof http://:6060/debug/pprof/heap |
定位高频分配位置及对象类型 |
| GC 压力分析 | go tool pprof http://:6060/debug/pprof/gc |
查看 GC pause 时间分布与触发频率 |
| 阻塞型 goroutine | curl 'http://:6060/debug/pprof/goroutine?debug=1' |
发现死锁、channel 阻塞、锁竞争 |
救火的本质是建立因果链:火焰图指出“症状”,runtime.ReadMemStats 和 GODEBUG=gctrace=1 日志验证“病理”,而 go tool trace 则提供纳秒级调度视图,串联起 Goroutine、Network、Syscall 全链路行为。每一次精准止损,都始于对 Go 运行时语义的敬畏与熟稔。
第二章:pprof原理透析与可视化本质解构
2.1 Go运行时采样机制与pprof数据生成链路
Go 运行时通过轻量级、低开销的采样机制持续收集性能事件,核心依赖 runtime.SetCPUProfileRate 和 runtime/pprof 的协同调度。
采样触发路径
- CPU 采样:由
SIGPROF信号驱动,每100μs(默认)触发一次栈快照 - Goroutine/Heap/Block 采样:基于事件驱动(如
newobject、gopark)或周期性轮询
数据同步机制
// 启用 CPU 分析(单位:纳秒/样本)
runtime.SetCPUProfileRate(100 * 1000) // 100μs 间隔
// 后续调用 pprof.StartCPUProfile 写入 io.Writer
该设置影响 runtime.sigprof 处理频率;值为 0 则禁用,负值启用精确模式(仅限调试)。采样结果经 runtime.profileWriter 序列化为 pprof 二进制格式(Protocol Buffer),含 sample、location、function 等核心 message。
核心数据结构映射
| pprof 字段 | 运行时来源 |
|---|---|
sample.value |
runtime.cputicks() 差值 |
location.id |
runtime.gentraceback 栈帧哈希 |
function.name |
runtime.funcname(func) |
graph TD
A[Go Runtime] -->|SIGPROF/Event| B[profile.add]
B --> C[profile.bucketize]
C --> D[profile.writeTo]
D --> E[pprof.Profile proto]
2.2 火焰图坐标系、调用栈压缩与自底向上归因逻辑
火焰图的横轴表示采样时间内的相对 CPU 占用宽度(归一化至 100%),纵轴为调用栈深度,每一层矩形代表一个函数帧,高度无物理意义,仅用于视觉分层。
坐标系本质
- 横轴:聚合所有采样点中该函数出现的总时长占比(非实时时间线)
- 纵轴:调用栈层级顺序,
main → parse → tokenize自顶向下绘制,但归因方向相反
调用栈压缩机制
main → http.Serve → handler → db.Query → db.exec → sql.Open
main → http.Serve → handler → cache.Get → redis.Get
→ 压缩为共享前缀的树结构,避免重复渲染 main → http.Serve → handler
自底向上归因逻辑
| 函数名 | 样本数 | 归因权重 | 说明 |
|---|---|---|---|
sql.Open |
127 | 100% | 叶节点,独占全部样本 |
db.exec |
127 | 100% | 直接调用者,继承全部权重 |
handler |
127 | 100% | 继承下游所有分支总和 |
graph TD
A[sql.Open] --> B[db.exec]
C[redis.Get] --> D[cache.Get]
B --> E[handler]
D --> E
E --> F[http.Serve]
F --> G[main]
归因始终从叶子函数出发,将耗时全额叠加至其直接父帧,实现性能瓶颈的精准定位。
2.3 CPU/内存/阻塞profile的底层差异与采样触发条件
不同 profile 类型依赖内核事件源与调度器协作机制,触发逻辑截然不同:
触发机制对比
| Profile 类型 | 底层事件源 | 采样频率控制方式 | 是否依赖用户态栈 |
|---|---|---|---|
| CPU | PERF_COUNT_SW_CPU_CLOCK(基于 hrtimer) |
可设 sample_period(如 10ms) |
是(需 perf_callchain()) |
| 内存分配 | kmem:kmalloc/kmem:kmalloc_node tracepoint |
仅在分配/释放时触发(事件驱动) | 否(内核态上下文直接捕获) |
| 阻塞(IO/锁) | sched:sched_blocked_reason + irq:irq_handler_entry |
仅当 state == TASK_UNINTERRUPTIBLE 且持续 > 1ms 时采样 |
是(需解析 task_struct->stack) |
典型采样代码片段(eBPF)
// bpf_program.c:阻塞采样触发条件判断
if (t->state == TASK_UNINTERRUPTIBLE &&
bpf_ktime_get_ns() - t->start_time > 1000000) { // >1ms
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &data, sizeof(data));
}
逻辑分析:该判断避免高频抖动采样,仅捕获显著阻塞;t->start_time 来自 sched_switch tracepoint 记录的入队时间,1000000 单位为纳秒,确保低开销与可观测性平衡。
graph TD A[CPU Profile] –>|hrtimer tick| B[寄存器快照+栈展开] C[Memory Profile] –>|tracepoint 触发| D[分配大小+调用栈] E[Blocking Profile] –>|TASK_UNINTERRUPTIBLE + time threshold| F[等待原因+持锁/IO设备]
2.4 pprof HTTP服务与离线分析的双模调试实践
Go 程序内置的 net/http/pprof 提供开箱即用的性能剖析端点,支持实时观测与离线深度分析两种模式。
启动 HTTP pprof 服务
import _ "net/http/pprof"
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil)) // 默认注册 /debug/pprof/* 路由
}()
// ... 应用逻辑
}
该代码启用标准 pprof HTTP 接口;ListenAndServe 绑定到 localhost:6060,仅限本地访问保障安全;nil handler 自动使用 http.DefaultServeMux,已由 pprof 包自动注册全部端点(如 /debug/pprof/profile, /debug/pprof/heap)。
离线采样与分析流程
- 访问
http://localhost:6060/debug/pprof/profile?seconds=30获取 30 秒 CPU profile(二进制格式) - 保存为
cpu.pprof,执行:go tool pprof cpu.pprof - 在交互式终端中输入
web生成火焰图,或top10查看热点函数
| 分析场景 | 推荐端点 | 输出格式 |
|---|---|---|
| CPU 使用热点 | /debug/pprof/profile |
profile.pb |
| 内存分配峰值 | /debug/pprof/heap |
pprof 二进制 |
| Goroutine 阻塞 | /debug/pprof/block |
需启用 runtime.SetBlockProfileRate() |
graph TD
A[启动 pprof HTTP 服务] --> B[实时诊断:curl /debug/pprof/heap]
A --> C[离线采样:curl -o mem.pprof /debug/pprof/heap]
C --> D[go tool pprof mem.pprof]
D --> E[web / top / list 命令深度分析]
2.5 常见火焰图误读陷阱:inlined函数、runtime开销、GC伪热点识别
火焰图并非“所见即所得”的性能真相,需警惕三类典型误读:
inlined 函数的消失幻象
当编译器内联 func add(a, b int) int { return a + b } 后,调用栈中不再出现 add,其 CPU 时间被“折叠”进调用者。若仅关注顶层函数,可能低估高频小函数的真实开销。
// go tool pprof -http=:8080 cpu.pprof
// 此处无 add 栈帧,但 -inlines=true 可还原(需调试符号)
分析:
-inlines=true参数强制展开内联帧;-show=inlined过滤显示;默认关闭因会显著增加栈深度和噪声。
runtime 与 GC 的伪热点
Go 的 runtime.mcall、runtime.gcDrain 常在火焰图顶部堆积,但它们是调度/回收的载体,非根本瓶颈——真正问题常在触发 GC 的大对象分配或阻塞型系统调用。
| 现象 | 真实根源 | 诊断建议 |
|---|---|---|
runtime.scanobject 占比高 |
频繁分配未及时释放的堆对象 | go tool pprof -alloc_space |
runtime.netpoll 持续燃烧 |
goroutine 阻塞于 I/O 或 channel | go tool pprof -goroutines |
graph TD
A[火焰图顶部热点] --> B{是否 runtime/GC 相关?}
B -->|是| C[检查内存分配模式]
B -->|否| D[定位业务逻辑栈帧]
C --> E[用 alloc_objects 对比 alloc_space]
第三章:CPU热点三步精确定位法
3.1 第一步:topN函数+调用频次+指令周期交叉验证
性能瓶颈定位需三重信号对齐:高频调用、高耗时指令、低效热点函数。topN 函数是起点,用于提取调用栈中前 N 个最常出现的函数名。
def topN_calls(profile_data, n=10):
from collections import Counter
calls = [frame[0] for frame in profile_data] # 提取函数名
return Counter(calls).most_common(n) # 返回 (func_name, count) 元组列表
该函数输出调用频次排名,但无法反映单次开销;需与 CPU 指令周期(如 perf stat -e cycles,instructions 采集的 IPC)联合分析。
| 函数名 | 调用次数 | 平均周期/调用 | IPC |
|---|---|---|---|
memcpy |
8421 | 12,450 | 0.82 |
json_loads |
3105 | 89,730 | 0.31 |
交叉验证逻辑
- 高频 + 高周期 → 真实热点(如
json_loads) - 高频 + 低周期 → 吞吐型轻量函数(如
memcpy)
graph TD
A[原始 perf.data] --> B[topN_calls]
A --> C[perf script -F sym,cycles]
B & C --> D[按函数名 join]
D --> E[筛选:count > 1000 ∧ avg_cycles > 50k]
3.2 第二步:源码级行号热点映射与编译器内联标注分析
在性能剖析中,将采样热点精准回溯至原始源码行号,是定位瓶颈的关键跃迁。JVM 的 AsyncGetCallTrace 或 Linux perf + libdw 需结合调试信息(DWARF)完成符号解折叠,并识别编译器内联引入的行号偏移。
行号映射核心逻辑
// HotSpot VM 中行号表解析片段(简化)
int getLineNumber(int bci) {
// bci: 字节码索引;lineNumberTable: 按bci升序排列的{start_bci → line_number}对
for (int i = lineNumberTable.length - 1; i >= 0; i--) {
if (lineNumberTable[i].start_bci <= bci) {
return lineNumberTable[i].line_number; // 返回最晚覆盖该bci的源码行
}
}
return -1;
}
该逻辑依赖 JVM 编译时生成的 LineNumberTable 属性,确保字节码偏移到源码行的单向确定性映射;若方法被内联,需额外叠加调用点行号上下文。
内联标注识别策略
- 通过
javac -g:lines,source保留完整调试信息 - 使用
-XX:+PrintInlining输出内联决策日志(含inline (hot)标记) - 分析
CompiledMethod::print_nmethod()中nmethod::scopes_data()提取嵌套作用域行号范围
| 内联层级 | DWARF DW_TAG_inlined_subroutine 属性 |
是否影响行号归属 |
|---|---|---|
| 顶层方法 | ❌ | 直接归属 |
| 一级内联 | ✅ DW_AT_call_line + DW_AT_call_file |
归属调用点行号 |
| 深度内联 | ✅ 多层嵌套 DW_TAG_inlined_subroutine |
需栈式回溯解析 |
graph TD
A[采样PC地址] --> B[查找对应nmethod]
B --> C{是否含DWARF调试段?}
C -->|是| D[解析scopes_data → 获取inlined call site]
C -->|否| E[退化为字节码行号表映射]
D --> F[递归展开内联链,还原原始源码行]
3.3 第三步:结合trace分析goroutine调度延迟与锁竞争放大效应
当 GOMAXPROCS=4 且高并发写入 sync.Map 时,runtime.traceEvent 暴露的 GoSched 与 BlockSync 事件常密集交织:
// 启用调度追踪(需编译时 -gcflags="-l" 避免内联干扰)
go tool trace trace.out
数据同步机制
GoSched频繁触发 → 表明 goroutine 主动让出 CPU,常因锁阻塞后被强制调度BlockSync持续 >100µs → 锁持有时间过长,引发后续 goroutine 排队雪崩
关键指标对比
| 事件类型 | 平均延迟 | 关联现象 |
|---|---|---|
| GoSched | 82 µs | 后续 Goroutine 创建延迟上升 3.2× |
| BlockSync | 147 µs | 紧随其后的 GoStartLocal 减少 68% |
调度放大链路
graph TD
A[goroutine A 尝试 Lock] --> B{锁已被持}
B -->|yes| C[进入 sync.Mutex.block]
C --> D[触发 GoSched]
D --> E[其他 goroutine 轮询失败 → 再次 GoSched]
E --> F[调度器负载不均 → P 队列积压]
第四章:内存与阻塞问题的纵深诊断策略
4.1 内存泄漏定位:heap profile增量对比与对象生命周期追踪
内存泄漏常表现为进程 RSS 持续增长但 GC 后 heap size 未回落。核心诊断路径是增量式 heap profile 对比与对象引用链回溯。
增量采样与 diff 分析
使用 pprof 在关键节点(如任务开始前/结束后)采集 heap profile:
# 采集基线(t0)
curl -s "http://localhost:6060/debug/pprof/heap?debug=1" > heap_t0.pb.gz
# 执行可疑操作后采集(t1)
curl -s "http://localhost:6060/debug/pprof/heap?debug=1" > heap_t1.pb.gz
# 差分分析:仅显示 t1 中新增/显著增长的分配
go tool pprof --base heap_t0.pb.gz heap_t1.pb.gz
--base参数启用 delta 模式,聚焦inuse_space或alloc_objects的净增量;-focus=".*Handler"可限定分析范围,避免噪声干扰。
对象生命周期追踪关键指标
| 指标 | 说明 | 泄漏信号 |
|---|---|---|
inuse_objects |
当前存活对象数 | 持续上升且不随 GC 下降 |
alloc_objects |
累计分配对象数(含已回收) | 与 inuse_objects 差值稳定扩大 |
stack_trace |
从 runtime.mallocgc 向上追溯调用栈 |
多次复现同一业务路径 |
引用链可视化
graph TD
A[Leaked Object] --> B[Finalizer Queue]
A --> C[Global Map Value]
C --> D[HTTP Handler Closure]
D --> E[Unstopped Timer]
闭环泄漏常见于:未关闭的 time.Ticker、全局 sync.Map 缓存未清理、HTTP handler 闭包捕获长生命周期结构体。
4.2 GC压力溯源:allocs vs inuse_objects vs live_heap_bytes三维建模
Go 运行时指标 allocs, inuse_objects, live_heap_bytes 构成 GC 压力诊断的黄金三角——分别刻画分配频次、活跃对象数与存活堆内存,三者偏离常态即暗示内存模式异常。
为何需三维联合分析?
- 单看
allocs高:可能仅短生命周期对象激增(如字符串拼接); inuse_objects持续攀升但live_heap_bytes平稳:小对象泄漏(如 map[string]*T 中 key 泄漏);live_heap_bytes高而inuse_objects低:大对象驻留(如未释放的 []byte 缓冲区)。
关键指标采集示例
// 从 runtime.ReadMemStats 获取实时快照
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Alloc = %v MiB, InuseObjects = %v, LiveHeapBytes = %v MiB\n",
m.Alloc/1024/1024, m.NumGC, m.HeapInuse/1024/1024) // 注意:NumGC 是 GC 次数,非 inuse_objects;正确字段为 m.HeapObjects
m.HeapObjects表示当前堆中活跃对象总数(非 GC 次数),m.Alloc是累计分配字节数(含已回收),m.HeapInuse是当前实际占用的堆内存字节数。三者需同采同析,避免时间漂移导致误判。
| 指标 | 含义 | 敏感场景 |
|---|---|---|
allocs |
累计分配字节数 | 高频小对象分配 |
inuse_objects |
当前堆中活跃对象数量 | 对象池未复用 / 弱引用失效 |
live_heap_bytes |
当前堆中存活数据总大小 | 大切片/缓存未释放 |
graph TD
A[allocs ↑] -->|+ inuse_objects ↑| B[对象创建失控]
A -->|+ live_heap_bytes ↑| C[大对象持续驻留]
inuse_objects ↑ -->|live_heap_bytes ↔| D[小对象泄漏]
4.3 goroutine阻塞分析:block profile中的mutex/chan/semaphore根因分离
Go 的 block profile 记录了 goroutine 因同步原语而阻塞的堆栈,但默认不区分阻塞类型。需结合 -blockprofile 与运行时标记精准归因。
数据同步机制
阻塞根源三类典型场景:
sync.Mutex:争用临界区channel:发送/接收端未就绪(尤其无缓冲 channel)semaphore:基于sync.WaitGroup或golang.org/x/sync/semaphore的计数器耗尽
根因分离实践
启用细粒度采样:
GODEBUG=blockprofilerate=1 go run main.go
go tool pprof -http=:8080 block.prof
blockprofilerate=1 强制捕获每次阻塞事件,避免采样丢失低频但关键的 semaphore 等待。
阻塞类型识别对照表
| 阻塞特征 | 典型调用栈片段 | 根因线索 |
|---|---|---|
| Mutex 争用 | sync.(*Mutex).Lock → runtime.semacquire1 |
semaRoot 中含 mutex |
| Channel 同步阻塞 | runtime.chansend1 / chanrecv1 |
调用栈含 chan + selectgo |
| Weighted Semaphore 等待 | semaphore.(*Weighted).Acquire |
包路径含 x/sync/semaphore |
// 示例:混合阻塞场景(需 profile 分离)
func worker(sem *semaphore.Weighted, mu *sync.Mutex, ch chan int) {
sem.Acquire(context.Background(), 1) // 可能阻塞于 semaphore
mu.Lock() // 可能阻塞于 mutex
ch <- 42 // 可能阻塞于 channel
mu.Unlock()
sem.Release(1)
}
该函数中三类阻塞共存;block profile 原始输出仅显示 runtime.semacquire1,需结合符号化堆栈与调用上下文反向映射至语义原语——这是根因分离的关键跳点。
4.4 死锁与活锁的pprof辅助判定:goroutine dump + block profile联合推演
死锁表现为所有 goroutine 阻塞于同步原语(如 mutex、channel、waitgroup),而活锁则呈现高频率自旋重试却无进展。二者在 pprof 中需交叉验证:
goroutine dump 定位阻塞点
执行 curl "http://localhost:6060/debug/pprof/goroutine?debug=2" 获取完整栈快照,重点关注 semacquire、chan receive 或 sync.(*Mutex).Lock 等阻塞调用链。
block profile 揭示争用热点
启用 runtime.SetBlockProfileRate(1) 后采集:
// 启用阻塞分析(每纳秒阻塞即记录)
import _ "net/http/pprof"
func init() {
runtime.SetBlockProfileRate(1) // 1 = 记录每次阻塞事件
}
SetBlockProfileRate(1)强制记录所有阻塞事件,代价较高,仅用于诊断;值为 0 则禁用,>1 表示平均每 N 纳秒采样一次。
联合推演逻辑
| 信号来源 | 死锁特征 | 活锁线索 |
|---|---|---|
| goroutine dump | 大量 goroutine 停留在同一锁/chan | 多 goroutine 循环调用 atomic.CompareAndSwap |
| block profile | 单一调用点 block 时间极长 | 多个短时 block 高频重复出现 |
graph TD
A[HTTP /debug/pprof/goroutine?debug=2] --> B[识别阻塞栈帧]
C[HTTP /debug/pprof/block] --> D[聚合阻塞调用频次与耗时]
B & D --> E[交叉比对:是否所有 goroutine 争抢同一资源?是否无进展重试?]
第五章:构建可持续演进的Go可观测性防御体系
核心原则:观测即契约,而非事后补救
在某电商中台服务迭代中,团队将 otelhttp 中间件与 prometheus.ClientGatherer 绑定为启动必检项,任何未注册 /metrics 端点或缺失 service.name resource attribute 的 Pod 均被 admission webhook 拒绝部署。该策略上线后,新服务平均接入可观测性耗时从 3.2 天压缩至 17 分钟。
自动化黄金信号采集管道
以下代码片段定义了统一的指标注册器,强制注入延迟分布直方图、错误率计数器与活跃连接数仪表:
func NewInstrumentedRouter() *chi.Mux {
r := chi.NewMux()
r.Use(otelhttp.NewMiddleware("api-gateway"))
r.Use(prometheus.NewMetricsMiddleware(
prometheus.WithHistogramBuckets([]float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5}),
))
return r
}
可观测性配置即代码(IaC)治理模型
采用 YAML Schema 定义服务级可观测性契约,CI 流程自动校验其与实际埋点一致性:
| 字段 | 必填 | 示例值 | 验证方式 |
|---|---|---|---|
service.name |
是 | payment-service |
对比 OpenTelemetry Resource 属性 |
metrics.scrape_interval |
是 | 15s |
校验 Prometheus scrape config |
traces.sampling_ratio |
否 | 0.05 |
检查 otelcol 配置文件中对应 exporter 设置 |
动态熔断式日志降噪机制
基于 Loki 查询延迟与错误率动态调整日志采样率:当 rate({job="loki"} |~ "error" [5m]) > 100 且 histogram_quantile(0.95, rate(loki_request_duration_seconds_bucket[5m])) > 2.0 时,自动将 loglevel=debug 的采样率从 1.0 降至 0.01,并通过 OpenTelemetry Logs SDK 注入 sampling_decision=auto_throttled 属性。
跨团队可观测性能力复用平台
内部构建的 go-observability-kit 模块已集成至 47 个 Go 服务,提供开箱即用的三件套:
otelgrpc.NewInterceptor()支持 gRPC 方法级 span 名称标准化(如payment.Process/ChargeV2)sqltrace.WrapDB()自动注入慢查询阈值告警(>200ms 触发db.query.slow事件)httptrace.NewRoundTripper()补全下游调用链路中的http.status_code与http.route属性
演进式 Schema 版本管理
所有 trace/span 属性均遵循语义化版本控制:span.SetAttributes(attribute.String("rpc.method.v2", "Charge")) 中的 v2 后缀与 go.mod 中 github.com/company/otel-schema v1.2.0 强绑定,Schema 变更需同步更新 proto 定义并生成新版 Go binding,避免跨服务字段歧义。
实时异常模式挖掘流水线
使用 Temporal 工作流编排异常检测任务:每 3 分钟拉取最近 5 分钟 Jaeger trace 数据,执行以下规则引擎匹配:
- 连续 3 次调用中
http.status_code=500且error.type="database_timeout" - 同一
service.name下span.kind=server的durationP99 上升 >300% 并持续 2 个周期
匹配结果直接写入 Alertmanager 并触发severity=high告警。
可观测性负债清零看板
运维团队每日晨会聚焦“可观测性技术债”看板,包含三类实时指标:
- 埋点缺口:未上报
process.runtime.version的实例数(当前:0) - 数据漂移:
http.status_code枚举值超出预设白名单的数量(当前:2 → 新增429已同步更新 schema) - 链路断裂:
trace_id在 Envoy access log 与应用 span 中不一致的请求占比(当前:0.0017%)
混沌工程可观测性验证闭环
每月执行一次 chaos-mesh 注入网络分区故障,自动运行以下验证脚本:
# 验证链路完整性
curl -s "http://jaeger-query:16686/api/traces?service=order-service&limit=10" | \
jq -r '.data[].spans[] | select(.tags[].key=="error" and .tags[].value==true) | .traceID' | \
wc -l
# 验证指标时效性
curl -s "http://prometheus:9090/api/v1/query?query=rate(http_server_requests_total{job=~\".*order.*\"}[1m])" | \
jq '.data.result[].value[1]' 