慢查询日志

Redis也有慢查询日志,可用于监视和优化查询

慢查询设置

在redis.conf中可以配置和慢查询日志相关的选项,此时设置是全局的

1
2
3
4
5
#执行时间超过多少微秒的命令请求会被记录到日志上 0 :全记录 <0 不记录
slowlog-log-slower-than 10000

#slowlog-max-len 存储慢查询日志条数
slowlog-max-len 128

Redis使用列表存储慢查询日志,采用队列方式(FIFO)

config set的方式可以临时设置,redis重启后就无效

config set slowlog-log-slower-than 微秒

config set slowlog-max-len 条数

查看日志: slowlog get [n]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
127.0.0.1:6379> config set slowlog-log-slower-than 0
OK
127.0.0.1:6379> config set slowlog-max-len 2
OK
127.0.0.1:6379> set name:001 zhaoyun
OK
127.0.0.1:6379> set name:002 zhangfei
OK
127.0.0.1:6379> get name:002
"zhangfei"

127.0.0.1:6379> slowlog get
1) 1) (integer) 7 #日志的唯一标识符(uid)
2) (integer) 1589774302 #命令执行时的UNIX时间戳
3) (integer) 65 #命令执行的时长(微秒)
4) 1) "get" #执行命令及参数
2) "name:002"
5) "127.0.0.1:37277"
6) ""
2) 1) (integer) 6
2) (integer) 1589774281
3) (integer) 7
4) 1) "set"
2) "name:002"
3) "zhangfei"
5) "127.0.0.1:37277"
6) ""

# set和get都记录,第一条被移除了。

慢查询记录的保存

在redisServer中保存和慢查询日志相关的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct redisServer {
// ...
// 下一条慢查询日志的 ID
long long slowlog_entry_id;

// 保存了所有慢查询日志的链表 FIFO
list *slowlog;

// 服务器配置 slowlog-log-slower-than 选项的值
long long slowlog_log_slower_than;

// 服务器配置 slowlog-max-len 选项的值
unsigned long slowlog_max_len;
// ...
};

lowlog 链表保存了服务器中的所有慢查询日志, 链表中的每个节点都保存了一个 slowlogEntry 结构, 每个 slowlogEntry 结构代表一条慢查询日志。

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct slowlogEntry {
// 唯一标识符
long long id;
// 命令执行时的时间,格式为 UNIX 时间戳
time_t time;
// 执行命令消耗的时间,以微秒为单位
long long duration;
// 命令与命令参数
robj **argv;
// 命令与命令参数的数量
int argc;
} slowlogEntry;

慢查询日志的阅览&删除

初始化日志列表

1
2
3
4
5
void slowlogInit(void) {
server.slowlog = listCreate(); /* 创建一个list列表 */
server.slowlog_entry_id = 0; /* 日志ID从0开始 */
listSetFreeMethod(server.slowlog,slowlogFreeEntry); /* 指定慢查询日志list空间 的释放方法 */
}

获得慢查询日志记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def SLOWLOG_GET(number=None):
# 用户没有给定 number 参数
# 那么打印服务器包含的全部慢查询日志
if number is None:
number = SLOWLOG_LEN()

# 遍历服务器中的慢查询日志
for log in redisServer.slowlog:
if number <= 0:
# 打印的日志数量已经足够,跳出循环
break
else:
# 继续打印,将计数器的值减一
number -= 1
# 打印日志
printLog(log)

查看日志数量的 slowlog len

1
2
3
def SLOWLOG_LEN():
# slowlog 链表的长度就是慢查询日志的条目数量
return len(redisServer.slowlog)

清除日志 slowlog reset

1
2
3
4
5
def SLOWLOG_RESET():
# 遍历服务器中的所有慢查询日志
for log in redisServer.slowlog:
# 删除日志
deleteLog(log)

添加日志实现

在每次执行命令的之前和之后, 程序都会记录微秒格式的当前 UNIX 时间戳, 这两个时间戳之间的差就是服务器执行命令所耗费的时长, 服务器会将这个时长作为参数之一传给 slowlogPushEntryIfNeeded 函数, 而 slowlogPushEntryIfNeeded 函数则负责检查是否需要为这次执行的命令创建慢查询日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 记录执行命令前的时间
before = unixtime_now_in_us()

//执行命令
execute_command(argv, argc, client)

//记录执行命令后的时间
after = unixtime_now_in_us()

// 检查是否需要创建新的慢查询日志
slowlogPushEntryIfNeeded(argv, argc, before-after)

void slowlogPushEntryIfNeeded(robj **argv, int argc, long long duration) {
if (server.slowlog_log_slower_than < 0) return; /* Slowlog disabled */ /* 负 数表示禁用 */

if (duration >= server.slowlog_log_slower_than) /* 如果执行时间 > 指定阈值*/
listAddNodeHead(server.slowlog,slowlogCreateEntry(argv,argc,duration)); /* 创建一个slowlogEntry对象,添加到列表首部*/

while (listLength(server.slowlog) > server.slowlog_max_len) /* 如果列表长度 > 指定长度 */
listDelNode(server.slowlog,listLast(server.slowlog)); /* 移除列表尾部元素 */
}

slowlogPushEntryIfNeeded 函数的作用有两个:

  1. 检查命令的执行时长是否超过 slowlog-log-slower-than 选项所设置的时间, 如果是的话, 就为命令创建一个新的日志, 并将新日志添加到 slowlog 链表的表头。

  2. 检查慢查询日志的长度是否超过 slowlog-max-len 选项所设置的长度, 如果是的话, 那么将多出来的日志从 slowlog 链表中删除掉。

慢查询定位&处理

使用slowlog get 可以获得执行较慢的redis命令,针对该命令可以进行优化:

    1. 尽量使用短的key,对于value有些也可精简,能使用int就int。
    1. 避免使用keys *、hgetall等全量操作。
    1. 减少大key的存取,打散为小key 100K以上
    1. 将rdb改为aof模式
    • rdb fork 子进程 数据量过大 主进程阻塞 redis性能大幅下降

    • 关闭持久化 , (适合于数据量较小,有固定数据源)

    1. 想要一次添加多条数据的时候可以使用管道
    1. 尽可能地使用哈希存储
    1. 尽量限制下redis使用的内存大小,这样可以避免redis使用swap分区或者出现OOM错误内存与硬盘的swap

监视器

Redis客户端通过执行MONITOR命令可以将自己变为一个监视器,实时地接受并打印出服务器当前处理的命令请求的相关信息。

此时,当其他客户端向服务器发送一条命令请求时,服务器除了会处理这条命令请求之外,还会将这条命令请求的信息发送给所有监视器。


Redis客户端1

1
2
3
4
5
127.0.0.1:6379> monitor
OK
1589706136.030138 [0 127.0.0.1:42907] "COMMAND"
1589706145.763523 [0 127.0.0.1:42907] "set" "name:10" "zhaoyun"
1589706163.756312 [0 127.0.0.1:42907] "get" "name:10"

Redis客户端2

1
2
3
127.0.0.1:6379> set name:10 zhaoyun
OK
127.0.0.1:6379> get name:10 "zhaoyun"

实现监视器

redisServer 维护一个 monitors 的链表,记录自己的监视器,每次收到 MONITOR 命令之后,将客户端追加到链表尾。

1
2
3
4
5
6
7
void monitorCommand(redisClient *c) {
/* ignore MONITOR if already slave or in monitor mode */
if (c->flags & REDIS_SLAVE) return;
c->flags |= (REDIS_SLAVE|REDIS_MONITOR);
listAddNodeTail(server.monitors,c);
addReply(c,shared.ok); //回复OK
}

向监视器发送命令信息

利用call函数实现向监视器发送命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// call() 函数是执行命令的核心函数,这里只看监视器部分
/*src/redis.c/call*/
/* Call() is the core of Redis execution of a command */
void call(redisClient *c, int flags) {
long long dirty, start = ustime(), duration;
int client_old_flags = c->flags;
/* Sent the command to clients in MONITOR mode, only if the commands are
* not generated from reading an AOF.
*/
if (listLength(server.monitors) && !server.loading && !(c->cmd->flags & REDIS_CMD_SKIP_MONITOR)){
replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
}
......
}

call 主要调用了 replicationFeedMonitors ,这个函数的作用就是将命令打包为协议,发送给监视器。

Redis监控平台

grafana、prometheus以及redis_exporter。

Grafana 是一个开箱即用的可视化工具,具有功能齐全的度量仪表盘和图形编辑器,有灵活丰富的图形化选项,可以混合多种风格,支持多个数据源特点。

Prometheus是一个开源的服务监控系统,它通过HTTP协议从远程的机器收集数据并存储在本地的时序数据库上。

redis_exporter为Prometheus提供了redis指标的导出,配合Prometheus以及grafana进行可视化及监控。