一次教科书级别的Redis高可用架构设计实践

冷正磊 2020-07-14 10:10:17

作者介绍

冷正磊,2018年2月加入去哪儿网DBA团队,主要负责公司Redis和机票业务MySQL数据库的运维工作,同时承担去哪儿网数据库自动化运维平台部分模块的开发工作,对数据库技术具有浓厚兴趣,具有多年Oracle、MySQL及Redis运维经验。

 

本文主要介绍了 Qunar Redis 高可用架构设计原理、安全机制及集群自动化运维方面的内容。

 

一、高可用架构设计原理

 

1、概述
 

 

Qunar Redis 集群是一个分布式的高可用架构,整个架构主要由以下几个重要部分组成:

 

  • Redis Server 节点:每个节点有一主一从两个实例,多个节点组成一份完整的集群数据,其中每个节点只有主库对外提供服务,从库仅仅用于节点高可用、数据持久化及定时备份。

  • Zookeeper 集群:由五个 zk 节点组成,Redis 集群配置变更后,通知客户端进行重连。

  • Redis Sentinel 集群:由五个 Sentinel 节点组成,用于 Reids Server 节点的高可用,主从切换、故障转移、配置更新等。

  • 配置中心集群:由五个 MySQL 节点组成的 PXC 集群,用于存储 Redis 集群的分片信息,即每个节点的 Master 实例信息及分配 key 的一致性 hash 值范围。

  • 应用程序客户端:监听 zk 变化,在配置中心获取 Redis 实例信息进行连接。

 

2、架构原理图
 

 

 

3、客户端实现
 

 

1)当客户端根据 Redis 集群的 namespace 建立连接时,会先从 zk 中查找/config_addr 节点, 该节点下存放的是配置中心集群的实例信息,从中随机选择一个数据库实例进行连接。

 

2)在配置中心的特定库表中,根据 Redis 的 namespace 查询集群的节点的连接配置,然后建立 Redis 连接。

 

3)客户端建立 Redis 连接后,会启动了两个线程:

 

  • 一个用于监听 zk 的地址的变化。每个 Redis 集群在 zk 中都会有一个/redis/namespace 的节点 ,如果集群配置发生变化,哨兵会通知 zk 更新此节点的值,客户端感知到 zk 配置变化, 将会去配置中心获取新的连接配置,重新建立连接。

  • 一个用于轮询配置中心的连接配置。为了防止 zk 通知失败,客户端会通过这个线程,每隔 10s 去轮询配置中心的配置信息,如果发现配置中心的配置和本地缓存的不一样,就会使用配置中心的配置建立新的连接。

 

客户端与其他组件的关系示意图如下:

 

 

4、数据分片方法
 

 

开发人员提交 Redis 集群申请工单信息后,DBA 会依据工单中的内存大小、QPS 大小等几项主要的数据,规划集群分片节点数量为 N,所有节点平均分配 0~4294967295 范围内的值,即共有 2 的 32 次方个 key 的值,某一个 key 使用 murmurhash2 算法计算哈希值后,只会落在集群的一个节点上。

 

分片节点示意图如下:

 

 

分片节点信息在配置中心的存储信息如下:

 

 

5、架构特点
 

 

Quanr Redis 高可用架构具有以下特点:

 

  • 实现自己的 Redis 客户端,客户端不再访问 Sentinel, Sentinel 只负责高可用。

  • 通过 ZK 集群和配置中心来实现配置的集中管理。

  • 将端口视作一种资源,即集群的一个节点的主从实例使用一个端口,下线的集群端口可复用。

  • 弱化了哨兵机器的地位, 降低了哨兵和集群之间直接的耦合度。

  • 减少了哨兵机器的使用量, 目前只使用了 5 台哨兵机器组成集群。

  • 客户端使用 namespace 访问集群, 将端口和 namespace 对应,namespace 和业务部门对应,方便 DBA 管理和运维,对应用透明。

 

6、架构局限性
 

 

Quanr Redis 高可用架构具有以下局限性:

 

  • 支持的客户端比较少。目前客户端仅支持 Java 和 Python。

  • 不支持快速水平扩容。当集群内存不足时可以快速扩大各个节点实例的内存大小,以此来增加整个集群大小,但单个实例的内存大小也有一定的限度,不能无限扩展。当需要增加集群节点个数时,由于各个节点的一致性哈希范围发生了变化,所有的 key 需要重新分配,对于比较大的集群,过程比较繁琐和耗时。

  • 整个架构依赖的组件比较多。虽然架构中的 zookeeper、配置中心、Sentinel 等都是多节点的高可用集群,但依赖的组件越多,发生故障的可能性也越大,运维难度和工作量也会随着增加,无疑对运维人员有更高的要求。

  • 部分 Redis 原生功能无法使用。由于客户端的限制,部分 Redis 原生功能无法使用,如不支持事务、Lua 脚本等。

 

二、安全机制

 

Redis 被设计成仅供可信环境下的可信用户才可以访问,并没有最大化的去优化安全方面,而是尽量可能的去优化高性能和易用性,因此 Redis 没有类似关系型数据库那样严格的权限控制,因此将 Redis 实例直接暴露在网络上或者让不可信的用户直接访问 Redis 的 TCP 端口,是非常危险的行为。

 

为了提高 Redis 使用的安全性,去哪儿网使用的 Redis Server 是在官方 Redis 4.0.14 版本上进行了部分的源代码改造,增加了一个白名单参数 trustedip,屏蔽了部分高危指令,除了 trustedip 中配置的 IP 之外,任何其他客户端连接都无法执行这些高危指令,同时为了提高 Redis 的性能,对主从实例进行了差异性配置。

 

1、clientcipher和IP白名单
 

 

Qunar Redis 客户端并没有直接通过 TCP 方式去连接 Redis 实例,而是首先要通过集群 namespace 和该集群唯一的 clientcipher 的验证,然后从配置中心获取真正的连接信息后,才可以连接 Redis 实例。同时白名单机制对客户端请求中的高危指令进行过滤,避免对线上 Redis 执行不合理的操作,进一步加强了其安全性。

 

  • 客户端使用 namespace 和 clientcipher 方式访问集群。

  • 不同 namespace 对应的 clientcipher 不同,在创建集群时通过随机生成的密码再次加密生成 clientcipher。

  • 即使知道密码,也无法使用屏蔽的危险命令,除非 IP 地址在白名单中。

  • 本地登陆和 IP 白名单登陆,命令不受限制,方便 DBA 管理和兼容各种监控统计脚本。

  • IP 白名单可以动态配置,最大支持 32 个 IP 白名单。

 

IP 白名单功能涉及修改代码的地方:

 

1)在 config.c 文件的 configGetCommand 方法中增加参数 trustedip

 

 

void configGetCommand(client *c) {

robj *o = c->argv[2];

void *replylen = addDeferredMultiBulkLength(c);

char *pattern = o->ptr;

char buf[128];

int matches = 0;

serverAssertWithInfo(c,o,sdsEncodedObject(o));

...

/* 增加trustedip参数 */

if (stringmatch(pattern,"trustedip",0)) {

sds buf = sdsempty();

int j;

int numips;

numips = server.trusted_ips.numips;

 

for (j = 0; j < numips; j++) {

buf = sdscat(buf, server.trusted_ips.ips[j]);

if (j != numips - 1)

buf = sdscatlen(buf," ",1);

}

 

addReplyBulkCString(c,"trustedip");

addReplyBulkCString(c,buf);

sdsfree(buf);

matches++;

}

setDeferredMultiBulkLength(c,replylen,matches*2);

}

 

2)在 server.c 文件的 processCommand 方法中增加对 issuperclient 的认证

 

 

typedef struct trustedIPArray {

int numips;

sds* ips;

} trustedIPArray;

 

3)在 networking.c 文件中增加 isTrustedIP 方法

 

 

/* 判断客户端IP是否在IP白名单中 */

int isTrustedIP(int fd) {

char ip[128];

int i, port;

anetPeerToString(fd,ip,128,&port);

 

if (strcmp(ip, "127.0.0.1") == 0) {

return 1;

}

for (i = 0; i < server.trusted_ips.numips; i++) {

if (strcmp(ip, server.trusted_ips.ips[i]) == 0) {

return 1;

}

}

return 0;

}

 

4)在 networking.c 文件的 createClient 方法中增加 issuperclient 的设置

 

 

client *createClient(int fd) {

client *c = zmalloc(sizeof(client));

 

/* passing -1 as fd it is possible to create a non connected client.

* This is useful since all the commands needs to be executed

* in the context of a client. When commands are executed in other

* contexts (for instance a Lua script) we need a non connected client. */

if (fd != -1) {

anetNonBlock(NULL,fd);

anetEnableTcpNoDelay(NULL,fd);

if (server.tcpkeepalive)

anetKeepAlive(NULL,fd,server.tcpkeepalive);

if (aeCreateFileEvent(server.el,fd,AE_READABLE,

readQueryFromClient, c) == AE_ERR)

{

close(fd);

zfree(c);

return NULL;

}

 

...

/* 设置is_super_client */

if (isTrustedIP(fd)) {

c->is_super_client = 1;

} else {

c->is_super_client = 0;

}

...

return c;

}

 

5)在 server.c 文件的 processCommand 方法中增加对 issuperclient 的认证

 

 

int processCommand(client *c) {

/* The QUIT command is handled separately. Normal command procs will

* go through checking for replication and QUIT will cause trouble

* when FORCE_REPLICATION is enabled and would be implemented in

* a regular command proc. */

if (!strcasecmp(c->argv[0]->ptr,"quit")) {

addReply(c,shared.ok);

c->flags |= CLIENT_CLOSE_AFTER_REPLY;

return C_ERR;

}

...

/* Check if the user is authenticated */

/* 增加is_super_client认证 */

if (!c->is_super_client && server.requirepass && !c->authenticated && c->cmd->proc != authCommand)

...

return C_OK;

}

 

6)在 db.c 文件中增加 checkCommandBeforeExec 方法

 

 

/* 如果是super client或者是master,返回1,否则返回0

* 因为在master-slave下,master(client)需要向slave执行危险命令*/

int checkCommandBeforeExec(client *c) {

if (c->is_super_client || (server.masterhost && (c->flags & CLIENT_MASTER))) {

return 1;

}

addReplyError(c,"No permission to execute this command");

return 0;

}

 

2、屏蔽高危指令
 

 

通过修改 Redis 源代码,在 Server 端屏蔽部分危险指令,规定只有通过白名单检查的客户端连接才可以执行这些指令。在执行高危指令前进行检查,如需对 save 指令进行屏蔽,可对 rdb.c 文件的 saveCommand 方法的第一行增加 checkCommandBeforeExec 检查。

 

 

void saveCommand(client *c) {

if (!checkCommandBeforeExec(c)) return; /* 执行指令之前进行检查,如不通过直接返回 */ 

if (server.rdb_child_pid != -1) {

addReplyError(c,"Background save already in progress");

return;

}

rdbSaveInfo rsi, *rsiptr;

rsiptr = rdbPopulateSaveInfo(&rsi);

if (rdbSave(server.rdb_filename,rsiptr) == C_OK) {

addReply(c,shared.ok);

} else {

addReply(c,shared.err);

}

}

 

屏蔽的高危指令有:

 

  • 比较耗时类指令:info、keys *。

  • 清空数据类指令:shutdown、flushdb、 flushall。

  • 数据持久化类指令:save、bgsave、bgrewriteaof。

  • 配置类指令:config get、config set、config rewrite。

  • 运维管理类指令:slaveof、monitor、client list、client kill。

 

在 Redis 源代码涉及这些指令的地方,都需要加上 checkCommandBeforeExec 方法进行检查。

 

3、配置优化
 

 

针对集群各个节点的主从实例进行差异化配置,由于每个节点只有主库对外提供服务,为了最大限度的提高主库的并发能力,一些比较耗时的操作可以放到从库去执行。

 

几项主要的配置如下:

 

  • 主库关闭 bgsave、bgrewriteaof 功能。

  • 从库开启 aof 功能,定时调度重写 aof 文件,释放服务器磁盘空间。

  • 从库定时执行 bgsave 操作,备份 rdb 文件。

  • 从库开启 slave-read-only 参数,只读。

 

当 Redis 集群部署完之后,会有定时任务去检查服务器上各个 Redis 实例的角色,根据角色的不同修改相关的配置参数,同时将修改后的持久化到配置文件。

 

三、自动化运维

 

1、初始化系统环境
 

 

在 Redis 服务器上部署集群之前,首先需要初始化系统环境,将这些环境配置添加到 Redis 的 rpm 打包程序的 spec 文件中,安装 Redis 软件包时会自动更改相关配置,主要的系统环境参数有以下几个:

 

 

sed -i -r '/vm.overcommit_memory.*/d' /etc/sysctl.conf

sed -i -r '/vm.swappiness.*/d' /etc/sysctl.conf

sed -i -r '/vm.dirty_bytes.*/d' /etc/sysctl.conf

echo "vm.overcommit_memory = 1" >> /etc/sysctl.conf

echo "vm.swappiness = 0" >> /etc/sysctl.conf

echo "vm.dirty_bytes = 33554432" >> /etc/sysctl.conf

/sbin/sysctl -q -p /etc/sysctl.conf

groupadd redis >/dev/null 2>&1 || true

useradd -M -g redis redis -s /sbin/nologin >/dev/null 2>&1 || true

sed -i -r '/redis soft nofile.*/d' /etc/security/limits.conf

sed -i -r '/redis hard nofile.*/d' /etc/security/limits.conf

echo "redis soft nofile 288000" >> /etc/security/limits.conf

echo "redis hard nofile 288000" >> /etc/security/limits.conf

sed -i -r '/redis soft nproc.*/d' /etc/security/limits.conf

sed -i -r '/redis hard nproc.*/d' /etc/security/limits.conf

echo "redis soft nproc unlimited" >> /etc/security/limits.conf

echo "redis hard nproc unlimited" >> /etc/security/limits.conf

echo never > /sys/kernel/mm/transparent_hugepage/enabled

 

2、统一运维管理工具
 

 

Qunar Redis 集群的统一管理套件,封装了系统环境初始化、实例安装、实例启动、实例关闭、监控报警、定时任务等脚本,实现了监控、统计、注册等自动化操作。

 

 

/etc/cron.d/appendonly_switch

/etc/cron.d/auto_upgrade_toolkit

/etc/cron.d/bgrewriteaof

/etc/cron.d/check_maxmemory

/etc/cron.d/dump_rdb_keys

/etc/cron.d/rdb_backup

/etc/profile.d/q_redis_path.sh

/xxx/collectd/etc/collectd.d/collect_redis.conf

/xxx/collectd/lib/collectd/collect_redis.py

/xxx/collectd/share/collectd/types_redis.db

/xxx/nrpe/libexec/q-check-redis-cpu-usage

/xxx/nrpe/libexec/q-check-redis-latency

/xxx/nrpe/libexec/q-check-redis-memory-usage

/xxx/nrpe/libexec/q-check-zookeeper-ruok

/xxx/redis/tools/cron_appendonly_switch.sh

/xxx/redis/tools/cron_bgrewrite_aof.sh

/xxx/redis/tools/cron_check_maxmemory.sh

/xxx/redis/tools/cron_dump_rdb_keys.sh

/xxx/redis/tools/cron_rdb_backup.sh

/xxx/redis/tools/dump_rdb_keys.py

/xxx/redis/tools/redis-cli5

/xxx/redis/tools/redis-latency

/xxx/redis/tools/redis_install.sh

/xxx/redis/tools/redis_start.sh

/xxx/redis/tools/redis_stop.sh

 

3、单机多实例多版本部署
 

 

Qunar Redis 的安装工具包支持单机多实例安装,安装脚本提供选项和配置文件模板,可以自定义安装不同版本的 Redis,目前支持的 Redis Server 版本有 2.8.6、3.0.7 以及 4.0.14。

 

 

/* 安装包及Redis实例目录结构 */

.

├── multi

│ ├── server_2800 /* Redis2.8.6软件包 */

│ │ ├── bin

│ │ └── utils

│ ├── server_3000 /* Redis3.0.7软件包 */

│ │ ├── bin

│ │ └── utils

│ └── server_4000 /* Redis4.0.14软件包 */

│ ├── bin

│ └── utils

├── redis10088 /* 端口为10088的Redis实例数据目录,用于存放该实例的配置文件、日志、AOF文件及RDB文件 */

│ ├── bin

│ └── utils

├── redis10803 /* 端口为10803的Redis实例数据目录,用于存放该实例的配置文件、日志、AOF文件及RDB文件 */

│ ├── bin

│ └── utils

├── redis11459 /* 端口为11459的Redis实例数据目录,用于存放该实例的配置文件、日志、AOF文件及RDB文件 */

│ ├── bin

│ └── utils

 

/* Redis实例安装程序用法 */

Usage: redis_install.sh -P <port> -v [2.8|3.0|4.0] -p <password> -m <size>

必选参数:

-P redis端口

-p redis密码

-v 将要安装的redis版本,强烈推荐4.0版本

-m redis实例允许的最大内存大小,单位是G

可选参数:

--cluster 集群模式,version>=3.0

--testenv 测试环境

example:

sudo redis_install.sh -P 6379 -v 4.0 -m 20 -p 1qaz2wsx

 

4、使用git管理Redis哨兵
 

 

使用 git 集中管理所有的哨兵配置,一个地方发生变更,哨兵集群的所有服务器同时拉取进行同步更新。同时详细的 commit log,方便跟踪配置文件修改历史。Qunar Redis 哨兵具有以下特点:

 

  • 一套哨兵只管理一个节点,即只对端口号相同的一组 Redis(一主一从或一主多从)实例进行监控和故障转移。

  • 哨兵只负责节点的高可用,客户端不需要通过哨兵来访问 Redis 实例。

  • 哨兵配置文件使用 git 统一管理,配置文件以[节点端口号+20000]_集群 namespace.conf 方式统一命名,例如 30708_redis_delay_test.conf,通过集群任意一个节点的端口号或者 namespace 可以获取集群全部节点的信息。

  • 当哨兵监控的节点发生切换时,会更新配置中心对应节点的主库配置和 zookeeper 中对应节点的 dataVersion,客户端检测到 zookeeper 的变化会去配置中心获取节点最新的信息进行重连,同时哨兵会将切换信息发送至 DBA 和运维事件平台。

  • 哨兵服务器的 IP 默认都添加到 Redis 实例的白名单中,即通过哨兵服务器可以访问任何一个 Redis 实例进行所有的操作,所以哨兵服务器的权限必须严格控制,只有 DBA 才有权限登陆。

 

5、运维操作平台化
 

 

以上几项规范统一的标准化流程,为 Qunar Redis 的整个运维平台化提供了有力的支撑,目前 Qunar Redis 的 90% 以上的运维操作都实现了平台自动化,包括工单申请及审核、集群部署、实例迁移、集群垂直伸缩、不同维度(服务器、集群、实例)的信息查看等,下面主要介绍下 Qunar Redis 集群部署和实例迁移的实现过程。

 

集群部署

 

Qunar Redis 集群部署时主要有以下步骤:

 

1)开发人员通过平台提交集群申请工单发起流程,TL 审核完成后流程扭转到 DBA。

 

2)DBA 根据申请工单的信息规划集群规模,如节点个数、内存大小、部署机房、Redis 版本等。

 

3)根据集群规划在 Redis 集群部署页面填写部署信息。

 

 

4)提交部署信息后平台会自动筛选资源空闲的服务器进行集群部署。

 

 

5)集群部署完成后会在在 Qtalk 上通知 DBA,集群的 clientcipher 会通过邮件方式通知开发人员,同时会将集群部署情况推送到公司运维事件平台,保留操作记录。

 

实例迁移

 

运维过程中实例迁移主要分为两大类:

 

1)部分实例迁移。当某台服务器的可用资源不足时,将这台机器上的部分实例迁移到其他资源比较空闲的服务器上。在页面输入实例的源主机和目前主机,提交后会自动生成迁移任务。

 

 

2)整机实例迁移。主要是替换过保服务器或者服务器需要停机维护时,将该机器上的所有实例自动迁移到其他资源比较空间的服务器上。在页面输入需要迁移的主机名,提交后会自动生成迁移任务。

 

 

迁移任务开始后,整个迁移过程无须人工介入,会自动更新执行进度并输出日志。

 

 

作者丨冷正磊
来源丨Qunar技术沙龙(ID:QunarTL)
dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn
活动预告