转载自:https://mp.weixin.qq.com/s?__biz=MzA3NDcyMTQyNQ==&mid=2649263292&idx=1&sn=b1703906840e177f854f543ca68e0f00&chksm=87675d42b010d454fed8ddcaa27a2f0a925e6d1db90596f25bef4d10dbe481e60af8d0390907&scene=0&xtrack=1&key=64880cbd3f974b3bfca947a6bbd0dc7de4371c1404947282463667d1fb05469c07e0e306f26cdd5919b8cdf7104c035d81f210dbd76cd38eaa5a7706365c7e6dfaf00d85f90df438dd0f27ce4c15fa34&ascene=1&uin=MTIxNjI3MjUwMg%3D%3D&devicetype=Windows+10&version=62070158&lang=zh_CN&pass_ticket=SfUM%2F5LwQwAMP7uwpn87NnU4UNBVUjA718uL3PfcgjaDszq3%2FyH5JCrCFuqbdfLG
前言
本文主要介绍了 Qunar Redis 高可用架构设计原理、安全机制及集群自动化运维方面的内容。
Qunar Redis 高可用架构设计原理
概述
Qunar Redis 集群是一个分布式的高可用架构,整个架构主要由以下几个重要部分组成:
Redis Server 节点:每个节点有一主一从两个实例,多个节点组成一份完整的集群数据,其中每个节点只有主库对外提供服务,从库仅仅用于节点高可用、数据持久化及定时备份。
Zookeeper 集群:由五个 zk 节点组成,Redis 集群配置变更后,通知客户端进行重连。
Redis Sentinel 集群:由五个 Sentinel 节点组成,用于 Reids Server 节点的高可用,主从切换、故障转移、配置更新等。
配置中心集群:由五个 MySQL 节点组成的 PXC 集群,用于存储 Redis 集群的分片信息,即每个节点的 Master 实例信息及分配key的一致性 hash 值范围。
应用程序客户端:监听 zk 变化,在配置中心获取 Redis 实例信息进行连接。
架构原理图
客户端实现
1.当客户端根据 Redis 集群的 namespace 建立连接时,会先从 zk 中查找/config_addr 节点, 该节点下存放的是配置中心集群的实例信息,从中随机选择一个数据库实例进行连接。
2.在配置中心的特定库表中,根据 Redis 的 namespace 查询集群的节点的连接配置,然后建立 Redis 连接。
3.客户端建立 Redis 连接后,会启动了两个线程:
一个用于监听 zk 的地址的变化。每个 Redis 集群在 zk 中都会有一个/redis/namespace 的节点 ,如果集群配置发生变化,哨兵会通知 zk 更新此节点的值,客户端感知到 zk 配置变化, 将会去配置中心获取新的连接配置,重新建立连接。
一个用于轮询配置中心的连接配置。为了防止 zk 通知失败,客户端会通过这个线程,每隔 10s 去轮询配置中心的配置信息,如果发现配置中心的配置和本地缓存的不一样,就会使用配置中心的配置建立新的连接。
客户端与其他组件的关系示意图如下:
数据分片方法
开发人员提交 Redis 集群申请工单信息后,DBA 会依据工单中的内存大小、QPS 大小等几项主要的数据,规划集群分片节点数量为 N,所有节点平均分配 0~4294967295 范围内的值,即共有 2 的 32 次方个 key 的值,某一个 key 使用 murmurhash2 算法计算哈希值后,只会落在集群的一个节点上。
分片节点示意图如下:
分片节点信息在配置中心的存储信息如下:
架构特点
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 地址在白名单中。
本地登陆和 IP 白名单登陆,命令不受限制,方便 DBA 管理和兼容各种监控统计脚本。
IP 白名单可以动态配置,最大支持 32 个 IP 白名单。
IP 白名单功能涉及修改代码的地方:
在 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);
}
在 server.h 文件中增加 trustedIPArray 结构体定义。
typedef struct trustedIPArray {
int numips;
sds* ips;
} trustedIPArray;
在 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;
}
在 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;
}
在 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;
}
在 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;
}
屏蔽高危指令
通过修改 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 方法进行检查。
配置优化
针对集群各个节点的主从实例进行差异化配置,由于每个节点只有主库对外提供服务,为了最大限度的提高主库的并发能力,一些比较耗时的操作可以放到从库去执行。
几项主要的配置如下:
主库关闭 bgsave、bgrewriteaof 功能。
从库开启 aof 功能,定时调度重写 aof 文件,释放服务器磁盘空间。
从库定时执行 bgsave 操作,备份 rdb 文件。
从库开启 slave-read-only 参数,只读。
当 Redis 集群部署完之后,会有定时任务去检查服务器上各个 Redis 实例的角色,根据角色的不同修改相关的配置参数,同时将修改后的持久化到配置文件。
Qunar Redis 自动化运维
初始化系统环境
在 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
统一运维管理工具
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
单机多实例多版本部署
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
使用 git 管理 Redis 哨兵
使用 git 集中管理所有的哨兵配置,一个地方发生变更,哨兵集群的所有服务器同时拉取进行同步更新。同时详细的 commit log,方便跟踪配置文件修改历史。Qunar Redis 哨兵具有以下特点:
一套哨兵只管理一个节点,即只对端口号相同的一组 Redis(一主一从或一主多从)实例进行监控和故障转移。
哨兵只负责节点的高可用,客户端不需要通过哨兵来访问 Redis 实例。
哨兵配置文件使用 git 统一管理,配置文件以[节点端口号+20000]集群 namespace.conf 方式统一命名,例如 30708redisdelaytest.conf,通过集群任意一个节点的端口号或者 namespace 可以获取集群全部节点的信息。
当哨兵监控的节点发生切换时,会更新配置中心对应节点的主库配置和 zookeeper 中对应节点的 dataVersion,客户端检测到 zookeeper 的变化会去配置中心获取节点最新的信息进行重连,同时哨兵会将切换信息发送至 DBA 和运维事件平台。
哨兵服务器的 IP 默认都添加到 Redis 实例的白名单中,即通过哨兵服务器可以访问任何一个 Redis 实例进行所有的操作,所以哨兵服务器的权限必须严格控制,只有 DBA 才有权限登陆。
运维操作平台化
以上几项规范统一的标准化流程,为 Qunar Redis 的整个运维平台化提供了有力的支撑,目前 Qunar Redis 的 90% 以上的运维操作都实现了平台自动化,包括工单申请及审核、集群部署、实例迁移、集群垂直伸缩、不同维度(服务器、集群、实例)的信息查看等,下面主要介绍下 Qunar Redis 集群部署和实例迁移的实现过程。
集群部署
Qunar Redis 集群部署时主要有以下步骤:
开发人员通过平台提交集群申请工单发起流程,TL 审核完成后流程扭转到 DBA。
DBA 根据申请工单的信息规划集群规模,如节点个数、内存大小、部署机房、Redis 版本等。
根据集群规划在 Redis 集群部署页面填写部署信息。
提交部署信息后平台会自动筛选资源空闲的服务器进行集群部署。
集群部署完成后会在在 Qtalk 上通知 DBA,集群的 clientcipher 会通过邮件方式通知开发人员,同时会将集群部署情况推送到公司运维事件平台,保留操作记录。
实例迁移
运维过程中实例迁移主要分为两大类:
部分实例迁移。当某台服务器的可用资源不足时,将这台机器上的部分实例迁移到其他资源比较空闲的服务器上。在页面输入实例的源主机和目前主机,提交后会自动生成迁移任务。
整机实例迁移。主要是替换过保服务器或者服务器需要停机维护时,将该机器上的所有实例自动迁移到其他资源比较空间的服务器上。在页面输入需要迁移的主机名,提交后会自动生成迁移任务。
迁移任务开始后,整个迁移过程无须人工介入,会自动更新执行进度并输出日志。
转载自:https://mp.weixin.qq.com/s?__biz=MzA3NDcyMTQyNQ==&mid=2649263292&idx=1&sn=b1703906840e177f854f543ca68e0f00&chksm=87675d42b010d454fed8ddcaa27a2f0a925e6d1db90596f25bef4d10dbe481e60af8d0390907&scene=0&xtrack=1&key=64880cbd3f974b3bfca947a6bbd0dc7de4371c1404947282463667d1fb05469c07e0e306f26cdd5919b8cdf7104c035d81f210dbd76cd38eaa5a7706365c7e6dfaf00d85f90df438dd0f27ce4c15fa34&ascene=1&uin=MTIxNjI3MjUwMg%3D%3D&devicetype=Windows+10&version=62070158&lang=zh_CN&pass_ticket=SfUM%2F5LwQwAMP7uwpn87NnU4UNBVUjA718uL3PfcgjaDszq3%2FyH5JCrCFuqbdfLG