慢查询日志
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 函数的作用有两个:
检查命令的执行时长是否超过 slowlog-log-slower-than 选项所设置的时间, 如果是的话, 就为命令创建一个新的日志, 并将新日志添加到 slowlog 链表的表头。
检查慢查询日志的长度是否超过 slowlog-max-len 选项所设置的长度, 如果是的话, 那么将多出来的日志从 slowlog 链表中删除掉。
慢查询定位&处理
使用slowlog get 可以获得执行较慢的redis命令,针对该命令可以进行优化:
尽量使用短的key,对于value有些也可精简,能使用int就int。
避免使用keys *、hgetall等全量操作。
减少大key的存取,打散为小key 100K以上
将rdb改为aof模式
想要一次添加多条数据的时候可以使用管道
尽可能地使用哈希存储
尽量限制下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进行可视化及监控。