Redis设计 - 服务器

2020-08-13  本文已影响0人  家硕先生

前言

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

命令请求的执行过程

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

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

1. 发送命令请求

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

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

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

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

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

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

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

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

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

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函数,检查子进程是否有信号发到服务器进程:

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

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

持久化流程检查
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状态时,只创建了命令表一个数据结构,其余数据结构在此步骤初始化:

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

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

4. 还原数据库状态

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

5. 执行事件循环

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

回顾

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

上一篇 下一篇

猜你喜欢

热点阅读