第14章 服务器
Redis服务器负责与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理来维持服务器自身的运转
14.1 命令请求的执行过程
14.1.1 发送命令请求
Redis服务器的命令请求来自Redis客户端,当用户在客户端中键入一个命令请求时,客户端会将这个命令请求转换成协议格式,然后通过连接到服务器的套接字,将协议格式的命令请求发送给服务器
14.1.2 读取命令请求
当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时,服务器将调用命令请求处理器来执行以下操作:
- 读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里面
- 对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数,以及命令参数的个数,然后分别将参数和参数个数保存到客户端状态的argv属性和argc属性里面
- 调用命令执行器,执行客户端指定的命令
之后,服务器将通过调用命令执行器来完成执行命令所需的余下步骤,以下几个小节将分别介绍命令执行器所执行的工作
14.1.3 命令执行器(1):查找命令实现
命令执行器要做的第一件事就是根据客户端状态的argv[0]参数,在命令表(command table)中查找参数所指定的命令,并将找到的命令保存到客户端状态的cmd属性里面
命令表是一个字典,字典的键是命令名字,值是redisCommand结构
redisCommand结构的主要属性如图
sflags属性可以使用的标识值,以及这些标识的意义如图
客户端状态下的cmd指针例子如图
14.1.4 命令执行器(2):执行预备操作
到目前为止,服务器已经将执行命令所需的命令实现函数(保存在客户端状态的cmd属性)、参数(保存在客户端状态的argv属性)、参数个数(保存在客户端状态的argc属性)都收集齐了,但是在真正执行命令之前,程序还需要进行一些预备操作,从而确保命令可以正确、顺利地被执行,这些操作包括:
-
检查客户端状态的cmd指针是否指向NULL
-
根据客户端cmd属性指向的redisCommand结构的arity 属性,检查命令请求所给定的参数个数是否正确
-
检查客户端是否已经通过了身份验证
。。。
14.1.5 命令执行器(3):调用命令的实现函数
执行client->cmd->proc(client)
产生的命令回复会被保存
在客户端状态的输出缓冲区里面(buf属性和reply 属性),之后实现函数还会为客户端的套接字关联命令回复处理器,这个处理器负责将命令回复返回给客户端
14.1.6 命令执行器(4):执行后续工作
在执行完实现函数之后,服务器还需要执行一些后续工作:
- 慢查询日志
- 更新被执行命令的redisCommand结构的milliseconds属性,并将命令的redisCommand结构的calls计数器的值增一
- AOF持久化
- 复制给所有从服务器
14.1.7 将命令回复发送给客户端
当客户端套接字变为可写状态时,服务器就会执行命令回复处理器,将保存在客户端输出缓冲区中的命令回复发送给客户端。当命令回复发送完毕之后,回复处理器会清空客户端状态的输出缓冲区
14.1.8 客户端接收并打印命令回复
当客户端接收到协议格式的命令回复之后,它会将这些回复转换成人类可读的格式,并打印给用户观看
14.2 serverCron函数
Redis服务器中的serverCron函数默认每隔100毫秒执行一次,这个函数负责管理服务器的资源,并保持服务器自身的良好运转
struct redisServer {
// ...
time_t unixtime; // 保存了秒级精度的系统当前UNIX 时间戳
long long mstime; // 保存了毫秒级精度的系统当前UNIX 时间戳
unsigned lruclock:22; // 默认每10秒更新一次的时钟缓存,用于计算键的空转(idle )时长
long long ops_sec_last_sample_time; // 上一次进行抽样的时间
long long ops_sec_last_sample_ops; // 上一次抽样时,服务器已执行命令的数量
long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES]; // REDIS_OPS_SEC_SAMPLES 大小(默认值为16 )的环形数组,数组中的每个项都记录了一次抽样结果。
int ops_sec_idx; // ops_sec_samples 数组的索引值,每次抽样后将值自增一,在值等于16 时重置为0 ,让ops_sec_samples 数组构成一个环形数组。
size_t stat_peak_memory; // 已使用内存峰值
int shutdown_asap; // 关闭服务器的标识:值为1时,关闭服务器,值为0时,不做动作。
int aof_rewrite_scheduled; // 如果值为1 ,那么表示有 BGREWRITEAOF 命令被延迟了。
pid_t rdb_child_pid; // 记录执行BGSAVE 命令的子进程的ID:如果服务器没有在执行BGSAVE,那么这个属性的值为-1 /* PID of RDB saving child
pid_t aof_child_pid; // 记录执行BGREWRITEAOF 命令的子进程的ID,如果服务器没有在执行BGREWRITEAOF,那么这个属性的值为-1 /* PID if rewriting process */
int cronloops; // serverCron 函数的运行次数计数器,serverCron 函数每执行一次,这个属性的值就增一
};
14.2.1 更新服务器时间缓存
serverCron函数默认会以每100毫秒一次的频率更新unixtime属性和mstime属性,所以这两个属性记录的时间的精确度并不高
- 服务器只会在打印日志、更新服务器的LRU时钟、决定是否执行持久化任务、计算服务器上线时间(uptime)这类对时间精确度要求不高的功能上
- 对于为键设置过期时间、添加慢查询日志这种需要高精确度时间的功能来说,服务器还是会再次执行系统调用,从而获得最准确的系统当前时间。
14.2.2 更新LRU时钟
每个Redis对象都会有一个lru属性,这个lru属性保存了对象最后一次被命令访问的时间:会以每10秒一次的频率更新lruclock属性的值
14.2.3 更新服务器每秒执行命令次数
serverCron函数中的trackOperationsPerSecond函数会以每100毫秒一次的频率执行,这个函数的功能是以抽样计算的方式,估算并记录服务器在最近一秒钟处理的命令请求数量
14.2.4 更新服务器内存峰值记录
服务器状态中的stat_peak_memory属性记录了服务器的内存峰值大小
14.2.5 处理SIGTERM信号
在启动服务器时,Redis会为服务器进程的SIGTERM信号关联处理器sigtermHandler函数,这个信号处理器负责在服务器接到SIGTERM信号时,打开服务器状态的shutdown_asap标识
14.2.6 管理客户端资源
serverCron函数每次执行都会调用clientsCron函数,clientsCron函数会对一定数量的客户端进行以下两个检查:
- 如果客户端与服务器之间的连接已经超时(很长一段时间里客户端和服务器都没有交互),那么程序释放这个客户端
- 如果客户端在上一次执行命令请求之后,输入缓冲区的大小超过了一定的长度,那么程序会释放客户端当前的输入缓冲区,并重新创建一个默认大小的输入缓冲区,从而防止客户端的输入缓冲区耗费了过多的内存
14.2.7 管理数据库资源
serverCron函数每次执行都会调用databasesCron函数,这个函数会对服务器中的一部分数据库进行检查,删除其中的过期键,并在有需要时,对字典进行收缩操作
14.2.8 执行被延迟的BGREWRITEAOF
在服务器执行BGSAVE命令的期间,如果客户端向服务器发来BGREWRITEAOF命令,那么服务器会将BGREWRITEAOF命令的执行时间延迟到BGSAVE命令执行完毕之后
服务器的aof_rewrite_scheduled标识记录了服务器是否延迟了BGREWRITEAOF命令
每次serverCron函数执行时,函数都会检查BGSAVE命令或者BGREWRITEAOF命令是否正在执行,如果这两个命令都没在执行,并且aof_rewrite_scheduled属性的值为1,那么服务器就会执行之前被推延的BGREWRITEAOF命令
14.2.9 检查持久化操作的运行状态
服务器状态使用rdb_child_pid属性和aof_child_pid属性记录执行BGSAVE命令和BGREWRITEAOF命令的子进程的ID,这两个属性也可以用于检查BGSAVE命令或者BGREWRITEAOF命令是否正在执行
14.2.10 将AOF缓冲区中的内容写入AOF文件
如果服务器开启了AOF持久化功能,并且AOF缓冲区里面还有待写入的数据,那么serverCron函数会调用相应的程序,将AOF缓冲区中的内容写入到AOF文件里面
14.2.11 关闭异步客户端
服务器会关闭那些输出缓冲区大小超出限制的客户端
14.2.12 增加cronloops计数器的值
服务器状态的cronloops属性记录了serverCron函数执行的次数
唯一的作用的执行是每执行serverCron函数N次就执行一次指定代码
if cronloops % N == 0:
# 执行指定代码...
14.3 初始化服务器
一个Redis服务器从启动到能够接受客户端的命令请求,需要经过一系列的初始化和设置过程,比如初始化服务器状态,接受用户指定的服务器配置,创建相应的数据结构和网络连接等等
14.3.1 初始化服务器状态结构
初始化服务器的第一步就是创建一个struct redisServer类型的实例变量server作为服务器的状态,并为结构中的各个属性设置默认值。由redis.c/initServerConfig函数完成,其主要工作如下
- 设置服务器的运行ID
- 设置服务器的默认运行频率
- 设置服务器的默认配置文件路径
- 设置服务器的运行架构
- 设置服务器的默认端口号
- 设置服务器的默认RDB持久化条件和AOF持久化条件
- 初始化服务器的LRU时钟
- 创建命令表
14.3.2 载入配置选项
在启动服务器时,用户可以通过给定配置参数或者指定配置文件来修改服务器的默认配置
14.3.3 初始化服务器数据结构
创建其他数据结构,服务器必须先载入用户指定的配置选项,然后才能正确地对数据结构进行初始化。执行initServe函数
服务器选择了将server状态的初始化分为两步进行,initServerConfig函数主要负责初始化一般属性,而
initServer函数主要负责初始化数据结构
当initServer函数执行完毕之后,服务器将用ASCII字符在日志中打印出Redis的图标,以及Redis的版本号信息,即Redis图标logo
14.3.4 还原数据库状态
在完成了对服务器状态server变量的初始化之后,服务器需要载入RDB文件或者AOF文件,并根据文件记录的内容来还原服务器的数据库状态
如果启动了AOF持久化功能,使用AOF文件来还原数据库状态。否则使用RDB文件来还原数据库状态
14.3.5 执行事件循环
开始执行服务器的事件循环(loop),接受客户端的连接请求,并处理客户端发来的命令请求
14.4 重点回顾
- 一个命令请求从发送到完成主要包括以下步骤:1)客户端将命令请求发送给服务器;2)服务器读取命令请求,并分析出命令参数;3)命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复;4)服务器将命令回复返回给客户端
- serverCron函数默认每隔100毫秒执行一次,它的工作主要包括更新服务器状态信息,处理服务器接收的SIGTERM信号,管理客户端资源和数据库状态,检查并执行持久化操作等等
- 服务器从启动到能够处理客户端的命令请求需要执行以下步骤:1)初始化服务器状态;2)载入服务器配置;3)初始化服务器数据结构;4)还原数据库状态;5)执行事件循环。
数;3)命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复;4)服务器将命令回复返回给客户端
- serverCron函数默认每隔100毫秒执行一次,它的工作主要包括更新服务器状态信息,处理服务器接收的SIGTERM信号,管理客户端资源和数据库状态,检查并执行持久化操作等等
- 服务器从启动到能够处理客户端的命令请求需要执行以下步骤:1)初始化服务器状态;2)载入服务器配置;3)初始化服务器数据结构;4)还原数据库状态;5)执行事件循环。