参考:<<Redis设计与实现>>
- 注:这本书是基于Redis3.0版本写的,和后面的版本有点差异
serverCron
函数:这个函数负责管理服务器的资源,并保持服务器自身的良好运转。默认每隔100ms执行一次。
下面介绍serverCron函数执行的操作,以及redisServer结构(服务器状态)和该函数有关的属性。
1. 更新服务器时间缓存
Redis服务器中有许多功能都需要获取系统当前时间,而每次获取都需要执行一次系统调用,为了减少系统执行次数,服务器状态的unixtime
和mstime
属性被用作当前时间的缓存。结构如下:
struct redisServer {
// ...
// 系统当前UNIX时间戳,单位:s
time_t unixtime;
// 系统当前UNIX时间戳,单位:ms
long long mstime;
// ...
}
serverCron函数默认每100ms更新unixtime
和mstime
属性,所以这两个属性记录的时间的精确度不高:
- 服务器只会在打印日志、更新服务器的LRU时钟、决定是否执行持久化功能、计算服务器上线时间(uptime)这类对时间精度要求不高的功能上使用这两个时间属性
- 为键设置过期时间、添加慢查询日志这种需要高精确度时间的功能,服务器会再次执行系统调用,获取精确的系统当前时间
2. 更新LRU时钟
服务器状态中的lrulock
属性保存了服务器LRU时钟,serverCron
函数默认每10s更新lrulock
属性的值。
这个属性和上面的unixtime
、mstime
属性一样,都是服务器时间缓存的一种:
struct redisServer {
// ...
// 系统当前UNIX时间戳,单位:s
time_t unixtime;
// 系统当前UNIX时间戳,单位:ms
long long mstime;
// 默认每10s更新一次时钟缓存,用于计算键的空转(idle)时长
unsigned lruclock:22;
// ...
}
通过INFO server
命令的lru_clock查看该属性的值:
redis> info server
# Server
# ...
redis_server:3.0
tcp_port:6379
lru_clock:4688208
# ...
2.1 计算键(值对象)的空转时间
每个Redis对象都有一个lru
属性,这个属性保存了对象最后一次被命令访问的时间。
typedef struct redisObject {
// ...
unsigned lru:22;
// ...
} robj;
服务器计算一个数据库键(也是键对应值对象)的空转时间,程序会使用:
- 服务器的lruclock属性值 - 对象的lru属性
注:由于lruclock不是实时的,所及计算出来的LRU时间也是一个模糊的值
示例:
redis> set num 1
OK
# 等几秒再执行
redis> object idletime num
(integer) 5
redis> get num
"1"
redis> object idletime num
(integer) 0
3. 更新服务器每秒执行命令次数
serverCron
函数中的trackOperationsPerSecond
函数每100ms执行一次,该函数主要是以抽样计算的方式,估算并记录服务器在最近1s处理的命令请求的数量。
通过info stats
命令的 instantaneous_ops_per_sec
属性查看查看该值,如值是2,表示在最近1s内,服务器处理了大约2个命令:
redis> info stats
# Stats
# ...
instantaneous_ops_per_sec:2
trackOperationsPerSecond
函数和服务器状态中的4个 ops_sec_ 开头的属性有关:
struct redisServer {
// ...
// 系统当前UNIX时间戳,单位:s
time_t unixtime;
// 系统当前UNIX时间戳,单位:ms
long long mstime;
// 默认每10s更新一次时钟缓存,用于计算键的空转(idle)时长
unsigned lruclock:22;
// 上一次进行抽样的时间
long long ops_sec_last_sample_time;
// 上一次抽样时,服务器已经执行命令的数量
long long ops_sec_last_sample_ops;
// 数组中每项记录了一次抽样结果,REDIS_OPS_SEC_SAMPLES默认值为16
long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES];
// ops_sec_samples数组的索引值,每次抽样后+1,等于16时重置为0,这样可以让ops_sec_samples构成一个环形数组
int ops_sec_idx;
// ...
}
trackOperationsPerSecond
函数每次运行时,通过公式计算每秒执行命令数量,这个估值会被作为一个新的数组项放入ops_sec_samples环形数组里,公式如下:
服务器当前时间−ops_sec_last_sample_time记录的上一次抽样时间服务器当前执行命令数−ops_sec_last_sample_ops记录的上一次抽样已执行命令数∗1000
在客户端执行info命令时,服务器调用getOperationsPerSecond
函数,然后根据ops_sec_samples环形数组中的抽样结果计算出平均值,也就是info stats
中的 instantaneous_ops_per_sec
属性的值。
4. 更新服务器内存峰值记录
服务器状态中的 stat_peak_memory 属性记录了服务器内存峰值大小:
struct redisServer {
// ...
// 已使用内存峰值
size_t stat_peak_memory;
// ...
}
serverCron
函数执行时,程序会查看服务器当前使用的内存,与 stat_peak_memory
属性记录的值进行比较,如果大于该值则更新该属性。
info memory
命令的 use_memory_peak
和 used_memory_peak_human
两个属性以两种格式记录了服务器内存峰值:
redis> info memory
# Memory
used_memory_peak:1056288
used_memory_peak_human:1.01M
...
5. 处理SIGTERM信号
在启动服务器时,Redis会为服务器进程的SIGTERM信号关联处理器sigtermHandler函数,这个信号处理器负责在服务器接到SIGTERM信号时,打开服务器状态的 shutdown_asap 标识:
// SIGTERM信号处理器
static void sigtermHandler(int sig) {
// 打印日志
redisLogFromHandler(REDIS_WARNING, "Received SIGTERM, scheduling shutdown...");
// 打开关闭标识
server.shutdown_asap = 1;
}
serverCron
函数执行时,程序会对服务器状态的 shutdown_asap 属性进行检查,并根据属性的值决定是否关闭服务器:
struct redisServer {
// ...
// 关闭服务器的标识,1关闭 0不做任何动作
int shutdown_asap;
// ...
}
在关闭服务器时候,可以看到如下日志:
33667:M 07 Aug 2019 23:22:05.537 # User requested shutdown...
33667:M 07 Aug 2019 23:22:05.537 * Calling fsync() on the AOF file.
33667:M 07 Aug 2019 23:22:05.537 * Saving the final RDB snapshot before exiting.
33667:M 07 Aug 2019 23:22:05.538 * DB saved on disk
33667:M 07 Aug 2019 23:22:05.538 * Removing the pid file.
33667:M 07 Aug 2019 23:22:05.538 # Redis is now ready to exit, bye bye...
从日志中可以看出,服务器关闭会调用fsync()函数将缓冲区数据同步到AOF文件中(开启AOF持久化),同时保存RDB快照(开启了RDB持久化)。
6. 管理客户端资源
serverCron
函数每次执行时都会调用clientCron
函数,该函数会对一定数量的客户端进行以下2个检查:
- 如果客户端与服务器之间的连接已经超时,那么程序释放这个客户端。
- 如果客户端上一次执行命令请求后,输入缓冲区大小超过了一定长度,程序会释放客户端当前的输入缓冲区,并重新创建一个默认大小的输入缓冲区,从而防止客户端的输入缓冲区耗费了过多内存。
7. 管理数据库资源
serverCron
函数每次执行时调用databasesCron
函数,这个函数会对服务器中的一部分数据库进行检查,删除其中的过期键,在有需要时,对字典进行收缩操作。
8. 执行被延迟的BGREWRITEAOF
在服务器执行BGSAVE
(RDB异步持久化)命令期间,如果客户端向服务器发送 BGREWRITEAOF
命令,那么服务器会将该命令延迟到 BGSAVE
命令执行完毕之后。
服务器的 aof_rewrite_scheduled
标识记录了服务器是否延迟了 BGREWRITEAOF
命令:
struct redisServer {
// ...
// 如果值为1,那么表示有 BGREWRITEAOF 命令被延迟了
int aof_rewrite_scheduled;
// ...
}
每次serverCron
函数执行时,函数都会检查BGSAVE
命令或者BGREWRITEAOF
是否正在执行,如果这两个命令都没在执行,并且 aof_rewrite_scheduled 属性值为1,那么执行之前被延迟的BGREWRITEAOF
命令。
9. 检查持久化操作的运行状态
服务器状态使用 rdb_child_pid
属性和 aof_child_pid
属性记录执行 BGSAVE
命令和 BGREWRITEAOF
命令的子进程ID,这两个属性也可以用于检查BGSAVE
命令和 BGREWRITEAOF
命令是否正在执行:
struct redisServer {
// ...
// 记录执行BGSAVE命令的子进程ID,没有执行该命令则值为-1
pid_t rdb_child_pid;
// 记录执行BGREWRITEAOF命令的zijinchegnID,没有执行该命令则值为-1
pid_t aof_child_pid;
// ...
}
每次serverCron
函数执行时,程序都会检查 rdb_child_pid 和 aof_child_pid 两个属性的值。
- 只要其中一个属性的值不为-1, 程序就会执行一次 wait3 函数,检查子进程是否有信号发来服务器进程:
- 如果有信号到达,表示新的RDB文件已经生成完毕或AOF文件已经重写完成。比如用新的RDB文件替换现有的RDB文件,或者用重写后的AOF文件替换现有的AOF文件。
- 如果没有信号到达,表示持久化操作未完成,程序不操作。
- 如果两个属性都为-1,表示服务器没有在进行持久化操作,此时,程序会执行以下3个检查:
- (1)查看是否有BGREWRITEAOF被延迟了,如果有,则开始一次新的 BGREWRITEAOF操作
- (2)检查服务器的自动保存条件是否已经满足,如果满足且服务器没有在执行其他持久化操作,那么服务器开始一次新的BGSAVE操作。(因为条件1会引发一次BGREWRITEAOF,所以在这个检查中会确认服务器是否已经在执行持久化操作)
- (3)检查服务器设置的AOF重写条件是否满足,如果满足且服务器没有在执行持久化操作,那么服务器开始一次新的BGREWRITEAOF操作。(条件1、2都会引起新的持久化操作,所以也要确认是否正在执行持久化操作)
判断是否需要持久化流程如下图所示:
10. 将AOF缓冲区内容写入AOF文件
如果服务器开启了AOF持久化功能,并且AOF缓冲区里还有待写入数据,那么serverCron
函数会调用相应的程序,将AOF缓冲区中的内容写入到AOF文件里。
11. 关闭异步客户端
服务器会关闭输出缓冲区大小超出限制的客户端。
12. 增加 cronloops计数器的值
服务器状态的 cronloops
属性记录了 serverCron
函数执行的次数:
struct redisServer {
// ...
// erverCron函数执行计数器,每执行一次就+1
int cronloops;
// ...
}
cronloops属性目前在服务器中唯一作用就是在复制模块中实现 “每执行serverCron
函数N次就执行一次指定代码功能”,方法伪代码如下所示:
if cronloops % N == 0 :
# 执行指定代码...