第一章:时序数据库核心概念与Go语言实现全景概览
时序数据库(Time-Series Database, TSDB)专为高效写入、压缩与查询带时间戳的序列化数据而设计,典型应用场景包括监控指标采集、IoT传感器数据、金融行情记录等。其核心特征在于数据按时间单调递增写入、高写入吞吐、按时间范围快速检索、支持降采样与聚合计算,并普遍采用列式存储与时间分区策略优化性能。
Go语言凭借其轻量级协程、高性能网络栈、静态编译与内存安全特性,成为构建TSDB服务的理想选择。主流开源TSDB如InfluxDB(早期v1.x)、VictoriaMetrics、Prometheus的存储引擎均大量使用Go实现——既保障并发写入能力,又简化部署运维复杂度。
时序数据建模关键要素
- 时间戳(Timestamp):毫秒/纳秒精度,作为主排序键
- 测量名称(Measurement):如
cpu_usage、temperature - 标签集(Tags):键值对元数据,用于多维索引(如
host=web01,region=us-east) - 字段集(Fields):实际数值型指标(如
value=92.4,unit=percent)
Go中典型TSDB结构定义示例
// Point 表示单个时序数据点
type Point struct {
Measurement string // 测量名
Tags map[string]string // 标签集合,用于索引
Fields map[string]interface{} // 字段值,支持float64/int64/bool/string
Timestamp time.Time // 纳秒级时间戳
}
// SeriesKey 由 measurement + tags 哈希生成,用于定位时间线
func (p *Point) SeriesKey() string {
tagStr := ""
for k, v := range p.Tags {
tagStr += k + "=" + v + ","
}
return p.Measurement + "," + strings.TrimSuffix(tagStr, ",")
}
该结构支持基于标签的快速分组与时间线隔离,是构建索引与内存缓存的基础单元。
主流Go实现模式对比
| 方案 | 存储后端 | 内存索引机制 | 典型适用场景 |
|---|---|---|---|
| WAL + LevelDB | 文件WAL+键值存储 | 哈希表+倒排索引 | 轻量级嵌入式监控代理 |
| 内存TSDB(如go-tsdb) | 纯内存+LRU淘汰 | 时间线ID映射表 | 实时告警与短期分析 |
| 分布式TSDB(如VictoriaMetrics) | 自研存储引擎+TSM文件 | 倒排索引+时间分区 | 百亿点/天规模集群 |
Go生态还提供丰富工具链:prometheus/client_golang 用于暴露指标;go-metrics 支持运行时性能观测;gRPC 与 protobuf 便于构建分布式写入网关。
第二章:WAL日志模块的Go原生实现与高可靠设计
2.1 WAL写入路径的原子性保证与fsync策略分析
数据同步机制
WAL(Write-Ahead Logging)要求日志必须在数据页落盘前持久化,否则将破坏崩溃一致性。原子性并非单次write()调用可保障——它依赖底层文件系统语义与fsync()协同。
fsync策略对比
| 策略 | 延迟 | 持久性保证 | 适用场景 |
|---|---|---|---|
fsync() |
高 | 强(磁盘刷写) | 金融级事务 |
fdatasync() |
中 | 元数据可不刷 | 高吞吐日志系统 |
O_DSYNC |
低 | 内核自动触发 | 平衡型OLTP负载 |
// PostgreSQL中WAL写入核心片段(简化)
if (wal_sync_method == WAL_SYNC_METHOD_FSYNC)
pg_fsync(wal_file_fd); // 强制刷写整个文件(含元数据)
else if (wal_sync_method == WAL_SYNC_METHOD_FDATASYNC)
pg_fdatasync(wal_file_fd); // 仅刷写数据块,跳过inode更新
pg_fsync()调用底层fsync(2),确保所有缓冲区数据及文件元数据(mtime、size等)落盘;pg_fdatasync()则仅保证数据块持久化,避免inode锁争用,提升并发写性能。
WAL刷写流程
graph TD
A[Backend进程生成WAL记录] --> B[写入WAL buffer内存]
B --> C{XLogFlush触发?}
C -->|是| D[调用XLogWrite → write()到OS buffer]
D --> E[根据sync_method选择fsync/fdatasync]
E --> F[硬件队列→磁盘介质]
- WAL buffer满或checkpoint临近时强制刷写
wal_writer_delay控制后台刷写频率,降低fsync频次但增加恢复窗口
2.2 日志格式序列化:Protocol Buffers vs 自定义二进制编码实践
日志序列化需在体积、解析速度与跨语言兼容性间权衡。Protocol Buffers(v3)提供强 schema 约束与高效二进制编码,而自定义二进制方案可极致压缩特定日志结构(如固定字段长度的审计日志)。
序列化效率对比
| 指标 | Protobuf(JSON) | Protobuf(Binary) | 自定义二进制 |
|---|---|---|---|
| 平均日志体积(KB) | 1.8 | 0.6 | 0.4 |
| 反序列化耗时(μs) | 125 | 28 | 19 |
Protobuf 定义示例
syntax = "proto3";
message LogEntry {
uint64 timestamp = 1; // Unix nanoseconds
int32 level = 2; // 0=DEBUG, 1=INFO, 2=ERROR
string trace_id = 3; // max 32 bytes, optional
bytes payload = 4; // compressed JSON blob
}
该定义启用 packed encoding 与 zero-copy parsing;timestamp 使用 varint 编码,小数值仅占1字节;payload 保留原始压缩数据,避免重复解压。
自定义编码核心逻辑
func EncodeLog(buf *bytes.Buffer, e *LogEntry) {
binary.Write(buf, binary.BigEndian, e.Timestamp) // 8B fixed
buf.WriteByte(byte(e.Level)) // 1B
buf.Write([]byte(e.TraceID[:min(len(e.TraceID),32)])) // padded
buf.Write(e.Payload)
}
固定字段布局跳过 tag 解析,直接内存拷贝;TraceID 零填充至32字节,确保随机访问对齐。
graph TD A[原始日志结构] –> B{序列化策略选择} B –> C[Protobuf: schema驱动/跨语言] B –> D[自定义: 领域特化/极致紧凑] C –> E[Schema变更需版本管理] D –> F[无元数据,升级需双写兼容]
2.3 WAL滚动与截断机制:基于时间窗口与大小阈值的双控逻辑
WAL(Write-Ahead Logging)滚动与截断并非简单轮转,而是由时间窗口与段大小阈值协同决策的动态过程。
双控触发条件
- 时间维度:单个WAL段存活超
wal_keep_time = '1h'(默认)即进入可截断候选集 - 空间维度:段文件体积 ≥
max_wal_size = 1GB(PostgreSQL默认)则强制触发滚动
截断决策流程
-- 查看当前WAL状态(关键字段)
SELECT
pg_walfile_name(pg_current_wal_lsn()) AS current_segment,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS unreferenced_bytes,
pg_walfile_name(restart_lsn) AS oldest_reachable_segment
FROM pg_control_checkpoint();
该查询返回当前活跃段名、自检查点起未被归档/复制的字节数,以及
restart_lsn指向的最早可安全截断位置。restart_lsn由最老活跃事务、复制槽或归档进度共同决定,是截断的安全边界。
双控优先级规则
| 条件类型 | 触发动作 | 优先级 |
|---|---|---|
| 时间超限(且无活跃依赖) | 标记为可回收 | 中 |
| 大小超限 | 立即滚动新段 + 启动旧段评估 | 高 |
| 存在复制槽滞后 | 暂停截断,即使双阈值满足 | 最高 |
graph TD
A[新WAL写入] --> B{是否达 max_wal_size?}
B -->|是| C[滚动新段]
B -->|否| D{是否超 wal_keep_time?}
D -->|是且无依赖| E[标记可截断]
D -->|否或存在复制槽| F[保留并重检]
C --> G[更新 restart_lsn]
G --> H[触发后台截断扫描]
2.4 崩溃恢复流程:从WAL重放构建内存状态的完整Go代码走读
崩溃恢复的核心是重放WAL日志,重建与持久化状态一致的内存视图。以下为关键恢复入口逻辑:
func (r *RecoveryManager) Recover() error {
logs, err := r.wal.ReadFromCheckpoint(r.checkpointLSN)
if err != nil {
return err
}
for _, record := range logs {
r.applyWALRecord(record) // 幂等应用:INSERT/UPDATE/DELETE
}
return nil
}
ReadFromCheckpoint 从检查点LSN开始顺序读取WAL条目;applyWALRecord 按事务原子性更新内存B+树索引与缓冲池页——不依赖磁盘数据页加载,仅靠日志重演。
WAL记录应用策略
- 所有操作必须幂等(如UPDATERECORD含prev/next版本号)
- REDO仅作用于未刷盘脏页,UNDO由事务状态决定是否跳过
关键参数说明
| 参数 | 含义 | 示例值 |
|---|---|---|
checkpointLSN |
最近完整检查点位置 | 0x1A3F00 |
record.Type |
日志类型(Insert/Update/Delete/Commit) | WAL_UPDATE |
graph TD
A[启动恢复] --> B[定位Checkpoint LSN]
B --> C[顺序读取WAL段]
C --> D{日志类型判断}
D -->|Update| E[定位PageID→BufferPool]
D -->|Commit| F[标记Txn完成]
E --> G[原地修改页镜像]
2.5 WAL性能压测与零拷贝优化:mmap映射与ring buffer实践
数据同步机制
WAL(Write-Ahead Logging)在高吞吐场景下常成为瓶颈。传统 write() + fsync() 路径涉及多次用户态/内核态拷贝及磁盘寻道,延迟陡增。
mmap 零拷贝加速
// 将WAL文件以MAP_SHARED | MAP_SYNC(Linux 5.8+)映射
int fd = open("/wal/log.bin", O_RDWR | O_CREAT, 0644);
void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_SYNC, fd, 0);
// 写入直接落至页缓存,内核异步刷盘
memcpy(addr + offset, data, len);
MAP_SYNC 确保写入后内存修改对其他进程/设备可见;MAP_SHARED 允许内核按需回写,规避 memcpy 到内核缓冲区的冗余拷贝。
ring buffer 结构设计
| 字段 | 类型 | 说明 |
|---|---|---|
head |
uint64 | 生产者写入位置(原子递增) |
tail |
uint64 | 消费者同步位置(fsync点) |
buffer[] |
byte[] | 循环存储区(size=2^N) |
graph TD
A[Producer App] -->|mmap写入| B[Ring Buffer]
B --> C{head == tail?}
C -->|否| D[Kernel Page Cache]
C -->|是| E[fsync触发刷盘]
D --> F[Storage Device]
压测关键指标
- 吞吐提升:
mmap+ ring buffer 使 1KB 小日志吞吐达 128K IOPS(对比write+fsync的 18K) - 延迟降低:P99 写入延迟从 8.3ms → 0.4ms
第三章:LSM-Tree存储引擎的Go并发架构实现
3.1 内存表(MemTable)的并发安全设计:skiplist vs B+tree in Go实测对比
MemTable 是 LSM-Tree 中写入路径的核心内存结构,其并发性能直接决定吞吐上限。Go 原生不提供线程安全的有序数据结构,需自主实现。
并发模型选择
- SkipList:基于 CAS 的无锁插入/查找,天然支持高并发写入
- B+Tree:通常依赖
sync.RWMutex,读多写少场景下更省内存
性能实测(16 线程,1M 随机键插入)
| 结构 | 平均延迟 (μs) | 吞吐 (Kops/s) | GC 增长率 |
|---|---|---|---|
| Concurrent SkipList | 24.1 | 41.5 | +12% |
| Mutex-B+Tree | 68.7 | 14.6 | +3% |
// 基于原子操作的 skiplist 节点插入片段
func (s *Skiplist) insert(key string, val []byte) {
level := s.randomLevel() // [1, MaxLevel],概率衰减
node := &node{key: key, val: val, level: level}
for i := 0; i < level; i++ {
node.forward[i] = s.header.forward[i] // 无锁快照
}
// CAS 更新各层前驱指针 —— 关键并发安全保障
}
该实现避免全局锁,randomLevel() 控制跳表高度分布,forward[i] 的原子赋值确保多线程视角一致;而 B+Tree 的 RWMutex 在密集写入时成为瓶颈。
数据同步机制
graph TD
A[写请求] –> B{SkipList CAS 插入}
A –> C[Mutex.Lock → B+Tree split/insert]
B –> D[WAL 日志追加]
C –> D
3.2 SSTable生成与布隆过滤器集成:Go标准库io/fs与mmap文件操作深度解析
SSTable(Sorted String Table)作为LSM-tree的核心持久化单元,其高效写入与快速点查依赖底层文件I/O的精细控制。
mmap加速SSTable读取
Go原生不支持mmap,需借助golang.org/x/sys/unix调用Mmap系统调用:
// 将SSTable数据文件映射为只读内存区域
data, err := unix.Mmap(int(f.Fd()), 0, int(stat.Size()),
unix.PROT_READ, unix.MAP_PRIVATE)
if err != nil {
return nil, err
}
// data可直接按偏移解析索引/数据块,规避read()系统调用开销
unix.Mmap参数依次为:文件描述符、起始偏移(0)、长度、保护标志(PROT_READ)、映射类型(MAP_PRIVATE)。零拷贝访问大幅提升布隆过滤器校验与key定位性能。
布隆过滤器嵌入策略
- 过滤器置于SSTable尾部,写入时通过
io/fs接口统一管理元数据; - 读取时先
mmap整个文件,再按固定偏移提取布隆位图。
| 组件 | 作用 | Go实现方式 |
|---|---|---|
| SSTable Builder | 序列化有序键值对 | encoding/binary + bufio.Writer |
| Bloom Filter | 概率型存在性预检 | github.com/willf/bloom |
| 文件元数据 | 记录过滤器偏移与大小 | io/fs.FileInfo + 自定义header |
graph TD
A[Write SSTable] --> B[序列化Data Block]
B --> C[构建Bloom Filter]
C --> D[追加Filter到文件末尾]
D --> E[写入Footer: filterOffset+size]
3.3 多层合并策略(Tiered vs Leveled)在时序场景下的Go调度器适配优化
时序数据写入高频、查询局部性强,传统Leveled Compaction在WAL回放与TSDB快照重建阶段易引发Goroutine阻塞。为此,我们改造runtime/proc.go中findrunnable()逻辑,引入层级感知的P优先级升降机制:
// 在schedule()中插入时序负载感知钩子
if tsdb.IsTieredMergeActive() {
incPPriority(p, tsdb.TierLevel()) // 按当前合并层级动态提升P权重
}
该调整使处于Tier-0(内存热层)合并的Goroutine获得更高P绑定概率,降低跨P迁移开销。
核心差异对比
| 维度 | Tiered(时序优化) | Leveled(默认) |
|---|---|---|
| 合并触发频率 | 按时间窗口分片触发 | 按大小阈值触发 |
| Goroutine阻塞 | ≤5ms(单次合并) | 20–200ms |
| P资源竞争 | 显式分级调度 | 全局公平轮转 |
调度路径优化示意
graph TD
A[NewTimerEvent] --> B{是否Tier-0写入?}
B -->|是| C[提升P优先级+绑定本地M]
B -->|否| D[走默认Leveled调度路径]
C --> E[加速LSM memtable flush]
关键参数:tsdb.TierLevel()返回0–3整数,对应内存/SSD/NVMe/冷存档四层,驱动P的pp.priority实时更新。
第四章:时间分区与压缩算法的工程化落地
4.1 时间分区模型设计:按小时/天/自适应窗口的Go struct tag驱动分区元数据管理
核心设计理念
将时间分区逻辑从业务代码解耦,交由结构体标签(partition:"hour")声明式定义,运行时自动解析生成分区路径与元数据。
结构体标签驱动示例
type Event struct {
ID string `json:"id"`
Timestamp time.Time `json:"ts" partition:"hour"` // 支持 hour/day/auto
UserID string `json:"user_id"`
}
逻辑分析:
partition:"hour"触发PartitionKey()方法提取ts.Truncate(time.Hour),生成路径如year=2024/month=05/day=21/hour=14;auto模式则依据数据倾斜度动态选择小时或天级粒度。
分区策略对比
| 策略 | 适用场景 | 路径示例 | 分区数增长 |
|---|---|---|---|
hour |
高频实时事件 | .../hour=14 |
快(~720/月) |
day |
日志归档 | .../day=21 |
中(~30/月) |
auto |
流量波动大 | 动态切换 | 自适应 |
元数据注册流程
graph TD
A[Struct Tag 解析] --> B[PartitionRule 实例化]
B --> C[TimeRange 推导]
C --> D[PathTemplate 渲染]
D --> E[Metadata Registry]
4.2 列式压缩算法选型:Delta-of-Delta + Gorilla编码的Go汇编级优化实现
核心思想融合
Delta-of-Delta(DoD)消除二阶趋势,Gorilla 编码针对浮点时间序列的 XOR+前导零压缩——二者协同可将典型监控指标压缩率提升至 12× 以上。
关键优化路径
- 将 DoD 差分链路与 Gorilla 的
XOR ^ prev合并在单次寄存器流水内完成 - 使用 Go 的
//go:assembly指令内联 x86-64 AVX2 指令加速前导零计数(lzcnt) - 避免 Go runtime 的栈逃逸,全部数据在
RSP偏移区原地处理
示例:核心循环汇编片段(简化版)
// DoD + Gorilla XOR + LZCNT in one pass
MOVQ (SI), AX // load current value
XORQ DX, AX // AX = curr ^ prev (Gorilla step)
SUBQ DX, BX // BX = prev - prev_prev (DoD base)
XORQ BX, AX // AX = (curr ^ prev) ^ (prev - prev_prev)
LZCNTQ AX, CX // count leading zeros → compression token length
逻辑说明:
DX保存上一值,BX保存上上值;XORQ BX, AX实现 DoD 与 Gorilla 的算子融合,消除中间存储;LZCNTQ直接产出 token 长度,驱动后续变长编码跳转。
| 组件 | 原生 Go 实现 | AVX2 汇编优化 | 吞吐提升 |
|---|---|---|---|
| DoD+Gorilla 编码 | 3.2 GB/s | 9.7 GB/s | 3.0× |
graph TD
A[原始 float64] --> B[Delta-of-Delta]
B --> C[Gorilla XOR with prev]
C --> D[AVX2 LZCNT + bit-packing]
D --> E[紧凑 bitstream]
4.3 分区合并调度器:基于Go context与channel的抢占式任务编排
核心设计思想
将大规模分区合并任务建模为可中断、可优先级抢占的协程流,利用 context.Context 传递取消信号与超时控制,通过 chan Task 实现任务队列的无锁分发。
抢占式调度实现
func (s *Scheduler) Run(ctx context.Context) {
for {
select {
case <-ctx.Done(): // 外部主动取消(如高优任务插入)
return
case task := <-s.taskCh:
s.executeWithPreemption(ctx, task)
}
}
}
ctx.Done() 触发立即退出当前调度循环;s.taskCh 为带缓冲的 channel,容量决定最大待处理分区数;executeWithPreemption 内部嵌套子 context,支持单任务粒度超时。
任务优先级映射表
| 优先级 | 触发条件 | 超时阈值 | 可抢占性 |
|---|---|---|---|
| High | 主键冲突或元数据变更 | 300ms | ✅ |
| Medium | 常规分区合并 | 2s | ⚠️(仅被High抢占) |
| Low | 后台压缩优化 | 10s | ❌ |
执行流程简图
graph TD
A[接收合并请求] --> B{是否High优先级?}
B -->|是| C[派生cancelable sub-context]
B -->|否| D[绑定父context]
C --> E[执行并监听preempt signal]
D --> E
E --> F[成功/失败/被抢占]
4.4 压缩后数据一致性校验:CRC32C + 时间戳区间验证的Go单元测试全覆盖方案
核心校验策略设计
采用双因子验证:
- CRC32C(IEEE 33097标准)提供强哈希抗碰撞能力,比CRC32B更适合大数据流;
- 时间戳区间(
min_ts/max_ts)约束逻辑时效性,防止重放或乱序写入。
单元测试覆盖要点
- 边界场景:空数据、单字节、跨区块压缩、时钟回拨;
- 错误注入:篡改CRC、伪造时间戳、截断压缩流;
- 并发安全:多goroutine并发校验同一数据块。
示例测试片段
func TestCompressedDataIntegrity(t *testing.T) {
data := []byte("hello world")
compressed, crc, minTS, maxTS := compressWithCRC(data) // 内部调用 gzip + crc32c.Sum64()
// 验证CRC32C与原始数据一致性
require.Equal(t, crc, crc32c.Checksum(compressed, crc32c.MakeTable(crc32c.Castagnoli))) // Castagnoli多项式表
// 验证时间戳区间合理性(纳秒级)
require.LessOrEqual(t, minTS, maxTS)
require.WithinDuration(t, time.Now(), time.Unix(0, minTS), 5*time.Second)
}
compressWithCRC返回压缩字节、CRC64值、起止纳秒时间戳;crc32c.MakeTable(crc32c.Castagnoli)使用硬件加速友好的Castagnoli多项式(0x1EDC6F41),吞吐提升约3×;WithinDuration容忍系统时钟漂移。
| 校验维度 | 合法范围 | 失败示例 |
|---|---|---|
| CRC32C | 64位校验值匹配 | 修改1字节导致CRC不等 |
| minTS | ≥ 0,≤ maxTS | 设置为负值或超前1小时 |
| maxTS | ≤ 当前时间+5s | 设为未来24小时 |
graph TD
A[原始数据] --> B[压缩+计算CRC32C]
B --> C[打时间戳区间]
C --> D[序列化存储]
D --> E[读取时双重校验]
E --> F{CRC匹配? ∧ 时间有效?}
F -->|是| G[接受数据]
F -->|否| H[拒绝并告警]
第五章:TSDB存储引擎的演进路径与生产就绪建议
存储模型的代际跃迁:从LSM到时序原生架构
早期TSDB(如InfluxDB 0.x、OpenTSDB)普遍基于LevelDB/RocksDB构建LSM-tree存储层,虽具备高写吞吐,但在高频乱序写入、多维标签查询与时间窗口聚合场景下暴露出显著瓶颈——索引膨胀率达300%以上,10亿级时间线下的Cardinality查询延迟超2s。以某车联网平台为例,其采用InfluxDB 1.8集群在接入50万辆车后,因Series Cardinality爆炸式增长导致元数据服务频繁OOM,最终通过迁移至VictoriaMetrics(基于倒排索引+时间分片压缩)将查询P99延迟压降至120ms以内。
压缩策略的工程权衡:Delta-of-Delta vs Gorilla
现代TSDB普遍放弃通用压缩算法,转向时序感知编码。Prometheus 2.0引入Gorilla编码,对浮点值进行XOR差分+变长整数压缩,在真实监控数据集上实现平均1.37 bytes/point;而TimescaleDB采用Delta-of-Delta + Simple8b组合,在IoT传感器数据(采样率1Hz)中达成1.82 bytes/point。某风电场SCADA系统实测显示:启用Gorilla后,3个月历史数据从42TB缩减至16.3TB,但CPU解压开销增加17%,需通过预计算物化视图补偿。
分区与分片的生产实践:按时间+标签双维度切分
单一时序分区易引发热点问题。阿里云TSDB在金融风控场景中采用“时间窗口(7天)+业务域标签(region_id, app_id)”二维哈希分片,使单节点承载时间线数量从≤50万提升至280万。关键配置如下:
| 维度 | 配置值 | 效果 |
|---|---|---|
| 时间分区粒度 | 7天 | 冷热数据分离,GC效率提升40% |
| 标签分片键 | region_id + app_id |
热点region流量均匀分布 |
| 分片副本数 | 3(跨AZ) | 故障域隔离,RPO=0 |
写入链路的可靠性加固:WAL与复制协议选型
VictoriaMetrics默认关闭WAL,依赖内存快照+定期fsync,但在突发断电场景下丢失最多1分钟数据。某支付网关将其改造为:启用WAL并设置--wal-dir独立SSD盘,同时将复制协议从异步改为半同步(quorum=2),在模拟网络分区测试中实现RPO
# vmstorage.yml
- --wal-dir=/data/wal
- --replication-factor=3
- --consistency-level=quorum
查询优化的硬核技巧:降采样与预聚合协同
单纯依赖实时Downsampling易造成精度损失。某智能楼宇系统采用“双轨制”策略:
- 实时查询走原始毫秒级数据(保留1h)
- 历史查询自动路由至预聚合表(5min/1h/1d三级Rollup)
通过Prometheus recording rules生成预聚合指标,并在Grafana中配置$__rate_interval动态适配降采样窗口,使30天趋势分析响应时间稳定在380ms内。
flowchart LR
A[原始写入] --> B{写入路径}
B --> C[实时WAL日志]
B --> D[内存Buffer]
C --> E[磁盘WAL]
D --> F[Time-partitioned SSTable]
E & F --> G[Compaction调度器]
G --> H[归档冷数据] 