使用 Redis Sentinel 重构现有架构
对于搭建高可用 Redis 服务,网上已有了很多方案,例如 Keepalived,Codis,Twemproxy,Redis Sentinel。
其中 Codis 和 Twemproxy 主要是用于大规模的 Redis 集群中,也是在 Redis 官方发布 Redis Sentinel 之前 豌豆荚 和 twitter 提供的开源解决方案。
Redis Sentinel 可以理解为一个监控 Redis Server 服务是否正常的进程,并且一旦检测到不正常,可以自动地将备份 (slave)Redis Server 启用,使得外部用户对
Redis 服务内部出现的异常无感知。
原有架构
存在的问题
- 配置部署复杂
- 不稳定
Redis Sentinel 高可用
下面以 1 个主节点、2 个从节点、3 个 Sentinel 节点组成的 Redis Sentinel 为例子
故障转移处理逻辑:
主节点出现故障, 此时两个从节点与主节点时区连接, 主从复制失败;
每个 Sentinel 节点通过定期监控发现主节点出现故障;
多个 Sentinel 节点对主节点的故障达成一致, 选举出 sentinel-3 节点作为领导者负责故障转移;
- 原来的从节点 slave-1 成为新的主节点后, 更新应用方的主节点信息;
- 客户端命令另一个从节点 slave-2 去复制新的主节点;
- 待原来的主节点恢复后, 让它去复制新的主节点;
故障转移后的结构图
Redis Sentinel 功能
- 监控: Sentinel 节点会定期检测 Redis 数据节点和其余 Sentinel 节点是否可达;
- 通知: Sentinel 节点会将故障转移的结果通知给应用方;
- 主节点故障转移: 实现从节点晋升为主节点并维护后续正确的主从关系;
- 配置提供者: 客户端在初始化时, 连接 Sentinel 节点集群, 从中获取主节点信息;
采用多个 Sentinel 节点的优点:
- 对于节点故障判断由多个 Sentinel 节点共同完成, 有效防止误判;
- 避免单点故障;
几个概念
三个定时监控任务
每隔 10 秒. 每个 Sentinel 节点会向主节点和从节点发送 info 命令获取最新的拓扑结构;
每隔 2 秒, 每个 Sentinel 节点向 sentinel:hello 频道上发送该 Sentinel 节点对于主节点的判断一级当前 Sentinel 节点的信息, 同时每个
Sentinel 节点也会订阅该频道;
每隔 1 秒, 每个 Sentine 会向主从节点, 其他 Sentinel 节点发送 ping 命令做心跳检测, 来确保节点当前是否可达
主观下线(Subjectively Down, 简称 SDOWN)
指的是单个 Sentinel 实例对服务器做出的下线判断。
每个 Sentinel 节点会每隔 1 秒对主节 点、从节点、其他 Sentinel 节点发送 ping 命令做心跳检测,当这些节点超过 down-after-milliseconds
没有进行有效回复,Sentinel 节点就会对该节点做失败 判定,这个行为叫做主观下线
客观下线(Objectively Down, 简称 ODOWN)
指的是多个 Sentinel 实例在对同一个服务器做出 SDOWN 判断, 并且通过 SENTINEL is-master-down-by-addr 命令互相交流之后, 得出的服务器下线判断。
(一个 Sentinel 可以通过向另一个 Sentinel 发送 SENTINEL is-master-down-by-addr 命令来询问对方是否认为给定的服务器已下线。)
Redis Sentinel 安装与部署
下面将以 3 个 Sentinel 节点、1 个主节点、2 个从节点组成一个 Redis Sentinel 进行说明
具体的物理部署:
角色 |
ip |
port |
别名 |
master |
127.0.0.1 |
6379 |
主节点 |
slave-1 |
127.0.0.1 |
6380 |
slave-1 |
slave-2 |
127.0.0.1 |
6381 |
slave-2 |
sentinel-1 |
127.0.0.1 |
26379 |
sentinel-1 |
sentinel-2 |
127.0.0.1 |
26380 |
sentinel-2 |
sentinel-3 |
127.0.0.1 |
26381 |
sentinel-3 |
1. master 配置
1 2 3 4 5 6
| daemonize yes dbfilename "6379.db" dir "/Users/codeai/Develop/logs/redis/db/" logfile "/Users/codeai/Develop/logs/redis/log/6379.log" port 6379 requirepass 1234
|
2. slave-1 配置
1 2 3 4 5 6 7 8 9 10
| daemonize yes dbfilename "6380.db" dir "/Users/codeai/Develop/logs/redis/db/" logfile "/Users/codeai/Develop/logs/redis/log/6380.log" port 6380 slaveof 127.0.0.1 6379 # 设置 master 验证密码 masterauth 1234 # 设置 slave 密码 requirepass 1234
|
3. slave-2 配置
1 2 3 4 5 6 7 8 9 10
| daemonize yes dbfilename "6381.db" dir "/Users/codeai/Develop/logs/redis/db/" logfile "/Users/codeai/Develop/logs/redis/log/6381.log" port 6381 slaveof 127.0.0.1 6379 # 设置 master 验证密码 masterauth 1234 # 设置 slave 密码 requirepass 1234
|
4. 启动 redis 服务
1
| redis-server redis-6379.conf; redis-server redis-6380.conf; redis-server redis-6381.conf
|
5. 确认主从关系
主节点视角
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| redis-cli -h 127.0.0.1 -p 6379 info replication # Replication # 主节点 role:master # 有 2 个 从节点 connected_slaves:2 # 从节点 1 信息 slave0:ip=127.0.0.1,port=6380,state=online,offset=112,lag=0 # 从节点 2 信息 slave1:ip=127.0.0.1,port=6381,state=online,offset=112,lag=0 master_replid:8f43dc48cca779f46fa1516a38e24fb8c5423d94 master_replid2:0000000000000000000000000000000000000000 master_repl_offset:112 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:112
|
从节点视角
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| redis-cli -h 127.0.0.1 -p 6380 info replication # Replication # 从节点 role:slave # 主节点信息 master_host:127.0.0.1 master_port:6379 master_link_status:up master_last_io_seconds_ago:2 master_sync_in_progress:0 slave_repl_offset:238 slave_priority:100 slave_read_only:1 connected_slaves:0 master_replid:8f43dc48cca779f46fa1516a38e24fb8c5423d94 master_replid2:0000000000000000000000000000000000000000 master_repl_offset:238 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:238
|
6. 部署 Sentinel 节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| port 26379 daemonize yes logfile "/Users/codeai/Develop/logs/redis/log/26379.log" dir "/Users/codeai/Develop/logs/redis/db/" # 监控 127.0.0.1:6379 主节点; 2 表示判断主节点失败至少需要 2 个 sentinel 节点同意 sentinel monitor mymaster 127.0.0.1 6379 2 # 设置监听的 master 密码 sentinel auth-pass mymaster 1234 # 30 秒内 ping 失败, sentinel 则认为 master 不可用 sentinel down-after-milliseconds mymaster 30000 # 在发生 failover 主备切换时,这个选项指定了最多可以有多少个 slave 同时对新的 master 进行同步 sentinel parallel-syncs mymaster 1 # 如果在该时间(ms)内未能完成 failover 操作,则认为该 failover 失败 sentinel failover-timeout mymaster 180000
|
其他节点只是端口不同
7. 启动 sentinel 节点
1 2 3 4
| # 方式一 redis-sentinel redis-sentinel-26379.conf; redis-sentinel redis-sentinel-26380.conf; redis-sentinel redis-sentinel-26381.conf # 方式二 redis-server redis-sentinel-26379.conf
|
8. 确认关系
1 2 3 4 5 6 7 8
| redis-cli -h 127.0.0.1 -p 26379 info sentinel # Sentinel sentinel_masters:1 sentinel_tilt:0 sentinel_running_scripts:0 sentinel_scripts_queue_length:0 sentinel_simulate_failure_flags:0 master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=3
|
部署方案
- Sentinel 部署在不同物理机上;
- 部署至少三个且奇数个的 Sentinel 节点;
一套 Sentinel 监控所有主节点
优点:
- 维护成本低
缺点:
- 集群出现异常, 将导致服务不可用
- 过多的网络连接
每个主节点各一套 Sentinel
优点:
- 某个 Sentinel 集群出现故障, 不会影响其他业务
- 网络连接少
缺点:
- 维护成本高
如果监控同一个业务的多个主节点集合, 推荐使用方案一
如果是多个业务不同主节点集合, 推荐方案二 (推荐)
Redis 连接数太多导致
原因分析
Redis 默认最大连接数为 10000 个
- 网络通信差, 按照 TCP 协议,客户端断开连接时,向服务器端发送 FIN 信号,但是服务端未接收到,客户端超时后放弃等待,直接断开,服务端由于通信故障,保持了
ESTABLISHED 状态;
- 客户端异常, 客户端连接之后,由于代码运行过程中产生异常,导致未正常释放或者关闭连接;
- client 设置不合理 (
client 数 * maxTotal
是不能超过 redis 的最大连接数)
解决方案
1. 修改 redis.config 配置
1 2 3 4
| # 连接的空闲实现超过 360s, 则主动关闭连接; 默认配置为 0 , 导致所有空闲 idle 连接未被释放, 服务端连接泄漏 timeout 360 # 默认关闭, 导致服务端不知客户端连接状态; 开启长连接, 服务端主动 (60s) 探测客户端 socket 状态 tcp-keeplive 60
|
2. 完善代码
客户端每次执行完 jedis 里面的方法之后必须关闭链接,释放资源
3. redis-proxy 服务化
重构方案
使用 Redis Sentinel 代替 Redis + Keepalived
架构
重构 redis-proxy
提供 6 种 连接模式
component-redis 介绍
项目中有单独使用 Redis 的, 也有使用分片方式连接的, 也有使用 redis-proxy 组件来连接 Redis 的.
造成代码不好管理, 因此使用 component-redis 组件为其他模块提供连接 Redis 的功能, 统一管理 Redis 相关代码.
作为连接 Redis 的工具模块, 为其他模块提供操作 Redis 的功能, 具有多种模式选择.
客户端不需要再写连接 Redis 相关代码, 只需要按照要求配置即可, 减少了冗余代码;
兼容原有代码, 只需要将 redis-proxy 依赖替换成 component-redis 即可使用.
功能
- 多种模式任君选择 (standalone, sentinel, shard, shard-sentinel, cluster, hybrid);
- 使用简单, 引入 jar 即可使用;
- 扩展方便, 如果
RedisService
满足不了现有业务需要, 可直接使用各种 Pool 获取 jedis, shardedJedis, jedisCluster 自由发挥;
使用方式
1. 引入依赖
1 2 3 4 5
| <dependency> <groupId>com.xxxx.msearch</groupId> <artifactId>component-redis</artifactId> <version> 最新版本 </version> </dependency>
|
2. 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| # jedis pool 配置 # 连接超时时间(毫秒) redis.connectionTimeout=2000 # 等待 Response 超时时间 (新增) redis.soTimeout=5000 redis.pool.maxActive=5000 redis.pool.maxWait=5000 redis.pool.maxIdle=200 redis.pool.minIdleTime=120 redis.pool.testOnBorrow=true redis.pool.testOnReturn=false # Redis 配置 redis.model=standalone redis.node=redis://127.0.0.1:6379
|
3. 注入 service
1 2 3 4 5 6
| @Autowired private RedisService redisService; @Test public void testRedisService() throws Exception { redisService.set("redisServiceTest", "redisServiceTest"); }
|
component-redis 配置规范
由于 component-redis 组件需要支持多种模式, 配置需要规范的格式才能避免出错.
- 节点使用
,
分隔, 模式分组使用 ;
分隔;
- 使用某个 Redis 时, 不要把所有配置全部加上;
jedisPool 配置是固定配置, 每种模式都会使用到, 只需要根据业务调整即可.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ## 连接超时时间(毫秒) redis.connectionTimeout=2000 # 等待 Response 超时时间 (新增) redis.soTimeout=5000 # 连接池最大连接数(使用负值表示没有限制) redis.pool.maxActive=5000 # 连接池最大阻塞等待时间(使用负值表示没有限制) redis.pool.maxWait=5000 # 连接池中的最大空闲连接 redis.pool.maxIdle=200 # 连接池中的最小空闲连接 redis.pool.minIdleTime=120 # 当调用 borrow Object 方法时,是否进行有效性检查 redis.pool.testOnBorrow=true # 调用 return 一个对象方法时,是否检查其有效性 redis.pool.testOnReturn=false
|
1 2
| redis.model= 模式名 redis.node= 业务名 #模式名://[:password@]ip:port[/database];...
|
这里以现有业务为例子:
1 2 3
| redis.model=hybrid # callout 使用单机模式, biz 使用哨兵模式 redis.node=callout#standalone://:1234@127.0.0.1:6382;biz#sentinel://:5678@127.0.0.1:26379,sentinel://127.0.0.1:26380,sentinel://127.0.0.1:26381
|
配置优化
使用标准的 uri 协议代替 host 和 port
避免手动解析出错
格式如下:
1 2 3 4 5 6 7 8
| # 完整格式 redis://user:password@ip:port/database # 不需要用户名的格式 redis://:password@ip:port/database # 不需要密码的格式 redis://ip:port/database # 不需要 database 的配置, 将默认使用 0 db redis://ip:port
|
密码设置
redis 的查询速度是非常快的,外部用户一秒内可以尝试多大 150K 个密码;所以密码要尽量长;
建议设置为 64 位长度密码
standalone (单机) 模式配置
1 2 3
| redis.model=standalone # password 前面的 : 不能少 redis.node=redis://:password@127.0.0.1:6382
|
demo:
1 2
| redis.model=standalone redis.node=redis://127.0.0.1:6379
|
sentinel
哨兵模式是对单机模式高可用的一种实现方式, 可以实现故障主从自动切换
哨兵模式需要配置 master name, 和 redis-sentinel
.conf 中的 sentinel monitor masterName xxx
保持一致
哨兵模式只能接收一个密码, 密码设置在任意节点即可 (第一个最好了)
1 2
| redis.model=sentinel redis.node=mymaster#redis://:1234@127.0.0.1:26379,redis://127.0.0.1:26380,redis://127.0.0.1:26381
|
哨兵模式就是单机模式的增强版, 需要配置多个哨兵节点 (避免造成主从切换失败, 最少 3 组哨兵), 节点之间使用 ,
分隔
demo:
1 2
| redis.model=sentinel redis.node=mymaster#redis://:s4jRcLhAcUdKrNmqv9XQxwbEUZ6p4sK3kTFE4k9ts3PLahnswEzE4aPgXEQ6QdMa@127.0.0.1:26379,redis://127.0.0.1:26380,redis://127.0.0.1:26381
|
sharding (分片) 模式配置
每组 redis 实例可以设置不同的密码
分片实例之间使用 ,
分隔
1 2
| redis.model=sharding redis.node=redis://:1234@127.0.0.1:6382,redis://:5678@127.0.0.1:6382
|
demo:
1 2 3
| # redis://:password@ip:port/database 没有密码则使用 redis://ip:port/database redis.model=sharding redis.node=redis://:1234@127.0.0.1:6382,redis://:s4jRcLhAcUdKrNmqv9XQxwbEUZ6p4sK3kTFE4k9ts3PLahnswEzE4aPgXEQ6QdMa@127.0.0.1:6379
|
sharding-sentinel
分片哨兵模式是对分片模式高可用的一种实现方式, 可以实现分片模式下, 故障主从自动切换
分片哨兵模式是哨兵模式和分片模式的结合, 配置可
demo:
1 2
| redis.model=sharding-sentinel redis.node=mymaster#redis://127.0.0.1:26379,redis://127.0.0.1:26380,redis://127.0.0.1:26381;mymaster1#redis://127.0.0.1:26379,redis://127.0.0.1:26380,redis://127.0.0.1:26381
|
cluster
redis 3.x 远程集群模式
demo:
1 2
| redis.model=cluster redis.node=redis://127.0.0.1:6379,redis://127.0.0.1:6380,redis://127.0.0.1:6381
|
hybrid
混合模式, 为了兼容现有 Redis 环境, 一种临时的解决方案, 改造完成后, 将配置修改为 sentinel
即可
demo:
1 2
| redis.model=hybrid redis.node=callout#standalone://:1234@127.0.0.1:6382;mymaster#sentinel://:s4jRcLhAcUdKrNmqv9XQxwbEUZ6p4sK3kTFE4k9ts3PLahnswEzE4aPgXEQ6QdMa@127.0.0.1:26379,sentinel://127.0.0.1:26380,sentinel://127.0.0.1:26381
|
混合模式实现了 standalone
, sentinel
, sharding
模式的混合使用
代码重构
- 使用
@Value
实现自动配置, 代替 xml 中的 bean 标签;
- 使用 logbok 简化代码;
- 不在使用 RedisServiceFacotry 获取 redisService, 某个模块需要使用该组件时, import xml 即可 (多容器问题);
- 使用 log4j2 代替 log4j;
- 编译版本由 jdk1.6 改为 jdk1.7;
- 使用代理类重构安全关闭 jedis 连接的方式;
重构前:
1 2 3 4 5 6 7 8 9
| @Override public String set(String flag, final String key, final String value) throws Exception { return new RedisCallBack<String>() { @Override public String doCallback(Jedis jedis) { return jedis.set(key, value); } }.callback(getJedisPoolByFlag(flag)); }
|
重构后:
1 2 3 4
| @Override public String set(String flag, final String key, final String value) { return RedisUtil.jedisProxy(model, flag).set(key, value); }
|