一组 Redis 实际应用中的异常场景及其根因分析和解决方案

时间:2022-06-05
本文章向大家介绍一组 Redis 实际应用中的异常场景及其根因分析和解决方案,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前言

在上一场 Chat《基于 Redis 的分布式缓存实现方案及可靠性加固策略》中,我已经较为全面的介绍了 Redis 的原理和分布式缓存方案。如果只是从“会用”的角度出发,已经有很多 Chat 和博客可供参考,但是,在实际应用中,异常场景时有出现,作为一名攻城狮,仅仅“会用”是不够的,还需要能够定位、解决实际应用中出现的异常问题。

我总结了一组 Redis 实际应用中遇到的异常场景,如 Redis 进程无法拉起,故障倒换失败,Slot 指派失败等,并针对这些异常场景给出了根因分析和可供参考的解决方案。如果你对 Redis 感兴趣并且在工作中可能使用 Redis,本文介绍的“踩坑”案例值得一看。

本场 Chat 涉及的实际应用异常场景及解决方案包括:

  1. 编译好的 Redis-Server 在 Linux 系统上无法启动;
  2. Redis 集群故障倒换失败,备节点无法升主;
  3. Redis 集群状态显示正常,但读写操作部分失败;
  4. Redis 集群 Slot 丢失后,重新指派 Slot 失败;

redis-server 启动报错

问题基本信息

xxx 项目中,使用 Redis 集群作为分布式缓存,它只是整个项目中的一个模块。 Redis 集群部署环境:Suse12(Linux) 每一次迭代,项目组都会编译一个大包进行验证,在同一套部署环境中,Redis 集群部署 “偶现” 失败,失败原因为部分节点上 redis-server 进程未能拉起,尝试用命令:./redis-server ./xxx/redis.conf 手动拉起 redis-server 进程报错:/lib64/libc.so.6: version `GLIBC_2.14' not found(required by /opt/…/redis-server)。

表因分析

很明显,报错显示安装环境 Linux 系统找不到 GLIBC2.14 版本库,而 redsi-server 依赖 GLIBC2.14,使用命令:strings /lib64/libc.so.6 | grep GLIBC 查看安装环境 GLIBC 版本,如下所示:

install_ENV:/opt/xxx/redis/bin # strings /lib64/libc.so.6 | grep GLIBCGLIBC_2.2.5
GLIBC_2.2.6
GLIBC_2.3
GLIBC_2.3.2
GLIBC_2.3.3
GLIBC_2.3.4
GLIBC_2.4
GLIBC_2.5
GLIBC_2.6
GLIBC_2.7
GLIBC_2.8
GLIBC_2.9
GLIBC_2.10
GLIBC_2.11
GLIBC_PRIVATE

可以看出,安装环境系统最高支持 GLIBC_2.11,低于需要的 2.14。至此,可初步定性为:编译 redis-server 的编译机 GLIBC 版本 (2.14) 高于安装环境的 GLIBC 版本 (2.11),即:高版本编译,低版本安装,因不兼容而安装失败。

进一步分析,使用命令:strings /lib/x86_64-linux-gnu/libc.so.6 | grep GLIBC (编译机是 Ubuntu)查看编译机的 GLIBC 版本:编译机 GLIBC 版本高达 2.18(为谨慎起见,查看 libc.so.6 的软连接,确认实际采用的 GLIBC 版本)。如果是 GLIBC 版本问题,编译机的版本远高于安装环境,上述问题不应该为 “偶现”,应该 “必现”,因此,GLIBC 版本不是导致上述问题的根因。

compile_ENV: # strings /lib/x86_64-linux-gnu/libc.so.6 | grep GLIBCGLIBC_2.2.5
GLIBC_2.2.6
GLIBC_2.3
GLIBC_2.3.2
GLIBC_2.3.3
GLIBC_2.3.4
GLIBC_2.4
GLIBC_2.5
GLIBC_2.6
GLIBC_2.7
GLIBC_2.8
GLIBC_2.9
GLIBC_2.10
GLIBC_2.11
GLIBC_2.12
GLIBC_2.13
GLIBC_2.14
GLIBC_2.15
GLIBC_2.16
GLIBC_2.17
GLIBC_2.18
GLIBC_PRIVATE

根因分析

在安装环境 redis-server 所在路径下输入命令:objdump -T redis-server| fgrep GLIBC2.14 查看 redis-server 依赖的 GLIBC2.14 版本库的具体函数:如下所示,只有一个函数 memcpy,依赖版本为 GLIBC_2.14。

install_ENV:/opt/xxx/redis/bin # objdump -T redis-server| fgrep GLIBC_2.140000000000000000      DF *UND*  0000000000000000  GLIBC_2.14 memcpy

输入命令:objdump -T /lib64/libc.so.6 | fgrep memcpy, 查看安装环境支持的 memcpy 版本:如下所示,安装环境支持的 memcpy 函数对应 GLIBC_2.2.5。

install_ENV:/opt/xxx/redis/bin # objdump -T /lib64/libc.so.6 | fgrep memcpy000000000008c400  w   DF .text  0000000000000009  GLIBC_2.2.5 wmemcpy
00000000000eef00 g    DF .text  000000000000001b  GLIBC_2.4   __wmemcpy_chk
0000000000084670 g    DF .text  0000000000000465  GLIBC_2.2.5 memcpy
0000000000084660 g    DF .text  0000000000000009  GLIBC_2.3.4 __memcpy_chk

输入命令:objdump -T /lib/x86_64-linux-gnu/libc.so.6 | fgrep memcpy 查看编译机支持的 memcpy 版本:

compile_ENV:# objdump -T /lib/x86_64-linux-gnu/libc.so.6 | fgrep memcpy00000000000alcc0  w   DF .text  0000000000000009  GLIBC_2.2.5 wmemcpy000000000010bdf0 g    DF .text  000000000000001b  GLIBC_2.4   __wmemcpy_chk0000000000091620 g    DF .text  0000000000000465  GLIBC_2.14 memcpy000000000008c420 g    DF .text  0000000000000465  (GLIBC_2.2.5) memcpy0000000000108990 g    DF .text  0000000000000009  GLIBC_2.3.4 __memcpy_chk

可见,编译机支持两种版本的 memcpy 函数 (2.14 和 2.2.5)。至此,根因已清晰:redis 源码依赖 GLIBC 提供的 memcpy 函数,在分布式编译中概率性的采用 memcpy[GLIBC2.2.5] 和 memcpy[GLIBC2.14] 编译 redis-server,而安装环境仅支持 memcpy[GLIBC2.2.5],由此导致 redis-server 概率性安装失败。

解决方案

  • 升级安装环境的 GLIBC 版本,这显然是非常不明智的,无异于削足适履;
  • 统一编译环境和安装环境,消除版本差异,这种方案需要满足一个约束:安装环境版本可控。如果你卖的是产品,用户将你的产品部署到什么系统中,你可能没办法控制,如是,该方案不可取;
  • 最佳方案:

可在 redis 源码中添加约束,显式指定所依赖的 memcpy 函数的 GLIBC 版本,需添加的约束代码如下:asm(".symver memcpy,memcpy@GLIBC_2.2.5"); 【注意】只需在调用函数 memcpy 的源文件中加入此约束

解决方案的验证例子

【步骤 1】编写一个简单的 C 测试程序:test.c,功能:将 src 中的字符串复制到字符数组 dest 中.

#include<stdio.h>#include<string.h>int main(){    char*src="Just for Testing";    char dest[20];    memcpy(dest,src,strlen(src));
    d[strlen(src)]='';    printf("%s",dest);
    getchar();    return 1;
}

【步骤 2】在同时具有 GLIBC_2.2.5 和 GLIBC2.14 版本 memcpy 的 Linux 系统上编译 test.c,执行命令:gcc -o test test.c,再运行 test 可执行文件:输出结果 Just for Testing。

【步骤 3】命令:objdump -T test| fgrep GLIBC2.14 确认 test 依赖的 memcpy 函数的 GLIBC 版本:将会发现 memcpy 采用的是 GLIBC2.14

【步骤 4】在源码中对依赖的 memcpy 函数进行版本约束,使其按指定版本编译。添加约束代码:asm(".symver memcpy,memcpy@GLIBC_2.2.5");

#include<stdio.h>#include<string.h>__asm__(".symver memcpy,memcpy@GLIBC_2.2.5");int main(){    char*src="Just for Testing";    char dest[20];    memcpy(dest,src,strlen(src));
    d[strlen(src)]='';    printf("%s",dest);
    getchar();    return 1;
}

【步骤 5】再次执行编译,运行,检测 memcpy 的版本,将会看到,test 依赖的 memcpy 的版本为 GLIBC_2.2.5,说明添加的版本约束生效。

openSSL 版本不兼容导致 Redis 进程拉起失败

问题基本信息

曾经遇到一个需求:出于安全考虑,在 Redis 中加入了证书机制,因此使用了 openSSL,正因为使用了 openSSL,在安装部署中遇到了 redis-server 进程无法拉起的问题。由于安装环境 (Centos6.2 系统)openssl 版本低于编译环境,两者不兼容,导致 redis-server 启动失败。

初步定位

部署 Redis 集群失败,部分节点 redis-server 进程无法拉起,没有报错信息。尝试 gdb 调试,命令:gdb ./redis-server,报错内容:

/opt/xxx/redis-server: symbol lookup error: /opt/xxx/redis-server: undefined symbol: TLSv1_2_server_method

根因分析

根据报错内容,很明显,redis-server 运行中,有一个函数:TLSv12servermethod 找不到,那么,直观的思路便是查询 TLSv12servermethod,根据 IBM 的介绍),获悉:此函数为 openSSL 库函数。根据报错提示,猜测为 openSSL 版本问题,于是,分别查询安装环境和编译环境的 openSSL 版本:

查看安装环境openSSL版本:命令openssl versionInstall-DEV:# openssl version OpenSSL1.0.0-fips 29 Mar 2010

然后查看编译环境的openSSL版本:Compile-DEV:# openssl versionOpenSSL 1.0.2h  3 May 2016

从查询结果可以看出,编译环境和安装环境的 openSSL 版本差距明显,到 openSSL 官网(https://www.openssl.org/)查询,确认 TLSv12server_method 函数在 OpenSSL1.0.1e 以后才出现,至此问题定位完成。定位结论:编译机和执行机 openSSL 版本相差过大,不兼容。

解决方案

鉴于实际安装部署中,操作系统版本较多,常用的有:CentOS7.1,CentOS6.2,CentOs7.4,Red Hat 7.0,Red Hat 6.4,SuSE11sp4,SuSE12sp2 等,这些系统搭载的 openSSL 版本差别较大,可能存在不兼容的问题,因此,设计解决方案如下:

通过静态链接的方式将对 openssl 的依赖打入 redis-server 中,解除 redis-server 对操作系统的 openssl 依赖。涉及修改例子如下:

#1.自定义脚本,准备好redis编译依赖的openssl,并放入新建文件夹include和libtar -zxvf ../openssl-1.0.2k.tar.gz
cd openssl-1.0.2k
./config -fPIC no-shared
make
cd ..
mkdir lib 
cp openssl-1.0.2k/libcrypto.a ./lib
cp openssl-1.0.2k/libssl.a  ./lib
mkdir includecp openssl-1.0.2k/include/openssl/*  ./include#2.修改redis原生的MakeFile文件else
        # All the other OSes (notably Linux)
        FINAL_LDFLAGS+= -rdynamic
        FINAL_LIBS+= -pthread -lrt        #此行有新增内容,添加静态链接库的路径
        FINAL_LIBS+= -L../../lib -lssl -lcryptoendifendifendif#Include paths to dependencies#此行有新增内容,添加需要库的名称FINAL_CFLAGS+= -I../deps/hiredis -I../deps/linenoise -I../deps/lua/src  -I../../include

nodes-xxx.conf 错误导致 Redis 进程拉起失败

问题基本信息

集群模式下,有一个 Redis 节点宕机,由于 Redis 集群本身有可靠性机制,通过故障倒换,备节点升主,集群仍可以提供服务。然而,宕机的节点经过修复,一段时间后重新上电,却发现 redis-server 进程无法拉起,查看服务端日志,报错信息如下:

=== REDIS BUG REPORT START: Cut & paste starting from here ===78114:M 02 Apr 21:59:54.538 #     Redis 3.0.7.6 crashed by signal: 1178114:M 02 Apr 21:59:54.538 #     SIGSEGV caused by address: 0x20000000478114:M 02 Apr 21:59:54.538 #     Failed assertion: <no assertion failed> (<no file>:0)78114:M 02 Apr 21:59:54.538 # --- STACK TRACE/xxx/bin/redis-server(logStackTrace+0x44)[0x494074]
/lib64/libc.so.6(+0x3703a)[0x7fe90a86603a]
/usr/java/jre1.8.0_162/lib/amd64/server/libjvm.so(+0x92b3c2)[0x7fe90b8f23c2]
/usr/java/jre1.8.0_162/lib/amd64/server/libjvm.so(JVM_handle_linux_signal+0xb6)[0x7fe90b8f9196]
/usr/java/jre1.8.0_162/lib/amd64/server/libjvm.so(+0x928253)[0x7fe90b8ef253]
/lib64/libpthread.so.0(+0xf7c0)[0x7fe90abb57c0]
/lib64/libc.so.6(+0x3703a)[0x7fe90a86603a]
/xxx/bin/redis-server(clusterLoadConfig+0x117)[0x49d257]
/xxx/bin/redis-server(clusterInit+0xfd)[0x49d99d]
/opt/xxx/bin/redis-server(initServer+0x595)[0x464ec5]
/opt/xxxs/bin/redis-server(main+0x412)[0x465ef2]
/lib64/libc.so.6(__libc_start_main+0xe6)[0x7fe90a84dc36]
/opt/xxx/bin/redis-server[0x45a029]78114:M 02 Apr 21:59:54.538 # --- INFO OUTPUT

问题根因

通过排查,我们发现问题根因为宕机节点上的 Redis 集群配置文件 nodes-xxx.conf 存在异常,最后一行信息不完整,如下所示:

正常的集群配置文件 nodes-xxx.conf 最后一行的形式如下:

vars currentEpoch 36 lastVoteEpoch 36

故障节点 nodes-xxx.conf 最后一行的形式:

vars currentEpoch

Redis 集群一旦创建完成,每一个节点都会生成一个保存集群基本信息的配置文件 (nodes-xxx.conf),当下线的节点重新上线时,会加载这个配置文件以恢复集群。这个过程中会调用一系列函数的调用,主要如下:

main()->initServer()->clusterInit(void)->clusterLoadConfig(char *filename)

加载配置文件的函数 clusterLoadConfig(char *filename) 部分代码如下:

/* Split the line into arguments for processing. */
        argv = sdssplitargs(line,&argc);        if (argv == NULL) goto fmterr;        /* Handle the special "vars" line. Don't pretend it is the last
         * line even if it actually is when generated by Redis. */
        if (strcasecmp(argv[0],"vars") == 0) {            for (j = 1; j < argc; j += 2) {                if (strcasecmp(argv[j],"currentEpoch") == 0) {
                    server.cluster->currentEpoch =
                            strtoull(argv[j+1],NULL,10);
                } else if (strcasecmp(argv[j],"lastVoteEpoch") == 0) {
                    server.cluster->lastVoteEpoch =
                            strtoull(argv[j+1],NULL,10);
                } else {
                    redisLog(REDIS_WARNING,                        "Skipping unknown cluster config variable '%s'",
                        argv[j]);
                }
            }
            sdsfreesplitres(argv,argc);            continue;
        }        /* Regular config lines have at least eight fields */
        if (argc < 8) goto fmterr;

如上代码所示,在加载配置文件时,由于配置文件存在上述错误,经过分割参数 argc=2(空格也计算在内)argv =["vars","currentEpoch"],由于 currentEpoch 存在,将会执行 strtoull(argv[j+1],NULL,10),即为:strtoull(argv[2],NULL,10),而 argv[2] 事实上是不存在的,因此报错。

解决方案

修改源码,增加校验机制防止发生此类错误:对于一个宕机的节点,它的 currentEpoch 必然是小于等于在线的节点的,一旦宕机的节点重新上线,也会根据收到的其它节点的报文更新自己的 currentEpoch,因此,可以考虑为 currentEpoch 设置一个默认值,当 nodes-xxx.conf 出错时,可以采用默认的默认值。 另外,需要对下面这一行代码进行容错处理,这行代码会检验 nodes-xxx.conf 最后一行是否完整,不完整则报错。

/* Regular config lines have at least eight fields */
        if (argc < 8) goto fmterr;

补充

Redis 集群配置文件 nodes-xxx.conf 如果出现错误,对应的节点宕机后无法自愈。除了上面介绍的报错案例,nodes-xxx.conf 的缺损情况不同,报错内容也有区别,比如,报错形式 2:

=== REDIS BUG REPORT START: Cut & paste starting from here ===55251:M 02 Apr 19:38:35.892 # ------------------------------------------------55251:M 02 Apr 19:38:35.892 # !!! Software Failure. Press left mouse button to continue55251:M 02 Apr 19:38:35.892 # Guru Meditation: "Unknown flag in redis cluster config file" #cluster.c:20855251:M 02 Apr 19:38:35.892 # (forcing SIGSEGV in order to print the stack trace)55251:M 02 Apr 19:38:35.892 # ------------------------------------------------55251:M 02 Apr 19:38:35.892 #     Redis 3.0.7.6 crashed by signal: 1155251:M 02 Apr 19:38:35.892 #     SIGSEGV caused by address: 0xffffffffffffffff55251:M 02 Apr 19:38:35.892 #     Failed assertion: <no assertion failed> (<no file>:0)55251:M 02 Apr 19:38:35.892 # --- STACK TRACE/opt/xxx/bin/redis-server(logStackTrace+0x44)[0x494074]
/opt/xxx/bin/redis-server(_redisPanic+0x7e)[0x493b4e]
/usr/java/jre1.8.0_162/lib/amd64/server/libjvm.so(+0x92b3c2)[0x7fe450d0f3c2]
/usr/java/jre1.8.0_162/lib/amd64/server/libjvm.so(JVM_handle_linux_signal+0xb6)[0x7fe450d16196]
/usr/java/jre1.8.0_162/lib/amd64/server/libjvm.so(+0x928253)[0x7fe450d0c253]
/lib64/libpthread.so.0(+0xf7c0)[0x7fe44ffd27c0]
/opt/xxx/bin/redis-server(_redisPanic+0x7e)[0x493b4e]
/opt/xxx/bin/redis-server(clusterLoadConfig+0x70e)[0x49d84e]
/opt/xxx/bin/redis-server(clusterInit+0xfd)[0x49d99d]
/opt/xxx/bin/redis-server(initServer+0x595)[0x464ec5]
/opt/xxxbin/redis-server(main+0x412)[0x465ef2]
/lib64/libc.so.6(__libc_start_main+0xe6)[0x7fe44fc6ac36]