flyEn'blog

redis docker swarm 集群部署

本文是自己前段时间在一沙箱服务器上进行redis集群部署的一次实践的记录和整理。
主要基于docker启动多个容器,搭建了一主多从+哨兵的redis架构模式,如图:

img

Redis一主多从模式,配合sentinel哨兵集群来进行监控管理,实现监控和故障转移,从来保证Redis的高可用。

当然,还有其他升级方案,比如:

  • 多主多从,多个Redis节点间共享数据的Redis集群,进行分区,也是为了解决单机Redis容量有限的问题,提升高并发性。

    官方文档说明最小的Redis集群至少需要3个主节点

    既然有3个主节点,那么主节点搭配至少一个从节点,最小的集群为三主三从。

以下实践在一台主机上部署,算是单机Redis伪集群架构

一般来说,master和slave节点分布在不同服务器上,而sentinel每个redis node的服务器上挂一个,如图:M1主节点、R2从节点、S1/S2/S3 sentinel node。

image-20220111163415181

因为如果slave节点和master同一个服务器,仅仅只是相当于实现了redis的多线程而已。而sentinel如果都挂在其中一个redis node的机子上,那么这台服务器挂了之后,sentinel也挂了。使用sentinel集群也是为了保证redis的高可用,避免哨兵节点挂了之后影响redis的使用。

部署背景:docker、docker compose、docker swarm

镜像选用:bitnami/redis、bitnami/redis-sentinel

用bitnami镜像的考虑:初始化容器方便,提供很多部署时的环境变量,容器启动后动态写入配置文件;优化了镜像的体积,更为小巧…

docker单点模式

因为之前开发某环境,单点的redis,当时想问下端口号是否能开放出来,以方便本地远程连接Redis数据库。但是被告知,Redis未设密码,之前好像设过一次没有生效…

我就比较奇怪,去看了看配置,发现docker部署的命令中是未指定配置路径的,以前的redis实际上用的是默认配置启动的,所以之前外部配置修改都不生效。

如果要指定配置文件,应该在创建service后,指定容器启动后的命令,redis-server /etc/redis/redis.conf。

1
2
3
4
5
6
7
8
9
docker service create --name redis \
--network hplus_network \
--limit-memory 2G \
--config source=redis-conf-6,target=/etc/redis/redis.conf,mode=0444 \
--publish 6379:6379 \
--mount type=volume,source=redis_data,destination=/data \
--mount type=bind,source=/usr/share/zoneinfo/Asia/Shanghai,destination=/usr/share/zoneinfo/Asia/Shanghai \
--mount type=bind,source=/etc/localtime,destination=/etc/localtime \
redis:6.2.1 redis-server /etc/redis/redis.conf

对上述命令的解释:

  • --name:service 名称

  • --network:指定网络

  • --limit-memory:限制最大内存

  • --config:指定配置文件。

    source:用的是容器外面docker管理的挂载资源,redis-conf-6。

    target:容器内部的配置资源目标路径。

    mode:配置文件权限。

  • --mount:其余的就是将挂载资源初始化到容器的操作。

关于挂载:

  1. 挂载方便在服务器外部修改配置,应用到容器内部。

  2. 方便数据容灾备份,比如redis数据库在容器下存储的持久化文件。如果不用挂载,容器一销毁,重新启动数据都清空了。

  3. 便于初始化容器

查看docker管理的挂载资源:docker volume ls

创建挂载资源(远程):

1
2
3
4
5
docker volume create --driver local \
--opt type=nfs \
--opt o=addr=xxxxxx.cn-hangzhou.nas.aliyuncs.com,rw,vers=4,minorversion=0,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport \
--opt device=:/sandbox-2/data/redis \
redis-data

远程的好处也是省去了数据同步到云端的备份容灾步骤。

创建挂载资源服务器本地文件路径的话,感觉也没有多少必要,直接通过source写入服务器中文件路径即可。

修改redis配置文件:

  1. 开启redis验证

    如 : requirepass 123456

  2. 允许redis对外连接

    bind 127.0.0.1 改为 bind 0.0.0.1

  3. 开启redis数据持久化

    appendonly yes

  4. daemonize no

    后台守护进程方式启动关闭,会和docker swarm启动容器默认用-d的参数冲突

    protected-mode ye

  5. 容器启动报错权限不够,需要加--privileged=true 命令(不太安全),或者指定权限范围mode=444

主从+sentinel模式

1主2从三哨兵的Redis集群模式。

三种docker compose编排配置文件的配置方式:

第一种:用 bitnami镜像支持的环境变量初始化容器时动态修改配置文件内容。

通过docker仓库内的对应bitnami/xxx的仓库查看可配置的环境变量进行配置修改

比如 - REDIS_SENTINEL_PORT_NUMBER=26379

第二种:将外部已修改完毕的配置文件加载进内部路径,再通过指定配置路径进行启动。

第三种:直接通过compose.yml里配置脚本命令(ccommand/entrypoint),修改默认的配置。

经过试验,我选用了第一种方案,比较简便。

最后部署步骤

  1. 查看docker管理的挂载目录

    docker volume ls

  2. 如果未配置挂载盘资源,如下创建一个:

    1
    2
    3
    4
    5
    docker volume create --driver local \
    --opt type=nfs \
    --opt o=addr=xxxxxx.cn-hangzhou.nas.aliyuncs.com,rw,vers=4,minorversion=0,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport \
    --opt device=:/sandbox-3/data/redis \
    redis-master-volume

    如挂载盘下没有这个目录,则创建对应目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    [root@sandbox-3 mnt]# docker volume inspect redis-master-volume
    [
    {
    "CreatedAt": "2022-01-10T17:44:43+08:00",
    "Driver": "local",
    "Labels": {},
    "Mountpoint": "/var/lib/docker/volumes/redis-master-volume/_data",
    "Name": "redis-master-volume",
    "Options": {
    "device": ":/sandbox-3/data/redis",
    "o": "addr=xxxxx.cn-hangzhou.nas.aliyuncs.com,rw,vers=4,minorversion=0,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport",
    "type": "nfs"
    },
    "Scope": "local"
    }
    ]
  3. 创建对应文件夹:/data/redis/slave

  4. 创建配置文件:docker-compose-redis-sentinel.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    version: '3.9'

    services:
    master:
    image: 'bitnami/redis:6.2.1'
    ports:
    - '6379'
    environment:
    - REDIS_REPLICATION_MODE=master
    - REDIS_PASSWORD=123456
    - REDIS_AOF_ENABLED=yes
    networks:
    - hplus_network
    volumes:
    - 'redis-master-volume:/bitnami'
    - "/etc/localtime:/etc/localtime:ro"
    - "/usr/share/zoneinfo/Asia/Shanghai:/usr/share/zoneinfo/Asia/Shanghai:ro"

    slave:
    image: 'bitnami/redis:6.2.1'
    user: root
    ports:
    - '6379'
    depends_on:
    - redis_master
    deploy:
    replicas: 2
    environment:
    - REDIS_REPLICATION_MODE=slave
    - REDIS_MASTER_HOST=redis_master
    - REDIS_MASTER_PORT_NUMBER=6379
    - REDIS_MASTER_PASSWORD=12345
    - REDIS_PASSWORD=123456
    networks:
    - hplus_network
    volumes:
    - '/data/redis/slave:/bitnami'
    - "/etc/localtime:/etc/localtime:ro"
    - "/usr/share/zoneinfo/Asia/Shanghai:/usr/share/zoneinfo/Asia/Shanghai:ro"

    sentinel:
    image: 'bitnami/redis-sentinel:6.2.1'
    user: root
    ports:
    - '26379-26381:26379'
    depends_on:
    - redis_master
    - redis_slave
    deploy:
    replicas: 3
    environment:
    - REDIS_MASTER_HOST=redis_master
    - REDIS_MASTER_PORT_NUMBER=6379
    - REDIS_MASTER_PASSWORD=123456
    - REDIS_PASSWORD=123456
    networks:
    - hplus_network
    volumes:
    - "/etc/localtime:/etc/localtime:ro"
    - "/usr/share/zoneinfo/Asia/Shanghai:/usr/share/zoneinfo/Asia/Shanghai:ro"

    volumes:
    redis-master-volume:
    external: true
    networks:
    hplus_network:
    external:
    name: hplus_network
  5. 以如下命令启动service

    docker stack deploy --compose-file docker-compose-redis-sentinel.yml redis

sentinel 可能容器启动失败,如下报错:

1
2
3
4
5
> [root@sandbox-3 ~]# docker logs ad0090705a64
> ...
> redis-sentinel 17:57:01.34 INFO ==> Initializing Redis Sentinel...
> chown: cannot access '/bitnami/redis-sentinel': No such file or directory
>

>

不能访问这个文件夹,也就是说镜像创建的时候将配置文件放在这个文件夹下,但是没有这个路径。

那就在对应挂载盘路径下,创建下/redis-sentinel这个文件夹,具体路径应该是/data/redis/sentinel/redis-sentinel/conf

重新启动,三个service都启动成功!

关于读写分离:

Redis主从模式已经实现主从和读写权限控制,至于实现读写分离,这个完全是客户端的行为,也就是完全由使用者决定的。

验证集群是否有效

  • 主从同步,master和slave节点数据同步 √

  • 配置密码是否生效 √

    1
    2
    3
    4
    127.0.0.1:6379> keys *
    (error) NOAUTH Authentication required.
    127.0.0.1:6379> auth 123456
    OK
  • 读写分离 √

    slave node 不能写数据。

    1
    2
    3
    127.0.0.1:6379> set a 123
    (error) READONLY You can't write against a read only replica.
    127.0.0.1:6379>
  • spring 配置生效 √

    进入redis-cli内 输入 monitor,即可实时查看redis请求日志

    1
    2
    127.0.0.1:6379> monitor
    OK

    image-20220110194843451

  • 挂载路径是否生效 ×

    修改配置文件:

    1
    2
    3
    volumes:
    redis-master-volume:
    external: true

    修改完,重新创建service,验证阿里云挂载盘目录下已同步到容器目录下的文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [root@dev ~]# cd /mnt/sandbox-3/data/redis/
    [root@dev redis]# ls // 目录下无文件
    [root@dev redis]# ls // 修改完之后
    redis
    [root@dev redis]# cd redis/
    [root@dev redis]# ls
    data
    [root@dev redis]# cd data/
    [root@dev data]# ls
    appendonly.aof dump.rdb
    [root@dev data]#

    因为如果不加这个的话(external: true),在服务启动会自动新建一个挂载名字叫做 redis_redis-master-volume:的挂载资源,并且绑定。

  • 持久化生效 √

    重启redis,初始化数据到redis内存,完成。

  • master down之后,故障转移,master重新选举 √
    image-20220114165821994

  • 故障恢复后,重新成为集群的salve节点。 √

    如果是master故障,docker swarm的监控故障恢复机制是会自动重启的,重启后master依然是正常master node。因为sentinel有个判定master宕机的时长,默认应该是1分钟,默认如果3个sentinel中有2个认为宕机,就会进行选举,选举出一个当master。但重启故障恢复的时间小于1分钟,所以还是master,不会切换。

踩坑记录

第一次成功连接之后,又修改了docker compose的配置(为了解决的是master node未挂载成功的问题),重新创建service之后,Spring连不通了,报错:

(后来配置改回来再重启还是不行)

1
reactor.core.Exceptions$ErrorCallbackNotImplemented: org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to 10.0.1.102:6379

连接这个IP连不上,IP错误,确实去看了master和slave的node的ip,没有一个对的上的,所以看起来是没有读取成功??客户端缓存了?请求的还是第一次连接成功的ip?但是没有道理啊。

经过各方面排查,发现sentinel service的日志:

image-20220111180834411

确实是因为sentinel内部绑定的master和slave的IP都不对原因导致的,而不是客户端Spring程序的问题。

查看了sentinel服务其中一个容器内部的sentinel.conf配置文件:

发现确实是绑的10.0.1.102

1
2
3
4
5
6
7
# Note: master name should not include special characters or spaces.
# The valid charset is A-z 0-9 and the three characters ".-_".
sentinel monitor mymaster 10.0.1.102 6379 2

# sentinel auth-pass <master-name> <password>
#
# Set the password to use to authenticate with the master and replicas.

网上找了一个类似的问题:

https://github.com/bitnami/charts/issues/1682

有人提出,sentinel有对应两个配置可以开启:

1
2
SENTINEL resolve-hostnames yes
SENTINEL announce-hostnames yes

查询对应bitnami的redis镜像,支持一个环境变量REDIS_SENTINEL_RESOLVE_HOSTNAMES默认no,改为yes,也就是说开启哨兵主机名支持。

1
2
来自 https://hub.docker.com/r/bitnami/redis-sentinel
REDIS_SENTINEL_RESOLVE_HOSTNAMES: Enables sentinel hostnames support. This is available only for Redis(TM) 6.2 or higher. Default: no.

试了还是不通,配置文件依然是同样的配方。。。

想到了挂载盘 。。。。才恍然

我照常把容器下的bitnami挂载到主机某个目录下的,sentinel服务的容器生成的目录是放sentinel的配置文件,然后容器每次启动就用挂载盘的第一次服务启动后生成的那个旧文件了,所以就发生了后来就一直调不通的情况了。。。

解决方法:

  1. 把挂载资源去掉,conf配置文件每次启动重新生成,也没必要挂载,挂载只是方便在主机去修改和数据备份。

  2. 挂载资源配置下加一个nocopy: true

    1
    2
    3
    4
    5
    6
    volumes:
    - type: volume
    source: mydata
    target: /data
    volume:
    nocopy: true

    nocopy:flag to disable copying of data from a container when a volume is created

关于Redis持久化机制

redis有两种持久化机制,一种是全量的RDB(Redis Database),一种是增量的AOF(Append Only File)。

在默认配置未开启,即appendonly:no的情况下,只会进行RDB。

  • RDB持久化方式能够在指定的时间间隔对数据进行快照存储,适合数据备份。

    比如也可以每天保存一份数据再传送到远端数据中心,非常使用于容灾恢复。

  • RDB在保存RDB文件时父进程fork出一个子进程,去做数据的持久化工作,RDB持久化方式可以最大化redis的性能。

  • 与AOF相比,在恢复大数据集的时候,RDB方式会更快一些。

  • 但是如果更在意丢失的数据最小化的话,那么更适合AOF的持久化方式。即RDB虽然可以设置每隔5分钟或者更短的时间间隔保存一次,但是这个操作是要保存整个数据集,是非常繁重的工作。所以时间间隔都会设置相对较长,万一意外宕机,可能会丢失一部分数据。

AOF:

  • AOF增量同步,也就是记录每次对服务器写的操作。当服务器重启后选择AOF持久化机制的话是会重新执行这些命令,来恢复原始的数据,AOF方式是以redis协议追加保存每次写的操作到文件末尾。Redis还会对AOF文件进行整理重写,以缩减体积,减少初始化时间。
  • AOF提供有不同的异步同步策略,比如无fsync,每秒fsync,每次写的时候fsync(默认)。fsync是由后台线程进行处理的,主线程会尽力处理客户端请求,所以性能不太影响,一旦出现故障,最多丢失1秒的数据。
  • Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写(重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合)。
  • AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。所以比如不小心执行了flushall命令,只要AOF文件还没重写,那么只需要停止服务,移除AOF文件末尾的FLUSHALL命令,就可以将数据集恢复到之前的状态。
  • AOF与RDB相比,相同数据集,一般AOF文件体积会大许多。

多主多从的cluster模式

当使用sentinel模式的时候,客户端就不要直接连接Redis,而是连接sentinel的ip和port,由sentinel来提供具体的可提供服务的Redis实现,这样当master节点挂掉以后,sentinel就会感知并将新的master节点提供给使用者。

sentinel模式基本可以满足一般生产的需求,具备高可用性。但是当数据量过大到一台服务器存放不下的情况时,主从模式或sentinel模式就不能满足需求了,这个时候需要对存储的数据进行分片,将数据存储到多个Redis实例中。

cluster的出现是为了解决单机Redis容量有限的问题,将Redis的数据根据一定的规则分配到多台机器。

未进行配置,后续更新。

资料:

Fork me on GitHub