第一章:Go服务升级后Trace断层现象与根因定位
在将Go服务从1.19升级至1.22后,可观测性平台(如Jaeger或OpenTelemetry Collector)中频繁出现Span链路断裂:上游HTTP请求生成的TraceID未透传至下游gRPC调用,导致单次请求的Trace呈现为多个孤立片段。该现象并非随机偶发,而是稳定复现在使用net/http标准库处理请求、再通过google.golang.org/grpc发起下游调用的典型链路中。
Trace断层的典型表现
- HTTP入口Span拥有完整TraceID和SpanID,但其子Span(如数据库查询、gRPC客户端调用)缺失parent_span_id;
- OpenTelemetry SDK日志中持续输出
"failed to extract trace context from carrier"警告; otelhttp.NewHandler中间件捕获的入参context中trace.SpanFromContext(ctx)返回nil span。
根因定位关键线索
Go 1.22默认启用了http.Transport的ForceAttemptHTTP2 = true,而旧版grpc-go(v1.58之前)未完全兼容HTTP/2流式上下文透传机制,导致otelgrpc.UnaryClientInterceptor无法从*http.Request中正确提取traceparent头字段。
验证与修复步骤
首先确认gRPC版本及HTTP/2启用状态:
# 检查依赖版本
go list -m google.golang.org/grpc
# 输出示例:google.golang.org/grpc v1.57.0 // < v1.59.0 存在兼容问题
升级gRPC并显式配置Transport以规避自动HTTP/2降级陷阱:
import "google.golang.org/grpc/credentials/insecure"
// 创建gRPC连接时禁用强制HTTP/2(临时方案)
conn, err := grpc.Dial("backend:8080",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
// 强制使用HTTP/1.1避免上下文丢失
tr := &http.Transport{ForceAttemptHTTP2: false}
req, _ := http.NewRequestWithContext(ctx, "GET", "http://"+addr, nil)
return tr.DialContext(ctx, "tcp", addr)
}),
)
| 组件 | 升级前状态 | 升级后要求 |
|---|---|---|
| Go版本 | 1.19 | ≥1.22 |
| grpc-go | ≤v1.58.3 | ≥v1.59.0 |
| otel-go | v1.17.0 | ≥v1.24.0(含修复context传递逻辑) |
完成升级后,需通过注入测试Header验证透传完整性:
curl -H 'traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' \
http://service-a:8080/api/v1/order
观察下游服务日志是否打印出一致的TraceID与parent_span_id。
第二章:Go module版本兼容性深度校验
2.1 Go module语义化版本规则与依赖解析机制解析
Go module 采用 vX.Y.Z 语义化版本格式,其中 X 为大版本(不兼容变更)、Y 为小版本(新增向后兼容功能)、Z 为补丁版本(向后兼容的缺陷修复)。
版本比较规则
v1.2.0v1.2.1 v1.3.0 v2.0.0- 大版本号变化即视为独立模块:
github.com/user/repo/v2是github.com/user/repo的不同模块
依赖解析核心原则
- 最小版本选择(MVS):为每个模块选取满足所有依赖约束的最小可行版本
- 避免重复引入同一模块的不同大版本(除非显式 require v2+)
# go.mod 片段示例
module example.com/app
go 1.21
require (
github.com/go-sql-driver/mysql v1.7.1
golang.org/x/net v0.14.0 # 实际可能降级至 v0.13.0 若上游仅需该版
)
逻辑分析:
go build运行时执行 MVS 算法,遍历所有require及其 transitive 依赖,构建版本约束图;v0.14.0可能被回退——只要v0.13.0满足所有调用方接口契约且未违反//go:requires约束。
| 版本类型 | 兼容性要求 | 是否触发模块路径变更 |
|---|---|---|
| v1.2.0 → v1.2.1 | ✅ 向后兼容补丁 | ❌ 否 |
| v1.2.0 → v1.3.0 | ✅ 新增兼容功能 | ❌ 否 |
| v1.9.0 → v2.0.0 | ❌ 不兼容变更 | ✅ 是(需 /v2 路径) |
graph TD
A[go build] --> B{解析 go.mod}
B --> C[收集所有 require]
C --> D[构建模块约束图]
D --> E[运行 MVS 算法]
E --> F[确定最终版本集]
F --> G[下载并编译]
2.2 升级前后go.sum校验失败的典型场景与修复实践
常见触发场景
- 依赖模块在新版本中重签名(如
golang.org/x/netv0.25.0 重构了http2包哈希) - 本地
go.mod显式降级但未更新go.sum(go get example.com/lib@v1.2.0后遗漏go mod tidy) - 使用
replace指向 fork 分支,而 fork 的 commit hash 与原始模块不一致
典型修复流程
# 清理缓存并强制重新计算校验和
go clean -modcache
go mod tidy -v # -v 输出每项校验细节
此命令会重新下载所有依赖、验证
.zip文件 SHA256 并生成新go.sum条目;-v可定位具体哪一行校验失败(如inconsistent versions: ...)
校验失败对比表
| 场景 | go.sum 行特征 | 推荐操作 |
|---|---|---|
| 版本升级未同步 | 同模块存在 v1.2.0/v1.3.0 两行 | go mod tidy |
| replace 路径变更 | sum 值与原始模块不匹配 |
go mod verify + 手动校验 |
graph TD
A[go build 失败] --> B{检查 go.sum 错误行}
B --> C[是否含 replace?]
C -->|是| D[验证 fork commit 签名]
C -->|否| E[执行 go mod tidy]
2.3 主版本跃迁(v1→v2+)引发的导入路径断裂诊断与迁移方案
当 Go 模块从 v1 升级至 v2+,语义化版本强制要求导入路径追加 /v2 后缀,否则 go build 将报错:module declares its path as ... but was required as ...。
常见断裂模式
import "github.com/org/pkg"→ 必须改为import "github.com/org/pkg/v2"go.mod中require github.com/org/pkg v1.9.0未同步更新为v2.1.0
诊断命令
# 扫描全部 import 语句并匹配非/vN 路径
grep -r 'github.com/org/pkg"' --include="*.go" . | grep -v '/v[2-9]'
逻辑说明:
grep -r递归查找 Go 文件中原始导入;grep -v '/v[2-9]'过滤已适配路径,暴露待修复项;正则[2-9]避免误伤v10+的复杂场景。
迁移检查表
| 项目 | 状态 | 说明 |
|---|---|---|
go.mod require 版本号 |
✅ | 必须 ≥ v2.0.0 且含 /v2 路径 |
所有 import 语句 |
✅ | 包含 github.com/org/pkg/v2 |
go list -m all 输出 |
✅ | 确认无重复解析(如同时存在 pkg 和 pkg/v2) |
自动化迁移流程
graph TD
A[检测 go.mod 中 v2+ 依赖] --> B{是否存在 /v2 导入?}
B -->|否| C[批量替换 import 路径]
B -->|是| D[验证类型兼容性]
C --> E[运行 go mod tidy]
2.4 replace与exclude指令在跨版本追踪链路中的副作用实测分析
数据同步机制
在跨版本链路追踪中,replace 与 exclude 指令常用于过滤或重写 span 标签。但实测发现:当 v1.8 客户端向 v2.3 服务端发送含 exclude: ["user_id"] 的 trace 时,v2.3 的采样器因缺失关键上下文字段触发降级采样(仅保留 5%)。
关键副作用复现代码
# tracing-config-v2.yaml
rules:
- service: "auth-service"
exclude: ["password", "token"]
replace:
"http.url": "/api/v2/health" # 强制归一化路径
逻辑分析:
exclude在序列化前移除字段,导致下游 v2.3 的链路聚合器无法匹配user_id→session_id关联规则;replace则在反序列化后覆盖原始值,破坏 v1.x 客户端的路径语义一致性。参数http.url替换未校验版本兼容性,引发跨版本 URL 聚类失真。
实测影响对比
| 指令类型 | v1.8 → v2.3 链路完整性 | 采样率偏差 | 根因定位耗时 |
|---|---|---|---|
| 无指令 | 100% | ±0.2% | 2min |
exclude |
63% | -42% | 17min |
replace |
79% | -18% | 9min |
跨版本传播流程
graph TD
A[v1.8 Client] -->|inject exclude| B[Trace Propagation]
B --> C{v2.3 Gateway}
C -->|drop excluded field| D[Missing correlation key]
D --> E[Failed span linking]
C -->|apply replace pre-serialize| F[URL normalization]
F --> G[Broken v1.x path-based SLO]
2.5 多模块协同升级时的版本对齐策略与自动化验证脚本编写
版本对齐核心原则
- 语义化约束:所有模块必须满足
MAJOR.MINOR主干一致,PATCH可独立演进 - 依赖图收敛:模块间依赖关系需构成有向无环图(DAG),避免循环引用
自动化校验流程
# verify-version-alignment.sh
#!/bin/bash
MODULES=("auth" "payment" "notification" "analytics")
BASE_VERSION=$(jq -r '.version' config/base.json) # 如 "2.3.0"
for mod in "${MODULES[@]}"; do
mod_ver=$(jq -r '.version' "modules/$mod/version.json")
major_minor=$(echo "$mod_ver" | cut -d. -f1,2)
base_mm=$(echo "$BASE_VERSION" | cut -d. -f1,2)
[[ "$major_minor" != "$base_mm" ]] && echo "[ERROR] $mod mismatch: $mod_ver ≠ $BASE_VERSION" && exit 1
done
echo "[OK] All modules aligned on $base_mm"
逻辑说明:脚本提取基线主干版本(如
2.3),逐模块解析其version.json,仅比对MAJOR.MINOR;cut -d. -f1,2确保忽略补丁号差异,符合灰度发布场景需求。
验证结果概览
| 模块 | 声明版本 | 主干匹配 | 状态 |
|---|---|---|---|
| auth | 2.3.7 | 2.3 | ✅ |
| payment | 2.3.12 | 2.3 | ✅ |
| notification | 2.4.0 | ❌ 2.4 | ⚠️ |
graph TD
A[读取基线版本] --> B[提取 MAJOR.MINOR]
B --> C{遍历各模块}
C --> D[解析 version.json]
D --> E[截取前两段]
E --> F{是否相等?}
F -->|是| G[继续下一模块]
F -->|否| H[报错退出]
第三章:OpenTelemetry-Go SDK语义版本演进影响分析
3.1 v1.0+ SDK中TracerProvider与SpanProcessor接口变更对埋点逻辑的冲击
v1.0+ SDK 将 TracerProvider 的 addSpanProcessor() 方法由可变参改为仅接受单个 SpanProcessor 实例,同时 SpanProcessor 接口移除了 onStart() 的 Context 参数,仅保留 Span 和 ParentContext。
埋点兼容性断裂点
- 旧版多处理器链式注册需显式封装为
CompositeSpanProcessor - 自定义上下文增强逻辑因
Context缺失需迁移至SpanBuilder.startSpan()
关键代码变更示例
// v0.35.x(兼容)
tracerProvider.addSpanProcessor(procA, procB);
// v1.0+(强制单实例)
tracerProvider.addSpanProcessor(
new CompositeSpanProcessor(Arrays.asList(procA, procB))
);
CompositeSpanProcessor 现为标准组合器,避免手动轮询;procA/procB 必须线程安全,因 SDK 不再同步调用其生命周期方法。
| 变更维度 | v0.35.x | v1.0+ |
|---|---|---|
| 处理器注册方式 | 可变参 | 单实例 + 组合器显式构造 |
| onStart() 上下文 | Context 全量传入 |
仅 ParentContext(轻量) |
graph TD
A[TracerProvider] --> B[addSpanProcessor]
B --> C{v1.0+}
C --> D[Single SpanProcessor]
D --> E[CompositeSpanProcessor]
E --> F[procA.onStart span]
E --> G[procB.onStart span]
3.2 Context传播机制(TextMapPropagator)在v1.17→v1.22间的行为差异复现与适配
差异根源:Fields() 方法语义变更
v1.17 中 TextMapPropagator.Fields() 返回静态键列表(如 ["traceparent", "tracestate"]),而 v1.22 改为动态键集合——依据当前 Context 是否含 SpanContext 决定是否包含 tracestate。
// v1.22 行为:条件式字段暴露
func (p *TraceContextPropagator) Fields() []string {
if span := trace.SpanFromContext(ctx); span.SpanContext().HasTraceState() {
return []string{"traceparent", "tracestate"} // ✅ 动态返回
}
return []string{"traceparent"} // ❌ v1.17 始终返回两者
}
逻辑分析:
ctx参数在 v1.22 中被隐式注入(通过Inject/Extract调用链传递),导致Fields()不再纯函数化;HasTraceState()判定依赖运行时 Span 状态,破坏了无状态传播器契约。
适配策略对比
| 方案 | 兼容性 | 风险 |
|---|---|---|
升级后强制注入空 trace.SpanContext{} |
✅ v1.17–v1.22 | 引入冗余 tracestate header |
实现 FallbackPropagator 包装器 |
✅ 精确控制字段 | 需重写 Inject/Extract |
数据同步机制
graph TD
A[HTTP Request] --> B{Propagator.Fields()}
B -->|v1.17| C["['traceparent','tracestate']"]
B -->|v1.22| D["['traceparent'] 或 ['traceparent','tracestate']"]
C & D --> E[Header 注入]
3.3 Metric与Trace双通道共存时SDK版本混用导致的上下文丢失实战排查
当应用同时集成 OpenTelemetry Java SDK 的 opentelemetry-api(v1.28.0)与 opentelemetry-sdk-metrics(v1.25.0),跨通道上下文传递即失效。
数据同步机制
Metric SDK v1.25.0 仍依赖 Context.current() 的弱引用快照,而 Trace SDK v1.28.0 已升级为 Scope 生命周期强绑定:
// 错误混用:MetricRecorder 在无 Scope 的线程中执行
meter.counter("http.requests").add(1); // 此处 Context.current() 为空
→ 该调用不感知当前 Span,导致 metric 标签缺失 trace_id、span_id。
版本兼容性矩阵
| API 版本 | Metrics SDK | Trace SDK | Context 跨通道透传 |
|---|---|---|---|
| 1.25.0 | ✅ | ❌ | 不支持 |
| 1.28.0 | ✅ | ✅ | ✅(需统一升级) |
排查流程图
graph TD
A[HTTP 请求进入] --> B{Span 是否激活?}
B -->|是| C[Trace SDK 注入 Context]
B -->|否| D[Metrics 调用丢失 trace 上下文]
C --> E[Metrics SDK v1.28.0 拦截 Context]
E --> F[自动注入 trace_id/span_id 标签]
第四章:gRPC wire协议层追踪元数据传递一致性验证
4.1 gRPC Metadata中traceparent字段的序列化/反序列化兼容性边界测试
traceparent 格式规范回顾
W3C Trace Context 要求 traceparent 必须为固定格式:00-<trace-id>-<span-id>-<flags>(共55字符),其中 trace-id 和 span-id 为16字节十六进制小写字符串。
兼容性边界用例验证
| 测试场景 | 输入值示例 | 是否被gRPC Go/Java/Python接受 |
|---|---|---|
| 合法标准格式 | 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 |
✅ |
| 大写十六进制 | 00-4BF92F35...-00F067AA...-01 |
❌(Go grpc-go v1.60+ 拒绝) |
| 缺失前导零 | 00-4bf92f35...-f067aa...-01 |
❌(span-id 长度不足16字符) |
反序列化健壮性测试代码
// 使用官方 w3c/trace-context-go 解析器校验
tp := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
parsed, err := traceparent.Parse(tp)
if err != nil {
log.Fatal("parse failed: ", err) // 如含大写或截断,此处 panic
}
该代码调用 traceparent.Parse() 执行严格正则匹配与长度校验(^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$),任何格式偏差均触发 ErrInvalidTraceParent。
关键约束图示
graph TD
A[Metadata.Set] --> B{traceparent value}
B -->|55-char lowercase hex| C[Accepted]
B -->|Uppercase/mismatched length| D[Rejected at parse time]
4.2 Unary与Streaming RPC中OpenTelemetry gRPC拦截器版本错配导致的Span截断复现
当 opentelemetry-go-contrib/instrumentation/google.golang.org/grpc 的拦截器版本(如 v0.43.0)与核心 SDK(如 v1.22.0)不兼容时,Streaming RPC 的 StreamServerInterceptor 可能提前结束 Span,导致 stream.Recv()/stream.Send() 调用脱离上下文。
根本原因
- Unary 拦截器隐式
span.End()在 handler 返回后; - Streaming 拦截器若未正确传递
context.WithValue(ctx, streamKey, stream),后续Recv()调用将 fallback 到全局无 span 上下文。
复现关键代码
// ❌ 错误:v0.42.0 中 StreamServerInterceptor 缺失 context 增强逻辑
func (i *Interceptor) StreamServerInterceptor(
ctx context.Context, // 未注入 stream-aware context
stream grpc.ServerStream,
info *grpc.StreamServerInfo,
handler grpc.StreamHandler,
) error {
// span.Start() 后未 wrap stream → 后续 Recv/Send 无法继承 span
return handler(ctx, stream) // span.End() 在此之后被错误触发
}
该实现导致 stream.Recv() 执行时 trace.SpanFromContext(ctx) 返回 nil,新 Span 被丢弃。
版本兼容对照表
| SDK 版本 | Interceptor 版本 | Streaming Span 完整性 |
|---|---|---|
| v1.22.0 | v0.42.0 | ❌ 截断(无 stream context 透传) |
| v1.22.0 | v0.47.0+ | ✅ 修复(显式 wrapServerStream) |
graph TD
A[Client Stream Send] --> B[Server StreamServerInterceptor]
B --> C{Interceptor v0.42.0?}
C -->|Yes| D[ctx 未增强 → span lost on Recv]
C -->|No| E[ctx.WithValue wrapped → span preserved]
4.3 TLS/HTTP/2帧头与自定义编码器对trace context透传的干扰定位方法论
当 trace context(如 traceparent)在 HTTP/2 流中被自定义序列化器篡改或截断,根本原因常隐匿于 TLS 记录层与 HPACK 压缩协同作用之下。
帧头污染检测点
- 检查
HEADERS帧是否携带:authority与traceparent同帧(HPACK 动态表索引冲突易致丢弃) - 验证 TLS 应用数据分片是否将单个
HEADERS帧跨多个TLSPlaintext记录(导致中间件解析错位)
自定义编码器典型陷阱
// 错误:JSON 序列化时未保留大小写,将 "traceparent" 转为 "traceParent"
Map<String, String> headers = new HashMap<>();
headers.put("TraceParent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01");
// → HTTP/2 解析器因严格匹配 header name 而忽略该字段
逻辑分析:HTTP/2 规范要求 header name 全小写且 ASCII 精确匹配;TraceParent 不会被识别为标准 trace context 字段。put() 参数 key 必须为 "traceparent"(RFC 9113 §8.3.1)。
| 干扰层级 | 可观测现象 | 定位工具 |
|---|---|---|
| TLS | Wireshark 显示 DATA 帧缺失 HEADERS |
tshark + http2.header.name == "traceparent" |
| HPACK | DECODE_ERROR RST_STREAM |
nghttp2 –verbose |
| 编码器 | 日志中 traceparent 值为空字符串 |
OpenTelemetry SDK debug log |
graph TD A[Client 发起请求] –> B{自定义Encoder注入} B –> C[traceparent 小写校验失败] C –> D[HPACK 动态表未缓存] D –> E[TLS 分片后 HEADER 帧不完整] E –> F[Server 解析丢失 trace context]
4.4 跨语言gRPC服务调用中wire协议版本协商失败的抓包分析与降级策略
抓包关键特征
Wireshark 中观察到 HTTP/2 SETTINGS 帧携带 SETTINGS_MAX_FRAME_SIZE=16384,但客户端(Go v1.21)与服务端(Java grpc-java v1.59)在 grpc-encoding 和 grpc-encoding: identity 头部存在隐式版本语义冲突。
协商失败典型日志
ERROR transport: loopyWriter.run returning. Err: connection error: desc = "transport is closing"
该错误实际源于 ALPN negotiation failed 后续未触发 fallback to HTTP/2 over TLS with prior-knowledge。
降级策略配置(Go 客户端)
conn, err := grpc.Dial("example.com:443",
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
NextProtos: []string{"h2"}, // 强制 ALPN 协商,禁用 h2c
})),
grpc.WithDisableRetry(), // 避免重试放大协商失败
)
NextProtos 显式限定为 ["h2"] 可绕过 gRPC-Go 自动探测逻辑,避免与 Java 服务端不兼容的 h2c 尝试。
| 组件 | 推荐版本 | 兼容性说明 |
|---|---|---|
| Go gRPC | v1.58+ | 修复 grpc-encoding 头解析 |
| Java gRPC | grpc-java 1.60.0+ | 支持 grpc-encoding: gzip 降级回退 |
graph TD
A[发起连接] --> B{ALPN协商}
B -->|h2 success| C[正常gRPC流]
B -->|fail| D[检查NextProtos]
D -->|含h2| E[降级为TLS+h2]
D -->|不含h2| F[连接拒绝]
第五章:全链路追踪稳定性保障体系构建与演进方向
核心稳定性指标闭环监控
在某电商大促场景中,我们落地了以 trace 采样率、span 丢失率、链路延迟 P99、跨服务上下文透传成功率为核心的四大黄金指标。通过 Prometheus + Grafana 构建实时看板,并设置动态基线告警:当 span 丢失率连续5分钟超过0.8%时,自动触发 Slack 告警并推送至值班工程师;跨服务透传失败率突增时,联动 Jaeger UI 自动高亮异常链路节点。该机制在2023年双11期间提前17分钟捕获到某支付网关因 OpenTracing SDK 版本不兼容导致的 context 丢弃问题。
熔断与降级策略嵌入追踪链路
我们在 TraceContext 传播层注入轻量级熔断器(基于 Resilience4j),当某下游服务错误率超阈值时,自动在 Span 中打标 error_rate_high=true,并触发本地缓存兜底逻辑。例如订单服务在调用库存服务失败后,不再发起远程调用,而是读取 Redis 缓存中的预热库存快照,同时将 is_fallback:true 写入 span tag。下表为熔断生效前后关键指标对比:
| 指标 | 熔断前 | 熔断后 | 变化 |
|---|---|---|---|
| 平均链路耗时 | 1280ms | 210ms | ↓83.6% |
| 全链路成功率 | 82.3% | 99.7% | ↑17.4pp |
| 库存服务错误数/分钟 | 426 | 0 | ↓100% |
追踪数据治理与生命周期管理
针对日均 2.4 亿 trace 数据带来的存储压力与查询抖动,我们实施分层存储策略:热数据(7天内)存于 Elasticsearch 集群(SSD+副本=2),温数据(7–30天)迁移至对象存储(S3兼容),冷数据(30天以上)归档至 Glacier 并建立索引映射表。同时开发 Trace GC 工具,基于 span 的 service.name、http.status_code、error 三个维度自动识别低价值 trace(如健康检查、静态资源请求),每日清理占比达31.7%的冗余数据。
flowchart LR
A[Trace 生成] --> B{是否含 error 或 latency > 2s?}
B -->|Yes| C[进入 Hot Tier]
B -->|No| D[标记为 low-value]
D --> E[24h 后自动归档]
C --> F[7天后迁移至 Warm Tier]
F --> G[30天后转入 Cold Tier]
客户端埋点与端到端可观测性补全
为覆盖小程序、iOS、Android 等客户端链路断点,我们封装统一 SDK,在 WebView 加载、Native API 调用、Flutter Engine 渲染等关键路径注入 client_start、render_complete 等自定义事件,并通过 HTTP Header 透传 x-trace-id 和 x-span-id 至后端。2024年Q1数据显示,移动端首屏加载链路完整率从63%提升至94%,其中 87% 的卡顿问题可定位至具体 JS 执行栈或网络 DNS 解析环节。
混沌工程驱动的追踪韧性验证
每季度执行一次“追踪链路混沌演练”:使用 ChaosMesh 注入网络延迟(模拟 150ms RTT)、随机丢包(5%)、DNS 故障(mock 服务域名解析失败)三类故障,观测 tracing agent 自愈能力。实测发现:当 eBPF-based agent 遇 DNS 失败时,会自动 fallback 至本地 UDP buffer 并启用异步批量上报;若连续3次上报失败,则启动本地磁盘暂存(最大容量 2GB),恢复后按优先级重发。该机制已在 12 个核心业务集群全量上线。
