美文网首页
Redis热升级详细设计

Redis热升级详细设计

作者: 白馨_1114 | 来源:发表于2020-08-18 18:31 被阅读0次

    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];

    相关文章

      网友评论

          本文标题:Redis热升级详细设计

          本文链接:https://www.haomeiwen.com/subject/cdhbzhtx.html