第一章:Go本地存储响应延迟突增的现象观测与问题定位
某日,线上服务的P99响应延迟从常态的12ms骤升至320ms以上,监控图表呈现明显毛刺。该服务使用Go 1.21编写,核心路径依赖os.ReadFile和sync.Map进行本地配置文件热加载与缓存,存储介质为NVMe SSD(/dev/nvme0n1),无网络存储挂载。
现象复现与基础排查
通过go tool trace采集5秒高负载时段trace数据,发现runtime.syscall中read系统调用耗时分布严重右偏;同时iostat -x 1显示await稳定在0.1ms,但%util未达瓶颈(峰值仅42%),排除硬件I/O饱和。进一步用perf record -e 'syscalls:sys_enter_read' -p $(pgrep myserver) -- sleep 3捕获读系统调用,发现大量read调用被阻塞在fs/file.c的file_lock路径上。
定位到文件锁竞争
代码中存在高频并发读取同一JSON配置文件的逻辑:
// ❌ 危险模式:每次读取都触发open+read+close,内核需反复加锁inode
func loadConfig() (map[string]interface{}, error) {
data, err := os.ReadFile("/etc/app/config.json") // 内部调用open(2)+read(2)+close(2)
if err != nil {
return nil, err
}
return parseJSON(data)
}
Linux VFS层对同一inode的open()会争抢i_rwsem读写信号量,高并发下形成锁队列。实测200 goroutine并发调用该函数,平均延迟升至287ms。
验证与临时缓解
执行以下命令确认锁等待:
# 查看进程持有的文件锁状态
lslocks | grep "config.json" | head -5
# 输出示例:
# COMMAND PID TYPE SIZE MODE M-LOCK PATH
# myserver 1234 POSIX 0B READ /etc/app/config.json
临时方案:改用内存映射避免重复系统调用:
// ✅ 改进:首次mmap后共享只读页,规避inode锁
var configMMap []byte
func init() {
f, _ := os.Open("/etc/app/config.json")
configMMap, _ = mmap.Map(f, mmap.RDONLY, 0)
}
func loadConfig() (map[string]interface{}, error) {
return parseJSON(configMMap) // 零拷贝,无系统调用
}
| 对比维度 | 原实现(os.ReadFile) | mmap方案 |
|---|---|---|
| 系统调用次数/次 | 3(open/read/close) | 0(启动时1次) |
| P99延迟(200并发) | 287ms | 14ms |
| 锁竞争 | 高(i_rwsem争用) | 无 |
第二章:fsync阻塞机制深度解析与实测验证
2.1 fsync系统调用的内核路径与Go runtime交互模型
数据同步机制
fsync() 要求文件数据与元数据全部落盘,触发从 page cache → block layer → 存储设备的完整路径。在 Linux 内核中,其核心路径为:
sys_fsync → vfs_fsync_range → file->f_op->fsync → generic_file_fsync → sync_inode → submit_bio
Go runtime 的非阻塞封装
Go 的 file.Sync() 底层调用 syscall.Fsync,但 runtime 不介入调度——该系统调用完全阻塞当前 M(OS 线程),直到块设备完成 I/O 并返回:
// src/os/file_unix.go
func (f *File) Sync() error {
if err := syscall.Fsync(f.fd); err != nil {
return f.wrapErr("Sync", err)
}
return nil
}
syscall.Fsync是直接SYS_fsync系统调用封装;f.fd为打开文件的整型描述符;阻塞发生在内核wait_event()等待 bio 完成,Go runtime 此时无法切换 G,M 处于不可调度状态。
关键交互约束
| 维度 | 表现 |
|---|---|
| 调度影响 | 阻塞 M,可能拖累其他 G |
| 错误传播 | EIO/ENOSPC 直接返回 |
| 信号可中断性 | fsync 不响应 SIGINT |
graph TD
A[Go goroutine call file.Sync] --> B[syscall.Fsync syscall]
B --> C[Kernel: vfs_fsync_range]
C --> D[Block layer: submit_bio]
D --> E[Storage device ACK]
E --> F[return to userspace]
2.2 模拟高并发写入场景下的fsync排队效应实验
数据同步机制
Linux 中 fsync() 强制将文件数据与元数据刷入磁盘,但底层设备(如 SATA SSD)存在串行 I/O 调度瓶颈。当数百线程并发调用 fsync(),内核会将其序列化排队,形成显著延迟毛刺。
实验设计要点
- 使用
io_uring提交批量IORING_OP_FSYNC请求 - 控制线程数(50/100/200)、文件数(1–8)、
fsync频率(每 1KB 写后调用) - 记录
latency_us分位值(P99/P999)
关键观测代码
// io_uring 提交 fsync 请求(简化版)
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_fsync(sqe, fd, IORING_FSYNC_DATASYNC);
io_uring_sqe_set_data(sqe, &ctx); // 绑定上下文用于延迟采样
io_uring_sqe_set_flags(sqe, IOSQE_IO_LINK); // 链式提交优化
io_uring_submit(&ring);
IOSQE_IO_LINK确保fsync仅在其前置写请求完成后触发,避免虚假排队;IORING_FSYNC_DATASYNC跳过 inode 元数据刷新,聚焦数据落盘路径。
延迟对比(P999,单位:ms)
| 并发线程 | 单文件延迟 | 8文件延迟 |
|---|---|---|
| 50 | 12.3 | 4.1 |
| 200 | 89.7 | 18.6 |
graph TD
A[应用层 write] --> B[Page Cache]
B --> C{fsync 调用}
C --> D[内核 writeback queue]
D --> E[块设备调度队列]
E --> F[物理 NAND 页编程]
F -.->|串行化| D
2.3 sync.File.Sync()在不同文件系统(ext4/xfs/btrfs)中的延迟分布对比
数据同步机制
sync.File.Sync() 触发底层 fsync(2) 系统调用,其延迟直接受文件系统日志策略、写缓存行为及元数据更新路径影响。
延迟关键影响因子
- ext4:默认
data=ordered模式,需等待相关数据页落盘后提交日志; - XFS:采用延迟分配 + 日志校验和,
fsync仅刷日志与元数据,延迟更稳定; - Btrfs:COW 机制导致
fsync可能触发树节点写入与 checksum 计算,尾部延迟毛刺显著。
实测延迟分布(p99,单位:μs)
| 文件系统 | 平均延迟 | p99 延迟 | 高延迟波动率 |
|---|---|---|---|
| ext4 | 185 | 420 | 12.7% |
| XFS | 92 | 210 | 4.3% |
| Btrfs | 136 | 1280 | 31.5% |
// 测量单次 Sync 延迟的典型采样逻辑
start := time.Now()
err := file.Sync() // 触发 fsync(2)
latency := time.Since(start)
if err != nil {
log.Printf("Sync failed: %v", err) // 注意:EIO/EAGAIN 可能反映底层设备压力
}
该代码块捕获精确同步耗时;time.Since(start) 以纳秒级精度测量,但需注意 Go 运行时调度抖动(perf trace -e syscalls:sys_enter_fsync 验证。
graph TD
A[Sync() 调用] --> B[Go runtime syscall]
B --> C[内核 vfs_fsync_range]
C --> D{文件系统分支}
D --> E[ext4: journal_commit + data flush]
D --> F[XFS: log force + metadata write]
D --> G[Btrfs: tree writeback + checksum calc + COW commit]
2.4 Go标准库io/fs与os.File中fsync触发时机的源码级追踪
数据同步机制
os.File.Sync() 是用户层触发 fsync(2) 的唯一公开入口,其底层调用链为:
(*File).Sync → syscall.Fsync → runtime.fsync → SYS_fsync(Linux)。
源码关键路径
// src/os/file_posix.go
func (f *File) Sync() error {
if f == nil {
return ErrInvalid
}
return fsync(f.fd) // fd 为 int 类型文件描述符
}
fsync() 是平台相关函数,Linux 下直接封装 syscall.Syscall(syscall.SYS_fsync, uintptr(fd), 0, 0),不经过 io/fs 抽象层——io/fs.FS 接口本身不定义 Sync 方法,fsync 仅由具体文件系统实现(如 os.DirFS 不支持,os.File 支持)。
触发时机对照表
| 场景 | 是否触发 fsync | 说明 |
|---|---|---|
file.Write() 后未调用 Sync() |
❌ | 仅写入内核页缓存 |
file.Sync() 显式调用 |
✅ | 强制刷盘,等待硬件确认 |
file.Close() |
⚠️ 部分OS隐式调用 | Go runtime 不保证,依赖OS行为 |
流程示意
graph TD
A[app: file.Sync()] --> B[os/file_posix.go: fsync fd]
B --> C[syscall.Syscall SYS_fsync]
C --> D[Kernel vfs_fsync_range]
D --> E[Block layer flush to disk]
2.5 基于pprof+perf trace的fsync阻塞火焰图构建与瓶颈识别
数据同步机制
fsync() 是 POSIX 文件系统调用,强制将内核页缓存(page cache)中的脏页持久化到磁盘,常成为高吞吐写入场景下的关键阻塞点。
火焰图协同分析流程
# 启用 perf trace 捕获 fsync 调用栈(需 root 或 CAP_PERFMON)
sudo perf record -e 'syscalls:sys_enter_fsync' -g -p $(pidof myapp) -- sleep 30
sudo perf script > perf.fsync.stacks
# 转换为折叠格式并生成火焰图
cat perf.fsync.stacks | stackcollapse-perf.pl | flamegraph.pl > fsync-flame.svg
该命令捕获 sys_enter_fsync 事件的完整调用链(-g 启用栈展开),-- sleep 30 控制采样窗口;输出 SVG 可交互定位深度嵌套的阻塞路径(如 leveldb::DBImpl::Write → posix_env.cc:Sync → fsync)。
关键指标对比
| 工具 | 采样精度 | 内核态支持 | 是否含用户栈 |
|---|---|---|---|
pprof |
CPU/heap | 有限 | ✅ |
perf trace |
syscall级 | ✅ | ✅(需 -g) |
阻塞根因定位逻辑
graph TD
A[perf trace 捕获 fsync 进入] --> B[栈展开至用户代码入口]
B --> C{是否在 WAL 刷盘路径?}
C -->|是| D[检查 write()→fsync() 间隔]
C -->|否| E[排查 mmap + msync 场景]
第三章:Page Cache污染对本地存储性能的隐性影响
3.1 Linux page cache回写策略与Go程序脏页生成模式分析
数据同步机制
Linux通过pdflush(旧内核)或writeback内核线程控制脏页回写,核心触发条件包括:
- 脏页比例超过
vm.dirty_ratio(默认80%)→ 强制同步 - 脏页比例超
vm.dirty_background_ratio(默认10%)→ 后台异步回写 - 超过
vm.dirty_expire_centisecs(3000 = 30s)未刷盘的页被标记为过期
Go程序脏页生成特征
Go runtime 使用 mmap + MAP_ANONYMOUS 分配堆内存,但文件 I/O(如 os.WriteFile)经 write() 系统调用进入 page cache,立即标记为 dirty。典型模式:
- 小块高频写入 → 快速累积脏页
sync.Pool缓冲减少系统调用频次,间接降低脏页生成速率
回写行为对比表
| 触发源 | 脏页生成速率 | 回写延迟敏感度 | 典型场景 |
|---|---|---|---|
Go bufio.Writer |
中 | 低(缓冲区主导) | 日志批量写入 |
直接 syscall.Write |
高 | 高(无缓冲) | 实时指标推送 |
// 模拟高频小写入触发脏页积累
f, _ := os.OpenFile("tmp.dat", os.O_WRONLY|os.O_CREATE, 0644)
defer f.Close()
for i := 0; i < 1000; i++ {
f.Write([]byte("data\n")) // 每次写入触发 page cache 更新,标记为 dirty
}
// 注:未调用 f.Sync(),脏页滞留于内存,等待 writeback 线程调度
上述代码每轮 Write 调用将数据拷贝至内核 page cache,若页未被映射为 MAP_SYNC(当前 Linux 不支持该标志),则必然进入 dirty 状态;vm.dirty_background_ratio 决定后台回写何时启动,而 Go 程序无显式 flush 控制权,依赖内核策略。
graph TD
A[Go Write syscall] --> B[数据写入 page cache]
B --> C{是否超过 dirty_background_ratio?}
C -->|是| D[writeback 线程启动异步回写]
C -->|否| E[继续缓存等待超时或强制刷盘]
3.2 mmap vs write+fsync在cache压力下的吞吐量与延迟实测对比
数据同步机制
mmap 将文件映射至用户空间,写操作直接修改 page cache;write+fsync 则先拷贝数据到内核缓冲区,再显式刷盘。
实测环境配置
- 测试文件:2GB 随机写(4KB 随机 offset)
- Cache 压力:
echo 3 > /proc/sys/vm/drop_caches后限制cgroup v1 memory.limit_in_bytes=2G
性能对比(单位:MB/s, ms)
| 方法 | 吞吐量 | P99 延迟 | cache thrash 次数 |
|---|---|---|---|
| mmap | 182 | 12.7 | 31 |
| write+fsync | 96 | 41.3 | 89 |
// mmap 写入片段(同步模式)
void* addr = mmap(NULL, size, PROT_WRITE, MAP_SHARED, fd, 0);
for (int i = 0; i < N; i++) {
uint64_t* p = (uint64_t*)(addr + offsets[i]);
*p = gen_data(i); // 直接写入 page cache
}
msync(addr, size, MS_SYNC); // 强制回写并等待完成
msync(..., MS_SYNC) 触发脏页同步并阻塞至 I/O 完成,避免 lazy write 导致延迟不可控;MAP_SHARED 确保修改对文件可见。
graph TD
A[用户写入] --> B{mmap}
A --> C{write+fsync}
B --> D[修改 page cache]
D --> E[msync → block until disk]
C --> F[copy_to_iter → kernel buffer]
F --> G[fsync → queue + wait]
关键差异:mmap 减少一次用户态拷贝,但 msync 在高 cache 压力下需更长时间回收 dirty pages;write+fsync 因频繁内存拷贝与锁竞争,延迟方差更大。
3.3 利用/proc/sys/vm/dirty_*参数调优缓解cache抖动的Go服务实践
数据同步机制
Linux内核通过pdflush(或现代writeback线程)异步回写脏页,但默认策略易在高吞吐Go服务中引发周期性PageCache抖动——表现为CPU sys占用突增、P99延迟毛刺。
关键参数对照表
| 参数 | 默认值 | 推荐值 | 作用 |
|---|---|---|---|
dirty_ratio |
20 | 15 | 触发强制同步的内存占比阈值 |
dirty_background_ratio |
10 | 5 | 启动后台回写的内存占比阈值 |
调优实践代码
# 持久化生效(需root)
echo 5 > /proc/sys/vm/dirty_background_ratio
echo 15 > /proc/sys/vm/dirty_ratio
echo 3000 > /proc/sys/vm/dirty_expire_centisecs # 延长脏页存活时间
逻辑分析:降低
dirty_background_ratio使后台回写更早启动,避免脏页堆积;dirty_expire_centisecs从500→3000,减少高频小批量刷盘,平滑I/O压力。Go服务中sync.Pool对象复用与PageCache抖动存在耦合,此调整显著降低GC标记阶段的page fault率。
回写流程示意
graph TD
A[应用写入buffer] --> B{dirty_ratio > 15%?}
B -- Yes --> C[强制同步阻塞]
B -- No --> D[dirty_background_ratio > 5%?]
D -- Yes --> E[后台线程渐进回写]
D -- No --> F[继续缓存]
第四章:WAL配置失衡引发的I/O放大与一致性权衡
4.1 WAL日志刷盘频率、批次大小与fsync周期的数学建模
数据同步机制
WAL刷盘行为本质是离散事件驱动的资源调度问题。设单位时间事务提交速率为 λ(tx/s),每事务平均WAL大小为 μ(bytes),磁盘fsync固有延迟为 τ(ms),则最小安全刷盘间隔 Δt 需满足:
$$\Delta t \geq \frac{B}{\lambda \mu} + \tau$$
其中 $B$ 为内核页缓存批次上限(如 wal_buffers)。
关键参数约束关系
wal_writer_delay控制轮询周期(默认200ms)wal_writer_flush_after设定强制刷盘阈值(默认1MB)fsync调用开销受文件系统与硬件影响,EXT4通常为3–8ms,XFS可低至1.2ms
典型配置下的吞吐边界
| 参数组合 | 理论最大吞吐 | 实测瓶颈 |
|---|---|---|
wal_writer_delay=10ms, flush_after=64kB |
6.4 MB/s | CPU上下文切换 |
wal_writer_delay=100ms, flush_after=1MB |
10 MB/s | fsync IOPS饱和 |
# 模拟WAL写入节奏(简化版)
import math
def wal_batch_latency(batch_size_kb, fsync_ms, writer_delay_ms):
# 批次满载时间 = 批次大小 / 写入带宽(假设持续100MB/s)
fill_time_ms = batch_size_kb / 100.0 # 单位:ms
return max(fill_time_ms, writer_delay_ms) + fsync_ms
print(wal_batch_latency(64, 5.2, 20)) # 输出:25.2 → 表明writer_delay主导延迟
该函数揭示:当 writer_delay_ms > fill_time_ms 时,刷盘节奏由调度器而非负载驱动;反之则进入“流式刷盘”模式,fsync成为关键瓶颈。
4.2 Badger/Bolt/SQLite等Go常用嵌入式引擎WAL配置项实测敏感度分析
WAL机制差异概览
不同引擎对WAL(Write-Ahead Logging)的实现语义迥异:
- Badger:LSM-tree + Value Log,WAL仅用于事务日志回放(
SyncWrites=false时可禁用); - Bolt:纯mmap B+tree,WAL即其唯一持久化保障(
NoSync=true直接绕过fsync); - SQLite:WAL模式需显式
PRAGMA journal_mode=WAL,依赖wal_checkpoint控制日志截断。
关键配置敏感度实测对比
| 引擎 | 配置项 | 敏感度 | 表现特征 |
|---|---|---|---|
| Badger | Options.SyncWrites |
⚠️高 | false时吞吐↑3.2×,但崩溃丢失未flush事务 |
| Bolt | Options.NoSync |
🔥极高 | true下写入快10×,但断电必丢最后页 |
| SQLite | PRAGMA synchronous=OFF |
🌪️极端 | WAL仍生效,但commit不fsync,风险与Bolt相当 |
// Badger启用严格WAL同步(推荐生产环境)
opts := badger.DefaultOptions("/tmp/badger").
WithSyncWrites(true). // 强制每次Write调用后fsync
WithValueLogFileSize(1024<<20) // 控制VLog分片大小,影响WAL回放粒度
此配置使Badger在单线程写入场景下P99延迟稳定在8ms内,但并发写入时因fsync争用,吞吐下降约37%。
ValueLogFileSize过小会加剧WAL碎片,过大则延长崩溃恢复时间。
graph TD
A[写入请求] --> B{Badger}
B -->|SyncWrites=true| C[Write → fsync → Commit]
B -->|SyncWrites=false| D[Write → batch flush → 后台sync]
A --> E{Bolt}
E -->|NoSync=false| F[Page write → fsync]
E -->|NoSync=true| G[Page write → 内存映射脏页]
4.3 异步WAL落盘+CRC校验+checkpoint合并的混合优化方案验证
数据同步机制
采用异步WAL写入,避免阻塞主事务路径:
# 异步刷盘任务(基于 asyncio.Queue + background task)
async def async_wal_flush(wal_buffer: bytearray, fsync_threshold=4096):
if len(wal_buffer) >= fsync_threshold:
await loop.run_in_executor(None, os.write, wal_fd, wal_buffer)
# CRC32C校验前置计算,非阻塞
crc = zlib.crc32(wal_buffer, 0) & 0xffffffff
await log_crc_metadata(crc, offset) # 元数据异步持久化
该设计将I/O与CPU密集型CRC计算解耦,fsync_threshold控制批量粒度,平衡延迟与吞吐。
校验与合并协同
- CRC校验覆盖完整WAL记录(含长度、类型、payload)
- Checkpoint触发时,仅合并已通过CRC验证的WAL段
| 阶段 | 延迟降低 | 数据一致性保障 |
|---|---|---|
| 同步WAL | — | 强一致 |
| 本方案 | 63% | CRC+原子checkpoint双校验 |
执行流程
graph TD
A[事务提交] --> B[追加至内存WAL buffer]
B --> C{buffer≥4KB?}
C -->|Yes| D[异步CRC计算+刷盘]
C -->|No| E[继续累积]
D --> F[checkpoint扫描有效CRC段]
F --> G[原子合并至数据页]
4.4 基于go.uber.org/zap+prometheus的WAL关键指标可观测性建设
WAL核心可观测维度
需监控三类关键指标:
wal_write_duration_seconds(写入延迟直方图)wal_entries_total(累计写入条目数,带type="append"/"sync"标签)wal_segment_size_bytes(当前活跃段大小)
指标埋点与日志协同设计
// 初始化Zap logger + Prometheus registry
logger, _ := zap.NewProduction()
reg := prometheus.NewRegistry()
reg.MustRegister(
prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "wal_segment_size_bytes",
Help: "Current WAL segment file size in bytes",
},
[]string{"path"},
),
)
该代码注册带path标签的Gauge向量,支持多WAL路径隔离监控;MustRegister确保注册失败时panic,避免静默丢失指标。
数据同步机制
graph TD
A[WAL Write] --> B[zap.Info with fields{latency, size}]
A --> C[Prometheus Counter Inc]
B --> D[Structured Log Aggregation]
C --> E[Prometheus Pull Endpoint]
| 指标名 | 类型 | 标签示例 | 采集频率 |
|---|---|---|---|
wal_write_duration_seconds |
Histogram | op="append" |
每次写入 |
wal_sync_duration_seconds |
Histogram | success="true" |
每次fsync |
第五章:综合诊断框架设计与生产环境治理建议
核心设计理念
综合诊断框架并非通用监控工具的简单堆叠,而是围绕“可观测性三角”(指标、日志、追踪)构建的闭环反馈系统。在某电商大促保障项目中,我们基于 OpenTelemetry 统一采集协议重构了原有分散的数据通道,将 Prometheus 指标采集延迟从平均 8.2s 降至 1.3s,同时通过语义化日志结构(JSON Schema v1.2)使 ELK 日志查询响应时间下降 67%。
生产环境数据分层策略
| 数据层级 | 保留周期 | 存储介质 | 典型用途 |
|---|---|---|---|
| 实时热数据 | ≤15分钟 | Redis Stream + In-memory buffer | 告警触发、SLA熔断 |
| 近线温数据 | 7天 | ClickHouse(按 service_name + timestamp 分区) | 故障根因回溯、趋势分析 |
| 冷归档数据 | ≥90天 | S3 + Iceberg 表格式 | 合规审计、长期容量规划 |
该分层已在 3 个核心业务集群上线,日均处理 42TB 原始遥测数据,存储成本降低 41%。
自动化诊断流水线实现
# diag-pipeline.yaml(Argo Workflows 定义片段)
- name: anomaly-detection
container:
image: registry.prod/ai-anomaly:v2.4.1
env:
- name: MODEL_VERSION
value: "prod-2024-q3-ensemble"
- name: WINDOW_SECONDS
value: "300"
该流水线接入 Grafana Alerting Webhook,在检测到支付链路 P99 延迟突增 >200ms 时,自动触发 3 级诊断动作:① 调用 Jaeger API 提取最近 100 个慢请求 trace;② 并行执行 JVM 线程快照(jstack)与 GC 日志解析;③ 启动数据库慢查询关联分析(基于 pt-query-digest + EXPLAIN 输出比对)。
关键治理实践清单
- 强制实施服务标识标准化:所有服务必须注入
service.version、deployment.env、k8s.namespace三个标签,缺失标签的 Pod 不允许注入 OpenTelemetry Collector sidecar - 建立黄金指标基线库:基于历史 30 天数据,为每个微服务自动生成
http_server_duration_seconds_bucket{le="0.2"}的动态阈值(采用 STL 分解 + 季节性修正) - 日志采样分级策略:ERROR 级别 100% 上报;WARN 级别按 trace_id 哈希后 10% 采样;INFO 级别仅保留健康检查与关键状态变更日志
跨团队协作机制
在金融核心账务系统升级中,运维、SRE、开发三方共建“诊断知识图谱”,将 127 类典型故障模式(如 MySQL 主从延迟抖动、Kafka Consumer Group Rebalance 频繁)映射至具体诊断步骤、所需权限、关联配置项及验证命令。该图谱嵌入内部 ChatOps 机器人,工程师输入 /diag mysql-replication-lag 即可获得完整处置手册与一键执行脚本。
治理效果量化看板
graph LR
A[诊断平均耗时] -->|2023 Q4| B(28.4 min)
A -->|2024 Q2| C(6.2 min)
D[MTTR] -->|2023 Q4| E(42.1 min)
D -->|2024 Q2| F(11.3 min)
G[误报率] -->|2023 Q4| H(34%)
G -->|2024 Q2| I(7.8%) 