一、背景
本文重点是实时数据的主从同步,以及使用eBPF做旁路转发的方案。
功能实现
仿照Redis实现的主从同步功能
Redis的主从同步分为两个阶段,第一个阶段:从机启动时,拉取主机的已有的数据,拉取完毕后从机正式上线。第二个阶段是从机上线后,主机实时将新的命令传播给从机,实现实时数据的变更。总结为:
- 已有数据同步
- 实时数据同步
阶段一:已有数据同步
- 从机配置为slave,并且指定主机的IP和端口,从机启动时向主机发送SYNC指令
- 主机收到SYNC指令,将从机添加到从机列表当中后,打开积压缓冲区,触发BGSAVE全量保存数据到RDB文件中
- 主机将保存的RDB文件发送到从机
- 主机将积压缓冲区的数据发送到从机
阶段二:实时数据同步
- 主机收到来自客户端的新指令
- 主机解析RESP格式后后将变更类的指令(不包括GET等指令)重新编码,转发到从机
二、send网络转发实现
指令解析后,再变更类指令的分发分支上调用feed_slave
voidkvs_replication_feed_slaves(constchar*cmd,constchar*key,size_tklen,constchar*value,size_tvlen){if(g_repl.role!=KVS_ROLE_MASTER||g_repl.slave_count==0)return;charbuf[REPL_CMD_BUF];intlen=0,encoded=0;inthas_online=0,has_syncing=0;// 统计从库状态for(inti=0;i<g_repl.slave_count;i++){if(g_slaves[i].state==SLAVE_ONLINE)has_online=1;elsehas_syncing=1;}// 有同步中的从库 → 写 Backlogif(has_syncing){len=resp_encode_cmd(...);encoded=1;ensure_backlog();buffer_add(g_backlog,buf,len);// 满了则踢掉同步中的从库// ...}// 有在线从库 → 直接推送if(has_online){if(!encoded)len=resp_encode_cmd(...);for(inti=0;i<g_repl.slave_count;i++){if(g_slaves[i].state==SLAVE_ONLINE){reactor_append(...);// 失败则 drop_slave(i--);}}}}待网络转发的数据填进reactor的wbuffer里,等待事件循环。
intreactor_append(intfd,constvoid*data,uint32_tlen){structconn*c=reactor_conn(fd);if(!c||!c->wbuffer||!data||len==0)return-1;if(buffer_add(c->wbuffer,data,len)!=0)return-1;set_event(fd,EPOLLIN|EPOLLOUT,0);return0;}可以看到,这种方案面临着比较重的开销:
- 整个过程中有两次编码
- 若干次拷贝
- 占用主机send的吞吐。
三、提出eBPF旁路转发的方案
eBPF 主从同步开启时,将hook点选择KV存储主机解析之后(RESP格式)的一个指定函数,此时hook函数的参数中已经获取了cmd、key、value以及klen、vlen等数据。
- 主机网络接收来自客户端的命令
- 解析完毕之后,拷贝到专门的hook函数(声明非内联,非优化,以便eBPF捕获)
- eBPF内核态程序捕获cmd,key,value等数据,抛入ringbuf(BPF map)中
- 用户态程序轮询获取ringbuf中的数据,并且编码
- send转发到从机
我们分析这条路径,不难发现,eBPF的思路是利用了旁路捕获并转发的思路,将编码、多次拷贝、send转发的开销旁路到了eBPF用户态进程中。KV存储中只需要维护一个类似这样的hook函数
__attribute__((noinline))voidkvs_eBPF_propagation_hook(constchar*cmd,constchar*key,size_tklen,constchar*value,size_tvlen){(void)cmd;(void)key;(void)klen;(void)value;(void)vlen;asmvolatile("");}eBPF的实现比较复杂,涉及libbpf-bootstrap程序的改造,vertifier下编程容易踩的坑等,将来我写新的博客专门讲解。