美文网首页
Redis设计 - 服务器

Redis设计 - 服务器

作者: 家硕先生 | 来源:发表于2020-08-13 21:02 被阅读0次

    前言

    Redis服务器负责与多个客户端建立网络连接,并处理客户端的请求,通过资源管理来维持服务器自身的运转。

    命令请求的执行过程

    一个命令请求从客户端发送到服务器,直至获得回复,都需要经过一系列的处理和交互,大致可以分为以下几个步骤:

    • 客户端向服务器发送命令请求 SET KEY VALUE
    • 服务器接收并处理客户端发来的命令请求 SET KEY VALUE,在数据库中执行操作,并产生命令回复OK
    • 服务器将命令回复OK发送给客户端
    • 客户端接收服务器返回的命令回复OK,并做对应的业务逻辑

    下面讲对这些操作的执行细节进行补充

    1. 发送命令请求

    首先客户端将命令转换成协议格式,然后通过连接服务器的套接字,将协议格式的命令发送给服务器。

    客户端发送命令过程
    2.读取命令请求

    服务器接收到客户端的命令后,调用命令请求处理器,执行以下操作:

    1. 读取套接字中的命令请求,并将其保存在客户端的输入缓冲区。
    2. 对输入缓冲区的命令进行分析,提取其中包含的命令参数和参数个数,并保存到redisClient的argv和argc属性。
    3. 调用命令执行器,执行客户端命令。
    3. 命令执行器

    3.1 查找命令实现
    命令执行器首先会根据redisClient保存的argv[0]参数,在命令表中查找所指定的命令,并将找到的命令保存到客户端状态的cmd属性中。

    命令表是一个字典,键是命令的名字,如 set、get等,值是redisCommand结构,该结构记录了命令的实现信息。

    3.2 执行预备操作
    目前为止,服务器已经得到了执行命令的实现函数、参数、参数个数,但是执行前还要一些预备操作(单机,集群需要的预备操作更多):

    • 检查客户端状态的cmd是否为NULL,为NULL则说明没有对应的命令实现
    • 检查客户端提供的参数数量是否符合要求
    • 如果服务端开启了认证,检查客户端是否通过了身份验证
    • 如果服务器打开了maxmemory,那么执行命令前,先检查服务器的内存占用情况,并在有需要时进行内存回收
    • 如果服务器上一次执行BGSAVE命令出错,并且打开了stop-writes-on-bgsave-error功能,那么拒绝执行修改命令
    • 如果客户端正在使用subscribe命令订阅频道,或者正在用psubscribe命令订阅模式,那么服务器只会执行此客户端发过来的,subscribe,psubscribe,unsubscribe, punsunscribe四个命令,其他命令都会拒绝
    • 如果服务器正在进行数据载入,那么客户端发送的命令必须带有i标识(比如info,shutdown,publish等等),才会被服务器执行
    • 如果服务器正在执行lua脚本而阻塞,那么服务器只会执行客户端发来的SHUTDOWN nosave命令和SCRIPT KILL命令,其他命令都会拒绝
    • 如果客户端正在执行事务,那么服务器只会执行客户端发来的EXEC、DISCARD、MULTI、WATCH四个命令,其他命令都会被放进事务队列中
    • 如果服务器打开了监视器功能,那么服务器会将要执行命令和参数的信息发给监视器

    3.3 调用命令的实现函数
    服务器已经将命令的实现(cmd)、命令参数(argv)和参数个数(argc)保存在redisClient中,接下来只需要将参数传递给命令的实现函数即可。执行完实现函数后的返回值会被写入客户端的输出缓冲区,并为客户端的套接字关联命令回复处理器。

    3.4 执行后续工作
    执行完实现函数之后,服务器需要执行一些后续工作:

    • 如果服务器开启了慢查询日志功能,那么慢查询日志模块就会检查刚才的查询是否需要记录
    • 根据执行命令的耗时,更新命令redisCommand对象的millseconds属性,并将calls计数器加1
    • 如果开启了AOF功能,且对数据库做了更改,那么需要将此条写入到AOF缓冲区中
    • 如果有其他从服务器复制当前这个服务器,那么服务器会将刚刚执行的命令传播给所有的从服务器。

    3.5 将结果回复给客户端
    客户端套接字变WRITEABLE时,命令回复处理器会将结果以协议格式发送给客户端,如 "+OK\r\n"
    客户端接收到后,按照协议格式进行转换成人类可读格式。

    serverCron函数

    serverCron函数每隔100毫秒执行一次,负责维护服务器资源,保持redis良好的运转。下面将介绍serverCron函数执行的操作,redisServer结构中的一些属性

    1. 更新服务器时间缓存

    Redis服务器中有不少功能需要获取系统当前时间,为了减少系统调用执行次数,服务器状态中的unixtime属性和mstime属性被用作当前时间的缓存。

    struct redisServer {
        // 保存秒级精度的系统当期unix时间戳
        time_t unixtime;
        // 保存毫秒级进度的系统当前unix时间戳
        long long mstime;
    };
    

    因为serverCron函数是默认每100毫秒执行一次,因此这个时间的精度并不高
    • 服务器只会在打印日志,更新服务器lru时钟,判断持久化条件,计算服务器上线时间等这类对精度要求不高的功能上
    • 对于为键设置过期时间,添加慢查询日志等需要高精度时间要求的来说,Redis依旧会调用系统函数,获得准确的时间

    2. 更新LRU时钟

    服务器状态中保存了用于计算LRU用的时钟lruclock,它和上面介绍的时间缓存一样,都是时间缓存的一种

    struct redisServer {
        // 默认每10秒更新一次时钟缓存,用于计算键的空转时长
          unsigned lruclock:22;
    }
    
    //每个redis对象都会有一个lru属性,这个lru属性保存了对象最后一次被命令访问的时间
    typedef struct redisObject {
        unsigned lru:22;
    }
    
    // 查看键的空转时间
    reids> OBJECT IDLETIME key
    (integer) 10
    

    数据库键的空转时间 = lruclock时间 - 对象的lru时间

    3. 更新服务器每秒执行命令次数

    serverCron中的trackOperationsPerSecond函数以每100毫秒一次的频率执行,以抽样计算的方式估算最近一秒钟处理的请求数量。

    可通过INFO status命令查看:

    redis> INFO status 
    ...
    instantaneous_ops_per_sec:6 // 最近一秒钟处理了6个命令
    ...
    
    4. 更新服务器内存峰值记录

    redisServer中的stat_peak_memory属性记录了服务器内存峰值

    每次执行serverCron时,程序都查看服务器当前使用的内存数量,并与当前的stat_peak_memory值进行比较,大则将之替换。

    5. 处理SIGTERM信号

    在启动服务器时,Redis会为服务器进程的SIGTERM信号关联处理器sigtermHandler函数,这个函数在服务器接收到SIGTERM信号时,打开服务器状态的shutdown_asap标记。

    static void sigtermHandler(int sig) {
        // 打印日志
        redisLogFromHandler(REDIS_WARING, "received SIGTERM,scheduling shutdown...");
        // 打开关闭标识
        server.shutdown_asap = 1;
    }
    

    serverCron函数运行时,会检查服务器状态的shutdown_asap,判断是否关闭服务器。

    6. 管理数据库资源

    serverCron函数每次执行都会调用databasesCron函数,这个函数对服务器的数据库进行检查,删除过期键,并在需要的时候对字典进行收缩操作。

    7. 执行被延时的BGREWRITEAOF

    在服务器执行BGSAVE命令的期间,如果客户端向服务器发送BGREWRITEAOF命令,那么该命令会被延时,直到BGSAVE执行完毕。

    服务器的aof_rewrite_scheduled标记记录了服务器是否延迟BGREWRITEAOF命令:

    struct redisServer {
        //如果值为1,表示BGREWRITEAOF命令被延迟
        int aof_rewrite_scheduled;
    }
    

    每次serverCron函数执行时,都会检查BGSAVE命令或者BGREWRITEAOF命令是否在执行,如果这个两个命令都没在执行,并且标记为1,则服务器就会执行BGREWRITEAOF命令。

    8. 检查持久化操作的运行状态

    服务器使用rdb_child_pid属性记录执行BGSAVE命令的子进程ID,aof_child_pid属性记录BGREWRITEAOF命令的子进程ID,这两个属性可用于检查BGSAVE或者BGREWRITEAOF命令是否在执行。

    struct redisServer {
        // 如果服务器没有在执行BGSAVE,值为 -1
        pid_t rdb_child_pid;
        // 如果服务器没有在执行BGREWRITEAOF,值为 -1
        pid_t aof_child_pid;
    }
    

    1)serverCron函数在运行时,都会检查两个属性的值是否为-1,只要其中一个不是-1,就会执行一次wait3函数,检查子进程是否有信号发到服务器进程:

    • 如果有信号到达,表示新的RDB文件或者AOF文件已经重写完成,服务器需要执行命令后续的操作,例如替换旧RDB文件,替换旧AOF文件等等
    • 如果没有信号到达,表示持久化操作还未完成,程序不做动作。

    2)如果两个属性值都为-1,那么服务器目前没有进行持久化操作,这种情况下程序执行以下三个检查:

    • 查看是否有BGREWRITEAOF被延迟了,也就是上面所说的情况
    • 检查服务器自动保存条件是否满足,如果满足,那么开始一场新的BGSAVE操作。
    • 检查服务器设置的AOF重写条件是否满足,如果满足且没有其他持久化操作,则进行后台AOF重写

    以下流程图展示了检查过程

    持久化流程检查
    9. 将AOF缓冲区中的内容写入AOF文件

    如果服务器开启了AOF持久化功能,并且AOF缓冲区里面还有待写入的数据,那么serverCron函数会调用相应的程序,将AOF缓冲区的内容写入到AOF文件里面。

    10. 关闭异步客户端

    服务器会关闭那些输出缓冲区超过限制的客户端。

    初始化服务器

    Redis服务器从启动到能够接受客户端的命令请求,需要经过一系列的初始化和设置过程,比如初始化服务器状态,设置用户指定的服务器配置,创建后续的数据结构和网络连接等等。

    1. 初始化服务器状态结构(redisServer)

    初始化就是创建一个redisServer类型的实例变量server作为服务器的状态,并为各个属性设置默认值。
    初始化工作由initServerConfig函数完成:

    void initServerConfig(void) {
        // 设置服务器的运行id
        getRandomHexChars(server.runid, REDIS_RUN_ID_SIZE);
        // 为运行id加上结尾字符
        server.runid[REDIS_RUN_ID_SIZE] = '\0';
        // 设置默认配置文件路径
        server.configfile = NULL;
        // 设置默认服务器频率
        server.hz = REDIS_DEFAULT_HZ;
        // 设置服务器的运行架构
        server.arch_bits = (sizeof(long) == 8) ? 64 : 32;
        // 设置默认服务器端口号
        server.port = REDIS_SERVERPORT;
        // ...
    }
    
    2. 载入配置选项

    启动服务时,用户可以通过修改配置文件,或者在启动命令追加配置来达到修改参数的目的,如果用户指定了具体参数的值,则进行更新,否则使用默认值。
    如:

    $ redis-server redis.conf
    
    3. 初始化服务器数据结构

    initServerConfig函数初始化server状态时,只创建了命令表一个数据结构,其余数据结构在此步骤初始化:

    • server.clients链表,记录了所有与服务器连接的客户端状态。
    • server.db 数组,包含了服务器所有的数据库。
    • server.pusubchanels字典:用于保存频道订阅信息,server.pubsubpatterns链表:保存模式订阅信息。
    • 执行lua脚本环境的server.lua
    • 保存慢查询日志的server.slowlog属性

    创建完以上属性后,开始调用initServer函数为数据结构分配内存。
    initServerConfig负责初始化一般属性,initServer负责初始化数据结构,之所以分成两个步骤,是考虑到用户可以通过修改配置选项修改和数据结构相关的服务器状态属性,所以等到载入用户配置后,再进行数据机构的初始化。

    除了初始化数据结构之外,还进行了一些非常重要的设置操作:

    • 为服务器设置进程信号处理器
    • 创建共享对象:这些对象包含了一些常用的字符串,整数1到1000的字符串
    • 打开服务器的监听端口,为监听套接字关联连接应答事件处理器,等待客户端连接
    • 为serverCron函数创建时间时间,等待服务器正式运行时执行serverCron函数
    • 如果AOF持久化功能已经打开,那么打开AOF文件,如果不存在AOF文件,则创建一个新的AOF文件,用作后续写入
    • 初始化服务器的后台I/O模块(bio),为将来I/O操作做好准备
    4. 还原数据库状态

    完成了对服务器状态server变量的初始化之后,服务器需要载入RDB文件或者AOF文件,并根据配置来恢复数据库的内容:

    • 如果服务器启用了AOF持久化功能,那么服务器使用AOF文件来还原数据库
    • 如果没有开启AOF功能,使用RDB文件来恢复数据库状态
    5. 执行事件循环

    初始化的最后一步,开始执行事件循环(loop),开始接受客户端的连接请求。

    回顾

    本文介绍了Redis服务器的初始化过程、命令请求的执行过程和serverCron函数的主要工作,都是偏细节的东西,算是科普了。

    相关文章

      网友评论

          本文标题:Redis设计 - 服务器

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