ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

【vemory】增量持久化方案设计

【vemory】增量持久化方案设计 一、背景vemory 是我自研的一个高性能 KV 存储项目本系列博客谈及的源码主要是是纯 C 实现的极致优化版。内存数据库的持久化KV 存储是一种内存数据库正如 Redis 这类 NoSQL 数据库一样其高性能得益于内存的访问速度那么如何在最小的性能损失下将数据引擎中的数据从内存中保存到磁盘上就是这类数据库必须直面的一个问题。博客内容本博客包含了 vemory 项目增量持久化Append Only FileAOF部分的设计思路、技术权衡、难点剖析以及部分的 C 源码讲解。性能参考实现AOFECHOSETGETVemory关闭134971206211700Vemory开启135131155411715Redis关闭121211133111477Redis开启12258968611762二、增量持久化什么是增量持久化增量持久化 AOF——所谓增量即用户输入的变更数据的指令如SETMODDEL等增量持久化即对变更数据的指令的持久化所谓变更数据那也就意味着不包含 PING、SAVE 这类指令最重要的是——AOF 不包含 GET这种持久化是日志式的就像流水账一样记录的是变更。2.1.功能入口与设计AOF 功能入口本身并不复杂将序列化格式的数据反序列化vemory 采用了 RESP 格式之后拿到具体命令的字符串 tokenscmdkeyvalue以 cmd 字符串进行 switch 分发在 SET/DEL/MOD 这类变更指令的分支挂 AOF 的入口参数就是 tokens。设计思路入口函数kvs_aof_append()总体的设计是将 tokens 序列化后写入 .aof 文件并进行刷盘但是如果在主线程中写 .aof 文件又刷盘那么这种阻塞式的设计将给 vemory 的吞吐量带来巨大灾难。所以我们采用了后台刷盘线程的设计避免阻塞主线程。技术权衡我是这样分析的首先我们的网络层采用了单主线程的设计即进程中会变更数据的线程只有一个刷盘线程也只有一个这显然是一个SPSC模型的问题所以可以大胆地采用无锁设计某些应避免竞态的地方使用原子操作来同步这么一来可以避免一切锁导竞争致的开销。RingBuffer 设计主线程找到 AOF 入口函数kvs_aof_append()然后函数内部只考虑将序列化后的数据调buffer_add()抛给无锁队列 RingBuffer抛出后立刻走。在 RingBuffer 的另一端刷盘线程不断从队列中取出数据并按照配置好的 always、everysec 两种策略执行刷盘。说明fflush(fp)后数据此时可能仍在内核的 page cache 中并不代表已经落盘。fsync()才是请求内核将对应文件的修改同步到持久化存储但设备、文件系统和硬件缓存的具体持久化保证仍是更底层的话题。//刷盘线程void*kvs_aof_flush_loop(void*arg){buffer_t*bufferarg;FILE*fpfopen(AOF_FILE,ab);chardata[AOF_BUFFER_SIZE];structtimespeclast_sync{0,0};// 刷盘线程持续消费 ringbuf进程退出前会先排空队列。if(!fp){atomic_store_explicit(g_aof_worker_failed,true,memory_order_release);// ... 打印错误日志 ...returnNULL;}while(1){// loopuint32_tavailablebuffer_len(buffer);if(available0){intlenbuffer_remove(buffer,data,availablesizeof(data)?sizeof(data):available);if(len0){if(fwrite(data,1,(size_t)len,fp)!(size_t)len){//... 打印错误日志 ...}fflush(fp);// always 每批同步everysec 按单调时钟最多每秒同步一次。bool should_syncg_persist_config.aof.fsync_policyKVS_AOF_FSYNC_ALWAYS;// ...// 省略一些通过时钟进行频率控制的逻辑// ..if(should_syncfsync(fileno(fp))!0){// ... 打印错误日志 ...}}}elseif(!atomic_load_explicit(g_aof_stopping,memory_order_acquire)){sched_yield();}if(atomic_load_explicit(g_aof_stopping,memory_order_acquire)buffer_len(buffer)0)break;}fflush(fp);fsync(fileno(fp));fclose(fp);returnNULL;}2.2. AOF 加载当然了AOF 有落盘就有加载加载这部分主要的思路就是打开 .aof 文件到内存然后读取日志由于 .aof 里存放的本来就是可被解析的序列化格式的更改命令重放过程可以直接复用协议解析反序列化的代码。三、难点剖析优雅关闭按下 Ctrl-C关闭服务器进程发送 SIGINT 信号如果没有优雅退出设计进程里的主线程、刷盘线程会被同时关闭进程直接退出如果这时候恰好在进行 AOF 将导致 RingBuffer 中残留数据丢失的问题这就是为什么我们要做排空机制那么在主线程结束数据推送后刷盘线程不能同时被杀死需要等待 RingBuffer 内的数据排空后再释放 RingBuffer、关闭刷盘线程理解主线程关闭和刷盘线程关闭之间的时间差是理解优雅退出的关键。信号处理按下 Ctrl-C 主动关闭发送 SIGINT此时主线程 Reactor 在关闭之前阻塞在 epoll_wait 当中关闭时无法执行 aof 的关闭函数shutdown也无法 return 解决的方案是捕获sigint/sigterm的时候通过信号处理器将 shutdown 置位然后利用一个定时器来定时检测 shutdown 标志位g_aof_shutdown_requested通过定时器回调来正确执行 shutdown。// 唯一一个响应sigint信号的函数设置shutdown标志staticvoidkvs_aof_signal_handler(intsignal_number){(void)signal_number;g_aof_shutdown_requested1;//将shutsdown标志位置位}// 安装信号处理器staticvoidkvs_aof_install_signal_handlers(void){// ...// 省略具体实现// ...}定时器回调优雅关闭我们通过信号处理器来进行 shutdown 标志位的置位用定时器轮询的方式检测 标志位定时器回调 shutdown进行优雅关闭。
返回列表