2026-03-04 07:20:09 +00:00
2026-02-02 07:40:01 +00:00
2026-03-04 07:20:09 +00:00
2026-03-03 14:24:44 +00:00
2026-02-11 11:59:40 +00:00
2026-02-11 11:59:40 +00:00
2024-05-25 14:23:43 +00:00
2026-03-04 07:20:09 +00:00
2026-03-04 07:20:09 +00:00
2026-01-29 10:47:24 +00:00
2026-03-04 07:20:09 +00:00
2026-03-03 12:56:07 +00:00
2026-03-03 12:56:07 +00:00
2026-03-03 14:24:44 +00:00
2026-01-31 15:38:52 +00:00
2026-03-03 12:56:07 +00:00
2024-05-25 14:23:43 +00:00
2026-03-03 12:56:07 +00:00
2026-03-04 07:20:09 +00:00
2024-05-18 14:10:08 +00:00
2026-03-04 07:20:09 +00:00
2026-03-03 08:05:43 +00:00
2024-05-25 14:23:43 +00:00

9.1 Kvstore

需求

  1. ntyco需要作为kvstore的submodule,通过git clone一次下载。 完成
  2. README需要包含编译步骤测试方案与可行性性能数据。 完成
  3. 全量持久化保存数据集。 BUG FIX
  4. 持久化的性能数据。 完成
  5. 特殊字符可以解决redis的resp协议。 完成
  6. 实现配置文件把日志级别端口ip主从模式持久化方案。 完成
  7. 持久化落盘用io_uring加载配置文件用mmap。 完成
  8. 主从同步的性能开启与关闭性能做到5%?。
  9. 主从同步600w条,出现的coredump。 BUG FIX
  10. 主从同步用ebpf实现。 完成
  11. 内存池测试qps与虚拟内存物理内存。 完成
  12. 实现一个内存泄露检测组件。 完成

环境安装与编译

# xml
sudo apt install libxml2 libxml2-dev
# hiredis client
sudo apt install -y libhiredis-dev
# bpftrace
sudo apt install -y bpftrace libelf libelf-dev clang
# jemalloc
sudo apt install libjemalloc-dev

git clone git@gitlab.0voice.com:lianyiheng/9.1-kvstore.git
cd 9.1-kvstore/
git submodule update --init --recursive
make


测试

测试1性能测试

测试条件:

  1. 不启用持久化。
  2. 不启用主从同步。
  3. pipline
    1. RSET 100w 条, p:i v:i -> +OK
    2. RGET 100w 条, p:i -> +v:i
    3. RDEL 100w 条。 p:i -> +OK
  4. 本机发送请求。

内存分配: malloc

lian@ubuntu:~/share/9.1-kvstore$ ./test-redis/testcase 192.168.10.129 8888 3
average qps:880462
ALL TESTS PASSED.

内存分配: 自实现内存池

lian@ubuntu:~/share/9.1-kvstore$ ./test-redis/testcase 192.168.10.129 8888 3
average qps:942837
ALL TESTS PASSED.

内存分配jemalloc

lian@ubuntu:~/share/9.1-kvstore$ ./test-redis/testcase 192.168.10.129 8888 3
average qps:892493
ALL TESTS PASSED.

测试2持久化

测试条件:

  1. 启用持久化。
  2. 不启用主从同步。
  3. pipline
    1. RSET 100w 条, p:i v:i -> +OK
    2. RGET 100w 条, p:i -> +v:i
    3. RDEL 100w 条。 p:i -> +OK
  4. 本机发送请求。
lian@ubuntu:~/share/9.1-kvstore$ ./test-redis/testcase 192.168.10.129 8888 4
average qps:870227
ALL TESTS PASSED.

测试3内存

malloc

VIRT    58504 
RES     4604

插入 20w 删除 10w重复 10 次,共计插入 200w 删除 100w。
BATCH (N=9000000) --> time_used=12897 ms, qps=1395673

VIRT    489M
RES     430M

插入 10w 删除 20w重复 10 次,共计插入 100w 删除 200w。
BATCH (N=9000000) --> time_used=10033 ms, qps=1794079

VIRT    208M
RES     155M

alt text alt text alt text

jemalloc

VIRT    69376 
RES     5408

插入 20w 删除 10w重复 30 次,共计插入 600w 删除 300w。
BATCH (N=9000000) --> time_used=9436 ms, qps=1907587

VIRT    356M
RES     294M

插入 10w 删除 20w重复 30 次,共计插入 300w 删除 600w。
BATCH (N=9000000) --> time_used=9353 ms, qps=1924516

VIRT    356M
RES     119M

alt text alt text alt text

mypool

VIRT    58504 
RES     4636

插入 20w 删除 10w重复 30 次,共计插入 600w 删除 300w。
BATCH (N=3000000) --> time_used=3184 ms, qps=1884422

VIRT    625M
RES     572M

插入 10w 删除 20w重复 10 次,共计插入 100w 删除 200w。
BATCH (N=3000000) --> time_used=3022 ms, qps=1985440

VIRT    122M
RES     71492

alt text alt text alt text

测试4主从同步

测试条件:

  1. 不启用持久化。
  2. 启用主从同步。
  3. pipline
    1. RSET 100w 条, p:i v:i -> +OK
    2. RGET 100w 条, p:i -> +v:i
    3. RDEL 100w 条。 p:i -> +OK
  4. 本机发送请求。
lian@ubuntu:~/share/9.1-kvstore$ ./test-redis/testcase 192.168.10.129 8888 4
Connected to 192.168.10.129:8888
BATCH (N=3000000) --> time_used=3702 ms, qps=810372
BATCH (N=3000000) --> time_used=3804 ms, qps=788643
BATCH (N=3000000) --> time_used=4076 ms, qps=736015
BATCH (N=3000000) --> time_used=3840 ms, qps=781250
BATCH (N=3000000) --> time_used=3824 ms, qps=784518
average qps:780159
ALL TESTS PASSED.
lian@ubuntu:~/share/9.1-kvstore$ ./test-redis/testcase 192.168.10.129 8888 4
Connected to 192.168.10.129:8888
BATCH (N=3000000) --> time_used=3958 ms, qps=757958
BATCH (N=3000000) --> time_used=4043 ms, qps=742023
BATCH (N=3000000) --> time_used=3729 ms, qps=804505
BATCH (N=3000000) --> time_used=3989 ms, qps=752068
BATCH (N=3000000) --> time_used=3603 ms, qps=832639
average qps:777838
ALL TESTS PASSED.

面试题

  1. 为什么会实现kvstore使用场景在哪里
  2. reactor, ntyco, io_uring的三种网络模型的性能差异
  3. 多线程的kvstore该如何改进
  4. 私有协议如何设计会更加安全可靠?
  5. 协议改进以后,对已有的代码有哪些改变?
  6. kv引擎实现了哪些
  7. 每个kv引擎的使用场景以及性能差异
  8. 测试用例如何实现并且保证代码覆盖率超过90%
  9. 网络并发量如何qps如何
  10. 能够跟哪些系统交互使用?

项目收获

reactor网络模型用户态网络缓冲区的写法。

特殊字符串支持的引擎层数据结构设计,支持\0作为键值存储。

  1. 长度前缀 + 内容的 binary-safe 字符串表示,支持 \0 作为普通字符。

实现RESP协议的服务端协议解析。

  1. 解析流程:
    1. 内核 拷贝到 用户态缓冲区
    2. 用户态缓冲区 就地解析 bulk-string 为执行引擎可以看得懂的结构体
    3. 执行引擎 拷贝 结构体的内容,插入到底层存储结构中
    4. 循环解析直到 用户态缓冲区为空
  2. 实现了 pipeline 支持:每次从 buffer 读到一个完整命令就交给应用层处理,应用层返回已消费字节数。如果 buffer 里有半包,连接层保留下次继续解析。

快照异步创建。

  1. 使用fork的Copy On Write机制实现的异步快照创建不会受到后续更新请求的影响

实现SPSC/SPMC专门uring线程实现异步的增量、全量落盘操作。

  1. 熟悉了SPSC无锁队列的实现方案。
    1. 无锁、cache friendly
  2. 流程:
    1. 生产者组装task丢给SPSC。
    2. 消费者从中取出然后执行置入destroy_queue触发eventfd。
    3. 生产者释放destroy_queue。
  3. 解决的问题:
    1. iouring 由于 cqe 接收不及时导致的 task 丢失无法释放。
      1. 通过背压解决设置inflight上限。
    2. 背压后,生产者速度 > 消费者速度ringbuffer 满导致只能阻塞主线程或降低速度。
      1. 通过修改SPSC为SPMC构建落盘线程组实现当task_queue满触发扩容线程组。
      2. 每个落盘线程私有一个SPSC减少竞争。
      3. 简易工作负载生产者线程随机选取两个uring线程选取任务少的push。
    3. 采用读写段采用轮询方案导致乒乓现象,性能下降。
      1. MESI协议定义了缓存行的四种状态Modified表示独占且已修改Exclusive表示独占且未修改Shared表示多核共享只读Invalid表示缓存行无效。
        • 关键点: 但从S状态读没有什么开销从I状态读则需要向其他CPU申请。
        • 关键点: 从E/M状态写也没有什么开销从S状态写则需要广播invalidate并且等待ACK(50-100时钟周期)。
        • 原子操作由于内存序的顺序性和可见性语义也有额外开销刷新invalidate queue、阻止指令重排
      2. 短临界区:分层退避。自旋-让出CPU时间片-较长时间休眠。允许生产者在期间无争用的写入一批数据,然后统一读。
      3. 更通用的情况事件驱动。用futex替代轮询。Fast Userspace Mutex。
        1. 消费者: 调用syscall(SYS_futex, &futex_word, FUTEX_WAIT, old_val, &timeout, NULL, 0) , 如果futex_word仍等于old_val线程进入内核等待队列真正睡眠不占用CPU。
        2. 生产者: 调用syscall(SYS_futex, &futex_word, FUTEX_WAKE, 1, NULL, NULL, 0) 唤醒一个等待线程。
  4. 优化:生产者速度 > 消费者速度则写端不需要关注读指针或者极少关注读指针缓冲区大小的1/2次写入才考虑一次。

基于BinLog上OffSet的主从同步(已有数据+实时数据)设计。

  1. 初始化阶段:
    1. master 持续将收到的更新请求+seq_id 落盘到本地 binlog 中。
    2. slave 向 master 发起连接并且发送本地binlog中最大的seq_id 为 slave_seq_id。
  2. 执行阶段:
    1. master启动一个独立同步线程与 slave 构建连接。同步线程有两个阶段:
      1. slave_seq_id < local_min_seq_idmaster通过fork创建内存快照发送且发送同时刻的local_max_seq_id。
      2. slave_seq_id < local_max_seq_id通过自实现同步协议批量发送binlog的seq并且回包new_slave_seq_id。
      3. slave_seq_id == local_max_seq_id线程休眠。
    2. master收到新的请求的时候会通过条件变量唤醒同步线程。

基于ebpf的实时数据同步设计。

基准性能Kvstore QPS ~90w无持久化/同步)。

ebpf对程序的影响要考虑如下方面

  1. eBPF 探针的触发频率(上下文切换)

  2. 数据拷贝方式

用户态探针 (Uprobe) 的开销

  1. 逐条命令探测。QPS大概在 25w左右。原因Uprobe 基于断点指令int3实现用户态 → 异常 → 内核 → eBPF → 返回用户态,高频触发会导致 CPU 流水线停顿严重。
  2. 批处理探测。QPS大概在85w左右。大幅减少了上下文切换次数平摊了中断开销。

内核态探针 (Tracepoint/Kprobe) 的开销

  1. Tracepoint (sys_exit_recvfrom)等
    1. 纯探测QPS ~85w。
    2. 带数据拷贝 (read_user)QPS 降至 ~70w。原因bpf_probe_read_user 是一个非常重的 helper开销远大于一次 memcpy。
  2. Kprobe (tcp_rcv_established)等
    1. 纯探测QPS ~86w。
    2. 带数据拷贝 (read_kernel)QPS ~83w。原因:比bpf_probe_read_user轻得多。
    3. 问题:此时数据位于 TCP 协议栈底层可能存在分片Fragment、乱序或非线性存储Paged SKB需要处理复杂的数据重组逻辑。

工作流程:

  1. 内核态捕获
    1. eBPF 程序挂载于内核网络路径( TC 或 TCP 层),拦截流量。
    2. 使用 bpf_probe_read_kernel 或 bpf_skb_load_bytes 高效提取数据载荷。
    3. 通过 bpf_ringbuf_submit 将数据写入环形缓冲区。
  2. 用户态转发
    1. 独立进程消费 RingBuffer。
    2. 将数据暂存入本地队列,平滑突发流量,防止 RingBuffer 溢出导致的数据丢失。
  3. 状态机控制
    1. SYNC 阶段:探测到 __ssync识别 Slave 连接信息IP/Port标记状态为“同步中”。
    2. READY 阶段:探测到 __sready标志全量同步完成。
    3. 实时转发Agent 启动独立线程,消耗 Pending Queue将增量数据通过标准 TCP 发送给 Slave。
TC 与 XDP

网络栈

-> [ 网卡 (NIC) ] 
-> [ XDP (eXpress Data Path) ]  <--- xdp 处理原始帧 no skb
-> [ sk_buff 分配 ]
-> [ TC (Traffic Control) Ingress ]  | tc 可操作 on skb
-> [ Netfilter (PREROUTING) ]  
-> [ IP  协议栈 ]  | ip_rcv -> ip_local_deliver
-> [ TCP 协议栈 ]  | tcp_v4_rcv -> tcp_rcv_established -> tcp_data_queue 
producer skb -> sk->sk_receive_queue
____________________________________________________________________
consumer sk->sk_receive_queue
-> [ Socket Layer ]	 | tcp_recvmsg 拷贝到内存
-> [ Syscall Interface ]  | sys_exit_recvfrom
-> [ 用户态应用 (Kvstore) ]

XDP 是网卡驱动层的探点操作系统刚刚收到数据包DMA读入ringbufferCRC校验还没有分配sk_buffer。数据形态是裸的以太网帧

TC 是在 sk_buff 分配之后IP 协议栈处理之前的探测点。数据形态是__sk_buffer。

TCP协议栈是分界点

内存泄露探测功能,实现热插拔。

方案1 基于bpf

  1. 通过预定义宏__FILE__等封装一个内存分配宏定义向真正的分配函数传递代码位置等信息。
  2. 通过bpf探测内存分配的地址、大小、文件、代码位置、函数等信息并且记录。
  3. 通过bpf探测内存释放的指针信息然后释放。
  4. 打印最终剩余的内存地址。
  5. 缺点bpf 探测 malloc 等对性能的影响非常的大。

方案2 基于全局变量启用的代码内置探测工具,在网络层接收启用/关闭探测工具的请求。

  1. 分配时打开一个文件,关闭时删除。
  2. 分配时使用kv保存删除时删除k。

都对性能和内存有很大的影响,不建议长时间启用。

实现支持分配可变长度内存块的内存池。

  1. 熟悉了glibc 的 ptmalloc的底层操作。默认阈值约 128KB且会根据分配行为动态调整。

    1. '<= 默认阈值 通过 brk/sbrk 扩展连续堆,堆里维护了 chunk 结构。
    2. '> 默认阈值 的块用 mmap 独立申请,释放时 munmap。
    3. 每个 chunk 头部存大小信息(通常 8~16 字节),用户拿到的是 chunk + 头部后的地址。
    4. 空闲 chunk 按大小分到多个 bintcache、fastbin、small bin、large bin 等fastbin 和 tcache 为了速度不合并相邻空闲块。
    5. 缺点:
      1. 分配路径有较多分支判断和链表遍历,不是严格 O(1)。
      2. 小块故意不合并fastbin/tcache会导致外部碎片。
      3. 长期运行内存利用率下降。
  2. 内存池实现:

    1. 内存池有多个桶,桶中存储固定大小的块。分配/释放均为 O(1)
    2. 以 huge page 为单位向系统申请内存并切分为固定块。
    3. 当页内块全部释放时整页归还系统,显著降低长期碎片。
    4. 通过地址对齐 O(1) 反推出页头元信息(所属桶、空闲计数)。
    5. malloc通常向上对齐桶对应的存储大小可以根据不同系统设定减少内部碎片。
    6. 使用 bitmap + freelist 防 double free 且无额外查找开销。
    7. 每线程独立内存池相对malloc更少的锁竞争。
    8. 大块分配自动退化为 malloc 处理。

    相比 ptmalloc该设计消除了外部碎片降低了系统调用次数并在多线程场景下显著提升分配性能与内存利用率。

Description
No description provided
Readme 28 MiB
Languages
C 87.1%
Shell 3.6%
Makefile 3.3%
CMake 2.6%
Lua 2.5%
Other 0.9%