前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一组 Redis 实际应用中的异常场景及其根因分析和解决方案

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

作者头像
CSDN技术头条
发布2018-07-30 11:16:24
2.2K0
发布2018-07-30 11:16:24
举报
文章被收录于专栏:CSDN技术头条

前言

在上一场 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 版本,如下所示:

代码语言:javascript
复制
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 版本不是导致上述问题的根因。

代码语言:javascript
复制
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。

代码语言:javascript
复制
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。

代码语言:javascript
复制
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 版本:

代码语言:javascript
复制
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 中.

代码语言:javascript
复制
#include<stdio.h>#include<string.h>int main(){    char*src="Just for Testing";    char dest[20];    memcpy(dest,src,strlen(src));
    d[strlen(src)]='\0';    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");

代码语言:javascript
复制
#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)]='\0';    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,报错内容:

代码语言:javascript
复制
/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 版本:

代码语言:javascript
复制
查看安装环境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 依赖。涉及修改例子如下:

代码语言:javascript
复制
#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 进程无法拉起,查看服务端日志,报错信息如下:

代码语言:javascript
复制
=== 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 最后一行的形式如下:

代码语言:javascript
复制
vars currentEpoch 36 lastVoteEpoch 36

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

代码语言:javascript
复制
vars currentEpoch

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

代码语言:javascript
复制
main()->initServer()->clusterInit(void)->clusterLoadConfig(char *filename)

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

代码语言:javascript
复制
/* 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 最后一行是否完整,不完整则报错。

代码语言:javascript
复制
/* Regular config lines have at least eight fields */
        if (argc < 8) goto fmterr;

补充

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

代码语言:javascript
复制
=== 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]

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-05-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 GitChat精品课 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis®
腾讯云数据库 Redis®(TencentDB for Redis®)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档