chainbuffer fixed

This commit is contained in:
2026-03-04 07:20:09 +00:00
parent 57720a3135
commit a190bdeea5
9 changed files with 1335 additions and 78 deletions

189
chainbuffer.plan.md Normal file
View File

@@ -0,0 +1,189 @@
# 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 线程用完后归还 chunkrefcnt--,归零后回 free_list
---
## 3. slot 队列设计(主线程零分配)
## 3.1 预分配 ring
定义单生产者单消费者 ringA->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. 申请一个空 slotring 未满)。
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
- 高水位触发时系统行为可预期。