源码阅读-Redis独立功能: 慢查询日志

Posted by keys961 on December 11, 2019

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的慢查询日志的实现非常简单,只是简单检查命令执行时间,并维护一个日志列表而已。