使用 Docker 容器组建 Reids Cluster 集群

最近在对 ohUrlShortener 短链接系统 做一些架构层面的调整,开发过程需要在 Reids Cluster 集群模式下做部分测试。Redis 官网在 Scaling with Redis Cluster 说 Redis Cluster 集群目前在 Docker 容器中实现。然而,怎么可能无法实现呢?假如想要在开发或者测试阶段利用 Docker 容器的话(开发及测试过程,甚至容器化交付,是常态),就没有办法实现了吗?于是乎,想到了本文里这种方法来实现一个简单的六节点 Redis Cluster 集群。

Redis Cluster and Docker
Currently, Redis Cluster does not support NATted environments and in eneral environments where IP addresses or TCP ports are remapped.
Docker uses a technique called port mapping: programs running inside Docker containers may be exposed with a different port compared to the one the program believes to be using. This is useful for running multiple containers using the same ports, at the same time, in the same server.
To make Docker compatible with Redis Cluster, you need to use Docker's host networking mode. Please see the --net=host option in the Docker documentation for more information.
https://redis.io/docs/manual/scaling/

关于 Redis 高可用架构

说到 Redis 高可用架构,业界存在很多不同的架构及实践方案。简单来说说几种不同的方式:

Redis 主从复制

主从模式下:应用侧向 Master 节点写入数据之后,所有数据都会被复制到 Replica 节点,而且数据可能会被同步到多个 Replica 节点(实现分离)。为避免单节点故障,通通常的做法是在不同的服务器上部署多个 Redis 并通过上述模式组件集群。这样的架构很好地保证了数据的可靠性,假使其中一台机器出现问题不可访问,那么 Redis 中的数据也可以通过其他节点找回来,不至丢失并造成不可逆转的损失(容灾恢复)。但是,这种模式存在一个严重的问题:任意主从节点出现故障时,会导致集群读写请求失败,需要人工介入切换读写节点才能保证服务恢复。

Redis 主从复制

Redis 主从复制

Reids 哨兵模式

哨兵模式下:Sentinel 哨兵以独立的进程运行,每隔一段时间向集群中的所有 Redis 节点发送命令并等待响应,从而实现监控 Redis 节点的目的。为了保证服务的可用性,一般会多设计几个哨兵共同监督集群中的 Redis 节点。当 集群中只有一个 Sentinel 哨兵认为集群 Master 节点不可用时,被称之为「主观下线」。当后面的哨兵也检测到 Master 节点不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行 failover 操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的 Replica 服务器实现切换主机,这个过程称为「客观下线」。这样对于客户端而言,一切都是透明的。这种模式下,不需要运维人员干预也能保证集群中节点切换。但是,当集群数量增长到一定程度时,维护起来将会是一场巨大的灾难。上图中的各种箭头和方块,已经能让人很反感,就不再继续说下去了。

Redis 哨兵模式

Redis 哨兵模式

总结一下:主从模式和哨兵模式,都实现了读写分离,并在一定程度上确实保证了高可用的服务。然而, 这两种模式下的 Redis 集群中所有节点的数据都是一模一样的,相同的数据会被复制到所有节点。 这简直是对宝贵内存资源的浪费。所以从 Redis 3.0开始,官网提供了 Redis Cluster 支持来实现真正的 Sharding 及高可用。

关于 Redis 高可用架构的基础概念及详情,可参考 《[Redis] 你了解 Redis 的三种集群模式吗?》

Redis Cluster 101

Redis 官网提供的 Cluster 解决方案主要优点在于:

  1. 纯原生支持,不需要任何第三方支持
  2. 能够自动将数据分割到各个节点,不会出现大量数据集中到一个节点的情况
  3. 集群中部分节点故障不会导致服务中断,并且数据可以自动转移

它是怎么做到的呢?

节点之间 TCP 通信

每个 Redis 节点通常需要两个 TCP 端口同时运行。其中一个端口 (默认端口 6379) 用来和客户端交互,也就是我们常用的端口号。另外一个称为 bus port 的端口 (16379,亦即:与客户端交互的那个端口号加1000) 负责与集群中其他 Redis 节点通过二进制的协议交互。节点之间的沟通包括:节点状态检测、配置更新、数据迁移等等。因此,在搭建 Redis Cluster 集群时,每一个 Redis 节点必须同时开启两个 TCP 端口,否则 Redis Cluster 集群将无法正常工作。

数据分片 Sharding

当客户端向 Redis 写入数据时,Redis 是如何把这些数据分散到集群中的节点的呢?Redis Cluster 集群没有使用一致性 hash,而是创造性的提出了 slot 的概念。

一般的 Redis Cluster 集群会有 16384 个 hash slot,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽。集群的每个节点负责一部分 hash slot,举个例子,比如当前集群有3个节点,那么:

  1. 节点 A 包含 0 到 5500 hash slot
  2. 节点 B 包含 5501 到 11000 hash slot
  3. 节点 C 包含 11001 到 16383 hash slot

节点之间数据备份

在保证高可用性方面,Redis Cluster 集群采用了上述讲过的主从架构来解决问题。也就是说,每一个主节点,都可以配置一个从节点。 但是,不要忘记:Redis 会把写入的数据 sharding 到任何一个可能的节点。 比如上述讲到的 A、B、C 三个节点,我们可以配置 A1、B1、C1 作为三个从节点。加入主节点不可访问的情况下,集群会提倡使用从节点。主从节点之间的数据,是一模一样的。

Redis Cluster 配置参数

为了能够搭建 Redis Cluster,在 Redis 启动时制定相关配置文件,其中一部分必要的参数如下:

cluster-enabled yes
cluster-config-file
cluster-node-timeout
cluster-slave-validity-factor
cluster-migration-barrier
cluster-require-full-coverage
cluster-allow-reads-when-down
  1. cluster-enabled 是否开启集群
  2. cluster-config-file 这个文件不是可编辑的,这个参数的作用是:Redis 会把当前节点的配置信息写入此文件供参考
  3. cluster-node-timeout 集群中节点不可达 unavailable 的最大时间,如果集群中某个超过这个时间的阀值仍不可达,则视为下线状态

Redis Cluster 集群创建步骤

根据上述的基础结合 Redis 官网提供的实践方案,创建一个 Redis Cluster 集群需要以下几个步骤:

  1. 使用集群配置文件 cluster-enabled yes 参数启动 Redis(每个节点都要这样启动)
  2. 使用 redis-cli --cluster create [ip:port,ip:port,...] --cluster-replicas 1 命令将所有节点相连(指定 ip:port 形式)
  3. 在上述过程中根据实际情况合理分配 hash slot (eg:有的机器内存大一些,可以考虑多分配一点 slot )
  4. 在任意一个节点上通过 redis-cli -c -p 6379 来连接集群并操作(注意 -c 参数表示连接的是集群)

牢记上述四点,就是建设 Redis Cluster 的步骤。使用 Docker 来实现容器化时,主要思路也就是将上述四个步骤分别在 Docker 中实现。

Docker 容器固定 IP 问题

假设有6台 IP 段从 172.30.0.11 ~ 172.30.0.16 的内存服务器,设计为3主3从的 Redis Cluster 实现:

  1. 提供稳定、高效的 Redis 读写服务,随时可扩充、裁剪 Redis 节点
  2. 每个 Redis 节点都可以提供读写服务,亦即:客户端写入或读取数据时,不需要区分 Master 或者 Replica 节点
  3. 每个 Redis 节点的数据都不可以丢失,需要有主从支持
  4. 假如某个 Redis 节点不可达,不需要人工干预即自动实现故障转移及恢复

根据上述的内容可以知道,在创建 Redis Cluster 集群过程中需要使用 redis-cli --cluster create 来创建集群。这个过程中需要知道这些机器的 IP 和 通信端口才可以完成创建。不过,Docker 容器在启动之前是无法预知 IP 地址的。

但是,也不是没有办法解决。

通过 Docker 官网提供的 桥接网络 方案,我们可以创建一个专用的网络组,将上述六台机器分别部署到网络中,并在配置文件中指定每个节点的固定内网地址即可。其大致网络结构如下:

Docker 网络中 Redis 节点示意图

Docker 网络中 Redis 节点示意图

docker-compse 过程中,可以通过如下配置来指定IP段及子网:

networks:
  network_redis_cluster:
    name: network_redis_cluster
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 172.30.0.0/24
          gateway: 172.30.0.1

指定了 network_redis_cluster 的 IP 段之后,在每个容器的 networks 配置中就可以指定固定 IP 地址,如下:

  rc_node1:
    image: redis:5.1
    healthcheck:
      test: [ "CMD", "redis-cli","-p","6379","-a","${REDIS_PASSWORD}"]
      timeout: 10s
      interval: 3s
      retries: 10     
    networks:
      network_redis_cluster:
        ipv4_address: 172.30.0.11

  rc_node2:
    image: redis:5.1
    healthcheck:
      test: [ "CMD", "redis-cli","-p","6379","-a","${REDIS_PASSWORD}"]
      timeout: 10s
      interval: 3s
      retries: 10         
    networks:
      network_redis_cluster:
        ipv4_address: 172.30.0.12

Redis Cluster Create 过程

按照上述的相关配置,可以很容易就创建N个需要的 Redis Cluster 节点。但是,如何在 Docker 容器中使用 redis-cli --cluster create [ip:port,ip:port,...] --cluster-replicas 1 将各个 Redis 节点连接起来组成集群呢?

通过 docker-compse 创建容器的过程中,需要手动分配 Redis hash slot才可以完成。我们查询 redis-cli --cluster create 相关命令可以发现,它竟然可以多附带一个 --cluster-yes 参数掉过手动分配 hash slot 的过程。其实这个参数的真正作用是:让 Redis 自动分配 slot 给指定的机器。所以,完整的创建集群命令将是:

redis-cli --cluster create 172.30.0.11:6379 172.30.0.12:6379 172.30.0.13:6379 172.30.0.14:6379 172.30.0.15:6379 172.30.0.16:6379 --cluster-replicas 1 --cluster-yes

此外,我们可以在 yaml 文件中指定6个 Redis 容器,还需要第七个容器来执行上述命令,因此,在 yaml 文件中新增 cluster_helper 节点,它的相关配置如下:

cluster_helper:
    image: redis:5.1
    command: redis-cli --cluster create 172.30.0.11:6379 172.30.0.12:6379 172.30.0.13:6379 172.30.0.14:6379 172.30.0.15:6379 172.30.0.16:6379 --cluster-replicas 1 --cluster-yes
    depends_on:
      rc_node1: 
        condition: service_healthy
      rc_node2: 
        condition: service_healthy
      rc_node3:
        condition: service_healthy
      rc_node4:
        condition: service_healthy
      rc_node5: 
        condition: service_healthy
      rc_node6: 
        condition: service_healthy
    networks:
      network_redis_cluster:
        ipv4_address: 172.30.0.17   

Redis 镜像映射参数

通过 hub.docker.com 我们选择 3.0 以上版本的任意镜像,注意有两个镜像配置需要映射:

  1. -v /data 映射 Redis 数据
  2. -v /usr/local/etc/redis/redis.conf 映射 Redis 配置文件

env-file 中定义必要的变量,方便后续操作:

REDIS_VERSION = 5.0.14
REDIS_PASSWORD = He110_
REDIS_PORT1 = 56531
REDIS_PORT2 = 56532
REDIS_PORT3 = 56533
REDIS_PORT4 = 56534
REDIS_PORT5 = 56535
REDIS_PORT6 = 56536

yaml 文件中完整的 Redis 节点配置:

  rc_node1:
    image: redis:${REDIS_VERSION}
    container_name: rc_node1
    hostname: rc_node1
    command: redis-server /usr/local/etc/redis/redis.conf
    volumes:
      - ./container-data/rc-node1:/data   
      - ./cluster_node.conf:/usr/local/etc/redis/redis.conf          
    ports:
      - ${REDIS_PORT1}:6379
    healthcheck:
      test: [ "CMD", "redis-cli","-p","6379","-a","${REDIS_PASSWORD}"]
      timeout: 10s
      interval: 3s
      retries: 10
    networks:
      network_redis_cluster:
        ipv4_address: 172.30.0.11

创建 Redis Cluster 容器化集群

编写完整的 yaml 文件及 env-file 之后,通过 docker-compose 开始开始构建:

docker-compose -p redis_cluster -f redis_cluster.yaml --env-file variables.env up -d --build --force-recreate

执行将会看到如下内容:

[+] Running 8/8
 ⠿ Network network_redis_cluster  Created                                                                                                                                                                        0.0s
 ⠿ Container rc_node1             Healthy                                                                                                                                                                        4.1s
 ⠿ Container rc_node2             Healthy                                                                                                                                                                        5.1s
 ⠿ Container rc_node5             Healthy                                                                                                                                                                        4.6s
 ⠿ Container rc_node6             Healthy                                                                                                                                                                        4.6s
 ⠿ Container rc_node3             Healthy                                                                                                                                                                        4.6s
 ⠿ Container rc_node4             Healthy                                                                                                                                                                        4.6s
 ⠿ Container cluster_helper       Started                                                                                                                                                                        5.1s

通过 docker ps 来查看运行的容器

CONTAINER ID   IMAGE          COMMAND                  CREATED              STATUS                        PORTS                     NAMES
34c3e4c5e5b7   redis:5.0.14   "docker-entrypoint.s…"   About a minute ago   Up About a minute (healthy)   0.0.0.0:56536->6379/tcp   rc_node6
fa708b8b1d2a   redis:5.0.14   "docker-entrypoint.s…"   About a minute ago   Up About a minute (healthy)   0.0.0.0:56531->6379/tcp   rc_node1
31c0a854c267   redis:5.0.14   "docker-entrypoint.s…"   About a minute ago   Up About a minute (healthy)   0.0.0.0:56533->6379/tcp   rc_node3
cc898895becc   redis:5.0.14   "docker-entrypoint.s…"   About a minute ago   Up About a minute (healthy)   0.0.0.0:56532->6379/tcp   rc_node2
84bb6a1489fb   redis:5.0.14   "docker-entrypoint.s…"   About a minute ago   Up About a minute (healthy)   0.0.0.0:56535->6379/tcp   rc_node5
5db8359fd616   redis:5.0.14   "docker-entrypoint.s…"   About a minute ago   Up About a minute (healthy)   0.0.0.0:56534->6379/tcp   rc_node4

我们可以通过 docker exec 到任意一个容器中,然后执行 redis-cli -c 连接到集群中的机器

docker exec -it rc_node5 bash

进入 rc_node5 容器之后,我们来查看一下当前集群状态(注意 redis-cli -a xxx 是指定密码,如果你设置了的话),关于 redis-cli 命令的使用,就不再这里多说了。

redis-cli -a He110_  cluster info

返回:

cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:8
cluster_my_epoch:2
cluster_stats_messages_ping_sent:1244
cluster_stats_messages_pong_sent:1215
cluster_stats_messages_sent:2459
cluster_stats_messages_ping_received:1215
cluster_stats_messages_pong_received:1244
cluster_stats_messages_received:2459

查看集群中的节点:

redis-cli -a He110_  cluster nodes

返回:

ba9de24b962c173d1eb029dcb65d1b5c33457c63 172.30.0.15:6379@16379 myself,slave c4b4b35522b9b8ca517ff5d9e16d7233621a41a0 0 1665398901000 2 connected
c4b4b35522b9b8ca517ff5d9e16d7233621a41a0 172.30.0.12:6379@16379 master - 0 1665398902792 2 connected 5461-10922
9fe3bcb7ad96d8b894fe3a19ec1075bcf1769b98 172.30.0.11:6379@16379 slave 0e4e08488a8b92222f3f455e399a08b2059a5e68 0 1665398901769 7 connected
0e4e08488a8b92222f3f455e399a08b2059a5e68 :0@0 master,noaddr - 1665398303614 1665398303614 7 disconnected 0-5460
6b3512e24721b53d736b9dbd79b419377d17697a 172.30.0.14:6379@16379 slave 6dbabaa32e7220bffd99dcc0493afcb7ff13aa79 0 1665398902075 3 connected
6dbabaa32e7220bffd99dcc0493afcb7ff13aa79 172.30.0.13:6379@16379 master - 0 1665398902590 3 connected 10923-16383

rc_node5 节点中,登录到 Redis 并向其中写入数据试试:

root@rc_node5:/data# redis-cli -a He110_ -c -p 6379
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> set foo bar
-> Redirected to slot [12182] located at 172.30.0.13:6379
OK
172.30.0.13:6379> keys *
1) "foo"
172.30.0.13:6379> quit
root@rc_node5:/data# 

就,很 Nice 。

相关代码

当然,完全不需要藏着噎着,完整代码访问 Github 仓库吧!

如果有问题,当然也可以在这个仓库里提 issue 。

https://github.com/barats/redis-cluster

版权声明:本站所有内容,未经书面授权禁止一切形式的转载、摘录及摘抄,违者依法追究其相关责任。

相关文章