第一章:Go可观测性基建标准方案:OpenTelemetry + Prometheus + Grafana一体化埋点规范(含自动instrumentation脚手架)
构建统一、可维护的可观测性体系,需在语言层、协议层与展示层实现端到端对齐。Go 服务应默认启用 OpenTelemetry SDK 进行分布式追踪、指标与日志(OTLP)三合一采集,并通过 Prometheus Exporter 暴露标准化指标,最终由 Grafana 统一可视化与告警。
核心依赖与初始化规范
在 main.go 中注入标准化初始化逻辑,确保所有服务启动时自动注册 tracing、metrics 和 health check 端点:
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/prometheus"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" // 自动 HTTP 埋点
)
func initTracingAndMetrics() {
// 启用 Prometheus exporter(无需额外 pushgateway)
exporter, _ := prometheus.New()
meterProvider := metric.NewMeterProvider(metric.WithReader(exporter))
otel.SetMeterProvider(meterProvider)
// 全局 tracer 用于手动 span(如 DB 调用)
otel.SetTracerProvider(trace.NewTracerProvider())
}
自动 Instrumentation 脚手架
使用 opentelemetry-go-contrib/instrumentation 提供的中间件,零侵入集成常见组件:
| 组件 | 推荐包路径 | 埋点能力 |
|---|---|---|
| HTTP Server | net/http/otelhttp |
请求延迟、状态码、路径标签 |
| PostgreSQL | github.com/jackc/pgx/v5/pgxpool/otel |
查询耗时、行数、错误类型 |
| Gin | contrib/gin/otelgin |
路由分组、响应大小、客户端 IP |
Prometheus 指标暴露配置
在 HTTP 服务中挂载 /metrics 端点(非默认 /debug/metrics),确保与 Prometheus scrape 配置一致:
http.Handle("/metrics", promhttp.Handler()) // 来自 github.com/prometheus/client_golang/prometheus/promhttp
Grafana 推荐导入预置仪表盘 ID 13072(Go Runtime Metrics)与 16123(OTel Collector Metrics),并为业务指标创建独立面板,标签统一使用 service.name、env、version 三元组。所有埋点必须通过 semantic conventions 命名,例如 http.server.request.duration 而非自定义 api_latency_ms。
第二章:Go可观测性核心原理与标准实践
2.1 OpenTelemetry Go SDK架构解析与生命周期管理
OpenTelemetry Go SDK采用可组合、可插拔的分层设计:TracerProvider → Tracer → Span,配合MeterProvider与LoggerProvider实现统一可观测性抽象。
核心组件生命周期契约
SDK严格遵循 Start() / Shutdown() / ForceFlush() 三阶段管理:
Start()初始化全局注册器与默认导出器Shutdown()阻塞等待未完成导出并释放资源ForceFlush()非阻塞触发当前批次数据同步
provider := otel.NewTracerProvider(
trace.WithBatcher(exporter), // 批处理导出器
trace.WithResource(res), // 关联资源元数据
)
defer provider.Shutdown(context.Background()) // 必须显式调用
Shutdown()接收context.Context支持超时控制;若上下文提前取消,将中止等待并尽力清理。WithBatcher默认启用 512 项缓冲与 5s 刷新间隔。
数据同步机制
graph TD
A[Span.Start] --> B[SpanProcessor.OnStart]
B --> C[BatchSpanProcessor.Queue]
C --> D{Timer/Full?}
D -->|Yes| E[Export via Exporter]
E --> F[Exporter.Export]
| 组件 | 线程安全 | 可重入 | 生命周期绑定对象 |
|---|---|---|---|
| TracerProvider | ✅ | ❌ | 应用进程 |
| Tracer | ✅ | ✅ | Provider |
| SpanProcessor | ✅ | ❌ | Provider |
2.2 分布式追踪(Tracing)在Go微服务中的语义约定与Span建模
OpenTelemetry 定义了一套通用语义约定(Semantic Conventions),用于标准化 Span 的属性命名与结构,确保跨语言、跨服务的可观察性一致性。
核心 Span 属性建模
span.kind:client/server/producer/consumer—— 决定上下文传播行为http.method,http.status_code,net.peer.name: 服务间调用的关键标识字段service.name,service.version: 用于后端聚合与服务拓扑识别
Go 中的 Span 创建示例
ctx, span := tracer.Start(ctx, "payment.process",
trace.WithSpanKind(trace.SpanKindServer),
trace.WithAttributes(
semconv.HTTPMethodKey.String("POST"),
semconv.HTTPStatusCodeKey.Int(200),
semconv.ServiceNameKey.String("payment-service"),
),
)
defer span.End()
该代码显式声明 Span 类型为服务端入口,并注入 OpenTelemetry 语义约定键(如 semconv.HTTPMethodKey),确保采集器能正确解析协议语义;trace.WithSpanKind 影响上下文注入/提取逻辑,是跨进程传播的基础。
Span 生命周期关键约束
| 阶段 | 约束说明 |
|---|---|
| 创建 | 必须绑定有效 context,不可 nil |
| 属性写入 | 仅限 Start 后、End 前生效 |
| 错误标记 | 调用 span.RecordError(err) |
graph TD
A[HTTP Handler] --> B[Start Span with server kind]
B --> C[Inject ctx into downstream call]
C --> D[Propagate traceparent header]
2.3 指标(Metrics)采集模型:Counter、Gauge、Histogram的Go原生实现与业务映射
Prometheus生态中,Counter、Gauge、Histogram是三大核心指标类型,其语义差异直接决定监控数据的可解释性与告警有效性。
核心语义与业务映射
Counter:单调递增累计值(如HTTP请求总数),不可重置,适用于事件计数;Gauge:可增可减瞬时值(如当前活跃连接数),反映系统“快照状态”;Histogram:分桶统计分布(如API响应延迟),需预设buckets以支持rate()与histogram_quantile()计算。
Go原生实现示例(使用prometheus/client_golang)
// 声明指标
httpRequestsTotal := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests.",
},
[]string{"method", "status"},
)
httpRequestDuration := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "Latency distribution of HTTP requests.",
Buckets: prometheus.DefBuckets, // [0.005, 0.01, ..., 10]
},
[]string{"route"},
)
逻辑分析:
CounterVec通过标签维度(method,status)实现多维计数,避免指标爆炸;HistogramVec的Buckets直接影响分位数精度与存储开销——过密浪费,过疏失真。
指标类型选择决策表
| 场景 | 推荐类型 | 原因说明 |
|---|---|---|
| 用户注册成功次数 | Counter | 单向累积,无回退语义 |
| 实时在线用户数 | Gauge | 可登录/登出双向变动 |
| 订单创建耗时P95延迟 | Histogram | 需分位数聚合,非简单平均 |
graph TD
A[业务事件触发] --> B{指标类型选择}
B -->|计数类| C[Counter.Inc()]
B -->|状态类| D[Gauge.Set()/Add()]
B -->|分布类| E[Histogram.Observe(latencySec)]
C & D & E --> F[Prometheus Pull]
2.4 日志(Logs)与追踪上下文的结构化关联:OTLP日志管道构建与字段注入实践
为实现日志与分布式追踪的语义对齐,需在日志采集阶段主动注入 trace_id、span_id 和 trace_flags 等 OpenTelemetry 标准字段。
关键字段注入示例(OpenTelemetry Collector 配置)
processors:
resource:
attributes:
- action: insert
key: trace_id
value: ${OTEL_TRACE_ID} # 从环境/上下文提取
- action: insert
key: span_id
value: ${OTEL_SPAN_ID}
该配置利用 Collector 的
resource处理器,在日志资源属性层注入追踪上下文。OTEL_TRACE_ID必须由应用运行时通过otel-trace-idHTTP header 或context显式透传,确保跨服务一致性。
OTLP 日志字段映射关系
| 日志原始字段 | OTLP 属性路径 | 语义作用 |
|---|---|---|
request_id |
attributes.request_id |
业务标识,非追踪标准但常用于关联 |
trace_id |
trace_id (bytes) |
二进制格式,16字节 UUID |
span_id |
span_id (bytes) |
8字节,必须与 trace_id 同源 |
数据流协同机制
graph TD
A[应用日志 emit] --> B{OTel SDK}
B -->|注入 trace_id/span_id| C[OTLP Log Exporter]
C --> D[Collector 接收]
D --> E[统一存储/分析平台]
2.5 资源(Resource)与属性(Attribute)标准化:服务身份、环境标签与K8s元数据自动注入
在可观测性与策略控制体系中,统一的资源标识是关联指标、日志、追踪的关键前提。OpenTelemetry SDK 支持通过 Resource 对象声明静态上下文,而 Attribute 则承载动态运行时特征。
自动注入 K8s 元数据示例
# otel-collector-config.yaml 片段:利用 k8sattributes processor
processors:
k8sattributes:
auth_type: serviceAccount
pod_association:
- from: resource_attribute
name: k8s.pod.ip
该配置使 Collector 在接收遥测数据时,自动补全 Pod 名称、命名空间、节点、Deployment 等 12+ 个标准字段,无需应用侧手动埋点。
标准化字段对照表
| 字段名 | 来源 | 示例值 |
|---|---|---|
service.name |
应用配置 | payment-service |
environment |
环境标签 | prod-us-east-1 |
k8s.namespace.name |
K8s API | default |
数据同步机制
graph TD
A[应用进程] -->|OTLP Export| B(OTel Collector)
B --> C[k8sattributes processor]
C --> D[ enriched Resource + Attributes]
D --> E[后端存储/分析系统]
第三章:Prometheus集成与Go指标工程化
3.1 Prometheus Go客户端深度配置:Registry注册、GaugeVec动态维度与Cardinality管控
Registry的显式管理与隔离实践
默认全局注册器易引发冲突,推荐显式创建独立prometheus.Registry实例:
reg := prometheus.NewRegistry()
gaugeVec := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "http_request_duration_seconds",
Help: "Latency of HTTP requests in seconds",
},
[]string{"method", "endpoint", "status"},
)
reg.MustRegister(gaugeVec) // 避免重复注册 panic
MustRegister()在注册失败时 panic,确保指标注册原子性;NewRegistry()实现监控域隔离,适用于多租户或模块化服务。
GaugeVec的动态标签注入与Cardinality风险防控
使用WithLabelValues()按需注入标签,但需警惕高基数维度(如user_id):
| 维度字段 | 安全建议 | 风险示例 |
|---|---|---|
method |
✅ 低基数(GET/POST) | — |
endpoint |
⚠️ 中等基数(需归一化) | /api/v1/users/123 → /api/v1/users/{id} |
user_id |
❌ 禁止直接使用 | 单实例生成数万指标 |
Cardinality管控策略
- 标签值预过滤(正则截断、哈希摘要)
- 引入采样率控制(如仅记录 P95 延迟)
- 使用
prometheus.Labels复用避免重复分配
graph TD
A[请求进入] --> B{是否命中白名单标签?}
B -->|是| C[WithLabelValues 注入]
B -->|否| D[降级为 default_label]
C --> E[写入 GaugeVec]
D --> E
3.2 自定义Exporter开发:从HTTP Handler到Instrumented HTTP Server的零侵入封装
传统Exporter常需手动注册指标并暴露/metrics端点,耦合HTTP路由逻辑。理想方案应剥离业务HTTP服务与可观测性埋点。
零侵入封装核心思想
- 复用现有
http.Handler实例,不修改其源码 - 通过装饰器模式包裹原始Handler,自动注入指标采集逻辑
- 所有HTTP状态码、响应时长、请求路径均被自动观测
Instrumented HTTP Server实现
func NewInstrumentedServer(handler http.Handler, reg *prometheus.Registry) *http.Server {
// 注册默认指标:http_requests_total、http_request_duration_seconds等
promhttp.InstrumentHandlerDuration(
prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "myapp",
Subsystem: "http",
Name: "request_duration_seconds",
Help: "HTTP request duration in seconds.",
}, []string{"code", "method", "path"}),
handler,
)
return &http.Server{Handler: handler}
}
该函数将原始
handler包装为可观测实例;promhttp.InstrumentHandlerDuration自动拦截每次请求,按code(如”200″)、method(”GET”)、path(”/api/users”)多维打点;reg参数可选,若未传则使用默认注册器。
关键优势对比
| 方式 | 代码侵入性 | 指标维度 | 部署灵活性 |
|---|---|---|---|
| 手动埋点 | 高(每处Handler需加metric.Inc()) | 有限(需手动定义标签) | 差(绑定具体路由) |
| Instrumented Server | 零(仅启动时包装一次) | 丰富(内置code/method/path) | 极高(兼容任意Handler) |
3.3 指标命名规范与语义一致性校验:基于go-swagger+OpenMetrics Schema的CI自动化验证
核心校验流程
# .github/workflows/metrics-validate.yml
- name: Validate OpenMetrics schema
run: |
go-swagger validate ./openapi/metrics.yaml
promtool check metrics <(curl -s http://localhost:9090/metrics)
该步骤在CI中串联API契约(metrics.yaml)与运行时指标输出,确保暴露的指标名、类型、标签符合OpenMetrics语义约束。
命名规范关键规则
- 前缀统一为
service_name_(如auth_api_request_total) - 类型后缀强制:
_total(计数器)、_seconds(直方图桶)、_gauge(瞬时值) - 禁止使用空格、大写字母、下划线连续(
__)
语义一致性检查项
| 检查维度 | 示例违规 | 自动修复建议 |
|---|---|---|
| 标签重复声明 | method 与 Method |
统一小写并标准化键名 |
| 类型不匹配 | http_request_duration_seconds 声明为 gauge |
强制校验 # TYPE 行 |
graph TD
A[CI触发] --> B[解析swagger metrics.yaml]
B --> C[提取指标名/类型/标签]
C --> D[对比Prometheus /metrics响应]
D --> E[报告语义偏差:如label cardinality mismatch]
第四章:Grafana可视化与全链路诊断体系构建
4.1 Grafana Loki+Tempo+Prometheus三端联动:Go服务日志-追踪-指标的TraceID穿透查询
实现 TraceID 贯穿日志、链路与指标,需在 Go 应用中统一注入上下文标识。
日志埋点(Loki)
// 使用 otellogrus 将 trace_id 注入 log fields
logger.WithFields(logrus.Fields{
"trace_id": trace.SpanContext().TraceID().String(),
"service": "auth-service",
}).Info("user login succeeded")
trace_id 字段使 Loki 可通过 {service="auth-service"} | logfmt | trace_id="..." 精准检索。
追踪关联(Tempo)
Tempo 通过 X-Trace-ID HTTP Header 或 trace_id 日志字段自动关联 Span;需确保 OpenTelemetry SDK 配置 ResourceAttributes 包含 service.name。
指标下钻(Prometheus)
| 指标名 | Label 示例 | 关联能力 |
|---|---|---|
http_request_duration_seconds |
service="auth-service", trace_id="..." |
支持 trace_id 标签过滤 |
数据同步机制
graph TD
A[Go App] -->|OTLP| B[Otel Collector]
B --> C[Loki: logs with trace_id]
B --> D[Tempo: traces]
B --> E[Prometheus: metrics + trace_id label]
三者通过 trace_id 字符串对齐,实现从 Prometheus 告警点击跳转 Tempo 追踪,再下钻至 Loki 原始日志。
4.2 基于Grafana Dashboard JSON API的Go驱动模板化生成与版本化管理
Grafana仪表盘的规模化运维依赖可复用、可审计的声明式交付能力。Go语言凭借强类型、跨平台编译与丰富生态,成为构建Dashboard自动化工具链的理想选择。
模板化生成核心流程
type DashboardTemplate struct {
Title string `json:"title"`
Variables []Variable `json:"templating,omitempty"`
Panels []Panel `json:"panels"`
}
func RenderDashboard(tmpl *DashboardTemplate, data map[string]interface{}) ([]byte, error) {
t := template.Must(template.New("dash").Parse(dashTmpl))
var buf bytes.Buffer
if err := t.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("template exec failed: %w", err)
}
return json.MarshalIndent(buf.Bytes(), "", " ")
}
该函数将结构化模板与运行时变量(如env=prod, region=us-east-1)结合,输出标准JSON格式Dashboard;json.MarshalIndent确保API兼容性与人工可读性。
版本化管理关键维度
| 维度 | 实现方式 |
|---|---|
| Schema校验 | 使用grafana-tools/schema验证JSON结构有效性 |
| Git追踪 | 每次生成自动提交至dashboards/目录并打语义化tag |
| 变更审计 | 提取__inputs与__requires字段生成变更摘要 |
graph TD
A[Go程序读取YAML模板] --> B[注入环境变量]
B --> C[渲染为JSON]
C --> D[Schema校验]
D --> E[Git commit + tag]
E --> F[调用Grafana API POST /api/dashboards/db]
4.3 SLO监控看板实战:错误率、延迟、饱和度(RED)指标在Go HTTP/GRPC服务中的自动提取与告警绑定
RED指标语义对齐
RED(Rate, Errors, Duration)是SLO可观测性的黄金三角:
- Rate:每秒请求数(HTTP
http_requests_total/ gRPCgrpc_server_handled_total) - Errors:错误响应占比(状态码 ≥400 或 gRPC
code != OK) - Duration:P95/P99 延迟直方图(
http_request_duration_seconds_bucket)
自动指标注入示例(Go + Prometheus)
import "github.com/prometheus/client_golang/prometheus"
var (
httpDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "Latency distribution of HTTP requests",
Buckets: prometheus.ExponentialBuckets(0.01, 2, 8), // 10ms–1.28s
},
[]string{"method", "status_code"},
)
)
func init() {
prometheus.MustRegister(httpDuration)
}
逻辑分析:
ExponentialBuckets(0.01, 2, 8)生成 8 个指数间隔桶(0.01s, 0.02s, …, 1.28s),适配 Web API 延迟分布;status_code标签支持错误率聚合(如rate(http_request_duration_seconds_count{status_code=~"5.."}[5m]))。
告警规则绑定(Prometheus YAML)
| 告警名称 | 表达式 | 触发阈值 |
|---|---|---|
HTTPErrorRateHigh |
rate(http_requests_total{status_code=~"4..|5.."}[5m]) / rate(http_requests_total[5m]) > 0.05 |
错误率 >5% |
HTTPSlowP95 |
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 2.0 |
P95 >2s |
graph TD
A[HTTP/GRPC Handler] --> B[Middleware: Observe]
B --> C[Prometheus Histogram + Counter]
C --> D[Scrape via /metrics]
D --> E[Prometheus Server]
E --> F[Alertmanager → PagerDuty/Slack]
4.4 故障根因分析工作流:从Grafana Explore跳转至Jaeger Trace再到pprof火焰图的Go调试链路打通
跳转协议配置(Grafana → Jaeger)
在 Grafana 的数据源配置中启用 Tracing 支持,并设置 Jaeger URL 与 Trace ID 模板:
# grafana.ini 配置片段
[tracing.jaeger]
url = http://jaeger-query:16686
# 支持 ${__value.raw} 自动注入 TraceID
该配置使 Explore 中点击 trace_id 标签时,自动构造 http://jaeger-query:16686/trace/{id} 跳转链接。
链路透传与 pprof 关联
Go 服务需在 HTTP handler 中注入 trace 上下文,并暴露 /debug/pprof 且绑定 trace 元数据:
func handler(w http.ResponseWriter, r *http.Request) {
span := opentracing.SpanFromContext(r.Context())
// 将 trace_id 注入响应头,供前端关联 pprof 采样
w.Header().Set("X-Trace-ID", span.Context().(jaeger.SpanContext).TraceID().String())
}
X-Trace-ID 可被前端工具用于请求 /debug/pprof/profile?seconds=30&trace_id=...(需自定义 pprof handler 支持 trace 过滤)。
调试链路全景流程
graph TD
A[Grafana Explore] -->|点击 trace_id| B[Jaeger Trace Detail]
B -->|复制 TraceID| C[调用 Go 服务 /debug/pprof/profile]
C --> D[生成 trace-scoped 火焰图]
| 组件 | 关键能力 | 依赖条件 |
|---|---|---|
| Grafana | 支持 trace_id 字段高亮与跳转 | 数据源含 traceID 字段 |
| Jaeger | 提供 trace 时间轴与 span 列表 | OpenTracing 兼容埋点 |
| Go pprof | 支持按 trace 上下文采样 CPU | 自定义 handler + ctx 传递 |
第五章:总结与展望
技术栈演进的实际影响
在某大型电商平台的微服务重构项目中,团队将原有单体架构迁移至基于 Kubernetes 的云原生体系。迁移后,平均服务部署耗时从 47 分钟降至 92 秒,CI/CD 流水线失败率下降 63%。关键变化在于:容器镜像统一采用 distroless 基础镜像(仅含运行时依赖),配合 Trivy 扫描集成到 GitLab CI,使高危漏洞平均修复周期压缩至 1.8 天。下表对比了迁移前后核心指标:
| 指标 | 迁移前 | 迁移后 | 变化幅度 |
|---|---|---|---|
| 日均发布次数 | 2.1 | 14.7 | +595% |
| 平均故障恢复时间(MTTR) | 28.4 分钟 | 3.2 分钟 | -88.7% |
| 集群资源利用率峰值 | 31% (CPU) | 68% (CPU) | +119% |
生产环境可观测性落地细节
某金融级支付网关在引入 OpenTelemetry 后,自定义了 17 个业务语义指标(如 payment_status_code{code="BUSINESS_REJECT", channel="wechat"}),并通过 Prometheus + Grafana 实现分钟级异常检测。当某次 Redis 连接池耗尽事件发生时,链路追踪自动关联出 3 个上游服务的 redis.timeout span 标签,并触发告警——该能力直接缩短根因定位时间达 41 分钟。以下为实际采集到的 Span 属性片段:
span:
name: "redis.GET"
attributes:
redis.command: "GET"
redis.key: "order:20240521:88765"
net.peer.name: "redis-cluster-prod-02"
otel.status_code: "ERROR"
架构治理的持续机制
团队建立“架构健康度看板”,每日自动计算 5 类技术债指数:
- 接口兼容性断裂率(通过 Swagger Diff 工具扫描)
- 跨服务循环依赖数(基于 Jaeger 服务图谱分析)
- 过期 TLS 协议使用比例(Nginx 日志正则匹配)
- 未覆盖核心路径的契约测试用例数(Pact Broker 统计)
- 热点方法 CPU 时间占比(Arthas profiler 抓取)
过去 6 个月数据显示,循环依赖数从 23 处降至 2 处,但 TLSv1.0 使用比例仍维持在 11.3%,暴露了遗留 IoT 设备接入层的升级瓶颈。
新兴技术的验证路径
针对 WebAssembly 在边缘计算场景的应用,团队在 CDN 边缘节点部署了 WASI 运行时,用于实时处理用户地理位置脱敏逻辑。实测表明:相同 JS 函数编译为 Wasm 后,执行耗时降低 42%,内存占用减少 67%,且成功拦截了 3 起因 V8 引擎 JIT 编译缺陷导致的越界读取漏洞。该方案已进入灰度阶段,覆盖华东区 12% 的边缘节点。
团队能力结构变化
通过内部技术雷达评估,SRE 工程师对 eBPF 的熟练度从 2.1 分(5 分制)提升至 4.3 分,而传统运维脚本编写能力评分下降 19%。这种能力迁移直接反映在生产问题解决方式上:2024 年 Q1 中,78% 的网络层故障通过 bpftrace 实时分析完成定位,而非依赖抓包回放。
未来三年技术路线图
- 2024 下半年:在订单履约服务中试点 Dapr + gRPC streaming 替代 Kafka,目标降低端到端延迟至 150ms 内
- 2025 年:将所有 Java 服务迁移到 GraalVM Native Image,启动 JVM 内存模型与 Wasm 内存模型的互操作实验
- 2026 年:构建跨云服务网格联邦控制平面,支持阿里云 ACK 与 AWS EKS 集群的策略统一下发
当前正在推进的混沌工程平台已接入 217 个核心服务,每月执行 89 次真实故障注入,最新一次模拟 DNS 劫持事件暴露出服务发现缓存刷新机制缺陷,已推动 Envoy xDS 协议升级至 v3.20.1 版本。
