1. Overview
和MySQL类似,Redis也有一个慢查询日志的功能,用于排查、监视和优化查询的速度。
启动和配置Redis的慢查询日志,主要有下面几个参数:
slowlog-log-slower-than
:当执行时间超过这个时间,就会添加一个慢查询日志项,单位微秒(当为负数时,不开启慢查询日志功能)slowlog-max-len
:慢查询日志的条目上限
通过SLOWLOG GET [count]
命令,即可看到Redis记录的慢查询日志,慢查询日志包含:
- 日志标识
- 命令执行的时间戳
- 命令执行的时长
- 命令本身
- 客户端信息
2. 慢查询日志的保存
慢查询日志保存在redisServer
的一个列表中,字段为slowlog
,列表的每一项都是slowlogEntry
:
struct redisServer {
// ...
list *slowlog; // 慢查询日志列表
long long slowlog_entry_id; // 最新的慢查询日志项ID
long long slowlog_log_slower_than; // 慢查询日志设置的时间界限
unsigned long slowlog_max_len; // 慢查询日志的最大项数
// ...
}
typedef struct slowlogEntry {
robj **argv; // 命令参数
int argc; // 参数个数
long long id; // 日志ID
long long duration; // 命令执行时长,单位为微秒
time_t time; // 命令执行时间戳
sds cname; // 执行命令的客户端名字
sds peerid; // 执行命令的客户端地址
} slowlogEntry;
Redis命令执行时,会统计执行的时长,当时间超过了配置的上限,就会创建慢查询日志项,并添加到这个日志列表中。这部分代码在下面有所体现,这里截取关键部分:
void call(client *c, int flags) {
// ...
// 1. 计算命令执行时长
start = ustime();
c->cmd->proc(c);
duration = ustime()-start;
// ...
if (flags & CMD_CALL_SLOWLOG && c->cmd->proc != execCommand) {
// 一般下,flag会带有CMD_CALL_SLOWLOG,因为通常flag为CMD_CALL_FULL
// 只要不是EXEC命令,一般都能进入这里
char *latency_event = (c->cmd->flags & CMD_FAST) ?
"fast-command" : "command";
latencyAddSampleIfNeeded(latency_event,duration/1000);
// 2. 检查命令执行时长,若超时则创建慢查询日志项,并添加到列表中
slowlogPushEntryIfNeeded(c,c->argv,c->argc,duration);
}
// ...
}
关键添加慢查询日志项的函数就是slowlogPushEntryIfNeeded
,也很简单,即:
- 检查是否超时,若超时则创建日志项,添加到表头中
- 删除多余的日志项,从最旧的开始删
void slowlogPushEntryIfNeeded(client *c, robj **argv, int argc, long long duration) {
// slowlog-log-slower-than为负数时,即关闭慢查询日志,直接返回
if (server.slowlog_log_slower_than < 0) return; /* Slowlog disabled */
// 若超时,则创建慢查询日志项,添加到列表头
if (duration >= server.slowlog_log_slower_than)
listAddNodeHead(server.slowlog,
slowlogCreateEntry(c,argv,argc,duration));
// 若日志项过多,删除最旧的几个日志项
while (listLength(server.slowlog) > server.slowlog_max_len)
listDelNode(server.slowlog,listLast(server.slowlog));
}
3. 查看慢查询日志
可通过命令SLOWLOG GET [count]
查询最新的几个慢查询日志项,其中count
参数可选(不指定则默认为10)。
其实现也很简单,遍历慢查询日志列表,并响应给客户端即可。响应的内容就是slowlogEntry
定义的全部内容:
void slowlogCommand(client *c) {
// ...
else if ((c->argc == 2 || c->argc == 3) &&
!strcasecmp(c->argv[1]->ptr,"get"))
{
long count = 10, sent = 0;
listIter li;
void *totentries;
listNode *ln;
slowlogEntry *se;
if (c->argc == 3 &&
getLongFromObjectOrReply(c,c->argv[2],&count,NULL) != C_OK)
return;
listRewind(server.slowlog,&li);
totentries = addDeferredMultiBulkLength(c);
// 遍历慢查询日志的前count项,并将日志项响应给客户端
while(count-- && (ln = listNext(&li))) {
int j;
se = ln->value;
addReplyMultiBulkLen(c,6);
addReplyLongLong(c,se->id);
addReplyLongLong(c,se->time);
addReplyLongLong(c,se->duration);
addReplyMultiBulkLen(c,se->argc);
for (j = 0; j < se->argc; j++)
addReplyBulk(c,se->argv[j]);
addReplyBulkCBuffer(c,se->peerid,sdslen(se->peerid));
addReplyBulkCBuffer(c,se->cname,sdslen(se->cname));
sent++;
}
setDeferredMultiBulkLength(c,totentries,sent);
}
// ...
}
4. 总结
相比其它关系式数据库,如MySQL,Redis的慢查询日志的实现非常简单,只是简单检查命令执行时间,并维护一个日志列表而已。