离线下载
PDF版 ePub版

郑思愿 · 更新于 2017-08-23 10:00:06

Redis 日志和断言

Redis 日志

linux 的世界里,最好用的调试工具不是 gdb,而是日志和 printf。日志在一个软件系统中是非常常见的,一个关键的作用即定位错误,当系统出问题首先想到就是日志,查看日志能快速定位问题。Redis 中的日志模块较为简单。我们在 Redis 源码中,到处都可以见到 redisLog()。

通常,日志会分为几个级别。在 Redis 中 5 个日志级别,在 redis.h 文件中有定义:

/* Log levels */
#define REDIS_DEBUG 0 // 调试级别,这一级别产生最多的日志信息
#define REDIS_VERBOSE 1
#define REDIS_NOTICE 2
#define REDIS_WARNING 3
#define REDIS_LOG_RAW (1<<10) /* Modifier to log without timestamp */
#define REDIS_DEFAULT_VERBOSITY REDIS_NOTICE

服务器的配置结构体中,struct redisServer.verbosity 是用来设定日志级别的,譬如将日志级别设定为REDIS_NOTICE 后,代码中 REDIS_VERBOSE 和REDIS_DEBUG 级别的日志都不会被打印。

日志级别值越是低,日志级别越高,产生了日志也就越多,开发人员在产品上线之前会将日志级别调至最低,方便发现定位或发现潜在的问题。而上线之后,可以将志级别降低,减少调试日志。如果日志级别过高,则日志量大,可能会对线上的服务产生影响,因为写日志就是写文件操作,系统调用是要消耗时间的。

日志是想要记录某一个时间点,在哪里发送了什么事情,以方便出现问题的时候,恢复现场,快速定位问题所在。“某一时间点”即添加时间戳;“在哪里”即程序执行的位置,对应的是源码的文件,行号函数等;“发生了什么事情”即记录一些关键数据。

// redis 日志函数,会将给定的数据写入日志文件,和常用的printf 函数用法差不多
void redisLog(int level, const char *fmt, ...) {
    va_list ap;
    char msg[REDIS_MAX_LOGMSG_LEN];
    // 如果日志级别小于预设的日志级别,直接返回
    if ((level&0xff) < server.verbosity) return;
    va_start(ap, fmt);
    vsnprintf(msg, sizeof(msg), fmt, ap);
    va_end(ap);
    // redisLogRaw() 函数将给定的信息,在增加时间戳和进程id 后写入日志文件
    redisLogRaw(level,msg);
}

Redis 断言

为什么需要断言?»»»»»»> TODO 当你认为某些事情在正常情况下不可能出现,应尽可能结束任务,而不是捕捉错误,尝试挽救。同样在西加加里,使用 try…catch() 会让程序的逻辑变乱,甚至让程序的行为变得不可预测,大胆的使用断言吧。

Redis 中不仅仅实现了断言,且在断言失败的时候会打印一些关键的信息。

在 Redis.h 中定义了两个断言相关的宏:

#define redisAssertWithInfo(_c,_o,_e) \
    ((_e)?(void)0 : \
    (_redisAssertWithInfo(_c,_o,#_e,__FILE__,__LINE__),_exit(1)))
#define redisAssert(_e) \
    ((_e)?(void)0 : \
    (_redisAssert(#_e,__FILE__,__LINE__),_exit(1)))

如果断言为真,执行一个空操作;断言为假,会打印关键的信息。

_redisAssert() 函数会记录断言发生的错误信息,文件名和行号

void _redisAssert(char *estr, char *file, int line) {
    // 向日志文件中写入BUG 头部
    bugReportStart();
    // 将文件名,行号,错误信息写入日志
    redisLog(REDIS_WARNING,"=== ASSERTION FAILED ===");
    redisLog(REDIS_WARNING,"==> %s:%d '%s' is not true",file,line,estr);
    // 如果需要,可以记录错误信息,文件名和行号,以便在进程崩溃后调试(gdb core?)
#ifdef HAVE_BACKTRACE
    server.assert_failed = estr;
    server.assert_file = file;
    server.assert_line = line;
    redisLog(REDIS_WARNING,"(forcing SIGSEGV to print the bug report.)");
#endif
    // 强制segmentation fault。无效的内存访问,可以产生SIGSEGV,如此会
    // 产生coredump 文件以供进程崩溃后调试使用
    *((char*)-1) = 'x';
}

这有个小有意思的语句:*((char*)-1) = ’x’;(char *)-1表示指向地址值为 -1 的指针,它所指向的内存肯定是非法的,对非法内存的操作会触发 SIGSEGV 信号,进程结束后会产生 coredump 文件,方便调试使用。使用gdb、可执行文件和 coredump 文件能快速定位问题所在,即使进程已经崩溃了。

_redisAssertWithInfo() 函数会打印 Redis 服务器当前服务的客户端和某个关键 Redis 对象的信息,具体请参看源码,在这不展开了。