Redis 架构重构
使用 Redis Sentinel 重构现有架构
redis-proxy 重构
[toc]
对于搭建高可用 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 配置
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
3
4
5
6
2. slave-1 配置
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
2
3
4
5
6
7
8
9
10
3. slave-2 配置
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
2
3
4
5
6
7
8
9
10
4. 启动 redis 服务
redis-server redis-6379.conf; redis-server redis-6380.conf; redis-server redis-6381.conf
5. 确认主从关系
主节点视角
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
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 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
6. 部署 Sentinel 节点
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
2
3
4
5
6
7
8
9
10
11
12
13
14
其他节点只是端口不同
7. 启动 sentinel 节点
# 方式一
redis-sentinel redis-sentinel-26379.conf; redis-sentinel redis-sentinel-26380.conf; redis-sentinel redis-sentinel-26381.conf
# 方式二
redis-server redis-sentinel-26379.conf --sentinel; redis-server redis-sentinel-26380.conf --sentinel; redis-server redis-sentinel-26381.conf --sentinel;
2
3
4
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
2
3
4
5
6
7
8
部署方案
- Sentinel 部署在不同物理机上;
- 部署至少三个且奇数个的 Sentinel 节点;
一套 Sentinel 监控所有主节点
优点:
- 维护成本低
缺点:
- 集群出现异常, 将导致服务不可用
- 过多的网络连接
每个主节点各一套 Sentinel
优点:
- 某个 Sentinel 集群出现故障, 不会影响其他业务
- 网络连接少
缺点:
- 维护成本高
如果监控同一个业务的多个主节点集合, 推荐使用方案一 如果是多个业务不同主节点集合, 推荐方案二 (推荐)
Redis 连接数太多导致
原因分析
Redis 默认最大连接数为 10000个
- 网络通信差, 按照 TCP 协议, 客户端断开连接时, 向服务器端发送 FIN 信号, 但是服务端未接收到, 客户端超时后放弃等待, 直接断开, 服务端由于通信故障, 保持了 ESTABLISHED 状态;
- 客户端异常, 客户端连接之后, 由于代码运行过程中产生异常, 导致未正常释放或者关闭连接;
- client 设置不合理 (client 数 * maxTotal 是不能超过redis的最大连接数)
解决方案
1. 修改 redis.config 配置
# 连接的空闲实现超过 360s, 则主动关闭连接;默认配置为 0 ,导致所有空闲 idle 连接未被释放, 服务端连接泄漏
timeout 360
# 默认关闭, 导致服务端不知客户端连接状态; 开启长连接, 服务端主动(60s)探测客户端 socket 状态
tcp-keeplive 60
2
3
4
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. 引入依赖
<dependency>
<groupId>com.iflytek.musicsearch</groupId>
<artifactId>component-redis</artifactId>
<version>最新版本</version>
</dependency>
2
3
4
5
2. 配置
# 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
2
3
4
5
6
7
8
9
10
11
12
13
14
3. 注入 service
@Autowired
private RedisService redisService;
@Test
public void testRedisService() throws Exception {
redisService.set("redisServiceTest", "redisServiceTest");
}
2
3
4
5
6
component-redis 配置规范
由于 component-redis 组件需要支持多种模式, 配置需要规范的格式才能避免出错.
- 节点使用
,
分隔, 模式分组使用;
分隔; - 使用某个 Redis 时, 不要把所有配置全部加上;
jedisPool 配置是固定配置, 每种模式都会使用到, 只需要根据业务调整即可.
## 连接超时时间(毫秒)
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
redis.model=模式名
redis.node=业务名#模式名://[:password@]ip:port[/database];...
2
这里以现有业务为例子:
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
2
3
配置优化
使用标准的 uri 协议代替 host 和 port
避免手动解析出错
格式如下:
# 完整格式
redis://user:password@ip:port/database
# 不需要用户名的格式
redis://:password@ip:port/database
# 不需要密码的格式
redis://ip:port/database
# 不需要 database 的配置, 将默认使用 0 db
redis://ip:port
2
3
4
5
6
7
8
密码设置
redis的查询速度是非常快的, 外部用户一秒内可以尝试多大150K个密码;所以密码要尽量长;
建议设置为 64 位长度密码
standalone (单机)模式配置
redis.model=standalone
# password 前面的 : 不能少
redis.node=redis://:password@127.0.0.1:6382
2
3
demo:
redis.model=standalone
redis.node=redis://127.0.0.1:6379
2
sentinel
哨兵模式是对单机模式高可用的一种实现方式, 可以实现故障主从自动切换
哨兵模式需要配置 master name, 和 redis-sentinel
.conf 中的 sentinel monitor masterName xxx
保持一致
哨兵模式只能接收一个密码, 密码设置在任意节点即可(第一个最好了)
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
2
哨兵模式就是单机模式的增强版, 需要配置多个哨兵节点(避免造成主从切换失败, 最少3组哨兵), 节点之间使用 ,
分隔
demo:
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
2
sharding (分片)模式配置
每组 redis 实例可以设置不同的密码
分片实例之间使用 ,
分隔
redis.model=sharding
redis.node=redis://:1234@127.0.0.1:6382,redis://:5678@127.0.0.1:6382
2
demo:
# 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
2
3
sharding-sentinel
分片哨兵模式是对分片模式高可用的一种实现方式, 可以实现分片模式下, 故障主从自动切换
分片哨兵模式是哨兵模式和分片模式的结合, 配置可
demo:
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
2
cluster
redis 3.x 远程集群模式
demo:
redis.model=cluster
redis.node=redis://127.0.0.1:6379,redis://127.0.0.1:6380,redis://127.0.0.1:6381
2
hybrid
混合模式, 为了兼容现有 Redis 环境, 一种临时的解决方案, 改造完成后, 将配置修改为 sentinel
即可
demo:
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
2
混合模式实现了 standalone
, sentinel
, sharding
模式的混合使用
代码重构
- 使用
@Value
实现自动配置, 代替 xml 中的 bean 标签; - 使用 logbok 简化代码;
- 不在使用 RedisServiceFacotry 获取 redisService, 某个模块需要使用该组件时, import xml 即可 (多容器问题);
- 使用 log4j2 代替 log4j;
- 编译版本由 jdk1.6 改为 jdk1.7;
- 使用代理类重构安全关闭 jedis 连接的方式;
重构前:
@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));
}
2
3
4
5
6
7
8
9
重构后:
@Override
public String set(String flag, final String key, final String value) {
return RedisUtil.jedisProxy(model, flag).set(key, value);
}
2
3
4