ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Docker部署Redis主从哨兵高可用架构实战

Docker部署Redis主从哨兵高可用架构实战

1. 为什么单机Redis不够用:从一次故障说起

很多团队在项目初期都是单机Redis一把梭,缓存、会话、排行榜全塞一个实例里。业务量小的时候确实没问题,QPS几千、内存占用几个G,跑得稳稳当当。但真到了线上环境,单机Redis有三个绕不过去的坎:单点故障、性能瓶颈、数据丢失风险。我见过一个电商项目,大促期间Redis实例因为内存碎片率过高触发OOM,整个商品详情页直接白屏,损失惨重。事后复盘发现,如果当初做了主从加哨兵,故障切换能在几十秒内自动完成,根本不会演变成事故。

主从复制的核心思路很简单:一个主节点负责写,多个从节点负责读,主节点的数据异步同步到从节点。这样做的好处是读写分离,读请求可以分摊到多个从节点上,写请求集中在主节点保证一致性。但主从模式有个致命问题——主节点挂了不会自动切换,需要人工介入。这时候哨兵就登场了,哨兵是一个独立的进程,专门用来监控Redis节点的健康状态,一旦发现主节点不可达,就会从从节点中选举出新的主节点,并通知客户端连接新地址。

这套架构适合谁呢?我认为凡是生产环境用Redis的团队都应该掌握。不管是中小型项目还是大型分布式系统,主从加哨兵都是性价比最高的高可用方案。它不需要额外的中间件,不依赖外部协调服务,用Docker编排起来也就几十行配置。接下来我会从架构设计、Docker编排、配置细节、故障演练四个维度,把整套方案拆开揉碎讲清楚。

2. 架构设计与Docker编排思路

2.1 节点规划与端口分配

在动手写Docker Compose之前,先把节点规划想清楚。我建议的最小生产级配置是1主2从3哨兵,总共6个容器。为什么是3个哨兵?因为哨兵选举需要多数票,3个哨兵允许1个挂掉还能正常选举,2个哨兵一旦挂1个就失去多数派,无法完成故障转移。从节点2个是为了保证读性能和高可用,主节点挂掉后至少还有一个从节点可以顶上来。

端口分配上,Redis默认6379,从节点可以用6380、6381,哨兵默认26379,依次用26380、26381。这里有个细节要注意:哨兵之间需要互相通信,所以每个哨兵的端口必须独立且可访问。我见过有人把哨兵都映射到同一个宿主机端口,结果哨兵集群根本建立不起来,排查了半天才发现是端口冲突。

角色容器名内部端口宿主机端口说明
Masterredis-master63796379主节点,负责写
Slave1redis-slave163796380从节点1,负责读
Slave2redis-slave263796381从节点2,负责读
Sentinel1redis-sentinel12637926379哨兵1,监控集群
Sentinel2redis-sentinel22637926380哨兵2,监控集群
Sentinel3redis-sentinel32637926381哨兵3,监控集群

2.2 网络模式选择:自定义bridge还是host

Docker网络模式的选择直接影响哨兵能否正确发现节点。我强烈建议使用自定义bridge网络,而不是默认的bridge或host模式。原因有三:第一,自定义bridge网络内置DNS解析,容器之间可以直接用容器名通信,配置里写redis-master就能解析到对应IP,不用硬编码IP地址;第二,host模式下端口冲突问题很头疼,尤其是宿主机上已经有Redis在跑的时候;第三,自定义bridge网络隔离性好,不会影响宿主机其他服务。

但这里有个坑:哨兵配置里需要写主节点的IP地址,而Docker容器的IP在重启后可能会变。解决方案是在哨兵配置中使用容器名而不是IP,Redis 5.0之后的哨兵支持主机名解析。如果用的是老版本,那就得在启动脚本里动态获取IP并写入配置。我实测下来,Redis 6.2和7.0对主机名支持都很好,直接写容器名就行。

2.3 数据持久化策略

主从架构下,持久化策略需要仔细考虑。我的建议是:主节点开启AOF,从节点开启RDB。主节点用AOF保证数据不丢,从节点用RDB做冷备和快速恢复。为什么不全部开AOF?因为AOF文件通常比RDB大很多,从节点如果也开AOF,磁盘IO和网络传输压力会翻倍。而且从节点的数据是从主节点同步过来的,主节点AOF已经保证了数据完整性,从节点用RDB做备份足够了。

具体配置上,主节点设置appendonly yes、appendfsync everysec,从节点设置save 900 1、save 300 10。这里有个经验值:appendfsync everysec是性能和安全的平衡点,最多丢1秒数据,对绝大多数业务够用了。如果业务要求零丢失,那就得用appendfsync always,但性能会下降一个数量级,得权衡。

3. Docker Compose编排文件详解

3.1 主节点配置与启动参数

先看主节点的Docker Compose配置。我习惯把配置拆成两部分:docker-compose.yml负责容器编排,redis.conf负责Redis参数。这样修改配置不用动编排文件,职责清晰。

version: '3.8' services: redis-master: image: redis:7.0-alpine container_name: redis-master ports: - "6379:6379" volumes: - ./master/redis.conf:/usr/local/etc/redis/redis.conf - ./master/data:/data command: redis-server /usr/local/etc/redis/redis.conf networks: - redis-net restart: always networks: redis-net: driver: bridge

主节点的redis.conf关键配置如下:

bind 0.0.0.0 protected-mode no port 6379 daemonize no appendonly yes appendfsync everysec requirepass YourStrongPassword masterauth YourStrongPassword

这里有几个点要解释。bind 0.0.0.0是让Redis监听所有网卡,Docker容器内必须这样配,否则外部连不上。protected-mode no是关闭保护模式,因为我们在Docker网络里,有网络隔离,不需要额外保护。requirepass和masterauth都设成一样的密码,前者是客户端连接密码,后者是从节点同步密码,设成一样省事,生产环境也可以用不同密码,但配置会复杂一些。

注意:daemonize no必须设置,因为Docker容器需要前台进程,如果设成yes,容器启动后Redis会后台运行,Docker以为进程退出了,容器直接停止。

3.2 从节点配置与主从同步

从节点的配置和主节点大同小异,核心区别是多了replicaof指令。在Redis 5.0之前叫slaveof,5.0之后推荐用replicaof,语义更准确。

bind 0.0.0.0 protected-mode no port 6379 daemonize no appendonly no save 900 1 save 300 10 requirepass YourStrongPassword masterauth YourStrongPassword replicaof redis-master 6379 replica-read-only yes

replicaof redis-master 6379这行是关键,告诉从节点去同步哪个主节点。这里直接写容器名redis-master,Docker的DNS会解析成正确IP。replica-read-only yes是从节点只读,防止误写导致数据不一致。我见过有人从节点没设只读,结果程序连错地址写入了从节点,主从数据直接乱掉,排查了一整天。

从节点的Docker Compose配置和主节点类似,只是端口映射和挂载路径不同:

redis-slave1: image: redis:7.0-alpine container_name: redis-slave1 ports: - "6380:6379" volumes: - ./slave1/redis.conf:/usr/local/etc/redis/redis.conf - ./slave1/data:/data command: redis-server /usr/local/etc/redis/redis.conf networks: - redis-net depends_on: - redis-master restart: always

depends_on保证主节点先启动,但注意这只是启动顺序控制,不代表主节点已经准备好接受连接。从节点启动时如果主节点还没就绪,会不断重试同步,最终会成功,所以不用太担心。

3.3 哨兵配置与监控参数

哨兵配置是整个方案里最容易出错的部分。先看配置文件:

port 26379 daemonize no sentinel monitor mymaster redis-master 6379 2 sentinel auth-pass mymaster YourStrongPassword sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 60000 sentinel parallel-syncs mymaster 1

逐行解释。sentinel monitor mymaster redis-master 6379 2这行定义了一个监控组,名字叫mymaster,监控的主节点是redis-master:6379,最后的2是quorum值,表示至少2个哨兵认为主节点挂了才触发故障转移。3个哨兵设2是合理的,既保证可靠性又不会太敏感。

sentinel auth-pass mymaster YourStrongPassword是哨兵连接Redis的密码,必须和Redis的requirepass一致,否则哨兵连不上节点,监控直接失效。sentinel down-after-milliseconds mymaster 5000是主观下线时间,哨兵在5秒内ping不通主节点就认为它挂了。这个值不能太小,网络抖动容易误判;也不能太大,故障转移太慢。5000毫秒是我实测比较平衡的值。

sentinel failover-timeout mymaster 60000是故障转移超时时间,60秒内没完成转移就重新尝试。sentinel parallel-syncs mymaster 1表示故障转移后,同时只有1个从节点去同步新主节点,避免新主节点被多个从节点同时同步压垮。

哨兵的Docker Compose配置需要额外注意一点:哨兵端口必须暴露且互相可访问。因为哨兵之间要互相通信来交换监控信息。

redis-sentinel1: image: redis:7.0-alpine container_name: redis-sentinel1 ports: - "26379:26379" volumes: - ./sentinel1/sentinel.conf:/usr/local/etc/redis/sentinel.conf command: redis-sentinel /usr/local/etc/redis/sentinel.conf networks: - redis-net depends_on: - redis-master - redis-slave1 - redis-slave2 restart: always

三个哨兵的配置除了端口号不同,其他完全一样。这里有个细节:哨兵启动后会自动发现其他哨兵,不需要在配置里手动指定。哨兵通过发布订阅机制在__sentinel__:hello频道上交换信息,自动组成集群。所以只要三个哨兵都监控同一个主节点,它们会自动找到彼此。

4. 完整部署流程与验证步骤

4.1 目录结构与文件准备

先把目录结构建好,我习惯这样组织:

redis-cluster/ ├── docker-compose.yml ├── master/ │ ├── redis.conf │ └── data/ ├── slave1/ │ ├── redis.conf │ └── data/ ├── slave2/ │ ├── redis.conf │ └── data/ ├── sentinel1/ │ └── sentinel.conf ├── sentinel2/ │ └── sentinel.conf └── sentinel3/ └── sentinel.conf

创建目录的命令:

mkdir -p redis-cluster/{master,slave1,slave2}/{data} mkdir -p redis-cluster/{sentinel1,sentinel2,sentinel3} cd redis-cluster

然后把前面写的配置文件分别放到对应目录。这里有个容易忽略的点:哨兵配置文件的权限。哨兵在运行时会重写配置文件(比如故障转移后更新主节点地址),如果文件权限不对,哨兵会报错。建议把哨兵配置文件权限设成644,并且确保容器内用户有写权限。

4.2 启动顺序与健康检查

启动顺序很重要,我建议按这个顺序来:先启动主节点,等主节点完全就绪后再启动从节点,最后启动哨兵。虽然Docker Compose的depends_on能控制启动顺序,但控制不了就绪状态。更稳妥的做法是分步启动:

# 第一步:启动主节点 docker-compose up -d redis-master # 等待5秒,确认主节点就绪 sleep 5 docker exec redis-master redis-cli -a YourStrongPassword ping # 第二步:启动从节点 docker-compose up -d redis-slave1 redis-slave2 # 等待5秒,确认从节点同步完成 sleep 5 docker exec redis-slave1 redis-cli -a YourStrongPassword info replication # 第三步:启动哨兵 docker-compose up -d redis-sentinel1 redis-sentinel2 redis-sentinel3

验证主从同步是否正常,看从节点的info replication输出:

docker exec redis-slave1 redis-cli -a YourStrongPassword info replication

输出里应该看到role:slave、master_host:redis-master、master_link_status:up。如果master_link_status是down,说明同步失败,检查密码和网络连通性。

验证哨兵是否正常,连上任意哨兵执行:

docker exec redis-sentinel1 redis-cli -p 26379 sentinel master mymaster

输出里会显示主节点的IP、端口、从节点数量等信息。再执行:

docker exec redis-sentinel1 redis-cli -p 26379 sentinel sentinels mymaster

应该能看到另外两个哨兵的信息。如果只看到自己,说明哨兵之间没发现彼此,检查网络和端口配置。

4.3 读写分离验证与数据一致性测试

部署完成后,做个简单的读写测试。先往主节点写数据:

docker exec redis-master redis-cli -a YourStrongPassword set test_key "hello_redis"

然后从从节点读:

docker exec redis-slave1 redis-cli -a YourStrongPassword get test_key docker exec redis-slave2 redis-cli -a YourStrongPassword get test_key

两个从节点都应该返回hello_redis。再测试从节点只读:

docker exec redis-slave1 redis-cli -a YourStrongPassword set test_key2 "should_fail"

应该返回READONLY You can't write against a read only replica.,说明只读配置生效。

数据一致性方面,主从复制是异步的,所以从节点数据可能比主节点慢几毫秒到几十毫秒。如果业务对一致性要求高,可以在写完后等一小会儿再读,或者用WAIT命令阻塞等待同步完成:

docker exec redis-master redis-cli -a YourStrongPassword wait 1 1000

这个命令表示等待至少1个从节点确认,超时1000毫秒。返回的数字是实际确认的从节点数量。

5. 故障转移演练与常见问题排查

5.1 模拟主节点宕机与自动切换

故障转移演练是验证整套架构是否可靠的关键步骤。先手动停掉主节点:

docker stop redis-master

然后观察哨兵日志:

docker logs -f redis-sentinel1

正常情况下,5秒后哨兵会标记主节点为主观下线,然后询问其他哨兵。当2个以上哨兵都确认主节点不可达时,标记为客观下线,触发故障转移。选举新主节点的过程大概需要10到30秒,取决于failover-timeout配置。

转移完成后,连上哨兵查看新的主节点:

docker exec redis-sentinel1 redis-cli -p 26379 sentinel get-master-addr-by-name mymaster

返回的应该是某个从节点的IP和端口。再检查原从节点是否变成了新主节点:

docker exec redis-slave1 redis-cli -a YourStrongPassword info replication

如果role:master,说明切换成功。原来的主节点恢复后,会自动变成新主节点的从节点,这个行为是哨兵自动处理的,不需要人工干预。

5.2 常见问题速查表

问题现象可能原因排查方法解决方案
从节点master_link_status:down密码错误或网络不通检查masterauth和requirepass是否一致统一密码配置
哨兵无法发现彼此端口未暴露或网络隔离docker exec进入容器ping其他哨兵确保哨兵端口映射正确
故障转移不触发quorum值设置过高检查sentinel monitor的quorum值调整为哨兵数量的多数派
切换后客户端连不上客户端未使用哨兵模式检查客户端连接方式改用哨兵连接池
数据丢失主从异步复制延迟检查master_repl_offset差值关键数据用WAIT命令

5.3 实操心得与避坑指南

第一个坑:哨兵配置文件的写权限。哨兵在故障转移后会重写配置文件,如果文件是只读的,哨兵会报错但不会退出,只是切换后配置不持久化,重启后回到旧配置。我建议把哨兵配置文件放在可写卷里,并且定期备份。

第二个坑:Docker重启后IP变化。虽然哨兵会自动发现新IP,但如果所有节点同时重启,哨兵可能来不及更新配置。解决方案是给容器分配固定IP,或者在哨兵配置里用容器名而不是IP。我实测下来,用容器名最省心。

第三个坑:客户端连接池配置。很多客户端连接池只配置了主节点地址,故障转移后不会自动切换。必须使用支持哨兵的客户端,比如Jedis的JedisSentinelPool、Lettuce的RedisSentinelConfiguration。配置时传入哨兵地址列表和主节点名称,客户端会自动从哨兵获取当前主节点地址。

第四个坑:脑裂问题。网络分区时,旧主节点可能还在接受写请求,新主节点也在接受写请求,导致数据不一致。缓解方案是配置min-replicas-to-write和min-replicas-max-lag,让主节点在从节点数量不足或延迟过大时拒绝写入:

min-replicas-to-write 1 min-replicas-max-lag 10

这两个参数表示至少1个从节点延迟不超过10秒,主节点才接受写入。这样网络分区时旧主节点会自动拒绝写入,减少数据不一致的风险。

6. 生产环境加固与性能调优

6.1 安全加固措施

生产环境不能裸奔,几个基本的安全措施必须做。第一,修改默认端口,虽然Docker映射了端口,但容器内还是6379,建议改成非标准端口,减少被扫描的风险。第二,启用密码认证,前面已经配了requirepass,但密码要足够复杂,建议用32位以上的随机字符串。第三,限制网络访问,Docker网络虽然隔离,但宿主机端口映射后外部还是能访问,建议用防火墙限制来源IP,只允许应用服务器访问。

第四,禁用危险命令,比如FLUSHALL、FLUSHDB、KEYS,这些命令在生产环境可能被误用或恶意利用。在配置里重命名或禁用:

rename-command FLUSHALL "" rename-command FLUSHDB "" rename-command KEYS ""

第五,开启慢查询日志,监控执行时间超过阈值的命令:

slowlog-log-slower-than 10000 slowlog-max-len 128

slowlog-log-slower-than 10000表示超过10毫秒的命令记录到慢查询日志,slowlog-max-len 128表示最多保留128条。定期检查慢查询日志,能发现很多性能问题。

6.2 性能调优参数

Redis性能调优涉及内存、网络、持久化多个方面。内存方面,设置maxmemory和maxmemory-policy:

maxmemory 2gb maxmemory-policy allkeys-lru

maxmemory根据容器内存限制设置,建议留20%余量给系统。maxmemory-policy推荐allkeys-lru,淘汰最近最少使用的键。如果数据都不允许丢失,那就用noeviction,但内存满了会报错,得配合监控告警。

网络方面,调整TCP backlog和keepalive:

tcp-backlog 511 tcp-keepalive 300

tcp-backlog是TCP连接队列长度,高并发场景可以调到1024。tcp-keepalive是TCP保活时间,300秒表示5分钟没通信就发探测包。

持久化方面,AOF重写策略影响性能:

auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb

表示AOF文件比上次重写后增长100%且至少64MB时触发重写。重写期间会fork子进程,内存占用翻倍,所以maxmemory要留足余量。

6.3 监控与告警配置

没有监控的架构就是裸奔。Redis自带info命令能获取大量指标,建议用Prometheus的redis_exporter采集,Grafana展示。关键监控指标包括:used_memory、connected_clients、instantaneous_ops_per_sec、keyspace_hits、keyspace_misses、master_link_status、master_repl_offset。

告警规则建议设置:内存使用率超过80%告警、主从同步断开告警、哨兵数量少于2告警、慢查询数量突增告警。我踩过的坑是只监控了Redis本身,没监控哨兵。结果哨兵进程挂了没人知道,主节点真挂了才发现没有哨兵来切换。所以哨兵的监控同样重要,至少监控哨兵的存活状态和sentinel master命令的响应时间。

7. 客户端接入与代码示例

7.1 Java客户端配置

Java生态里最常用的是Jedis和Lettuce。Jedis的哨兵配置:

Set<String> sentinels = new HashSet<>(); sentinels.add("192.168.1.100:26379"); sentinels.add("192.168.1.100:26380"); sentinels.add("192.168.1.100:26381"); JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxTotal(100); poolConfig.setMaxIdle(20); poolConfig.setMinIdle(5); JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels, poolConfig, "YourStrongPassword"); Jedis jedis = pool.getResource(); jedis.set("key", "value");

注意JedisSentinelPool的第一个参数是主节点名称,必须和哨兵配置里的mymaster一致。密码是Redis的requirepass,不是哨兵的密码。

Lettuce的配置更简洁:

RedisSentinelConfiguration config = new RedisSentinelConfiguration() .master("mymaster") .sentinel("192.168.1.100", 26379) .sentinel("192.168.1.100", 26380) .sentinel("192.168.1.100", 26381) .password("YourStrongPassword"); LettuceConnectionFactory factory = new LettuceConnectionFactory(config); factory.afterPropertiesSet(); RedisTemplate<String, String> template = new RedisTemplate<>(); template.setConnectionFactory(factory); template.afterPropertiesSet();

Lettuce默认是异步的,性能比Jedis好,但配置稍复杂。Spring Boot项目里直接用spring-boot-starter-data-redis,配置spring.redis.sentinel.master和spring.redis.sentinel.nodes就行。

7.2 连接池参数调优

连接池参数直接影响性能。maxTotal是最大连接数,根据应用并发量设置,一般QPS的1.5倍左右。maxIdle是最大空闲连接,建议是maxTotal的20%到30%。minIdle是最小空闲连接,建议5到10个,避免频繁创建连接。maxWaitMillis是获取连接的最大等待时间,建议1000到3000毫秒,超时直接抛异常,避免请求堆积。

有个经验值:连接池不是越大越好。我见过有人把maxTotal设成1000,结果Redis端connected_clients爆表,反而影响性能。Redis单线程处理命令,连接数太多会增加上下文切换开销。一般应用maxTotal设50到200就够了,具体看压测结果。

7.3 故障转移时的客户端行为

哨兵故障转移时,客户端会短暂报错。Jedis和Lettuce的处理方式不同。Jedis在获取连接时如果发现主节点变了,会重新从哨兵获取新地址,但已经获取的连接会失效,抛JedisConnectionException。Lettuce会自动重连,对应用透明,但会有短暂的命令失败。

应用层需要做重试。建议在Redis操作外层包一层重试逻辑,捕获连接异常后重试2到3次,间隔100到500毫秒。Spring Retry或Resilience4j都能实现。注意重试只对幂等操作安全,非幂等操作要谨慎,避免重复写入。

8. 扩展思路与架构演进

8.1 从哨兵到Cluster的演进路径

哨兵模式解决了高可用问题,但没解决水平扩展问题。单主节点写入能力有限,内存也受单机限制。当数据量超过单机内存或写入QPS超过单节点上限时,就需要考虑Redis Cluster。Cluster把数据分片到多个主节点,每个主节点负责一部分槽位,写入能力线性扩展。

从哨兵迁移到Cluster不是无缝的,客户端需要改用Cluster模式,数据需要重新分片。迁移方案有两种:一是停机迁移,用redis-cli --cluster工具重新分片;二是双写迁移,新旧集群同时写,逐步切流量。我建议新项目直接上Cluster,老项目如果哨兵够用就别折腾,迁移成本不低。

8.2 多机房部署考虑

如果业务有多机房需求,哨兵模式可以跨机房部署,但要注意网络延迟。哨兵的down-after-milliseconds要大于机房之间的网络延迟,否则容易误判。比如同城机房延迟1到2毫秒,设5000没问题;异地机房延迟30到50毫秒,就得设10000以上。

跨机房部署还有个问题:脑裂风险更高。网络分区时,两个机房可能各自选举主节点。缓解方案是设置min-replicas-to-write,并且把多数哨兵放在同一个机房,保证只有多数派机房能完成选举。

8.3 容器化部署的注意事项

Docker部署Redis有几点要特别注意。第一,内存限制,Docker容器的内存限制和Redis的maxmemory要匹配,容器限制2GB,Redis就设1.6GB左右,留余量给系统。第二,CPU限制,Redis是单线程的,给1到2个CPU核心就够了,给多了浪费。第三,磁盘IO,AOF持久化对磁盘IO要求高,建议用SSD,并且把数据卷挂载到宿主机的高性能磁盘上。

第四,时钟同步,哨兵依赖时间来判断节点是否下线,容器时钟必须和宿主机同步。Docker默认使用宿主机时钟,一般没问题,但如果是跨主机部署,要确保所有宿主机时钟同步。第五,日志收集,容器日志默认存在宿主机上,建议配置日志驱动转发到集中日志系统,方便排查问题。

我个人在实际操作中的体会是,Docker部署Redis主从哨兵,最难的不是配置本身,而是理解每个参数背后的含义和边界条件。配置文件抄一份很容易,但出了问题知道去哪里找原因,才是真正掌握这套架构的标志。建议大家在测试环境多演练几次故障转移,把各种异常情况都模拟一遍,比如主节点宕机、从节点宕机、哨兵宕机、网络分区,观察系统行为,积累排查经验。最后再分享一个小技巧:哨兵的日志级别设成notice就够了,debug级别日志量太大,生产环境容易把磁盘写满。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进