5.0 KiB
5.0 KiB
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 链表”:
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,改为:
chain_buffer_prepare_recv_iov(buf, iov, max_iov)- 收集可写空闲段(优先尾 chunk,不够就从 free_list 取或新建 chunk)。
readv(fd, iov, iovcnt)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):
#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 主线程流程(所有命令都落盘)
对每条完整命令:
- 从 chainbuffer 解析得到
cmd_len。 - 申请一个空 slot(ring 未满)。
detach_prefix(cmd_len)得到payload。- 写入 slot:
seq/cmd_len/payload。 - 发布 head。
注意:
- 不做命令类型判断。
- 不构造 uring task。
- 不进行额外 payload malloc。
3.3 WAL 线程流程
循环:
- 从 ring 取 slot。
- 生成长度头(4B)+ payload iov。
- 调
submit_write()(当前打包拷贝发生在此线程)。 chain_buffer_release_payload()归还 chunk。- 清 slot,推进 tail。
4. 一致性与回压
4.1 日志顺序
g_log_off只在 WAL 线程维护。- 单消费者天然保证写入顺序与入队顺序一致。
4.2 回压(必须)
当 ring 达到高水位(例如 80%):
- 暂停该连接读事件(优先)。
- 若持续超时(如 200ms)仍高水位,关闭连接保护系统。
本方案不再做“回退旧路径”。
4.3 异常处理
submit_write失败:记录错误并触发保护动作(可选停机/拒绝新连接)。- 连接断开:已入 slot 的 payload 继续由 WAL 线程完成归还。
- 进程退出:先停读,再 drain wal_ring,再 shutdown uring。
5. 实施步骤(实际落地)
- 重构
network/chainbuffer.*- 回环 chunk + free_list
prepare_recv_iov/commit_recviter/detach_prefix/release_payload
- 修改
reactor.crecv_cb使用readv+ commit
- 修改
kvstore.c- 按命令边界循环
- 每条命令统一入 wal_slot(不分类)
- 新增
dump/wal_slot_queue.*(或放dump/kvs_oplog.c)- SPSC ring + WAL worker
- 调整
dump/kvs_oplog.c- 改为 WAL 线程消费 slot 后调用
submit_write
- 改为 WAL 线程消费 slot 后调用
- 收敛退出与错误路径
- close_conn / shutdown / submit 失败全覆盖
6. 验收标准
- 功能:
- 半包/多包/pipeline 正确;
- 重启回放后数据一致。
- 性能:
- 主线程不再出现 oplog task 构造热点;
- 接收路径 memcpy 次数下降(去掉 tmp 中转)。
- 稳定性:
- 慢盘压测下无泄漏、无 double free、无 UAF;
- 高水位触发时系统行为可预期。