第一章:Go流媒体日志系统重构:从text log到结构化Zap+Loki+LogQL的PB级日志治理实践
在高并发流媒体服务场景下,原始 log.Printf 生成的纯文本日志已无法支撑PB级日志的采集、检索与根因分析。单节点日志写入吞吐达200MB/s,传统ELK架构因JVM内存压力与索引膨胀导致查询延迟飙升至15s+,且字段缺失严重,无法关联用户ID、StreamID、CDN节点、编码参数等关键维度。
我们采用零侵入式重构策略,将日志输出层统一迁移至 Uber 的 Zap 日志库,并启用 zapcore.NewJSONEncoder 配合自定义 EncodeDuration 和 EncodeLevel,确保每条日志为严格结构化 JSON:
// 初始化高性能结构化Logger
logger, _ := zap.NewProduction(zap.WithCaller(true))
defer logger.Sync()
// 记录带上下文的流媒体事件(自动注入trace_id、stream_id等)
logger.Info("segment uploaded",
zap.String("stream_id", "live-7a9f2b"),
zap.String("cdn_node", "sh-az3-edge-04"),
zap.Int64("duration_ms", 4000),
zap.String("codec", "h264"),
zap.String("trace_id", r.Header.Get("X-Trace-ID")),
)
日志经 File Rotator 写入本地 /var/log/streamd/ 后,由 Promtail(v2.9+)以 scrape_configs 方式采集,通过 pipeline_stages 提取并增强字段:
- job_name: streamd-json
static_configs:
- targets: [localhost]
pipeline_stages:
- json: # 自动解析Zap输出的JSON
expressions:
stream_id: stream_id
cdn_node: cdn_node
- labels: # 提升为Loki标签,支持高基数过滤
stream_id: ""
cdn_node: ""
Loki集群采用 boltdb-shipper + S3 存储后端,按 stream_id 哈希分片,单租户日志保留90天,压缩比达1:12。关键运维能力通过 LogQL 实现:
| 场景 | LogQL 示例 | 说明 |
|---|---|---|
| 高延时Segment定位 | {job="streamd-json"} | json | duration_ms > 10000 | line_format "{{.stream_id}} {{.duration_ms}}" | __error__="" |
过滤非错误行,避免噪声干扰 |
| CDN节点质量对比 | rate({job="streamd-json", cdn_node=~"sh.*"} | json | level="error" [1h]) by (cdn_node) |
按节点聚合错误率,支持告警阈值联动 |
重构后,日志写入延迟稳定在
第二章:视频流媒体场景下的日志痛点与结构化演进路径
2.1 视频编解码、GOP分片与RTMP/WebRTC会话中非结构化日志的爆炸式增长机理
视频流媒体系统中,每个 GOP(Group of Pictures)触发一次关键帧日志记录,而 RTMP 推流与 WebRTC PeerConnection 建立均伴随毫秒级信令交互日志。单路 1080p@30fps 流在 H.264 编码下每秒产生约 60 个 P/B 帧 + 2 个 I 帧,对应日志事件激增。
日志膨胀核心动因
- 每次 GOP 切换生成
gop_start/gop_end非结构化文本行(含时间戳、SSRC、编码参数等无 schema 字段) - WebRTC ICE 连接过程平均产生 17+ 条 debug 级日志(如
candidate-pair-state-changed、dtls-handshake-timeout) - RTMP
onStatus消息每 500ms 心跳上报,日志格式随厂商 SDK 差异极大
典型日志片段示例
[2024-05-22T14:23:18.921Z] INFO rtmp:connect stream=live/abc ssr=1234567890 codec=h264 profile=main gop=25 bitrate=3200k
# ↑ 包含 7 个语义字段,但无固定分隔符或 JSON 结构;gop=25 表示每 25 帧一个 I 帧 → 每秒触发 ~1.2 次 GOP 日志
日志量级对比表(单路流/秒)
| 协议 | 平均日志条数 | 典型单条体积 | 主要来源 |
|---|---|---|---|
| RTMP | 3–8 | 120–350 B | connect/publish/heartbeat |
| WebRTC | 22–65 | 80–520 B | ICE/DTLS/SDP/NACK/PLI |
graph TD
A[视频采集] --> B[H.264/H.265 编码]
B --> C[GOP 分片:I/P/B 帧序列]
C --> D[RTMP 推流 or WebRTC Sender]
D --> E[每 GOP 触发日志]
D --> F[每信令交互触发日志]
E & F --> G[非结构化日志爆炸]
2.2 基于Go runtime/pprof与net/http/pprof的实时日志吞吐瓶颈定位与压测验证
日志服务在高并发场景下常因锁竞争或内存分配激增导致吞吐骤降。需结合运行时剖析与 HTTP 接口暴露能力协同诊断。
启用标准 pprof 端点
import _ "net/http/pprof"
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil)) // 默认注册 /debug/pprof/*
}()
// ... 主业务逻辑
}
net/http/pprof 自动注册 /debug/pprof/ 下全套端点(如 /goroutine?debug=1、/heap),无需额外路由;监听地址应限制为本地,避免生产环境暴露。
关键诊断路径与指标含义
| 端点 | 用途 | 触发条件 |
|---|---|---|
/debug/pprof/goroutine?debug=2 |
查看阻塞型 goroutine 栈 | 日志写入卡顿 |
/debug/pprof/profile?seconds=30 |
CPU 采样 30 秒 | 持续高 CPU 占用 |
/debug/pprof/heap |
当前堆内存快照 | 频繁 GC 或对象泄漏 |
定位日志缓冲区竞争热点
// 在日志写入关键路径添加 runtime.SetMutexProfileFraction(1)
import "runtime"
func init() {
runtime.SetMutexProfileFraction(1) // 100% 记录互斥锁持有事件
}
启用后,/debug/pprof/mutex 将暴露锁竞争最频繁的调用栈,精准定位 sync.Pool 复用不足或 bytes.Buffer 频繁分配问题。
graph TD A[压测请求] –> B{pprof 数据采集} B –> C[/debug/pprof/heap] B –> D[/debug/pprof/mutex] C –> E[分析对象存活周期] D –> F[识别 WriteLog 锁热点] E & F –> G[优化 buffer 复用 + 无锁环形队列]
2.3 Zap高性能结构化日志引擎在高并发流会话中的零分配日志写入实践
Zap 通过预分配缓冲区与无反射序列化,规避运行时内存分配。核心在于 zap.Logger 与 zapcore.Core 的组合复用。
零分配关键机制
- 复用
[]byte缓冲池(zapcore.BufferPool) - 日志字段预编译为
zap.Field结构体(栈分配) - JSON 编码器跳过
fmt.Sprintf和reflect.Value调用
流会话日志写入示例
// 使用预构建的 logger 实例(全局复用,非每次 new)
logger := zap.New(zapcore.NewCore(
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
zapcore.AddSync(&sessionWriter{sessionID: "sess_abc123"}),
zapcore.InfoLevel,
)).With(zap.String("session_id", "sess_abc123"))
logger.Info("stream packet received",
zap.Int64("seq", 12345),
zap.Duration("latency_ms", 12*time.Millisecond))
逻辑分析:
logger.With()返回新 logger 仅拷贝指针与字段 slice(栈上 32 字节),不触发堆分配;zap.Int64等构造函数返回预分配的Field值类型,内部直接写入 buffer,全程无 GC 压力。
| 优化维度 | 传统 logrus | Zap(零分配模式) |
|---|---|---|
| 字段序列化 | fmt.Sprintf + heap |
直接二进制写入 buffer |
| 结构体编码 | reflect + alloc |
预编译 encoder 路径 |
| 并发安全 | mutex 锁粒度粗 | 每 session 独立 writer |
graph TD
A[Log Call] --> B{Field 构造}
B --> C[栈分配 Field 值]
C --> D[BufferPool.Get]
D --> E[直接 WriteTo buffer]
E --> F[Sync.Write]
2.4 自定义Zap Core实现流媒体上下文透传:trace_id、stream_id、codec_type、bitrate_kbps字段动态注入
Zap 默认 Core 不支持运行时动态注入流媒体专属字段。需继承 zapcore.Core 并重写 Check() 和 Write() 方法,在日志事件构建阶段注入上下文。
关键字段注入时机
trace_id:从context.Context的value中提取(如ctx.Value("trace_id").(string))stream_id/codec_type/bitrate_kbps:通过zap.Fields透传至Entry,或在Write()中从entry.LoggerName或entry.Caller元数据推导
示例:自定义 Core Write 实现
func (c *StreamingCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
// 动态注入流媒体上下文字段(优先级:fields < context < fallback)
ctx := entry.Context
if traceID, ok := ctx.Value("trace_id").(string); ok {
fields = append(fields, zap.String("trace_id", traceID))
}
if sid, ok := ctx.Value("stream_id").(string); ok {
fields = append(fields, zap.String("stream_id", sid))
}
// ... 同理注入 codec_type、bitrate_kbps
return c.nextCore.Write(entry, fields)
}
逻辑说明:
Write()是字段最终合并点;ctx.Value提供无侵入式上下文传递能力;zap.String确保字段类型安全且可被结构化日志系统(如 Loki、ES)索引。
| 字段名 | 类型 | 来源 | 是否必需 |
|---|---|---|---|
trace_id |
string | OpenTelemetry Context | ✅ |
stream_id |
string | RTMP/HTTP-FLV Session | ✅ |
codec_type |
string | AVCodecContext | ✅ |
bitrate_kbps |
int | Encoder stats | ⚠️(若支持码率自适应) |
graph TD
A[Log Entry] --> B{Has streaming context?}
B -->|Yes| C[Inject trace_id/stream_id/...]
B -->|No| D[Use default fallback values]
C --> E[Serialize to JSON/Proto]
D --> E
2.5 日志采样策略设计:基于QoS分级(关键帧/丢包/卡顿)的动态采样率调控与OpenTelemetry桥接
为保障可观测性与资源开销的平衡,需对日志实施QoS感知的动态采样。核心思路是将媒体流QoS事件映射为采样权重:关键帧日志必须100%保留,丢包事件按RTT抖动程度阶梯降采(30%–70%),卡顿事件则依据持续时长动态升采(≥2s时强制100%)。
采样权重映射规则
| QoS事件类型 | 触发条件 | 基础采样率 | 动态调节因子 |
|---|---|---|---|
| 关键帧 | is_keyframe == true |
100% | ×1.0(恒定) |
| 丢包 | packet_loss_rate > 1% |
50% | ×(1 − jitter_ms/100) |
| 卡顿 | freeze_duration ≥ 2000ms |
20% | ×min(5, duration/1000) |
OpenTelemetry桥接逻辑
def qos_aware_sampler(span: Span) -> bool:
attrs = span.attributes
if attrs.get("media.qos.type") == "keyframe":
return True # 强制采样
if attrs.get("media.qos.type") == "freeze":
dur = attrs.get("media.freeze.duration_ms", 0)
return dur >= 2000 or random.random() < min(0.2 * (dur / 1000), 1.0)
return random.random() < 0.5 * max(0.3, 1.0 - attrs.get("network.jitter_ms", 0)/100)
该函数将QoS属性实时注入OpenTelemetry Sampler 接口,实现Span级细粒度决策;media.* 属性由前端SDK注入,network.jitter_ms 来自WebRTC stats API,确保采样策略与真实媒体体验强耦合。
graph TD A[QoS事件上报] –> B{事件类型判断} B –>|关键帧| C[采样率=100%] B –>|丢包| D[按抖动衰减采样率] B –>|卡顿| E[按持续时间放大采样率] C & D & E –> F[OpenTelemetry Tracer.inject]
第三章:Loki日志后端的PB级存储与流式索引优化
3.1 Loki v2.9+多租户架构下视频服务日志的tenant_id划分与chunk压缩策略调优
视频服务日志天然具备高基数、强时序、多租户特征。Loki v2.9+ 引入 tenant_id 的显式路由能力,需结合业务维度精准切分:
tenant_id 划分原则
- 按 CDN 区域 + 清晰度等级组合:
cn-east-4k、us-west-1080p - 避免使用用户ID(基数爆炸)或毫秒级时间戳(碎片化严重)
chunk 压缩策略调优配置
# loki-config.yaml
chunks:
compression:
encoding: zstd
level: 3 # 平衡压缩率(~3.2x)与CPU开销(<8%增量)
zstd level=3在视频日志(含大量重复trace_id、codec字段)场景下,较默认snappy提升27%存储密度,且解压延迟稳定在1.2ms内(实测百万行/秒吞吐)。
关键参数对比表
| 参数 | 默认值 | 推荐值 | 影响面 |
|---|---|---|---|
max_chunk_age |
1h | 2h | 减少小chunk数量 |
chunk_idle_period |
5m | 10m | 提升单chunk聚合度 |
graph TD
A[原始日志流] --> B{按tenant_id路由}
B --> C[cn-east-4k → Zone-A存储池]
B --> D[us-west-1080p → Zone-B存储池]
C & D --> E[统一zstd-3压缩+2h flush]
3.2 基于periodic table manager的TSDB分片机制与流媒体日志按stream_id+date双维度水平扩展实践
Periodic Table Manager(PTM)是专为时序数据设计的元数据驱动分片调度器,其核心能力在于将逻辑表自动映射为物理分片集合,支持多维路由策略。
双维度分片路由策略
采用 stream_id % shard_count 进行哈希预分片,再结合 date(如 20240520)作为二级分区键,形成 (stream_id, date) 复合路由键,确保同一流的日志按天隔离、跨流负载均衡。
分片配置示例
# ptm-config.yaml
sharding:
strategy: composite
keys: [stream_id, date]
stream_id: {type: hash, buckets: 128}
date: {type: range, format: "YYYYMMDD"}
该配置使 PTM 在写入时自动解析
stream_id=abc123和date=20240520,定位至唯一分片shard-73_20240520;buckets: 128保障初始扩缩容粒度可控,range类型便于按天 TTL 清理。
分片生命周期管理
| 阶段 | 触发条件 | 自动行为 |
|---|---|---|
| 创建 | 首次写入新 stream_id | 按模板生成对应 date 分片 |
| 扩容 | 单分片写入 QPS > 5k | 拆分 stream_id 哈希桶 |
| 归档 | date ≤ now – 90d | 标记只读并触发冷备迁移 |
graph TD
A[写入请求] --> B{PTM 路由解析}
B --> C[提取 stream_id & date]
C --> D[哈希 + 范围查表]
D --> E[定位物理分片]
E --> F[写入 TSDB 实例]
3.3 日志索引轻量化设计:仅索引labels(如app, stream_id, status_code),放弃全文索引的存储-查询权衡分析
在高吞吐日志场景下,全文索引导致写放大与内存开销激增。我们转而仅对结构化 label 字段建索引,显著降低倒排索引体积。
核心索引字段定义
# prometheus-style labels used for indexing (no __text__ or message field)
index_labels: ["app", "stream_id", "status_code", "env", "region"]
该配置禁用 message 和 log_line 的分词索引,仅保留高选择性、低基数 label;stream_id 作为请求链路主键,支撑毫秒级 trace 关联。
存储-查询权衡对比
| 维度 | 全文索引 | Label-only 索引 |
|---|---|---|
| 写入延迟 | ↑ 3.2× | 基线 |
| 索引存储占比 | 68% of total | |
app="api" & status_code="500" 查询延迟 |
120ms | 8ms |
查询能力约束
- ✅ 支持等值/范围/多 label 组合过滤(如
app="auth" && env="prod") - ❌ 不支持
message =~ "timeout.*DB"类模糊文本检索
graph TD
A[原始日志] --> B{提取label}
B --> C[app=“payment”, stream_id=“s123”, status_code=“429”]
C --> D[写入label倒排索引]
D --> E[跳过message分词与存储]
第四章:LogQL驱动的流媒体可观测性闭环构建
4.1 LogQL高级模式匹配:提取HLS切片延迟、WebRTC PLI/FIR触发频次与Jitter分布直方图
LogQL 支持正则捕获组与内联聚合,可从非结构化媒体日志中精准提取关键QoE指标。
HLS切片延迟提取
{job="media-encoder"} |~ `hls.*segment_id=(\d+).*delay=([\d.]+)ms`
| unwrap $2
| histogram_quantile(0.95, sum by (le) (rate(histogram_bucket{metric="hls_delay_ms"}[5m])))
|~ 执行正则匹配,$2 提取延迟毫秒值;unwrap 展开为数值流;后续通过 Prometheus 直方图桶聚合计算 P95 延迟。
WebRTC控制帧频次统计
| 触发类型 | 日志关键词 | 聚合表达式 |
|---|---|---|
| PLI | pli_received |
count_over_time({job="webrtc-gw"} |= "pli" [1h]) |
| FIR | fir_requested |
sum by (instance) (rate(fir_triggered_total[1h])) |
Jitter分布直方图(Mermaid)
graph TD
A[原始日志] --> B[正则提取 jitter_us]
B --> C[转换为整数微秒]
C --> D[映射至预设桶:0,5000,15000,50000]
D --> E[histogram_quantile]
4.2 跨日志-指标-链路的关联分析:LogQL + PromQL + Tempo TraceID反查实现“卡顿→编码超时→GPU占用飙升”根因定位
数据同步机制
Grafana Loki、Prometheus 与 Tempo 通过统一 TraceID(X-Trace-ID)和标签对齐(cluster, service, pod),实现跨系统上下文传递。
关联分析三步法
-
Step 1(卡顿定位):用 LogQL 在 Loki 中筛选前端卡顿日志
{job="frontend"} |~ `lag.*ms` | line_format "{{.traceID}} {{.msg}}" // 提取含 lag 字样的日志行,并暴露 traceID 用于下钻→ 输出 TraceID 列表,作为后续查询起点。
-
Step 2(链路下钻):在 Tempo 中按 TraceID 查看全链路耗时分布,定位
encode_videospan 异常(P99 > 8s)。 -
Step 3(指标佐证):用 PromQL 关联该 TraceID 所属 Pod 的 GPU 指标
gpu_used_percent{pod=~"video-encoder-.*"} offset 1m // offset 1m 补偿日志采集延迟,匹配编码阶段真实负载峰值
关键字段对齐表
| 系统 | 关联字段 | 示例值 |
|---|---|---|
| Loki | traceID label |
0192ab3c4d5e6f7890123456 |
| Tempo | Trace ID | 同上 |
| Prometheus | pod, namespace |
video-encoder-7c8d9 |
graph TD
A[前端卡顿日志] -->|LogQL提取traceID| B[Tempo链路详情]
B -->|定位encode_span| C[获取pod标签]
C -->|PromQL聚合| D[GPU使用率突增曲线]
4.3 基于LogQL alerting rule的实时质量告警:连续3秒AV sync offset > 200ms自动触发流切换预案
数据同步机制
音视频同步偏移(AV sync offset)由客户端埋点以毫秒级精度上报至Loki,日志格式含 level="warn" av_sync_offset_ms="217" 字段。
告警规则设计
# 连续3个采样点(间隔1s)均超阈值
count_over_time({job="player-metrics"} |~ `av_sync_offset_ms="([2-9][0-9]{2,}|[3-9][0-9]{3,})"` [3s]) == 3
逻辑分析:
|~执行正则匹配提取高偏移日志;[3s]窗口内统计命中次数;== 3强制要求严格连续(非滑动窗口),避免瞬时抖动误触。参数200ms隐含在正则中([2-9][0-9]{2,}匹配 ≥200 的整数)。
自动化响应流程
graph TD
A[LogQL告警触发] --> B{偏移持续性校验}
B -->|Yes| C[调用流切换API]
B -->|No| D[丢弃告警]
C --> E[下发新CDN URL至播放器]
关键阈值对照表
| 场景 | 偏移阈值 | 持续时间 | 动作 |
|---|---|---|---|
| 可感知卡顿 | >200ms | ≥3s | 切换备用流 |
| 瞬时网络抖动 | >300ms | 仅记录不告警 |
4.4 日志驱动的A/B测试验证:通过LogQL对比新旧编码器在相同CDN节点下的buffer underrun发生率差异
为精准归因 buffer underrun(缓冲区欠载)波动,我们在灰度发布阶段对同一CDN边缘节点(如 edge-ny-07)并行部署 v2.3(旧)与 v3.1(新)编码器,并统一采集 loki 中结构化日志。
数据同步机制
所有编码器日志均携带如下关键字段:
encoder_version="v2.3|v3.1"cdn_node="edge-ny-07"event_type="buffer_underrun"stream_id,timestamp
LogQL 查询示例
sum by (encoder_version) (
count_over_time({
job="encoder",
cdn_node="edge-ny-07",
event_type="buffer_underrun"
} |~ `underrun` [1h])
) / sum by (encoder_version) (
count_over_time({
job="encoder",
cdn_node="edge-ny-07"
} [1h])
)
逻辑说明:分子统计每版本下1小时内 buffer underrun 事件数;分母统计该版本总流会话数(每条日志代表一次会话心跳),最终输出发生率(%)。
|~ 'underrun'确保仅匹配含关键词的完整事件行,避免误计。
对比结果(过去1小时)
| 编码器版本 | Buffer Underrun 次数 | 总会话数 | 发生率 |
|---|---|---|---|
| v2.3 | 187 | 24,310 | 0.77% |
| v3.1 | 42 | 23,985 | 0.18% |
根因分析路径
graph TD
A[LogQL聚合] --> B{发生率差异 >3x?}
B -->|Yes| C[检查v3.1的预缓冲策略配置]
B -->|No| D[排查CDN节点网络抖动]
C --> E[确认adaptive_prebuffer_ms=800]
第五章:总结与展望
核心技术栈的协同演进
在实际交付的三个中型微服务项目中,Spring Boot 3.2 + Jakarta EE 9.1 + GraalVM Native Image 的组合显著缩短了容器冷启动时间——平均从 2.8 秒降至 0.37 秒。某电商订单履约系统上线后,Kubernetes Horizontal Pod Autoscaler 响应延迟下降 63%,关键指标如下表所示:
| 指标 | 传统JVM模式 | Native Image模式 | 提升幅度 |
|---|---|---|---|
| 启动耗时(P95) | 3240 ms | 368 ms | 88.6% |
| 内存常驻占用 | 512 MB | 186 MB | 63.7% |
| API首字节响应(/health) | 142 ms | 29 ms | 79.6% |
生产环境灰度验证路径
某金融风控平台采用双通道发布策略:新版本以 v2.4.1-native 标签部署至 5% 流量灰度集群,同时保留 v2.4.0-jvm 作为基线。通过 OpenTelemetry Collector 聚合两组 trace 数据,发现原生镜像在 JSON 序列化环节存在 12% 的 CPU 热点偏移(com.fasterxml.jackson.databind.ser.std.StringSerializer.serialize() 调用栈深度增加),最终通过预注册 @RegisterForReflection 解决。
# 实际执行的构建脚本片段(已脱敏)
native-image \
--no-fallback \
--enable-http \
--enable-https \
--initialize-at-build-time=org.springframework.core.io.buffer.DataBuffer \
-H:Name=payment-service \
-H:Class=io.example.PaymentApplication \
-H:+ReportExceptionStackTraces \
--report-unsupported-elements-at-runtime \
-jar payment-service-2.4.1.jar
运维体系适配挑战
原生镜像导致 JVM Agent 类加载机制失效,传统 SkyWalking Java Agent 无法注入。团队改用 eBPF 方案,在 Kubernetes DaemonSet 中部署 bpftrace 脚本实时捕获 connect() 系统调用,结合 Envoy Sidecar 的 access_log,重构了服务间依赖拓扑图生成逻辑。该方案在 2023 年 Q4 故障定位平均耗时从 18 分钟压缩至 4.2 分钟。
开源社区协作实践
向 Quarkus 社区提交的 PR #28412 已合并,修复了 quarkus-jdbc-postgresql 在 ARM64 架构下连接池初始化失败问题。该补丁被同步反向移植至 Spring Native 0.12.x 分支,影响 17 个使用 PostgreSQL 的客户生产环境。协作过程中,我们维护了包含 32 个真实故障场景的 native-test-cases 仓库,并持续更新 CI 流水线中的 test-native-arm64 阶段。
未来技术债管理方向
当前项目中 41% 的第三方库尚未完成原生兼容性验证,其中 org.apache.pdfbox:pdfbox 和 com.h2database:h2 存在反射元数据缺失风险。计划在 2024 年 H1 推行“原生就绪认证”流程:所有新引入依赖必须通过 native-image --dry-run 验证,并在 Maven BOM 中标注 native-support:full/partial/none 属性。Mermaid 流程图展示了该流程的自动化校验节点:
flowchart TD
A[依赖声明] --> B{Maven Central扫描}
B -->|存在native-support标签| C[跳过验证]
B -->|无标签| D[执行dry-run构建]
D --> E{构建成功?}
E -->|是| F[标记为full]
E -->|否| G[触发人工审查]
G --> H[补充reflection-config.json]
H --> D 