Files
ldb/chainbuffer.plan.md
2026-03-04 07:20:09 +00:00

5.0 KiB
Raw Blame History

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,改为:

  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

#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. 写入 slotseq/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
    • 高水位触发时系统行为可预期。