Files
ldb/README.md
2026-03-07 07:29:32 +00:00

386 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 9.1 Kvstore
## 环境安装与编译
```shell
# 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
```
## REDIS 对比测试
### 数据口径2026-03-07 大 key 5轮复测
- 参数:`requests=1000000 pipeline=128 keyspace=1000000 value-size=256`
- 轮次:每个策略 `5` 轮,按场景剔除 `1` 个异常轮次后取 `4` 轮均值
- kvstore 源数据:`test-redis/results/hash_bench_fair_summary_20260307_062549.csv`
- redis 源数据:`test-redis/results/redis_bigkey_summary_20260307_063158.csv`
- 去异常结果:
- `test-redis/results/hash_bench_fair_trimmed_20260307_062549.csv`
- `test-redis/results/redis_bigkey_trimmed_20260307_063158.csv`
### kvstore 协议开销基线nopersist去异常后
| 策略 | set 均值QPS | set 均值us/op | get 均值QPS | get 均值us/op |
|---|---:|---:|---:|---:|
| nopersist | 150084.25 | 6.67 | 163760.25 | 6.11 |
| persist_no | 140206.50 | 7.13 | 164554.25 | 6.08 |
| persist_everysec | 133105.75 | 7.52 | 163358.25 | 6.12 |
| 相对 nopersist 的开销 | set QPS 变化 | set us/op 变化 | get QPS 变化 | get us/op 变化 |
|---|---:|---:|---:|---:|
| persist_no | -6.58% | +6.90% | +0.48% | -0.49% |
| persist_everysec | -11.31% | +12.74% | -0.25% | +0.16% |
### Redis 协议开销基线none去异常后
| 策略 | set 均值QPS | set 均值us/op | get 均值QPS | get 均值us/op |
|---|---:|---:|---:|---:|
| none | 207470.00 | 4.82 | 217642.50 | 4.59 |
| aof_no | 143399.50 | 6.98 | 214023.50 | 4.68 |
| aof_everysec | 141893.00 | 7.05 | 208713.75 | 4.79 |
| 相对 none 的开销 | set QPS 变化 | set us/op 变化 | get QPS 变化 | get us/op 变化 |
|---|---:|---:|---:|---:|
| aof_no | -30.88% | +44.81% | -1.66% | +1.96% |
| aof_everysec | -31.61% | +46.27% | -4.10% | +4.36% |
结论:本轮数据下,持久化开销仍主要体现在 `set``get` 相对更稳Redis 的 AOF 写路径开销显著高于无持久化。
## 调用开销
### gprof Flat ProfileTop 12按 self time
| 排名 | 函数 | self time % | self seconds | calls |
|---:|---|---:|---:|---:|
| 1 | `rbtree_node_get_key` | 58.34 | 0.56 | 103209091 |
| 2 | `rbtree_search` | 7.29 | 0.07 | 1874362 |
| 3 | `kvs_keycmp` | 5.73 | 0.06 | 74287566 |
| 4 | `ascii_casecmp` | 3.13 | 0.03 | 21490996 |
| 5 | `task_init` | 3.13 | 0.03 | 1397974 |
| 6 | `mp_page_create` | 3.13 | 0.03 | 23556 |
| 7 | `need` | 2.08 | 0.02 | 14042759 |
| 8 | `parse_i64` | 2.08 | 0.02 | 7029783 |
| 9 | `mp_page_alloc` | 2.08 | 0.02 | 5122487 |
| 10 | `rbtree_node_size` | 2.08 | 0.02 | 1860739 |
| 11 | `submit_write` | 2.08 | 0.02 | 1394599 |
| 12 | `rbtree_insert` | 2.08 | 0.02 | 926531 |
## 其他测试
### 测试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
```bash
lian@ubuntu:~/share/9.1-kvstore$ ./test-redis/testcase 192.168.10.129 8888 3
average qps:880462
ALL TESTS PASSED.
```
#### 内存分配: 自实现内存池
```bash
lian@ubuntu:~/share/9.1-kvstore$ ./test-redis/testcase 192.168.10.129 8888 3
average qps:942837
ALL TESTS PASSED.
```
#### 内存分配jemalloc
```shell
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
5. 本机发送请求。
```shell
lian@ubuntu:~/share/9.1-kvstore$ ./test-redis/testcase 192.168.10.129 8888 4
average qps:870227
ALL TESTS PASSED.
```
### 测试3内存
#### malloc
```shell
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](https://disk.0voice.com/p/wl)
![alt text](https://disk.0voice.com/p/wm)
![alt text](https://disk.0voice.com/p/wO)
#### jemalloc
```shell
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](https://disk.0voice.com/p/wl)
![alt text](https://disk.0voice.com/p/wP)
![alt text](https://disk.0voice.com/p/wQ)
#### mypool
```shell
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](https://disk.0voice.com/p/wR)
![alt text](https://disk.0voice.com/p/wn)
![alt text](https://disk.0voice.com/p/wo)
### 测试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
5. 本机发送请求。
```shell
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.
```
## 项目收获
#### 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该设计消除了外部碎片降低了系统调用次数并在多线程场景下显著提升分配性能与内存利用率。