include/persist.h
定义文件格式、结构体、对外接口。
真正的持久化实现,包含 mmap、io_uring、文件序列化、恢复逻辑。
src\\kvstore.c
把持久化接到命令处理流程里,负责启动加载、SAVE、写命令追加 AOF。
src/ntyco.c
网络层,只负责收包、回调kvs protocol。
array.c rbtree.c hash.c skiptable.c
各个引擎的内存实现
在持久化落盘的时候使用io_ruing的方式。在加载日志的时候使用mmap的方式
整体架构:
客户端
cpp
客户端
|
v
ntyco / reactor / proactor
|
v
kvs_protocol()
|
v
kvs_filter_protocol()
|
+--> SET / MOD / DEL 先改内存引擎
| |
| +--> persist_append_increment() 追加 AOF
|
+--> SAVE
|
+--> persist_save_all() 写全量快照
+--> persist_clear_increment() 清空旧 AOF
kvstore.h源代码:
#ifndef __PERSIST_H__
#define __PERSIST_H__
/**
* 持久化模块头文件
*
* 这一层只负责定义规则, 不负责真正把数据写进文件
*
* 文件应该长什么样
* 对外提供哪些函数
* 每条记录有哪些字段
*/
#include <stdint.h>
/*
* 这里不能 include "kvstore.h"。
*
* 原因:
* kvstore.h 会 include persist.h;
* 如果 persist.h 又 include kvstore.h,就会形成循环包含。
*
* 这里的函数参数只用到了这些结构体的"指针",
* 所以我们不需要知道结构体里面有哪些成员,
* 只要提前告诉编译器"这些类型以后会存在"就够了。
*/
typedef struct kvs_array_s kvs_array_t;
typedef struct _rbtree kvs_rbtree_t;
typedef struct hashtable_s kvs_hash_t;
typedef struct kvs_skiplist_s kvs_skiplist_t;
/**
* 这个枚举用来标记"当前这条数据来自哪个容器"。
* 既可以用用到文件头
* 也可以用到单条命令的头
* 用掩码,因为后面可以同时保存多个容器
*/
typedef enum persist_engine_e{
PERSIST_ENGINE_ARRAY = 1u << 0,
PERSIST_ENGINE_RBTREE = 1u << 1,
PERSIST_ENGINE_HASH = 1u << 2,
PERSIST_ENGINE_SKIPLIST = 1u << 3,
PERSIST_ENGINE_ALL = PERSIST_ENGINE_ARRAY | PERSIST_ENGINE_RBTREE | PERSIST_ENGINE_HASH | PERSIST_ENGINE_SKIPLIST,
}persist_engine_t;
/*
* 增量持久化保存的不是"最终数据",而是"发生过什么写操作"。
*
* SET:新增一条 key/value
* MOD:修改一条 key/value
* DEL:删除一条 key
*/
typedef enum persist_op_e {
PERSIST_OP_SET = 1,
PERSIST_OP_MOD = 2,
PERSIST_OP_DEL = 3,
} persist_op_t;
/*文件头里的"魔数", 用来识别这是不是持久化文件*/
#define PERSIST_MAGIC "KVS1"
#define PERSIST_MAGIC_LEN 4
/*版本号,方便以后升级文件的格式*/
#define PERSIST_VERSION 1
/**
* 文件头
* 一份持久化文件,最前面先写这个结构
*/
typedef struct persist_file_header_s{
char magic[PERSIST_MAGIC_LEN];
uint32_t version;
uint32_t engine_mask;
uint32_t record_count;
}persist_file_header_t;
/**
* 每一条键值对都先写这个"记录头"
*/
typedef struct persist_record_header_s{
uint32_t engine;
uint32_t key_len;
uint32_t value_len;
}persist_record_header_t;
/*
* 增量日志里每一条操作的头。
*
* 增量日志是一条一条追加的:
* [increment_header][key bytes][value bytes]
*
* DEL 操作没有 value,所以 value_len 可以是 0。
*/
#define PERSIST_INCR_MAGIC "AOF1"
#define PERSIST_INCR_MAGIC_LEN 4
#define PERSIST_INCR_VERSION 1
typedef struct persist_increment_header_s {
char magic[PERSIST_INCR_MAGIC_LEN];
uint32_t version;
uint32_t op;
uint32_t engine;
uint32_t key_len;
uint32_t value_len;
} persist_increment_header_t;
/*对外的接口:保存所有数据、加载所有数据*/
int persist_save_all(const char *path);
int persist_load_all(const char *path);
/* 增量持久化接口 */
int persist_append_increment(const char *path, persist_op_t op, persist_engine_t engine, const char *key, const char *value);
int persist_load_increment(const char *path);
int persist_clear_increment(const char *path);
/*各引擎保存*/
int persist_save_array(const char *path, const kvs_array_t *inst);
int persist_save_rbtree(const char *path, const kvs_rbtree_t *inst);
int persist_save_hash(const char *path, const kvs_hash_t *inst);
int persist_save_skiplist(const char *path, const kvs_skiplist_t *inst);
/*引擎加载*/
int persist_load_array(const char *path, kvs_array_t *inst);
int persist_load_rbtree(const char *path, kvs_rbtree_t *inst);
int persist_load_hash(const char *path, kvs_hash_t *inst);
int persist_load_skiplist(const char *path, kvs_skiplist_t *inst);
#endif
启动回复流程:
cpp
main()
|
+--> init_kvengine()
|
+--> persist_load_all("kvstore.data")
| |
| +--> mmap 读快照
| +--> 清空旧引擎
| +--> 一条条恢复到内存
|
+--> persist_load_increment("kvstore.aof")
|
+--> mmap 读 AOF
+--> 一条条回放写操作
文件格式:
kvstore.data 不是简单文本,是二进制结构:
cpp
[文件头]
magic
version
engine_mask
record_count
[记录1]
engine
key_len
value_len
key bytes
value bytes
[记录2]
...
kvstore.aof 也是二进制,但记录的是操作:
[增量头]
magic
version
op
engine
key_len
value_len
[key bytes]
[value bytes]
关键函数:
cpp
网络入口
- ntyco_start(port, kvs_protocol)
- kvs_protocol() 把一行命令交给协议解析
命令分发
- kvs_process_one_cmd()
- kvs_filter_protocol()
写内存
- kvs_array_set / mod / del
- kvs_rbtree_set / mod / del
- kvs_hash_set / mod / del
- kvs_skiplist_set / mod / del
追加增量日志
- kvs_append_increment_by_cmd()
- persist_append_increment()
保存全量快照
- persist_save_all()
- persist_count_*()
- persist_dump_*_to_buf()
- persist_write_fd_by_uring()
恢复
- persist_load_all()
- persist_load_snapshot_from_mem()
- persist_reinit_all_globals()
- persist_apply_snapshot_record()
- persist_load_increment()
- persist_load_increment_from_mem()
- persist_replay_increment_op()
mmap 是用法:
mmap 只用在"读文件"阶段,不是单独一个大模块。
cpp
map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
ret = persist_load_snapshot_from_mem((const char *)map, st.st_size);
munmap(map, st.st_size);
它的意义是:
- 不用
fread一段一段拷贝 - 直接把文件映射成内存
- 解析函数用指针
p一直往后走 - 读快照和读 AOF 都是这个套路
可以理解成:
"把磁盘文件变成一块只读内存,在按字节指针解析"
io_uring是怎么用的:
io_uring 只用在 persist_save_all() 的落盘阶段。
流程是:
- 先把快照内容拼进内存缓冲区
persist_buf_t - 打开
kvstore.data.tmp - 调
persist_write_fd_by_uring() - 分块写入,单块最多 1MB
- 最后
fsync rename(tmp, path)原子替换正式快照
也就是说:
内存缓冲区 -> io_uring 写 tmp 文件 -> fsync -> rename 成正式 data 文件。
这样设计的好处是:
- 写快照时更稳
- 避免半写坏文件直接变正式文件
rename之后要么是旧文件,要么是新文件,不会中间态暴露给加载器
注意:
AOF 追加没有用 io_uring ,它现在是 fopen("ab") + fwrite + fflush + fclose。
这是刻意简化的设计:快照是大块写,AOF 是一条条小写,处理方式不一样。
全量持久化涉及到的代码:
当输入 SAVE的时候,会触发全量持久化:
cpp
//触发全量持久化
int kvs_filter_protocol(kvs_command_t *req, kvs_response_t *out, int from_replica){
if (cmd == KVS_CMD_SAVE) {
ret = persist_save_all(persist_file);
if (ret == 0) {
ret = persist_clear_increment(persist_incr_file);
}
if (ret == 0) {
return kvs_reply_status(out, resp_mode, "OK");
}
return kvs_reply_error(out, resp_mode, "save failed");
}
}
int persist_save_all(const char *path)
{
if(!path) return -1;
persist_buf_t buf;
uint32_t array_count = 0;
uint32_t rbtree_count = 0;
uint32_t hash_count = 0;
uint32_t skiplist_count = 0;
uint32_t record_count = 0;
uint32_t engine_mask = 0;
char *tmp_path = NULL;
int fd = -1;
int ret = -1;
memset(&buf, 0, sizeof(buf));
/*
* 先统计所有引擎里一共有多少条数据。
* 这样文件头里的 record_count 就是准确的。
*/
array_count = persist_count_array(&global_array);
rbtree_count = persist_count_rbtree_node(&global_rbtree, global_rbtree.root);
hash_count = persist_count_hash(&global_hash);
skiplist_count = persist_count_skiplist(&global_skiplist);
record_count = array_count + rbtree_count + hash_count + skiplist_count;
if(array_count > 0) engine_mask |= PERSIST_ENGINE_ARRAY;
if(rbtree_count > 0) engine_mask |= PERSIST_ENGINE_RBTREE;
if(hash_count > 0)engine_mask |= PERSIST_ENGINE_HASH;
if(skiplist_count > 0)engine_mask |= PERSIST_ENGINE_SKIPLIST;
if(persist_buf_append_header(&buf, engine_mask, record_count) < 0){
goto done;
}
if(persist_dump_array_to_buf(&buf, &global_array) < 0) goto done;
if(persist_dump_rbtree_to_buf(&buf, &global_rbtree, global_rbtree.root) < 0) goto done;
if(persist_dump_hash_to_buf(&buf, &global_hash) < 0) goto done;
if(persist_dump_skiplist_to_buf(&buf, &global_skiplist) < 0) goto done;
tmp_path = (char *)kvs_malloc(strlen(path) + 5);
if(!tmp_path){
goto done;
}
snprintf(tmp_path, strlen(path) + 5, "%s.tmp", path);
fd = open(tmp_path, O_CREAT | O_TRUNC | O_WRONLY, 0644);
if(fd < 0) goto done;
if(persist_write_fd_by_uring(fd, buf.data, buf.len) < 0) goto done;
close(fd);
fd = -1;
if(rename(tmp_path, path) < 0){
goto done;
}
ret = 0;
done:
if(fd >= 0) close(fd);
if(tmp_path){
if(ret != 0) unlink(tmp_path);
kvs_free(tmp_path);
}
persist_buf_free(&buf);
return ret;
}
static int persist_buf_append_header(persist_buf_t *buf, uint32_t engine_mask, uint32_t record_count){
persist_file_header_t header;
memset(&header, 0, sizeof(header));
memcpy(header.magic, PERSIST_MAGIC, PERSIST_MAGIC_LEN);
header.version = PERSIST_VERSION;
header.engine_mask = engine_mask;
header.record_count = record_count;
return persist_buf_append(buf, &header, sizeof(header));
}
static int persist_buf_append(persist_buf_t *buf, const void *src, size_t len){
if(!buf || !src) return -1;
if(len == 0) return 0;
if(persist_buf_reserve(buf,buf->len + len) < 0){
return -1;
}
memcpy(buf->data + buf->len, src, len);
buf->len += len;
return 0;
}
static int persist_buf_reserve(persist_buf_t *buf, size_t need){
size_t new_cap = 0;
char *new_data = NULL;
if(!buf || need == 0) return -1;
if(buf->cap >= need) return 0;
new_cap = (buf->cap > 0) ? buf->cap : 4096;
while(new_cap < need){
new_cap *= 2;
}
new_data = (char *)kvs_malloc(new_cap);
if(!new_data) return -1;
if(buf->data && buf->len > 0){
memcpy(new_data, buf->data, buf->len);
}
if(buf->data){
kvs_free(buf->data);
}
buf->data = new_data;
buf->cap = new_cap;
return 0;
}
static int persist_write_fd_by_uring(int fd, const char *buf, size_t len){
struct io_uring ring;
size_t offset = 0;
if(io_uring_queue_init(8, &ring, 0) != 0){
return -1;
}
while(offset < len){
size_t chunk = len - offset;
struct io_uring_sqe *sqe = NULL;
struct io_uring_cqe *cqe = NULL;
if(chunk > (1u << 20)){
chunk = (1u << 20);
}
sqe = io_uring_get_sqe(&ring);
if(!sqe){
if(io_uring_submit(&ring) < 0){
io_uring_queue_exit(&ring);
return -1;
}
continue;
}
io_uring_prep_write(sqe, fd, buf + offset, chunk, (off_t)offset);
if(io_uring_submit(&ring) < 0){
io_uring_queue_exit(&ring);
return -1;
}
if(io_uring_wait_cqe(&ring, &cqe) < 0){
io_uring_queue_exit(&ring);
return -1;
}
if(cqe->res <= 0){
io_uring_cqe_seen(&ring, cqe);
io_uring_queue_exit(&ring);
return -1;
}
offset += (size_t)cqe->res;
io_uring_cqe_seen(&ring, cqe);
}
{
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
struct io_uring_cqe *cqe = NULL;
if(!sqe){
io_uring_queue_exit(&ring);
return -1;
}
io_uring_prep_fsync(sqe, fd, 0);
if(io_uring_submit(&ring) < 0){
io_uring_queue_exit(&ring);
return -1;
}
if(io_uring_wait_cqe(&ring, &cqe) < 0){
io_uring_queue_exit(&ring);
return -1;
}
if(cqe->res < 0){
io_uring_cqe_seen(&ring, cqe);
io_uring_queue_exit(&ring);
return -1;
}
io_uring_cqe_seen(&ring, cqe);
}
io_uring_queue_exit(&ring);
return 0;
}
全量持久化加载:
cpp
/*
* 读取全部数据。
*
*/
int persist_load_all(const char *path)
{
if(!path) return -1;
int fd = -1;
struct stat st;
void *map = NULL;
int ret = -1;
fd = open(path, O_RDONLY);
if(fd < 0) return -1;
if(fstat(fd, &st) < 0){
close(fd);
return -1;
}
if(st.st_size == 0){
close(fd);
return 0;
}
map = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if(map == MAP_FAILED){
close(fd);
return -1;
}
ret = persist_load_snapshot_from_mem((const char *)map, (size_t)st.st_size);
munmap(map, (size_t)st.st_size);
close(fd);
return ret;
}
static int persist_load_snapshot_from_mem(const char *base, size_t size){
const char *p = base;
const char *end = base + size;
persist_file_header_t file_header;
if(size < sizeof(file_header)){
return -1;
}
memcpy(&file_header, p, sizeof(file_header));
p += sizeof(file_header);
if(memcmp(file_header.magic, PERSIST_MAGIC, PERSIST_MAGIC_LEN) != 0){
return -1;
}
if(file_header.version != PERSIST_VERSION){
return -1;
}
if(file_header.engine_mask & ~PERSIST_ENGINE_ALL){
return -1;
}
/**
* 先清空,再回复
*/
if(persist_reinit_all_globals() < 0){
return -1;
}
for(uint32_t i = 0; i < file_header.record_count; i++){
persist_record_header_t record_header;
char *key_buf = NULL;
char *value_buf = NULL;
if((size_t)(end - p) < sizeof(record_header)){
return -1;
}
memcpy(&record_header, p, sizeof(record_header));
p += sizeof(record_header);
if((size_t)(end - p) < (size_t)record_header.key_len + (size_t)record_header.value_len){
return -1;
}
key_buf = (char *) kvs_malloc((size_t)record_header.key_len + 1);
value_buf = (char *) kvs_malloc((size_t)record_header.value_len + 1);
if(!key_buf || !value_buf){
if(key_buf) kvs_free(key_buf);
if(value_buf) kvs_free(value_buf);
return -1;
}
memset(key_buf, 0, (size_t)record_header.key_len + 1);
memset(value_buf, 0, (size_t)record_header.value_len + 1);
memcpy(key_buf, p, record_header.key_len);
p += record_header.key_len;
memcpy(value_buf, p, record_header.value_len);
p += record_header.value_len;
key_buf[record_header.key_len] = '\0';
value_buf[record_header.value_len] = '\0';
if (persist_apply_snapshot_record((persist_engine_t)record_header.engine,
key_buf, record_header.key_len,
value_buf, record_header.value_len) < 0) {
kvs_free(key_buf);
kvs_free(value_buf);
return -1;
}
kvs_free(key_buf);
kvs_free(value_buf);
}
return (p == end) ? 0 : -1;
}
增量持久化:
每次执行写操作(set mod delte)操作后,往日志里追加本次的改动。
cpp
int kvs_filter_protocol(kvs_command_t *req, kvs_response_t *out, int from_replica)
{
if (ret == 0 && kvs_is_write_cmd(cmd)) {
if (kvs_append_increment_by_cmd(cmd, key, value) < 0) {
out->len = 0;
return kvs_reply_error(out, resp_mode, "persist failed");
}
if (from_replica) {
kvs_replication_incr_offset();
} else {
kvs_replication_sync_write_to_slave(cmd, key, value);
}
}
}
void kvs_replication_incr_offset(void){
g_repl_offset++;
kvs_save_repl_offset();
printf("incr repl offset:%lld\n", g_repl_offset);
}
static void kvs_save_repl_offset(void){
FILE *fp = fopen(repl_offset_file, "w");
if(!fp) return;
fprintf(fp, "%lld\n", g_repl_offset);
fclose(fp);
}
/*
* 对外的一层同步函数。
*
* kvs_filter_protocol 执行完本地写入以后调用它。
*/
void kvs_replication_sync_write_to_slave(int cmd, char *key, char *value)
{
if (!kvs_replica_enabled()) return;
char plain_cmd[KVS_REPL_CMD_SIZE] = {0};
char replica_cmd[KVS_REPL_CMD_SIZE] = {0};
if(kvs_build_plain_write_cmd(cmd, key, value, plain_cmd, sizeof(plain_cmd)) < 0){
return;
}
/**
* 不管从库现在又没有连上,主库都先把写命令放进backlog
* 这样从库以后可以用PSYNC追数据
*/
kvs_repl_backlog_append(plain_cmd);
if (kvs_build_replica_cmd(cmd, key, value,
replica_cmd, sizeof(replica_cmd)) < 0) {
return;
}
if (kvs_send_cmd_to_slave(replica_cmd) < 0) {
printf("replica sync failed: %s\n", replica_cmd);
}
}
static int kvs_build_plain_write_cmd(int cmd, char *key, char *value, char *out, int out_size){
if(!key || !out || out_size <= 0) return -1;
switch (cmd)
{
case KVS_CMD_DEL:
case KVS_CMD_RDEL:
case KVS_CMD_HDEL:
case KVS_CMD_SDEL:
/**
* 删除命令没有value
*/
return snprintf(out, out_size, "%s %s\r\n", command[cmd], key);
default:
if(value == NULL) return -1;
return snprintf(out, out_size, "%s %s %s\r\n", command[cmd], key, value);
}
}
static void kvs_repl_backlog_append(char *plain_cmd){
if(!plain_cmd) return;
/**
* 新写命令产生一个新的offset。
*/
g_repl_offset++;
int idx = (int)((g_repl_offset - 1) % KVS_REPL_BACKLOG_SIZE);
g_repl_backlog[idx].offset = g_repl_offset;
memset(g_repl_backlog[idx].cmd, 0, KVS_REPL_CMD_SIZE);
strncpy(g_repl_backlog[idx].cmd, plain_cmd, KVS_REPL_CMD_SIZE - 1);
printf("bakclog append offset=%lld cmd=%s", g_repl_offset, plain_cmd);
}
/*
* 把一条写命令拼成文本协议。
*
* 例如:
* SET name Tom
* DEL name
*
* 注意:
* 当前项目的协议本来就不支持 key/value 中带空格,
* 所以这里也先保持一致。
*/
static int kvs_build_replica_cmd(int cmd, char *key, char *value,
char *out, int out_size)
{
char plain_cmd[KVS_REPL_CMD_SIZE] = {0};
if(kvs_build_plain_write_cmd(cmd, key, value, plain_cmd, sizeof(plain_cmd)) < 0){
return -1;
}
/**
* 真正发给从库时,加 REPL前缀
* 从库看到REPL,才会走 kvs_protocol_replica
*/
return snprintf(out, out_size, "REPL %s", plain_cmd);
}
static int kvs_send_cmd_to_slave(char *cmd)
{
if(!cmd) return -1;
/**
* 尝试两次:
* 第一次:复用旧链接,如果是第一次连,就新建连接。
* 如果旧链接已经断了,就关闭它
* 第二次:重新connect 从库,再发送
*/
for(int retry = 0; retry < 2; retry++){
int fd = kvs_connect_slave();
if(fd < 0) return -1;
if(kvs_send_all(fd, cmd, strlen(cmd)) < 0){
kvs_replication_close_slave_conn();
continue;
}
/**
* 等从库返回执行结果
* 这里先不严格判断OK / ERROR,只要能收到响应,
* 救认为这条同步命令已经送到 并且 被从库处理
*/
char buf[128] = {0};
int n = recv(fd, buf, sizeof(buf) - 1, 0);
if(n <= 0){
printf("recv slave ack failed, reconnect next time\n");
kvs_replication_close_slave_conn();
continue;
}
printf("replica sync ok:%s -> %s", cmd, buf);
return 0;
}
return -1;
}
增量持久化加载:
cpp
persist_load_increment(persist_incr_file);
/*
* 加载增量日志。
*
* 启动恢复时的顺序应该是:
* 1. persist_load_all("kvstore.data")
* 先恢复上一次全量快照
*
* 2. persist_load_increment("kvstore.aof")
* 再把快照之后发生过的写操作重新执行一遍
*/
int persist_load_increment(const char *path)
{
if(!path) return -1;
int fd = -1;
struct stat st;
void *map = NULL;
int ret = -1;
fd = open(path, O_RDONLY);
if(fd < 0) return 0;
if(fstat(fd, &st) < 0){
close(fd);
return -1;
}
if(st.st_size == 0){
close(fd);
return 0;
}
map = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if(map == MAP_FAILED){
close(fd);
return -1;
}
ret = persist_load_increment_from_mem((const char *)map, (size_t)st.st_size);
munmap(map, (size_t)st.st_size);
close(fd);
return ret;
}
static int persist_load_increment_from_mem(const char *base, size_t size){
const char *p = base;
const char *end = base + size;
while(p < end){
persist_increment_header_t header;
char *key_buf = NULL;
char *value_buf = NULL;
if((size_t)(end - p) < sizeof(header)) return-1;
memcpy(&header, p, sizeof(header));
p += sizeof(header);
if(memcmp(header.magic, PERSIST_INCR_MAGIC, PERSIST_INCR_MAGIC_LEN) != 0) return -1;
if(header.version != PERSIST_INCR_VERSION) return -1;
if(header.key_len == 0) return -1;
if((header.op == PERSIST_OP_SET || header.op == PERSIST_OP_MOD) && header.value_len == 0) return -1;
if((size_t)(end - p) < (size_t)header.key_len + (size_t)header.value_len) return -1;
key_buf = (char *)kvs_malloc((size_t)header.key_len + 1);
value_buf = (char *)kvs_malloc((size_t)header.value_len + 1);
if(!key_buf || !value_buf){
if(key_buf) kvs_free(key_buf);
if(value_buf) kvs_free(value_buf);
return -1;
}
memset(key_buf, 0, (size_t)header.key_len + 1);
memset(value_buf, 0, (size_t)header.value_len + 1);
memcpy(key_buf, p, header.key_len);
p += header.key_len;
memcpy(value_buf, p, header.value_len);
p += header.value_len;
key_buf[header.key_len] = '\0';
value_buf[header.value_len] = '\0';
if (persist_replay_increment_op((persist_op_t)header.op,
(persist_engine_t)header.engine,
key_buf, value_buf,
header.key_len, header.value_len) < 0)
{
kvs_free(key_buf);
kvs_free(value_buf);
return -1;
}
kvs_free(key_buf);
kvs_free(value_buf);
}
return 0;
}
/*
* 回放一条增量操作。
*
* 加载增量日志时,我们不是直接改文件,
* 而是把日志里记录的 SET/MOD/DEL 重新在内存里执行一遍。
* 增量回放:SET 允许覆盖,MOD 也允许补救式覆盖,像 Redis 一样恢复最终态
*/
static int persist_replay_increment_op(persist_op_t op, persist_engine_t engine,
char *key, char *value,
uint32_t key_len, uint32_t value_len)
{
int ret = -1;
switch (engine) {
case PERSIST_ENGINE_ARRAY:
if (op == PERSIST_OP_SET) {
ret = kvs_array_set(&global_array, key, (int)key_len, value, (int)value_len);
if (ret > 0) ret = kvs_array_mod(&global_array, key, (int)key_len, value, (int)value_len);
} else if (op == PERSIST_OP_MOD) {
ret = kvs_array_mod(&global_array, key, (int)key_len, value, (int)value_len);
if (ret > 0) ret = kvs_array_set(&global_array, key, (int)key_len, value, (int)value_len);
} else if (op == PERSIST_OP_DEL) {
ret = kvs_array_del(&global_array, key, (int)key_len);
if (ret > 0) ret = 0;
}
break;
case PERSIST_ENGINE_RBTREE:
if (op == PERSIST_OP_SET) {
ret = kvs_rbtree_set(&global_rbtree, key, (int)key_len, value, (int)value_len);
if (ret > 0) ret = kvs_rbtree_mod(&global_rbtree, key, (int)key_len, value, (int)value_len);
} else if (op == PERSIST_OP_MOD) {
ret = kvs_rbtree_mod(&global_rbtree, key, (int)key_len, value, (int)value_len);
if (ret > 0) ret = kvs_rbtree_set(&global_rbtree, key, (int)key_len, value, (int)value_len);
} else if (op == PERSIST_OP_DEL) {
ret = kvs_rbtree_del(&global_rbtree, key, (int)key_len);
if (ret > 0) ret = 0;
}
break;
case PERSIST_ENGINE_HASH:
if (op == PERSIST_OP_SET) {
ret = kvs_hash_set(&global_hash, key, (int)key_len, value, (int)value_len);
if (ret > 0) ret = kvs_hash_mod(&global_hash, key, (int)key_len, value, (int)value_len);
} else if (op == PERSIST_OP_MOD) {
ret = kvs_hash_mod(&global_hash, key, (int)key_len, value, (int)value_len);
if (ret > 0) ret = kvs_hash_set(&global_hash, key, (int)key_len, value, (int)value_len);
} else if (op == PERSIST_OP_DEL) {
ret = kvs_hash_del(&global_hash, key, (int)key_len);
if (ret > 0) ret = 0;
}
break;
case PERSIST_ENGINE_SKIPLIST:
if (op == PERSIST_OP_SET) {
ret = kvs_skiplist_set(&global_skiplist, key, (int)key_len, value, (int)value_len);
if (ret > 0) ret = kvs_skiplist_mod(&global_skiplist, key, (int)key_len, value, (int)value_len);
} else if (op == PERSIST_OP_MOD) {
ret = kvs_skiplist_mod(&global_skiplist, key, (int)key_len, value, (int)value_len);
if (ret > 0) ret = kvs_skiplist_set(&global_skiplist, key, (int)key_len, value, (int)value_len);
} else if (op == PERSIST_OP_DEL) {
ret = kvs_skiplist_del(&global_skiplist, key, (int)key_len);
if (ret > 0) ret = 0;
}
break;
default:
return -1;
}
return (ret < 0) ? -1 : 0;
}
什么是 io_uring?
io_uring 是 Linux 提供的一种高性能异步 I/O 接口。它允许你提交多个 I/O 操作(比如读文件、写文件、网络收发等),然后内核高效地完成这些操作并通知你结果。
传统方式(如 read、write)是同步的:你调用 write 后程序会阻塞,直到数据写完才返回。而 io_uring 采用提交队列 和完成队列的模型:
-
提交队列(SQ):你把要做的 I/O 操作(称为 SQE,Submission Queue Entry)放入这个队列。
-
完成队列(CQ):内核完成操作后,会把结果(称为 CQE,Completion Queue Entry)放入这个队列,你可以从中取出结果。
它的好处是:可以一次性提交多个 I/O 请求,减少系统调用开销,并且支持异步操作,适合高性能服务器。
我们的函数 persist_write_fd_by_uring 就是利用 io_uring 将整个缓冲区 buf 的内容写入文件描述符 fd,并在最后调用 fsync 确保数据真正落盘。
SAVE指令:使用到io_uring。之前的保存是遍历每个引擎,然后挨个存储。使用iouring后,只需要把任务存储任务提交到sqe队列里,内核会自动完成。节省大量时间。
cpp
/**
* 使用io_uring实现全量持久化
* static:函数仅在本文件内使用。
int fd:已打开的文件描述符,要写入的目标文件。
const char *buf:要写入的数据缓冲区。
size_t len:要写入的字节数。
返回值:成功返回 0,失败返回 -1。
函数的目标是把 buf 中的 len 个字节完整地写入 fd,并且保证数据刷到磁盘上(通过最后 fsync)
*/
static int persist_write_fd_by_uring(int fd, const char *buf, size_t len){
//声明一个 io_uring 结构体,用来管理整个 I/O 队列。
struct io_uring ring;
// 记录当前已经写入的字节数,后面会用到。
size_t offset = 0;
/**
* 第一个参数 8:提交队列的深度(entries),即最多可以同时有多少个未完成的 I/O 请求。这里设为 8。
第二个参数 &ring:指向 ring 结构体的指针。
第三个参数 0:标志位,这里不使用特殊标志。
如果初始化失败(返回值不为 0),函数返回 -1。
*/
if(io_uring_queue_init(8, &ring, 0) != 0){
return -1;
}
/**
* 由于一次 write 可能不能写完所有数据,所以需要循环,每次写入一部分
* offset < len:只要还没写完,就继续循环。
*/
while(offset < len){
//chunk = len - offset:剩余还没写入的字节数。
size_t chunk = len - offset;
//sqe:指向提交队列
//cqe:指向完成队列
struct io_uring_sqe *sqe = NULL;
struct io_uring_cqe *cqe = NULL;
/**
* 如果剩余数据超过 1 MB(1u << 20 即 2^20 = 1048576 字节),则本次只写入 1 MB
* 这样做的目的是避免一次提交过大的写请求,可能超过内核限制,也方便管理
*/
if(chunk > (1u << 20)){
chunk = (1u << 20);
}
//io_uring_get_sqe 从 ring 的提交队列中取一个空闲的 SQE。
sqe = io_uring_get_sqe(&ring);
//如果返回 NULL,说明提交队列已满,需要先提交已有的请求。
if(!sqe){
if(io_uring_submit(&ring) < 0){
io_uring_queue_exit(&ring);
return -1;
}
continue;
}
/**
* io_uring_prep_write 设置这个 SQE 为"写文件"操作。
* sqe:要设置的提交队列条目。
* fd:文件描述符。
* buf + offset:源数据地址(当前要写入的数据起始位置)
* chunk:要写入的字节数
* (off_t)offset:文件偏移量。意思是从文件的这个位置开始写。因为是顺序写,
* 所以偏移量就是已经写入的字节数 offset。
*/
io_uring_prep_write(sqe, fd, buf + offset, chunk, (off_t)offset);
/**
* 将当前设置的 SQE 提交给内核执行写操作。
* 如果提交失败,清理并返回 -1。
*/
if(io_uring_submit(&ring) < 0){
io_uring_queue_exit(&ring);
return -1;
}
/**
* 等待写操作完成
* io_uring_wait_cqe 等待一个完成事件(CQE)。它会阻塞直到有一个 I/O 操作完成
* 完成后,cqe 指向该完成条目
* 如果等待出错,清理并返回 -1。
*/
if(io_uring_wait_cqe(&ring, &cqe) < 0){
io_uring_queue_exit(&ring);
return -1;
}
/**
* cqe->res 保存了 I/O 操作的结果。对于写操作,返回值是实际写入的字节数。
* 如果返回值 <= 0,表示写入失败(0 或负值),则进行清理并返回 -1
*/
if(cqe->res <= 0){
io_uring_cqe_seen(&ring, cqe);
io_uring_queue_exit(&ring);
return -1;
}
/**
* 更新已写入字节数
* offset 加上本次实际写入的字节数
* io_uring_cqe_seen 标记这个 CQE 已被处理,内核可以回收它的资源。
* 循环回到 while(offset < len) 继续写入剩余数据。
*/
offset += (size_t)cqe->res;
io_uring_cqe_seen(&ring, cqe);
}
/**
* 最后执行 fsync(确保数据落盘)
* 写完所有数据后,文件内容可能还在操作系统的缓存中,没有真正写入磁盘。
* 所以需要调用 fsync 强制刷盘。
*/
{
/**
* 再次获取一个 SQE,如果获取失败则清理并返回 -1。
*/
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
struct io_uring_cqe *cqe = NULL;
if(!sqe){
io_uring_queue_exit(&ring);
return -1;
}
/**
* io_uring_prep_fsync 设置这个 SQE 为 fsync 操作。
* fd:要同步的文件描述符
* 最后一个参数 0 是 fsync 的 flags,通常传 0
*/
io_uring_prep_fsync(sqe, fd, 0);
//提交 fsync 请求。
if(io_uring_submit(&ring) < 0){
io_uring_queue_exit(&ring);
return -1;
}
//等待 fsync 完成。
if(io_uring_wait_cqe(&ring, &cqe) < 0){
io_uring_queue_exit(&ring);
return -1;
}
/**
* 检查 fsync 结果,如果返回值 < 0 表示失败,清理并返回 -1。
* 成功则标记 CQE 已处理。
*/
if(cqe->res < 0){
io_uring_cqe_seen(&ring, cqe);
io_uring_queue_exit(&ring);
return -1;
}
io_uring_cqe_seen(&ring, cqe);
}
/**
* o_uring_queue_exit 释放 io_uring 相关的资源。
* 返回 0 表示整个写入和同步过程成功。
*/
io_uring_queue_exit(&ring);
return 0;
}
总结
这个函数展示了如何使用 io_uring 进行文件的异步写入和同步:
-
初始化
io_uring。 -
循环分块(每次最多 1 MB)将数据写入文件,每块都提交写请求并等待完成。
-
写完后,发起
fsync请求,确保数据落到磁盘。 -
释放资源并返回结果。
使用 io_uring 的好处是能更高效地提交多个 I/O 操作,但在这里我们实际上是同步地提交并等待每个请求,所以性能提升有限。不过它至少演示了 io_uring 的基本用法,也为后续优化(比如批量提交多个块)打下了基础。
加载数据:
无论是全量还是增量数据,加载数据的时候,都使用了mmap。
什么是 mmap?
mmap 是一种内存映射文件 的技术。它把磁盘上的文件直接"映射"到进程的虚拟内存地址空间中。映射后,你可以像操作普通内存数组一样读写文件内容,而不需要手动 malloc 一块缓冲区,再用 read 把文件内容读进去。
cpp
/*
* 加载增量日志。
*
* 启动恢复时的顺序应该是:
* 1. persist_load_all("kvstore.data")
* 先恢复上一次全量快照
*
* 2. persist_load_increment("kvstore.aof")
* 再把快照之后发生过的写操作重新执行一遍
* 整体逻辑:打开文件 → 获取大小 → 映射到内存 → 调用解析函数处理内存块 → 解除映射并关闭文件。
*/
int persist_load_increment(const char *path)
{
if(!path) return -1;
/**
* st:用于存放文件的状态信息(如大小)。
* map:指向映射后的内存区域,初始为 NULL。
* ret:函数最终返回值,初始为 -1,后续根据情况修改。
*/
int fd = -1;
struct stat st;
void *map = NULL;
int ret = -1;
/**
* open(path, O_RDONLY):以只读方式打开文件。
* 这里返回 0 而不是 -1,是因为没有增量日志文件并不是错误,而是正常情况
* (比如系统第一次启动,还没有写 AOF 文件)。
* 所以返回 0 表示"没有数据可加载",让上层继续运行
*/
fd = open(path, O_RDONLY);
if(fd < 0) return 0;
/**
* fstat(fd, &st):获取文件状态信息,存入 st 结构体
* st.st_size 就是文件的大小(字节数)。
* 如果 fstat 失败,说明出现错误,关闭文件并返回 -1。
*
*/
if(fstat(fd, &st) < 0){
close(fd);
return -1;
}
/**
* 如果文件大小为 0,说明没有内容,直接关闭文件并返回 0(成功,但没数据)
* 这是一种边界情况,避免后面 mmap 映射一个空文件导致问题。
*/
if(st.st_size == 0){
close(fd);
return 0;
}
/**
* mmap 是一种内存映射文件的技术。它把磁盘上的文件直接"映射"到进程的虚拟内存地址空间中。
* 映射后,可以像操作普通内存数组一样读写文件内容,而不需要手动 malloc 一块缓冲区,
* 再用 read 把文件内容读进去。
* 第一个参数 NULL:表示让内核自动选择映射的起始地址。我们通常传 NULL,让系统决定。
第二个参数 (size_t)st.st_size:要映射的字节数,这里就是整个文件的大小。
第三个参数 PROT_READ:映射区域的保护权限。PROT_READ 表示只读,我们只需要读取文件内容,不修改。
第四个参数 MAP_PRIVATE:映射类型。MAP_PRIVATE 表示私有映射,对映射区域的修改不会写回文件,也不会影响其他进程。这里只读,所以无所谓。
第五个参数 fd:要映射的文件描述符。
第六个参数 0:文件偏移量,从文件的哪个位置开始映射。0 表示从文件开头开始。
返回值:
成功返回映射区域的起始地址,类型是 void *。
失败返回 MAP_FAILED(通常定义为 (void *)-1)。
所以 if(map == MAP_FAILED) 表示映射失败,此时关闭文件并返回 -1。
好处:
避免手动分配大块内存和读取文件,操作系统负责将文件内容按需加载到物理内存。
访问映射区域就像访问数组一样,代码简洁高效。
对于大文件,mmap 可以做到按需分页加载,节省内存
*/
map = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if(map == MAP_FAILED){
close(fd);
return -1;
}
/**
* 将映射后的内存地址 map 强制转换为 const char *,并传入文件大小。
这个函数负责在内存中解析 AOF 帧格式或旧版文本格式,逐条执行命令恢复数据。
返回值保存在 ret 中,作为当前函数的最终返回值。
*/
ret = persist_load_increment_from_mem((const char *)map, (size_t)st.st_size);
/**
* munmap(map, size):解除内存映射,释放虚拟地址空间。
* return ret:返回解析函数的执行结果。
*/
munmap(map, (size_t)st.st_size);
close(fd);
return ret;
}
整体流程总结
-
打开文件,如果不存在则返回 0(正常)。
-
获取文件大小,空文件返回 0。
-
用
mmap将文件内容映射到内存。 -
调用
persist_load_increment_from_mem在内存中解析并恢复数据。 -
解除映射,关闭文件,返回结果。
为什么用 mmap?
-
简化代码:不需要手动分配缓冲区和循环读取文件。
-
性能好:对于大文件,mmap 可以避免多次系统调用,并且利用操作系统的页缓存。
-
方便直接解析二进制帧或文本行。