Qunar Redis 高可用架构设计

Posted Qunar技术沙龙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Qunar Redis 高可用架构设计相关的知识,希望对你有一定的参考价值。

Qunar Redis 高可用架构设计

自我介绍

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



前言

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

Qunar Redis 高可用架构设计原理

概述

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

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

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

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

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

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

架构原理图

Qunar Redis 高可用架构设计

客户端实现

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

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

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

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

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

Qunar Redis 高可用架构设计

数据分片方法

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

分片节点示意图如下:

Qunar Redis 高可用架构设计

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

Qunar Redis 高可用架构设计

架构特点

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

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

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

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

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

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

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

架构局限性

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

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

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

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

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

Qunar Redis 安全机制

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

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

客户端使用 clientcipher 和 IP 白名单机制

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

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

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

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

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

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

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

 
   
   
 
  1. void configGetCommand(client *c) {

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

  3. void *replylen = addDeferredMultiBulkLength(c);

  4. char *pattern = o->ptr;

  5. char buf[128];

  6. int matches = 0;

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

  8. ...

  9. /* 增加trustedip参数 */

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

  11. sds buf = sdsempty();

  12. int j;

  13. int numips;

  14. numips = server.trusted_ips.numips;


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

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

  17. if (j != numips - 1)

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

  19. }


  20. addReplyBulkCString(c,"trustedip");

  21. addReplyBulkCString(c,buf);

  22. sdsfree(buf);

  23. matches++;

  24. }

  25. setDeferredMultiBulkLength(c,replylen,matches*2);

  26. }

  • 在 server.h 文件中增加 trustedIPArray 结构体定义。

 
   
   
 
  1. typedef struct trustedIPArray {

  2. int numips;

  3. sds* ips;

  4. } trustedIPArray;

  • 在 networking.c 文件中增加 isTrustedIP 方法。

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

  2. int isTrustedIP(int fd) {

  3. char ip[128];

  4. int i, port;

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


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

  7. return 1;

  8. }

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

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

  11. return 1;

  12. }

  13. }

  14. return 0;

  15. }

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

 
   
   
 
  1. client *createClient(int fd) {

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


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

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

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

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

  7. if (fd != -1) {

  8. anetNonBlock(NULL,fd);

  9. anetEnableTcpNoDelay(NULL,fd);

  10. if (server.tcpkeepalive)

  11. anetKeepAlive(NULL,fd,server.tcpkeepalive);

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

  13. readQueryFromClient, c) == AE_ERR)

  14. {

  15. close(fd);

  16. zfree(c);

  17. return NULL;

  18. }

  19. }

  20. ...

  21. /* 设置is_super_client */

  22. if (isTrustedIP(fd)) {

  23. c->is_super_client = 1;

  24. } else {

  25. c->is_super_client = 0;

  26. }

  27. ...

  28. return c;

  29. }

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

 
   
   
 
  1. int processCommand(client *c) {

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

  3. * go through checking for replication and QUIT will cause trouble

  4. * when FORCE_REPLICATION is enabled and would be implemented in

  5. * a regular command proc. */

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

  7. addReply(c,shared.ok);

  8. c->flags |= CLIENT_CLOSE_AFTER_REPLY;

  9. return C_ERR;

  10. }

  11. ...

  12. /* Check if the user is authenticated */

  13. /* 增加is_super_client认证 */

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

  15. ...

  16. return C_OK;

  17. }

  • 在 db.c 文件中增加 checkCommandBeforeExec 方法。

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

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

  3. int checkCommandBeforeExec(client *c) {

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

  5. return 1;

  6. }

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

  8. return 0;

  9. }

屏蔽高危指令

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

 
   
   
 
  1. void saveCommand(client *c) {

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

  3. if (server.rdb_child_pid != -1) {

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

  5. return;

  6. }

  7. rdbSaveInfo rsi, *rsiptr;

  8. rsiptr = rdbPopulateSaveInfo(&rsi);

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

  10. addReply(c,shared.ok);

  11. } else {

  12. addReply(c,shared.err);

  13. }

  14. }

屏蔽的高危指令有:

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

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

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

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

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

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

配置优化

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

几项主要的配置如下:

  • 主库关闭 bgsave、bgrewriteaof 功能。

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

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

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

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

Qunar Redis 自动化运维

初始化系统环境

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

统一运维管理工具

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

 
   
   
 
  1. /etc/cron.d/appendonly_switch

  2. /etc/cron.d/auto_upgrade_toolkit

  3. /etc/cron.d/bgrewriteaof

  4. /etc/cron.d/check_maxmemory

  5. /etc/cron.d/dump_rdb_keys

  6. /etc/cron.d/rdb_backup

  7. /etc/profile.d/q_redis_path.sh

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

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

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

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

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

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

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

  15. /xxx/redis/tools/cron_appendonly_switch.sh

  16. /xxx/redis/tools/cron_bgrewrite_aof.sh

  17. /xxx/redis/tools/cron_check_maxmemory.sh

  18. /xxx/redis/tools/cron_dump_rdb_keys.sh

  19. /xxx/redis/tools/cron_rdb_backup.sh

  20. /xxx/redis/tools/dump_rdb_keys.py

  21. /xxx/redis/tools/redis-cli5

  22. /xxx/redis/tools/redis-latency

  23. /xxx/redis/tools/redis_install.sh

  24. /xxx/redis/tools/redis_start.sh

  25. /xxx/redis/tools/redis_stop.sh

单机多实例多版本部署

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

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

  2. .

  3. ├── multi

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

  5. ├── bin

  6. └── utils

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

  8. ├── bin

  9. └── utils

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

  11. ├── bin

  12. └── utils

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

  14. ├── bin

  15. └── utils

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

  17. ├── bin

  18. └── utils

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

  20. ├── bin

  21. └── utils


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

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

  24. 必选参数:

  25. -P redis端口

  26. -p redis密码

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

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

  29. 可选参数:

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

  31. --testenv 测试环境

  32. example:

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

使用 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 才有权限登陆。

运维操作平台化

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

集群部署

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

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

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

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

Qunar Redis 高可用架构设计

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

Qunar Redis 高可用架构设计

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

实例迁移

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

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

Qunar Redis 高可用架构设计

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

Qunar Redis 高可用架构设计

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

Qunar Redis 高可用架构设计

Qunar Redis 高可用架构设计


以上是关于Qunar Redis 高可用架构设计的主要内容,如果未能解决你的问题,请参考以下文章

架构设计:系统存储(17)——Redis集群方案:高可用

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

高可用架构设计ES+Redis+MySQL

Redis高可用架构设计以及常见问题分析

高并发高性能下的 会员系统[同程艺龙] — 高可用架构设计实践

如何构建 Redis 高可用架构?