第一章:K8s Init Container中预热pprof+trace的核心价值与风险边界
在高并发微服务场景下,应用容器首次启动时若未预热性能分析端点(如 /debug/pprof 和 OpenTelemetry trace exporter),将导致生产流量突增时出现可观测性“黑盒期”——请求已进入但 profiling 数据不可采集、trace span 丢失或延迟上报。Init Container 提供了精准的、与主容器解耦的预热时机,在主应用启动前完成 pprof handler 注册、trace SDK 初始化及采样器 warm-up,从而规避 runtime 竞态引发的指标缺失。
预热带来的确定性收益
- ✅ 消除首次 HTTP 请求触发 pprof handler 动态加载的锁竞争开销
- ✅ 强制 trace provider 完成 exporter 连接池初始化与健康探测(如 OTLP gRPC 连接预拨号)
- ✅ 触发 Go runtime 的
runtime.SetMutexProfileFraction(1)等低开销 profile 配置生效,避免主容器启动后因高负载抑制采样
不可忽视的风险边界
- ❗ Init Container 中执行
curl http://localhost:6060/debug/pprof/heap类操作无效:pprof 端点需由主进程监听,init 容器无法访问其内存空间 - ❗ 过度预热(如调用
/debug/pprof/goroutine?debug=2获取全量 goroutine stack)可能阻塞 init 容器退出,违反 Kubernetes “init 容器必须终止”的语义 - ❗ trace exporter 若配置了非幂等的认证逻辑(如 JWT token 刷新),在 init 阶段误触发可能导致主容器凭证失效
实践:安全预热 trace + pprof 的 Init Container 示例
initContainers:
- name: warmup-observability
image: curlimages/curl:8.9.1
command: ["/bin/sh", "-c"]
args:
- |
# 1. 向主容器预留端口发送轻量 probe,触发 trace provider 初始化(不采集数据)
curl -f -s -o /dev/null --max-time 3 http://localhost:8080/healthz || true;
# 2. 确保 pprof mux 已注册(仅验证 handler 可达,不读取敏感 profile)
curl -f -s -o /dev/null --max-time 2 http://localhost:6060/debug/pprof/ || true;
# 3. 显式调用 trace SDK warmup(需应用支持,例如 otel-go 的 TracerProvider.ForceFlush() 封装接口)
echo "Warmup completed";
ports:
- containerPort: 6060 # 显式声明,便于网络策略识别
该模式将可观测性就绪从“尽力而为”提升为“强保障”,但必须严格限定在无副作用、非侵入、可快速终止的操作范围内。
第二章:Init Container预热pprof+trace的5个强制步骤详解
2.1 构建可调试Golang二进制并注入pprof/trace启动参数
为支持生产级诊断,需在构建阶段保留调试信息,并在运行时启用标准性能分析能力。
编译时关键标志
go build -gcflags="all=-N -l" -ldflags="-s -w" -o myapp .
-N:禁用变量和函数内联,保留完整符号表;-l:禁用函数内联(补充-N效果);-s -w:仅剥离符号表与 DWARF 调试信息(不剥离行号),平衡体积与调试能力。
运行时启用分析端点
启动命令需注入环境变量或显式参数:
// main.go 中需注册 pprof/trace
import _ "net/http/pprof"
import "runtime/trace"
func init() {
go func() { http.ListenAndServe("localhost:6060", nil) }()
}
net/http/pprof自动注册/debug/pprof/*;runtime/trace需手动启动trace.Start()并写入文件或 HTTP handler。
启动参数对照表
| 参数类型 | 示例值 | 作用 |
|---|---|---|
| 环境变量 | GODEBUG=asyncpreemptoff=1 |
禁用异步抢占,提升 trace 稳定性 |
| 命令行 | -cpuprofile=cpu.pprof |
直接采集 CPU profile 到文件 |
| HTTP 查询 | ?seconds=30(/debug/pprof/profile) |
动态指定采样时长 |
graph TD
A[go build -N -l] --> B[保留行号与变量名]
B --> C[运行时 http.ListenAndServe]
C --> D[/debug/pprof/ endpoints]
C --> E[trace.Start os.Stdout]
2.2 编写幂等性Init Container镜像,确保单次可靠执行
Init Container 的核心契约是必须成功终止后,主容器才能启动。若初始化逻辑非幂等(如重复创建同名数据库、重复写入配置文件),在 Pod 重建或节点漂移时将导致失败。
幂等设计原则
- 使用外部状态锚点(如 Kubernetes Secret/ConfigMap 的
last-applied-hash标签) - 所有写操作前校验目标是否存在且内容一致
- 采用
set -euxo pipefail保障脚本健壮性
示例:带校验的数据库初始化脚本
#!/bin/sh
# 检查是否已初始化(通过 ConfigMap 标记)
if kubectl get cm db-init-marker -n "$NAMESPACE" &>/dev/null; then
echo "✅ Init already completed"; exit 0
fi
# 执行初始化(幂等SQL含 CREATE TABLE IF NOT EXISTS)
psql -h $DB_HOST -U $DB_USER $DB_NAME <<'EOF'
CREATE TABLE IF NOT EXISTS migrations (
id SERIAL PRIMARY KEY,
applied_at TIMESTAMPTZ DEFAULT NOW()
);
EOF
# 标记完成(原子性创建)
kubectl create cm db-init-marker -n "$NAMESPACE" --from-literal=done=yes
逻辑分析:脚本首行通过
kubectl get cm探测标记存在性——返回 0 表示已执行,直接退出;仅当标记不存在时才执行 SQL(IF NOT EXISTS保证语句级幂等);最后用kubectl create cm原子写入标记,避免竞态。$NAMESPACE、$DB_HOST等需通过 Downward API 或 Secret 注入。
初始化状态决策流程
graph TD
A[Pod 启动] --> B{Init Container 运行}
B --> C[检查 db-init-marker ConfigMap]
C -->|存在| D[跳过初始化,exit 0]
C -->|不存在| E[执行幂等SQL]
E --> F[创建 db-init-marker]
F --> G[成功退出]
2.3 在Init Container中触发HTTP pprof端点预热与profile采集验证
Init Container 是保障主容器启动前完成关键诊断准备的理想载体。通过预热 /debug/pprof/ 端点,可规避首次 profile 请求因 JIT 编译、路由初始化导致的延迟抖动。
预热脚本执行逻辑
# 使用 curl 并忽略证书错误(适用于自签名环境)
curl -s --max-time 5 -k http://localhost:8080/debug/pprof/ > /dev/null
curl -s --max-time 5 -k http://localhost:8080/debug/pprof/profile?seconds=1 > /tmp/init.prof
--max-time 5防止阻塞 Init 容器超时;?seconds=1触发 1 秒 CPU profile 采集,验证端点活性与写入权限。
验证流程示意
graph TD
A[Init Container 启动] --> B[GET /debug/pprof/]
B --> C{响应状态码 == 200?}
C -->|是| D[GET /debug/pprof/profile?seconds=1]
C -->|否| E[Exit 1]
D --> F[检查 /tmp/init.prof 是否存在且 >1KB]
| 检查项 | 预期结果 | 失败影响 |
|---|---|---|
| pprof 根路径可达性 | HTTP 200 | 主容器 profile 失效 |
| profile 文件生成 | size > 1024B | 性能分析数据缺失 |
2.4 注入OpenTelemetry SDK并完成trace exporter初始化与健康探测
OpenTelemetry SDK 的注入需在应用启动早期完成,确保所有组件(如 HTTP 客户端、数据库驱动)能自动捕获 span。
初始化 SDK 实例
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
exporter = OTLPSpanExporter(
endpoint="http://otel-collector:4318/v1/traces",
timeout=10,
)
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
该代码构建了基于 OTLP/HTTP 协议的导出器,timeout=10 防止阻塞主线程;BatchSpanProcessor 提供异步批量发送能力,降低 I/O 开销。
健康探测机制
- 向
/health端点注入tracer.start_span("health-check") - 检查导出器连接状态(如 HTTP HEAD 请求 collector 端点)
- 通过 Prometheus 指标
otel_exporter_send_failed_spans_total监控失败率
| 探测项 | 检查方式 | 失败阈值 |
|---|---|---|
| 连通性 | HTTP HEAD to collector | >3s |
| 导出队列积压 | span_processor.queue_size() |
>1000 |
| 最近导出延迟 | last_export_duration_ms |
>5000ms |
graph TD
A[App Start] --> B[Init TracerProvider]
B --> C[Attach BatchSpanProcessor]
C --> D[Register Health Endpoint]
D --> E[Periodic Exporter Liveness Check]
2.5 通过共享Volume传递预热状态信号,阻断主容器启动时序依赖
核心设计思想
利用 emptyDir Volume 作为轻量级进程间通信媒介,使 init 容器与主容器通过文件系统原子操作协同状态。
状态信号约定
/shared/ready: 存在即表示预热完成(空文件)/shared/health: JSON 格式健康摘要(可选)
示例 Pod 配置
volumes:
- name: shared-state
emptyDir: {}
initContainers:
- name: warmer
image: alpine:latest
command: ["/bin/sh", "-c"]
args: ["echo 'warming...' && sleep 5 && touch /shared/ready"]
volumeMounts:
- name: shared-state
mountPath: /shared
containers:
- name: app
image: nginx:alpine
readinessProbe:
exec:
command: ["test", "-f", "/shared/ready"]
volumeMounts:
- name: shared-state
mountPath: /shared
逻辑分析:
readinessProbe.exec直接检测文件存在性,规避了端口监听、HTTP 健康检查等延迟与竞争问题;emptyDir生命周期与 Pod 绑定,确保信号强一致性;touch操作具备 POSIX 原子性,无竞态风险。
启动时序对比表
| 阶段 | 传统方式 | Volume 信号方式 |
|---|---|---|
| 依赖判定 | 主容器自行探测端口 | 内核级文件存在性检查 |
| 信号延迟 | 秒级(probe interval) | 微秒级(stat syscall) |
| 故障隔离 | 探测失败导致反复重启 | 仅阻塞就绪,不中断运行 |
graph TD
A[InitContainer 启动] --> B[执行预热逻辑]
B --> C{预热完成?}
C -->|是| D[创建 /shared/ready]
C -->|否| B
D --> E[主容器 readinessProbe 成功]
E --> F[主容器进入 Ready 状态]
第三章:Golang运行时与K8s调度协同的关键约束分析
3.1 Go runtime.GOMAXPROCS与Init Container CPU Limit的冲突规避
当 Init Container 设置 cpu limit: 100m,而主容器中调用 runtime.GOMAXPROCS(0) 时,Go runtime 可能错误推导出 GOMAXPROCS=1(基于 cgroups v1 的 cpu.shares 或 quota/period 计算偏差),导致后续应用并发能力受限。
根本原因:CPU 资源视图不一致
- Init Container 的 CPU limit 仅作用于其生命周期;
- 主容器启动时,cgroups 已切换,但 Go 在进程启动早期(
runtime.main前)读取/sys/fs/cgroup/cpu/cpu.shares等路径,可能残留 Init Container 时期的限制快照。
推荐实践:显式覆盖 GOMAXPROCS
func init() {
// 优先从容器实际可用 CPU 数推导(需挂载 /sys/fs/cgroup)
if n := numCPUsFromCgroup(); n > 0 {
runtime.GOMAXPROCS(n)
} else {
runtime.GOMAXPROCS(runtime.NumCPU()) // fallback
}
}
逻辑分析:
numCPUsFromCgroup()应解析/sys/fs/cgroup/cpu.max(cgroups v2)或cpu.cfs_quota_us/cpu.cfs_period_us(v1),避免依赖runtime.NumCPU()这一宿主机视角值。参数n为整数型可用逻辑 CPU 数,向下取整确保安全。
| 场景 | GOMAXPROCS 实际值 | 风险 |
|---|---|---|
| Init 容器限 100m + 未干预 | 1 | 严重串行化 |
显式设为 2 |
2 | 匹配 200m 预期 |
graph TD
A[Pod 启动] --> B[Init Container 执行]
B --> C[切换至主容器 cgroups]
C --> D[Go runtime 初始化]
D --> E{读取 cpu.cfs_quota_us?}
E -->|是,但值过小| F[GOMAXPROCS=1]
E -->|否/已修复| G[按真实 quota/period 计算]
3.2 pprof MutexProfile/BlockProfile在容器冷启阶段的采样有效性验证
容器冷启动时,Go 运行时尚未稳定,MutexProfile 与 BlockProfile 的默认采样率(runtime.SetMutexProfileFraction(1) / runtime.SetBlockProfileRate(1))可能因 goroutine 初始化延迟而漏采早期锁竞争。
启用高保真采样
func initColdStartProfiling() {
runtime.SetMutexProfileFraction(1) // 每次互斥锁获取均记录(非默认0,即禁用)
runtime.SetBlockProfileRate(1) // 每次阻塞事件均采样(默认0 → 禁用)
// 注意:需在 main.init() 或容器入口 early stage 调用
}
逻辑分析:MutexProfileFraction=1 强制记录所有 sync.Mutex 获取/释放事件;BlockProfileRate=1 捕获全部 chan send/recv、net、time.Sleep 等阻塞点。二者在冷启首秒内可暴露初始化锁争用(如 http.ServeMux 注册竞争)。
采样有效性对比(冷启前500ms)
| 指标 | 默认配置 | 显式启用(Fraction/Rate=1) |
|---|---|---|
| 捕获 mutex 事件数 | 0 | 172 |
| 捕获 block 事件数 | 0 | 89 |
| 首个有效 profile 时间 | >3s | 127ms |
验证流程
graph TD
A[容器启动] --> B[init() 中调用 Set*Profile*]
B --> C[main() 前 200ms 内触发 sync.Mutex.Lock]
C --> D[pprof.WriteTo 输出 mutex/block profile]
D --> E[火焰图确认 stack trace 完整性]
3.3 trace.SpanContext跨Init→Main容器传递的上下文延续机制
在 Kubernetes Init Container 向 Main Container 传递分布式追踪上下文时,SpanContext 无法通过环境变量或标准输出直接透传——因其包含二进制元数据(如 TraceID、SpanID、采样标志)及 W3C TraceContext 兼容字段。
关键传递载体:共享 EmptyDir Volume
- Init 容器将序列化后的
SpanContext写入/shared/trace-context.json - Main 容器启动前挂载同一 volume,读取并反序列化恢复上下文
{
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"traceFlags": 1,
"traceState": "congo=t61rcWkgMzE"
}
该 JSON 是 OpenTelemetry SDK 的
SpanContext#toTraceState()与SpanContext#isValid()验证后的标准化输出;traceFlags=1表示采样启用,traceState支持多供应商上下文链路扩展。
数据同步机制
| 字段 | 类型 | 用途 |
|---|---|---|
traceId |
string | 全局唯一追踪标识(16字节hex) |
spanId |
string | 当前 Span 局部唯一标识(8字节hex) |
traceFlags |
uint8 | 低比特位控制采样/调试等行为 |
graph TD
A[Init Container] -->|JSON序列化+写入| B[/shared/trace-context.json]
B --> C[Main Container]
C -->|反序列化+注入Tracer| D[otel.Tracer.StartSpan]
第四章:生产级落地的可观测性加固实践
4.1 基于kustomize注入Init Container的声明式模板与校验钩子
Kustomize 通过 patchesStrategicMerge 或 patchesJson6902 可在不修改源基线的前提下,精准注入 Init Container,实现环境无关的校验逻辑。
校验钩子设计原则
- 所有校验容器必须幂等、无副作用
- 超时需显式设为
activeDeadlineSeconds - 失败应阻断主容器启动(Kubernetes 默认行为)
示例:数据库连通性校验模板
# patch.yaml
- op: add
path: /spec/template/spec/initContainers/-
value:
name: wait-for-db
image: busybox:1.35
command: ['sh', '-c']
args:
- |
until nc -z database 5432; do
echo "Waiting for DB...";
sleep 2;
done
resources:
requests: {memory: "32Mi", cpu: "10m"}
该 patch 使用 JSON6902 操作向 Deployment 的 PodSpec 注入 Init Container。
nc -z实现轻量端口探测;resources约束资源避免调度争抢;until循环确保最终一致性。
| 字段 | 作用 | 推荐值 |
|---|---|---|
activeDeadlineSeconds |
全局超时防护 | 120 |
restartPolicy |
Init Container 不可重启 | Always(由 Pod 控制) |
securityContext.runAsNonRoot |
强化安全 | true |
graph TD
A[Pod 创建] --> B{Init Containers 启动}
B --> C[wait-for-db 执行 nc 探测]
C -->|成功| D[主容器启动]
C -->|失败/超时| E[Pod 状态 Pending → Failed]
4.2 Prometheus指标自动发现pprof预热成功状态的ServiceMonitor配置
为实现pprof端点预热完成后的可观测性闭环,需让Prometheus自动识别并采集/debug/pprof/health等健康指标。
关键匹配逻辑
ServiceMonitor通过标签选择器关联Service,要求目标Pod同时满足:
- 暴露
pprof端口(如8080) - 注解
prometheus.io/scrape: "true"且含pprof-ready: "true"
ServiceMonitor配置示例
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: pprof-ready-monitor
labels: { release: "prometheus-stack" }
spec:
selector:
matchLabels:
app: my-service # 匹配含pprof的Deployment Pod
namespaceSelector:
matchNames: ["default"]
endpoints:
- port: "pprof-port"
path: /debug/pprof/health
interval: 15s
scheme: http
该配置使Prometheus每15秒拉取
/debug/pprof/health,返回200 OK即代表pprof预热就绪。port需与Service中定义的targetPort名称一致;path不可省略,否则默认抓取/metrics,无法感知预热状态。
| 字段 | 作用 | 必填 |
|---|---|---|
selector.matchLabels |
定位启用pprof的Pod | ✅ |
endpoints.path |
显式指定健康检查路径 | ✅ |
endpoints.interval |
缩短探测周期以加速状态感知 | ⚠️推荐≤30s |
graph TD
A[Pod启动] --> B{pprof初始化完成?}
B -- 是 --> C[写入/health返回200]
B -- 否 --> D[返回503]
C --> E[ServiceMonitor采集到up{job=“pprof-ready”}=1]
4.3 Jaeger UI中识别Init阶段trace span的Tag标注与过滤策略
Init阶段的span通常承载服务启动时的关键上下文,Jaeger UI依赖标准化Tag实现精准识别。
常见Init阶段Tag标注规范
span.kind: "server"+component: "init"jaeger.version: "1.0"(标识初始化探针版本)- 自定义Tag:
init.phase: "config_load"、init.phase: "dependency_ready"
Jaeger UI过滤策略示例
{
"tags": {
"component": "init",
"init.phase": "config_load"
}
}
该JSON结构在Jaeger搜索栏中等价于 component=init AND init.phase=config_load;component为必选Tag,确保跨服务一致性;init.phase支持枚举值过滤,提升诊断粒度。
| Tag键名 | 取值示例 | 说明 |
|---|---|---|
component |
"init" |
标识初始化生命周期 |
init.status |
"success", "fail" |
启动结果状态 |
init.duration.ms |
127.5 |
初始化耗时(毫秒,float) |
Init span识别流程
graph TD
A[Jaeger Collector接收span] --> B{Tag匹配 component==\"init\"?}
B -->|Yes| C[提取init.phase/init.status]
B -->|No| D[归入常规span流]
C --> E[标记为Init类span并高亮显示]
4.4 使用kubectl debug + ephemeral container回溯预热失败现场
当 Pod 预热失败且主容器已崩溃或持续 CrashLoopBackOff 时,传统 kubectl exec 无法进入——此时临时容器(ephemeral container)成为唯一可观测入口。
启动调试会话
kubectl debug -it my-app-pod \
--image=nicolaka/netshoot \
--target=my-app-container \
--share-processes
--target指定共享 PID 命名空间的主容器,使ps,cat /proc/*/stack可见其进程栈;--share-processes启用进程命名空间共享,是诊断僵死/阻塞的关键前提;nicolaka/netshoot集成strace,tcpdump,jq等调试工具,无需镜像预置。
关键诊断路径
- 检查
/proc/1/root/proc/sys/vm/pressure判断内存压力; - 追踪预热脚本执行痕迹:
ls -l /var/log/prewarm.log /tmp/.prewarm.lock; - 用
strace -p 1 -e trace=connect,openat,stat实时捕获主容器系统调用。
| 工具 | 适用场景 | 限制条件 |
|---|---|---|
crictl inspect |
查看 CRI 层容器状态与退出码 | 需节点级 SSH 权限 |
kubectl debug |
无侵入式运行时观测 | 要求 Kubernetes ≥ v1.23 |
graph TD
A[预热失败Pod] --> B{主容器是否存活?}
B -->|否| C[启用ephemeral container]
B -->|是| D[直接kubectl exec]
C --> E[共享PID+IPC命名空间]
E --> F[读取/proc/1/fd/ & /proc/1/environ]
第五章:规范演进与未来可观测性基建融合路径
OpenTelemetry 1.0 到 1.25 的语义约定落地实践
自 OpenTelemetry(OTel)发布 1.0 稳定版以来,语义约定(Semantic Conventions)已迭代至 v1.25.0,覆盖 AWS Lambda、Kubernetes Pod、HTTP/3、WebAssembly 等新兴运行时。某金融级支付平台在 2023 Q4 升级 OTel SDK 至 v1.24.0 后,将 http.route 属性从硬编码字符串统一替换为符合 http.server.route 规范的正则匹配模板,并通过 otelcol-contrib 的 transformprocessor 动态注入 service.namespace=prod-fintech 标签。该变更使跨服务链路过滤准确率从 78% 提升至 99.2%,且 Prometheus 指标 http_server_duration_seconds_count 的标签基数下降 43%。
多源协议归一化映射表
为兼容遗留系统,团队构建了协议语义对齐映射表,确保 Zipkin、Jaeger、Datadog Trace 采集的数据在接入 OTel Collector 后能无损转换:
| 原始协议字段 | OTel 标准属性 | 转换规则示例 |
|---|---|---|
jaeger.tags.http.status_code |
http.status_code |
parseInt(tag.value) |
zipkin.binaryAnnotations.env |
deployment.environment |
直接赋值,若为空则 fallback 为 "unknown" |
dd.span.service |
service.name |
截断超过 128 字符部分并添加 _truncated 后缀 |
基于 eBPF 的内核态指标增强方案
在 Kubernetes 集群中部署 pixie-io/pixie 作为 OTel eBPF 数据源,捕获 socket 层 TLS 握手耗时、重传率、连接建立延迟等传统应用探针无法获取的指标。通过 otelcol-contrib 的 k8sattributesprocessor 关联 Pod UID,将 eBPF 数据与应用 span 关联,实现“应用逻辑耗时 + 网络栈耗时”双维度下钻。某核心交易服务在灰度期间发现 TLS 1.3 handshake 平均耗时突增 210ms,定位到是 Istio 1.18 中 Envoy 的 ALPN 协商策略缺陷,推动基础组件升级。
flowchart LR
A[eBPF Probe] -->|raw socket events| B(OTel Collector)
C[Java Agent] -->|OTLP/gRPC| B
D[Prometheus Scrape] -->|Metrics Pull| B
B --> E[Attribute Normalizer]
E --> F[Service Graph Builder]
F --> G[Alerting Engine]
可观测性即代码的 CI/CD 流水线嵌入
使用 Terraform + OTel Configuration-as-Code 模板,在 GitOps 流水线中自动校验新服务的 otel-collector-config.yaml 是否满足以下硬性约束:
- 必须启用
memory_limiterprocessor 且limit_mib≥ 512; - 所有 exporters 必须配置
timeout≤ 10s; resource_detection必须包含env,k8s.pod.name,cloud.account.id三项。
该检查集成在 Argo CD 的 PreSync hook 中,未通过则阻断部署。上线三个月内,因资源配置不当导致的 Collector OOM 事件归零。
边缘场景的采样策略动态协同
针对 IoT 设备端轻量级 Agent(如 otel-cpp-sdk + ESP32),采用基于 trace_id 哈希前缀的分层采样:高频设备(ID 前两位为 00–1F)固定 1% 采样,低频设备(E0–FF)提升至 100%。该策略通过 OTel Collector 的 probabilistic_sampler 与 tail_sampling_processor 联合实现,并通过 gRPC 流式下发采样率配置,避免设备重启。在某智能电表集群中,日均 trace 量从 120 亿降至 2.3 亿,关键故障路径覆盖率仍保持 100%。
