有梦就能飞 2020-07-14
一、高可用架构设计原理
1、概述
Qunar Redis 集群是一个分布式的高可用架构,整个架构主要由以下几个重要部分组成:
2、架构原理图
3、客户端实现
1)当客户端根据 Redis 集群的 namespace 建立连接时,会先从 zk 中查找/config_addr 节点, 该节点下存放的是配置中心集群的实例信息,从中随机选择一个数据库实例进行连接。
2)在配置中心的特定库表中,根据 Redis 的 namespace 查询集群的节点的连接配置,然后建立 Redis 连接。
3)客户端建立 Redis 连接后,会启动了两个线程:
客户端与其他组件的关系示意图如下:
4、数据分片方法
开发人员提交 Redis 集群申请工单信息后,DBA 会依据工单中的内存大小、QPS 大小等几项主要的数据,规划集群分片节点数量为 N,所有节点平均分配 0~4294967295 范围内的值,即共有 2 的 32 次方个 key 的值,某一个 key 使用 murmurhash2 算法计算哈希值后,只会落在集群的一个节点上。
分片节点示意图如下:
分片节点信息在配置中心的存储信息如下:
5、架构特点
Quanr Redis 高可用架构具有以下特点:
6、架构局限性
Quanr Redis 高可用架构具有以下局限性:
二、安全机制
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 执行不合理的操作,进一步加强了其安全性。
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); } }
屏蔽的高危指令有:
在 Redis 源代码涉及这些指令的地方,都需要加上 checkCommandBeforeExec 方法进行检查。
3、配置优化
针对集群各个节点的主从实例进行差异化配置,由于每个节点只有主库对外提供服务,为了最大限度的提高主库的并发能力,一些比较耗时的操作可以放到从库去执行。
几项主要的配置如下:
当 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 哨兵具有以下特点:
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)整机实例迁移。主要是替换过保服务器或者服务器需要停机维护时,将该机器上的所有实例自动迁移到其他资源比较空间的服务器上。在页面输入需要迁移的主机名,提交后会自动生成迁移任务。