1、目标
Redis是一种内存数据库,通过内存快照或者AOF方式支持持久化。导致Redis版本迭代,都需要从磁盘上读取数据文件,加载到内存中。当存储数据量较大时,这是一个非常耗时的过程。Redis热升级就是为了解决这个问题。
2、设计方案
将Redis逻辑代码打包到libredis.so中,在外层动态加载libredis.so。调用加载库中原redis main函数,开始提供Redis服务。做版本迭代,只需要通过redis-cli发送config set libredis path/libredis.so.new,Redis自动完成版本迭代。版本迭代成功,则返回+OK,否则失败。Redis停止服务。
3、热升级原理
动态链接共享库被加载后,存放在共享库的内存映射区域(堆栈之间),包含非malloc数据与函数。当共享库被卸载之后,这个存储区域将被释放,维护数据库状态的数据结构也将被释放。Redis运行期间,通过malloc分配的数据存放在堆中,不会由于共享库的卸载而被释放。但是在热升级的过程中,维护数据库状态的数据是不能够被释放的。所以数据库状态数据需要存储在进程的数据段,而不是放在内存映射区。
Redis中维护数据库状态的数据基本都存放在redisServer中,我们在外层也定义这样的数据结构Global,它的数据项基本和redisServer一致。在加载共享库之前,redis定义的一些struct类型变量,Global是不能获取的。所以这些数据类型都以void *的形式对应。redisServer非指针类型的struct变量转化为指针变量。指针所占用的空间只与机器本身有关,保证Golbal与redisServer强制转换的正确性。
Global结构设计:
```
typedef struct _global {
/* General */
char *configfile; /* Absolute config file path, or NULL */
int hz; /* serverCron() calls frequency in hertz */
void **db;
void *commands; /* Command table */
void *orig_commands; /* Command table before command renaming. */
……………………
……………………
/*For upgrade */
char *libredis;
void *redis_handle;
redis_main redismain;
char *newlib;
size_t stat_used_memory;
int zmalloc_thread_safty;
uint32_t dict_hash_function_seed;
} Global
```
3.1正确退出Redis服务
Redis热升级是通过给Redis发送升级命令,Redis退出当前服务,之后动态加载新so。需要首先保证Redis服务正常退出。分为以下几个部分:
3.1.1退出时Redis缓冲区处理
client_querybuffer(输入缓冲区):
Redis只有一个主进程处理文件事件和时间事件,所以处理与客户端交互时,也只能顺序执行每个请求。同时,redis-cli采用同步阻塞机制发送请求,那么在系统socket这层,最多存在n个客户端的请求缓冲。同一时刻,redis只能响应一个请求,回调readQueryFromClient,将socket中的数据读入client_querybuffer中,每次最多读取REDIS_REQ_MULTIBULK(16kb)数据。如果不构成一个命令,则等待下次回调。所有的请求信息都将缓存在socket中,或者querybuffer中,保存在系统中或者保存在redisServer中。不需要做特殊处理。
client_outputbuffer(输出缓冲区):
client_outputbuffer,redis处理完每个请求,只要这个客户端注册可写事件,触发回调sendReplyToClient,将缓冲区数据一次全部发送到socket中。Redis处理完命令,将结果发送到缓冲区。可能并不会立即被处理,什么时候处理由在epoll决定的。总体来说,client_outputbuffer也可能会积压大量数据。但所有的信息都存放在socket或者redisServer中。不需要做特殊处理。
AOF缓冲:
这里AOF缓冲指的是aof_buf,属于redis应用层的缓冲。要想最终保存在磁盘中,首先要调用write,然后调用fsync,才会将write到系统缓冲中的数据刷入磁盘。所以这里有两个缓冲,aof_buf称为应用缓冲,另外一个称为系统缓冲。
升级不成功,可能导致进程退出。也没有其他客户端或者进程进行监控,保险起见,需要将应用缓冲区的数据aof_buf调用write写入AOF文件,保证数据的安全性。下文3.1.2退出时线程处理,来保证write的数据真正刷入到磁盘中。
升级时,如果正在做aof rewrite。缓冲aof_rewrite_buf_blocks将不会被处理,这部分数据由malloc申请,其中包含的函数指针变更将在reinit中进行。
3.1.2退出时线程处理
从动态库函数中返回,而不卸载动态库。再载入动态库,全局变量或者静态变量的值会处于不确定状态(初始值是否已经被更改过)。所以即使是载入同一个动态库,也最好是先卸载动态库,再加载。动态库函数返回,在动态库中产生的线程仍旧会继续执行。动态库卸载,而线程未退出,则会造成core dump。
Redis线程的处理封装在bio.c中,动态库卸载之前,要保证线程退出。目前Redis有两个类型后台i/o任务,分别负责将aof系统缓冲区数据(追加命令与重写aof缓冲)刷入磁盘以及关闭旧aof文件。
综上,既关系到系统安全又关系到数据安全。所以从动态库函数中返回之前,要检测试是否还有后台任务在,如果存在则等待并调用pthread_cond_signal唤醒任务,让任务继续执行。直至完成后,安全退出。
3.1.3退出时子进程处理
主进程中卸载动态库时,如果子进程未退出,动态库是不会真正被卸载(close)的。主进程再加载新动态库时,原来的子进程依旧在运行旧动态库代码。主进程又可能会开始一个新的子进程,与原来的子进程做相同的事情,出现一些不可预知的混乱状况(主进程与子进程运行的是两套代码)。Redis通过快照或者AOF的方式支持久化,这两种方式都是通过fork子进程来实现。Redis通过server->rdb_child_pid 或者server->aof_child_pid来判断是否有子进程在后台运行,这两个值保存在global中。加载新动态库,不会同时存在两个子进程做持久化。假设在做持久化的过程中,开始进行升级。升级过程中,以及升级之后,子进程运行旧的动态库代码完成持久化,直到资源再被运行新的动态库的主进程回收。做持久化的过程,是不涉及到函数指针的相关使用,所以替换动态库不会对持久化造成影响。
注意:如果本身对持久化的代码做了改动,那么要等到升级完成之后,再做持久化,才能达到改动的效果。
综上,理论上说直接退出而不等待子进程结束,这种处理是有风险的。但是经过详细的考察,Redis可以规避这个风险,所以热升级不处理子进程。
3.1.4退出事件处理
热升级替换so,事件的回调函数的地址会发生变化,所以退出时需要删除文件事件和时间事件。时间时间比较好恢复,文件事件主要是是client这部分的恢复。
3.1.5其他退出处理
命令table:server->command,升级可能会做一些添加一些新的命令,所以最好是重新加载。退出时,释放掉旧的table。
在配置中会rename一些危险命令,随着table会被释放掉,需要重新保存。
Shared.object并没有保存在redisServer中,也没有保存的必要,升级时重新初始化。
3.1.6退出时可以探讨的一些处理
Lua客户端是在退出时,直接释放掉的。主要是由于引用lua包,打包新so会重新打包lua.a。目前看起来处理起比较复杂,可能会造成内存泄漏等情况。所以采取的方式是直接释放。
3.2升级后,服务状态恢复
3.2.1禁止重复初始化
[if !supportLists]1. [endif]环境变量
redis调用spt_init()修改环境变量,重复加载会造成环境变量被清空,造成core。在该函数中,涉及到很多对环境变量的设置。退出时需要恢复设置,再次调用时,才不会出现错误。而我们需要保持环境变量不变,无需退出,只能初始化一次环境变量。
[if !supportLists]2. [endif]redis中db其实是一个hashtable,调用hashFunction来决定key落到哪个桶中。在hash中seed生成由进程的pid和当前时间共同生成。重复初始化,会造成同一个key落到不同的桶中,所以这个值需要在redisServer中保存。保证热升级前后,key使用的是同一个hash方法。
3.2.2恢复局部变量
这部分的恢复主要参考initServerConfig()以及initServer()中的初始化来做恢复。还有一部分,比如只有建立主从关系,才会初始化的一些数据类型,需要细致的检测,来做恢复。具体不赘述。
3.2.3系统环境恢复
信号重新注册、线程重新初始化等。
3.2.4 Kv数据恢复
查看redis数据结构,找到所有相关的函数指针,进行函数指针重置。主要是关于dictType,list函数指针相关重置。Redis本身是个dict,里面的非string类型value又是由dictType和list函构成。所以DB和value都需要重置,这部分重置由于要轮询所有的key,所以是比较耗时的操作。
3.2.5客户端恢复
Redis客户端需要恢复两部分,一部分重置事件回调函数,一部分是客户端函数指针重置。
Clients中维护所有客户端,函数指针重置,只对clients做处理即可,但是事件回调函数需要根据情况做特殊处理。
Lua, monitor客户端由于直接被释放,暂不讨论(处理可做探讨)。在redis中面向用户客户端分为NORMAL、SLAVE、MONITOR三种,但是redis内部维护着众多的客户端:
客户端数据结构事件
Clients读事件
readQueryFromClient
写事件
sendReplyToClient
clients_to_close无,不涉及交互操作
current_client属于clients,用于崩溃报告
Master、cached_master、slaves属于clients,用于主从同步。
未建立主从同步关系,事件需要特殊处理。
建立主从同步关系,可在clients中处理。
migr_client数据迁移过程中,不做热升级。clients处理
Monitor、lua_caller、lua_client直接释放,可探讨。
以上,主从同步客户端需要做特殊处理,在下面主从同步恢复中,将详细探讨如何恢复。
3.2.6主从同步恢复
处理方案:在主从同步时,可进行热升级。
主从同步过程中,热升级会对实例中epoll事件和同步中间状态造成变化。需要保证,升级之后,这两者状态跟升级之前一致。如果不能做到一致,需要正确释放资源。根据主从关系建立情况,分以下三部分进行讨论。
一、两个redis实例已经建立好主从关系,即主库中server->slaves中客户端client->replstate = REDIS_REPL_ONLINE,从库server->repl_state = REDIS_REPL_CONNECTED。此时主库与从库的交互跟客户端和服务器交互是一样的,且没有特殊的中间变量需要维护,所以从库只需要在server->clients中处理master,主库在server->clients中处理slaves,不需要进行特殊的处理。
二、主从做半同步,分别从主库和从库角度进行分析。
1. 从库需要保存cached_master,如果cached_master为空,那么会进行全同步。断线时的操作会将master从clients中删除,但是master本身不会被free,只会删除读写事件。然后将cached_master赋值为master。需要对客户端函数指针进行修改,事件删除不需要做特殊处理。如果能正常进行半同步,则后面的过程同一,不需要做特殊处理。否则由三处理。
2.主库在从库做半同步的过程中,是不需要为这个已经失联的从库,保存任何信息的。不需要做特殊的处理。
三、主从正在做全量同步,分别从主库和从库的角度进行分析。又在2.8中支持无盘复制(Diskless replication) ,加上这个因素,分成三个部分讨论(从库处理不关心是否落盘)。
1. 落盘复制,主库在全量同步过程中,中间状态以及事件注册如下:
(注意这些状态都是的是属于clienct的slave的状态,由c->replstate记录)
状态事件 链接
REDIS_REPL_WAIT_BGSAVE_START
(Waiting for next BGSAVE for SYNC)
无 Client:
listAddNodeTail(server->slaves,c)
无任何读写事件,但保存在客户端中。恢复时直接退出客户端恢复代码。
REDIS_REPL_WAIT_BGSAVE_END
(Waiting for end of BGSAVE for SYNC)
无 Client:
listAddNodeTail(server->slaves,c)
无任何读写事件,但保存在客户端中。恢复时直接退出客户端恢复代码。
REDIS_REPL_SEND_BULK删除写事件,
注册新的写事件sendBulkToSlave
读事件为
readQueryFromClient
Client:
保存于server->slaves
REDIS_REPL_ONLINE
(允许半同步或者同步完成)
删除写事件,注册新的写事件
sendReplyToClient
Client:
保存于server->slaves
热升级可能在其中任何一个状态中发生,但我们需要特殊处理的状态是REDIS_REPL_SEND_BULK,REDIS_REPL_WAIT_BGSAVE_START
,REDIS_REPL_WAIT_BGSAVE_END
其他状态事件可以再server-clinets中处理,不需要特殊处理。
if ((c->flags & REDIS_SLAVE) && server->rdb_child_type == REDIS_RDB_CHILD_TYPE_DISK && c->replstate == REDIS_REPL_SEND_BULK)
{
if (aeCreateFileEvent(server->el, c->fd, AE_WRITABLE, sendBulkToSlave, c) == AE_ERR) {
redisLog(REDIS_WARNING, "register SEND_BULK slave write event fail");
return REDIS_ERR;
}
}
中间状态的客户端在server->slaves保存,不会因为升级而丢失。
资源释放:我们恢复资源可能会不成功,这时候就需要释放相应资源。让同步恢复到一个最初始的状态。在这个部分我们将调用freeClient(),该函数负责对资源进行恢复。
[if !supportLists]3. [endif]从库在全量同步过程中,中间状态以及事件注册如下
(以下状态都是属于slave实例状态,由server->repl_state记录)
状态事件链接
REDIS_REPL_NONE无无
REDIS_REPL_CONNECT
(同步异常)
删除读写事件关闭
REDIS_REPL_CONNECTING注册读写事件
syncWithMaster
Socket: repl_transfer_s
REDIS_REPL_RECEIVE_PONG删除写事件,只有读可触发
syncWithMaster
Socket: repl_transfer_s
REDIS_REPL_TRANSFER删除读事件,注册读事件
readSyncBulkPayload
Socket: repl_transfer_s
REDIS_REPL_CONNECTED删除读事件,注册读事件
readQueryFromClient
处理完master传播过来命令之后,注册写事件sendReplyToClient,将处理结果返回给master。
Client:server->master
从库在真正建立主从同步之前(server->repl_state=REDIS_REPL_CONNECTED),这个链接保存在repl_transfer_s中,还没有以client的形式保存在server->master中。REDIS_REPL_CONNECTING、REDIS_REPL_RECEIVE_PONG、REDIS_REPL_TRANSFER需要特殊处理,
REDIS_REPL_CONNECTED在server->clients中处理。
代码以及资源恢复:
void restoreSlaveFullSyncEvent() {
if (server->repl_state == REDIS_REPL_NONE ||
server->repl_state == REDIS_REPL_CONNECT || server->repl_state == REDIS_REPL_CONNECTED)
return;
if (server->repl_state == REDIS_REPL_CONNECTING) {
if (aeCreateFileEvent(server->el,server->repl_transfer_s,AE_READABLE|AE_WRITABLE,syncWithMaster,NULL) == AE_ERR)
{
redisLog(REDIS_WARNING,"Can't create readable event for SYNC when upgrade 1");
goto err;
}
return;
}
if (server->repl_state == REDIS_REPL_RECEIVE_PONG) {
if (aeCreateFileEvent(server->el,server->repl_transfer_s,AE_READABLE,syncWithMaster,NULL) == AE_ERR)
{
redisLog(REDIS_WARNING,"Can't create readable event for SYNC when upgrade 2");
goto err;
}
return;
}
if (server->repl_state == REDIS_REPL_TRANSFER) {
if (aeCreateFileEvent(server->el,server->repl_transfer_s,AE_READABLE,readSyncBulkPayload,NULL) == AE_ERR)
{ redisLog(REDIS_WARNING,"Can't create readable event for SYNC when upgrade 3");
redisLog(REDIS_WARNING,
"Can't create readable event for SYNC: %s (fd=%d)",
strerror(errno),server->repl_transfer_s);
goto err;
}
return;
}
err:
server->repl_state = REDIS_REPL_CONNECT;
redisLog(REDIS_WARNING,"test wrong 1 redis.c");
close(server->repl_transfer_s);
server->repl_transfer_s = -1;
return;
}
3. 无磁盘复制,主库是fork了一个子进程来做落盘中的dump rdb + trsf rdb的操作。子进程创建同步socket,将获取(key,value)直接发送给从库。主进程做热升级时,退出so,子进程资源也不会释放,当子进程处理完,将以管道的方式告诉主进程。无磁盘复制不需要做特殊处理。
3.3禁止热升级
数据迁移时的处理:
数据迁移是个比较复杂的过程,涉及到与外部环境的交互,状态变化,函数指针更改。Redis做数据迁移过程中,禁止操作热升级。
3.4检验是否恢复正常
可排查redisServer中特殊的数据结构,来核查看是否在热升级之后,是否全部恢复。
RedisServer中除了c原生的数据类型,其他数据类型或多或少都与函数指针重置相关,可参照这些数据结构,来核查,热升级代码是否恢复完所有的数据结构类型:
客户端:
1.list *clients ,已经处理
2.clients_to_close,不会涉及到客户端函数操作
3.current_client, 属于list *clients,用于崩溃报告,不需要再另外处理。
4. redisClient *master,已经处理
5. redisClient *cached_master 已经处理
6. lua客户端 redisClient *lua_client 全部清空
7. lua客户端 redisClient *lua_caller
8. 从客户端slaves,
9. 监控客户端monitors
所有的客户端都在clients中,那么处理clients就可以。其他不需要做处理。
命令集:
1 dict *commands, dict *orig_commands
2.struct redisCommand *delCommand, *multiCommand, *lpushCommand, *lpopCommand *rpopCommand;
慢日志:
已经处理
list *slowlog;
AOFchange缓冲:
list *aof_rewrite_buf_blocks;
dict *repl_scriptcache_dict
队列:无需处理
list *repl_scriptcache_fifo;
list *clients_waiting_acks;
list *unblocked_clients;
list *ready_keys;
发布与订阅pubsub:
dict *pubsub_channels;
list *pubsub_patterns;
lua脚本相关:
lua_State *lua
dict *lua_scripts
dict *latency_events
迁移期间不做升级以下数据不变更,无需处理:
redisClient *migr_client;
dict *migrated_keys;
dict *direct_propagate_keys;
keylog:
robj *keyLog[64];
网友评论