# ChainBuffer + 全量 WAL(单一方案,slot 驱动) ## 1. 目标与约束 目标: - 所有命令都落盘(不再区分 update/get)。 - 主线程 A 尽量不做内存分配,不构造 uring task。 - 主线程 A 只做:收包、解析命令边界、摘取 payload、写入预分配 slot。 - WAL 线程 B 负责:从 slot 取 payload、构造 task、提交 io_uring、归还内存块。 约束: - 只保留一个实现方案,不引入 v1/v2 双路径。 - 正确性优先:半包、多包、pipeline、断连、慢盘都可控。 --- ## 2. ChainBuffer 设计(重点) ## 2.1 结构定义 采用“回环 chunk 链表”: ```c typedef struct cb_chunk { struct cb_chunk *next; uint32_t cap; // 固定大小,默认 4096 uint32_t rpos; // 读指针 [0, cap) uint32_t wpos; // 写指针 [0, cap) uint32_t used; // 已用字节数 [0, cap] uint32_t refcnt; // 被 WAL payload 持有的引用计数 uint8_t data[]; } cb_chunk_t; typedef struct chain_buffer { cb_chunk_t *head; cb_chunk_t *tail; size_t total_len; uint32_t chunk_size; // 节点池:避免频繁 malloc/free cb_chunk_t *free_list; uint32_t free_count; uint32_t free_limit; } chain_buffer_t; ``` 说明: - chunk 内部可回环读写(避免 memmove)。 - 多 chunk 串联用于扩容。 - 空 chunk 不释放到系统,先回收到 `free_list`。 ## 2.2 接收路径(readv 直写) 主线程不再 `recv -> tmp -> memcpy`,改为: 1. `chain_buffer_prepare_recv_iov(buf, iov, max_iov)` - 收集可写空闲段(优先尾 chunk,不够就从 free_list 取或新建 chunk)。 2. `readv(fd, iov, iovcnt)` 3. `chain_buffer_commit_recv(buf, nread)` - 推进 `wpos/used/total_len`。 这样可去掉接收中转拷贝。 ## 2.3 消费与摘取 提供三个核心能力: - `chain_buffer_iter_*`:按字节流遍历,供 RESP 解析命令边界。 - `chain_buffer_detach_prefix(buf, len, cb_payload_t *out)`:把前缀字节摘成 payload(尽量零拷贝,边界切分允许一次小拷贝)。 - `chain_buffer_release_payload(buf, payload)`:WAL 线程用完后归还 chunk(refcnt--,归零后回 free_list)。 --- ## 3. slot 队列设计(主线程零分配) ## 3.1 预分配 ring 定义单生产者单消费者 ring(A->B): ```c #define WAL_SLOT_CAP 65536 typedef struct wal_slot { uint64_t seq; uint32_t cmd_len; cb_payload_t payload; // 命令字节 uint8_t in_use; } wal_slot_t; typedef struct wal_ring { wal_slot_t slots[WAL_SLOT_CAP]; _Atomic uint32_t head; // producer write _Atomic uint32_t tail; // consumer read _Atomic uint32_t size; } wal_ring_t; ``` 特点: - slot 全预分配。 - 主线程写 slot 不 malloc。 - WAL 线程消费后清空 slot 并前移 tail。 ## 3.2 主线程流程(所有命令都落盘) 对每条完整命令: 1. 从 chainbuffer 解析得到 `cmd_len`。 2. 申请一个空 slot(ring 未满)。 3. `detach_prefix(cmd_len)` 得到 `payload`。 4. 写入 slot:`seq/cmd_len/payload`。 5. 发布 head。 注意: - 不做命令类型判断。 - 不构造 uring task。 - 不进行额外 payload malloc。 ## 3.3 WAL 线程流程 循环: 1. 从 ring 取 slot。 2. 生成长度头(4B)+ payload iov。 3. 调 `submit_write()`(当前打包拷贝发生在此线程)。 4. `chain_buffer_release_payload()` 归还 chunk。 5. 清 slot,推进 tail。 --- ## 4. 一致性与回压 ## 4.1 日志顺序 - `g_log_off` 只在 WAL 线程维护。 - 单消费者天然保证写入顺序与入队顺序一致。 ## 4.2 回压(必须) 当 ring 达到高水位(例如 80%): 1. 暂停该连接读事件(优先)。 2. 若持续超时(如 200ms)仍高水位,关闭连接保护系统。 本方案不再做“回退旧路径”。 ## 4.3 异常处理 - `submit_write` 失败:记录错误并触发保护动作(可选停机/拒绝新连接)。 - 连接断开:已入 slot 的 payload 继续由 WAL 线程完成归还。 - 进程退出:先停读,再 drain wal_ring,再 shutdown uring。 --- ## 5. 实施步骤(实际落地) 1. 重构 `network/chainbuffer.*` - 回环 chunk + free_list - `prepare_recv_iov/commit_recv` - `iter/detach_prefix/release_payload` 2. 修改 `reactor.c` - `recv_cb` 使用 `readv` + commit 3. 修改 `kvstore.c` - 按命令边界循环 - 每条命令统一入 wal_slot(不分类) 4. 新增 `dump/wal_slot_queue.*`(或放 `dump/kvs_oplog.c`) - SPSC ring + WAL worker 5. 调整 `dump/kvs_oplog.c` - 改为 WAL 线程消费 slot 后调用 `submit_write` 6. 收敛退出与错误路径 - close_conn / shutdown / submit 失败全覆盖 --- ## 6. 验收标准 - 功能: - 半包/多包/pipeline 正确; - 重启回放后数据一致。 - 性能: - 主线程不再出现 oplog task 构造热点; - 接收路径 memcpy 次数下降(去掉 tmp 中转)。 - 稳定性: - 慢盘压测下无泄漏、无 double free、无 UAF; - 高水位触发时系统行为可预期。