Redis学习笔记 - 服务器(2) - serverCron函数

参考:<<Redis设计与实现>>

  • 注:这本书是基于Redis3.0版本写的,和后面的版本有点差异

serverCron函数:这个函数负责管理服务器的资源,并保持服务器自身的良好运转。默认每隔100ms执行一次。

下面介绍serverCron函数执行的操作,以及redisServer结构(服务器状态)和该函数有关的属性。

1. 更新服务器时间缓存

Redis服务器中有许多功能都需要获取系统当前时间,而每次获取都需要执行一次系统调用,为了减少系统执行次数,服务器状态的unixtimemstime属性被用作当前时间的缓存。结构如下:

struct redisServer {
	// ...
	// 系统当前UNIX时间戳,单位:s
	time_t unixtime;
	// 系统当前UNIX时间戳,单位:ms
	long long mstime;
	// ...
}

serverCron函数默认每100ms更新unixtimemstime属性,所以这两个属性记录的时间的精确度不高:

  • 服务器只会在打印日志、更新服务器的LRU时钟、决定是否执行持久化功能、计算服务器上线时间(uptime)这类对时间精度要求不高的功能上使用这两个时间属性
  • 为键设置过期时间、添加慢查询日志这种需要高精确度时间的功能,服务器会再次执行系统调用,获取精确的系统当前时间

2. 更新LRU时钟

服务器状态中的lrulock属性保存了服务器LRU时钟,serverCron函数默认每10s更新lrulock属性的值。

这个属性和上面的unixtimemstime属性一样,都是服务器时间缓存的一种:

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_opsops_sec_last_sample_time1000 \frac {服务器当前执行命令数 - ops\_sec\_last\_sample\_ops记录的上一次抽样已执行命令数} {服务器当前时间 - ops\_sec\_last\_sample\_time记录的上一次抽样时间} *1000 服务器当前时间−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_peakused_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都会引起新的持久化操作,所以也要确认是否正在执行持久化操作)

判断是否需要持久化流程如下图所示:
Redis学习笔记 - 服务器(2) - serverCron函数

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 :
	# 执行指定代码...
上一篇:C# 从零开始 vol.1


下一篇:解决svn update 产生Node remains in conflict的报错问题