Redis分布式篇 1 为什么 需要 Redis 集群 1.1 为什么需要集群? 1.1.1 性能 ​ Redis 本身的 QPS 已经很高了,但是如果在一些并发量非常高的情况下,性能还是会受到影响。这个时候我们希望有更多的 Redis 服务来完成工作。 1.1.2 扩展 ​ 第二个是出于存储的考虑。因为 Redis 所有的数据都放在内存中,如果数据量大,很容易受到硬件的限制。升级硬件收效和成本比太低,所以我们需要有一种横向扩展的方法 1.1.3 可用性 ​ 第三个是可用性和安全的问题。如果只有一个 Redis 服务,一旦服务宕机,那么所有的客户端都无法访问,会对业务造成很大的影响。另一个,如果硬件发生故障,而单机的数据无法恢复的话,带来的影响也是灾难性的。 ​ 可用性、数据安全、性能都可以通过搭建多个 Reids 服务实现。其中有一个是主节点(master),可以有多个从节点(slave)。主从之间通过数据同步,存储完全相同的数据。如果主节点发生故障,则把某个从节点改成主节点,访问新的主节点。 2 Redis 主从 复制 ( replication ) 2.1 主从 复制配置 例如一主多从,203 是主节点,在每个 slave 节点的 redis.conf 配置文件增加一行 slaveof 192.168.8.203 6379 在主从切换的时候,这个配置会被重写成: # Generated by CONFIG REWRITE replicaof 192.168.8.203 6379 或者在启动服务时通过参数指定 master 节点 ./redis-server --slaveof 192.168.8.203 637 或在客户端直接执行 slaveof xx xx,使该 Redis 实例成为从节点。 启动后,查看集群状态: redis> info replication 从节点不能写入数据(只读),只能从 master 节点同步数据。get 成功,set 失败。 127.0.0.1:6379> set sunda 666 (error) READONLY You can't write against a read only replica. 主节点写入后,slave 会自动从 master 同步数据。 断开复制: redis> slaveof no one 此时从节点会变成自己的主节点,不再复制数据。 2.2 主从复制原理 2.2.1 连接 阶段 ​ 1、slave node 启动时(执行 slaveof 命令),会在自己本地保存 master node 的信息,包括 master node 的 host 和 ip。 ​ 2、slave node 内部有个定时任务 replicationCron(源码 replication.c),每隔 1秒钟检查是否有新的 master node 要连接和复制,如果发现,就跟 master node 建立socket 网络连接,如果连接成功,从节点为该 socket 建立一个专门处理复制工作的文件事件处理器,负责后续的复制工作,如接收 RDB 文件、接收命令传播等。当从节点变成了主节点的一个客户端之后,会给主节点发送 ping 请求。 2.2.2 数据 同步阶段 ​ 3、master node 第一次执行全量复制,通过 bgsave 命令在本地生成一份 RDB 快照,将 RDB 快照文件发给 slave node(如果超时会重连,可以调大 repl-timeout 的值)。slave node 首先清除自己的旧数据,然后用 RDB 文件加载数据。 问题:生成 RDB 期间,master 接收到的命令怎么处理? 开始生成 RDB 文件时,master 会把所有新的写命令缓存在内存中。在 slave node保存了 RDB 之后,再将新的写命令复制给 slave node。 2.2.3 命令传播阶段 ​ 4、master node 持续将写命令,异步复制给 slave node ​ 延迟是不可避免的,只能通过优化网络。 repl-disable-tcp-nodelay no 当设置为 yes 时,TCP 会对包进行合并从而减少带宽,但是发送的频率会降低,从节点数据延迟增加,一致性变差;具体发送频率与 Linux 内核的配置有关,默认配置为40ms。当设置为 no 时,TCP 会立马将主节点的数据发送给从节点,带宽增加但延迟变小。 一般来说,只有当应用对 Redis 数据不一致的容忍度较高,且主从节点之间网络状况不好时,才会设置为 yes;多数情况使用默认值 no。 问题:如果从节点有一段时间断开了与主节点的连接是不是要重新全量复制一遍?如果可以增量复制,怎么知道上次复制到哪里? 通过 master_repl_offset 记录的偏移量 redis> info replication 1571747119946.png 2.3 主从复制的 不足 主从模式解决了数据备份和性能(通过读写分离)的问题,但是还是存在一些不足: ​ 1、RDB 文件过大的情况下,同步非常耗时。 ​ 2、在一主一从或者一主多从的情况下,如果主服务器挂了,对外提供的服务就不可用了,单点问题没有得到解决。如果每次都是手动把之前的从服务器切换成主服务器,这个比较费时费力,还会造成一定时间的服务不可用。 3 可用性保证之 Sentinel 3.1 Sentinel 原理 ​ 如何实现主从的自动切换?我们的思路: ​ 创建一台监控服务器来监控所有 Redis 服务节点的状态,比如,master 节点超过一定时间没有给监控服务器发送心跳报文,就把 master 标记为下线,然后把某一个 slave变成 master。应用每一次都是从这个监控服务器拿到 master 的地址。 问题是:如果监控服务器本身出问题了怎么办?那我们就拿不到 master 的地址了,应用也没有办法访问。 那我们再创建一个监控服务器,来监控监控服务器……似乎陷入死循环了,这个问题怎么解决?这个问题先放着。 Redis 的 Sentinel 就是这种思路:通过运行监控服务器来保证服务的可用性。 官网: https://redis.io/topics/sentinel ​ 从 Redis2.8 版本起,提供了一个稳定版本的 Sentinel(哨兵),用来解决高可用的问题。它是一个特殊状态的 redis 实例。 ​ 我们会启动一个或者多个 Sentinel 的服务(通过 src/redis-sentinel),它本质上只是一个运行在特殊模式之下的 Redis,Sentinel 通过 info 命令得到被监听 Redis 机器的master,slave 等信息。 1571747203355.png 为了保证监控服务器的可用性,我们会对 Sentinel 做集群的部署。Sentinel 既监控所有的 Redis 服务,Sentinel 之间也相互监控。 注意:Sentinel 本身没有主从之分,只有 Redis 服务节点有主从之分。 概念梳理:master,slave(redis group),sentinel,sentinel 集合 3.1.1 服务 下线 ​ Sentinel 默认以每秒钟 1 次的频率向 Redis 服务节点发送 PING 命令。如果在down-after-milliseconds 内都没有收到有效回复,Sentinel 会将该服务器标记为下线(主观下线)。 # sentinel.conf sentinel down-after-milliseconds ​ 这个时候 Sentinel 节点会继续询问其他的 Sentinel 节点,确认这个节点是否下线,如果多数 Sentinel 节点都认为 master 下线,master 才真正确认被下线(客观下线),这个时候就需要重新选举 master。 3.1.2 故障 转移 ​ 如果 master 被标记为下线,就会开始故障转移流程。 ​ 既然有这么多的 Sentinel 节点,由谁来做故障转移的事情呢? ​ 故障转移流程的第一步就是在 Sentinel 集群选择一个 Leader,由 Leader 完成故障转移流程。Sentinle 通过 Raft 算法,实现 Sentinel 选举。 3.1.2.1 Ratf 算法 ​ 在分布式存储系统中,通常通过维护多个副本来提高系统的可用性,那么多个节点之间必须要面对数据一致性的问题。Raft 的目的就是通过复制的方式,使所有节点达成一致,但是这么多节点,以哪个节点的数据为准呢?所以必须选出一个 Leader ​ 大体上有两个步骤:领导选举,数据复制。 ​ Raft 是一个共识算法(consensus algorithm)。比如比特币之类的加密货币,就需要共识算法。Spring Cloud 的注册中心解决方案 Consul 也用到了 Raft 协议。 ​ Raft 的核心思想:先到先得,少数服从多数。 ​ Raft 算法演示: http://thesecretlivesofdata.com/raft/ 总结: Sentinle 的 Raft 算法和 Raft 论文略有不同。 1、master 客观下线触发选举,而不是过了 election timeout 时间开始选举。 2、Leader 并不会把自己成为 Leader 的消息发给其他 Sentinel。其他 Sentinel 等待 Leader 从 slave 选出 master 后,检测到新的 master 正常工作后,就会去掉客观下线的标识,从而不需要进入故障转移流程。 3.1.2.2 故障转移 ​ 问题:怎么让一个原来的 slave 节点成为主节点? ​ 1、选出 Sentinel Leader 之后,由 Sentinel Leader 向某个节点发送 slaveof no one命令,让它成为独立节点。 ​ 2、然后向其他节点发送 slaveof x.x.x.x xxxx(本机服务),让它们成为这个节点的子节点,故障转移完成。 ​ 问题:这么多从节点,选谁成为主节点? ​ 关于从节点选举,一共有四个因素影响选举的结果,分别是断开连接时长、优先级排序、复制数量、进程 id。 ​ 如果与哨兵连接断开的比较久,超过了某个阈值,就直接失去了选举权。如果拥有选举权,那就看谁的优先级高,这个在配置文件里可以设置(replica-priority 100),数值越小优先级越高。 ​ 如果优先级相同,就看谁从 master 中复制的数据最多(复制偏移量最大),选最多的那个,如果复制数量也相同,就选择进程 id 最小的那个。 3.2 Sentinel 的功能总结 Monitoring. Sentinel constantly checks if your master and slave instances are working as expected. Notification. Sentinel can notify the system administrator, another computer programs, via an API, that something iswrong with one of the monitored Redis instances. Automatic failover. If a master is not working as expected, Sentinel can start a failover process where a slave is promoted to master, the other additional slaves are reconfigured to use the new master, and the applications using the Redis server informed about the new address to use when connecting. Configuration provider. Sentinel acts as a source of authority for clients service discovery:clients connect to Sentinels in order to ask for the address of the current Redis master responsible for a given service. If a failover occurs, Sentinels will report the new address. ​ 监控:Sentinel 会不断检查主服务器和从服务器是否正常运行。 ​ 通知:如果某一个被监控的实例出现问题,Sentinel 可以通过 API 发出通知。 ​ 自动故障转移(failover):如果主服务器发生故障,Sentinel 可以启动故障转移过程。把某台服务器升级为主服务器,并发出通知。 ​ 配置管理:客户端连接到 Sentinel,获取当前的 Redis 主服务器的地址。 3.3 Sentinel 实战 3.3.1 Sentinel 配置 为了保证 Sentinel 的高可用,Sentinel 也需要做集群部署,集群中至少需要三个Sentinel 实例(推荐奇数个,防止脑裂)。 hostname IP 地址 节点角色& 端口 master 192.168.8.203 Master:6379 / Sentinel : 26379 slave1 192.168.8.204 Slave :6379 / Sentinel : 26379 slave2 192.168.8.205 Slave :6379 / Sentinel : 26379 以 Redis 安装路径/usr/local/soft/redis-5.0.5/为例。 在 204 和 205 的 src/redis.conf 配置文件中添加 slaveof 192.168.8.203 6379 在 203、204、205 创建 sentinel 配置文件(安装后根目录下默认有 sentinel.conf): cd /usr/local/soft/redis-5.0.5 mkdir logs mkdir rdbs mkdir sentinel-tmp vim sentinel.conf 三台服务器内容相同: daemonize yes port 26379 protected-mode no dir "/usr/local/soft/redis-5.0.5/sentinel-tmp" sentinel monitor redis-master 192.168.2.203 6379 2 sentinel down-after-milliseconds redis-master 30000 sentinel failover-timeout redis-master 180000 sentinel parallel-syncs redis-master 1 ​ 上面出现了 4 个'redis-master',这个名称要统一,并且使用客户端(比如 Jedis)连接的时候名称要正确。 hostname IP protected-mode 是否允许外部网络访问 dir sentinel 的工作目录 sentinel monitor sentinel 监控的 redis 主节点 down-after-milliseconds(毫秒) master 宕机多久,才会被 Sentinel 主观认为下线 sentinel failover-timeout(毫秒 1 同一个 sentinel 对同一个 master 两次 failover 之间的间隔时间。 2. 当一个 slave 从一个错误的 master 那里同步数据开始计算时间。直到 slave 被纠正为向正确的 master 那里同步数据时。 3.当想要取消一个正在进行的 failover 所需要的时间。 4.当进行 failover 时,配置所有 slaves 指向新的 master 所需的最大时间。 parallel-syncs 这个配置项指定了在发生 failover 主备切换时最多可以有多少个 slave 同时对新的 master 进行 同步,这个数字越小,完成 failover 所需的时间就越长,但是如果这个数字越大,就意味着越 多的 slave 因为 replication 而不可用。可以通过将这个值设为 1 来保证每次只有一个 slave 处于不能处理命令请求的状态 3.3.2 Sentinel 验证 启动 Redis 服务和 Sentinel cd /usr/local/soft/redis-5.0.5/src # 启动 Redis 节点 ./redis-server ../redis.conf # 启动 Sentinel 节点 ./redis-sentinel ../sentinel.conf # 或者 ./redis-server ../sentinel.conf --sentinel 查看集群状态: redis> info replication 203 1571747818355.png 204 和 205 1571747829582.png 模拟 master 宕机,在 203 执行: redis> shutdown 205 被选为新的 Master,只有一个 Slave 节点。 1571747852515.png 注意看 sentinel.conf 里面的 redis-master 被修改了! 模拟原 master 恢复,在 203 启动 redis-server。它还是 slave,但是 master 又有两个 slave 了。 slave 宕机和恢复省略. 3.3.3 Sentinel 连接使用 Jedis 连接 Sentinel package sentinel; import redis.clients.jedis.JedisSentinelPool; import java.util.HashSet; import java.util.Properties; import java.util.Set; public class JedisSentinelTest { private static JedisSentinelPool pool; private static JedisSentinelPool createJedisPool() { // master的名字是sentinel.conf配置文件里面的名称 String masterName = "redis-master"; Set sentinels = new HashSet(); sentinels.add("192.168.8.203:26379"); sentinels.add("192.168.8.204:26379"); sentinels.add("192.168.8.205:26379"); pool = new JedisSentinelPool(masterName, sentinels); return pool; } public static void main(String[] args) { JedisSentinelPool pool = createJedisPool(); pool.getResource().set("qingshan", "qq"+System.currentTimeMillis()); System.out.println(pool.getResource().get("qingshan")); } } master name 来自于 sentinel.conf 的配置 private static JedisSentinelPool createJedisPool() { String masterName = "redis-master"; Set sentinels = new HashSet(); sentinels.add("192.168.8.203:26379"); sentinels.add("192.168.8.204:26379"); sentinels.add("192.168.8.205:26379"); pool = new JedisSentinelPool(masterName, sentinels); return pool; } Spring Boot 连接 Sentinel import com.gupaoedu.util.RedisUtil; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class RedisAppTests { @Autowired RedisUtil util; @Test public void contextLoads() { util.set("boot", "2673--" +System.currentTimeMillis()); System.out.println(util.get("boot")); } } spring.redis.sentinel.master=redis-master spring.redis.sentinel.nodes=192.168.8.203:26379,192.168.8.204:26379,192.168.8.205:26379 ​ 无论是 Jedis 还是 Spring Boot(2.x 版本默认是 Lettuce),都只需要配置全部哨兵的地址,由哨兵返回当前的 master 节点地址。 3.4 哨兵机制的不足 ​ 主从切换的过程中会丢失数据,因为只有一个 master。 ​ 只能单点写,没有解决水平扩容的问题。 ​ 如果数据量非常大,这个时候我们需要多个 master-slave 的 group,把数据分布到不同的 group 中。 ​ 问题来了,数据怎么分片?分片之后,怎么实现路由? 4 Redis 分布式方案 ​ 如果要实现 Redis 数据的分片,我们有三种方案。第一种是在客户端实现相关的逻辑,例如用取模或者一致性哈希对 key 进行分片,查询和修改都先判断 key 的路由。 ​ 第二种是把做分片处理的逻辑抽取出来,运行一个独立的代理服务,客户端连接到这个代理服务,代理服务做请求的转发。 ​ 第三种就是基于服务端实现。 4.1 客户端 Sharding 1571748107273.png Jedis 客户端提供了 Redis Sharding 的方案,并且支持连接池。 4.1.1 ShardedJedis public class ShardingTest { public static void main(String[] args) { JedisPoolConfig poolConfig = new JedisPoolConfig(); ​ // Redis 服务器 JedisShardInfo shardInfo1 = new JedisShardInfo("127.0.0.1", 6379); JedisShardInfo shardInfo2 = new JedisShardInfo("192.168.8.205", 6379); ​ // 连接池 List infoList = Arrays.asList(shardInfo1, shardInfo2); ShardedJedisPool jedisPool = new ShardedJedisPool(poolConfig, infoList); ShardedJedis jedis = null; try{ jedis = jedisPool.getResource(); for(int i=0; i<100; i++){ jedis.set("k"+i, ""+i); } for(int i=0; i<100; i++){ System.out.println(jedis.get("k"+i)); } ​ }finally{ if(jedis!=null) { jedis.close(); } } } } ​ 使用 ShardedJedis 之类的客户端分片代码的优势是配置简单,不依赖于其他中间件,分区的逻辑可以自定义,比较灵活。但是基于客户端的方案,不能实现动态的服务增减,每个客户端需要自行维护分片策略,存在重复代码。 ​ 第二种思路就是把分片的代码抽取出来,做成一个公共服务,所有的客户端都连接到这个代理层。由代理层来实现请求和转发。 4.2 代理 Proxy 1571748259203.png 典型的代理分区方案有 Twitter 开源的 Twemproxy 和国内的豌豆荚开源的 Codis。 4.2.1 Twemproxy two-em-proxy https://github.com/twitter/twemproxy 1571748281722.png Twemproxy 的优点:比较稳定,可用性高。 不足: 1、出现故障不能自动转移,架构复杂,需要借助其他组件(LVS/HAProxy + Keepalived)实现 HA 2、扩缩容需要修改配置,不能实现平滑地扩缩容(需要重新分布数据)。 4.2.2 Codis https://github.com/CodisLabs/codis Codis 是一个代理中间件,用 Go 语言开发的。 功能:客户端连接 Codis 跟连接 Redis 没有区别。 Codis Tewmproxy Redis Cluster 重新分片不需要重启 Yes No Yes pipeline Yes Yes 多 key 操作的 hash tags {} Yes Yes Yes 重新分片时的多 key 操作 Yes No 客户端支持 所有 所有 支持 cluster 协议的客户端 1571748437326.png ​ 分片原理:Codis 把所有的 key 分成了 N 个槽(例如 1024),每个槽对应一个分组,一个分组对应于一个或者一组 Redis 实例。Codis 对 key 进行 CRC32 运算,得到一个32 位的数字,然后模以 N(槽的个数),得到余数,这个就是 key 对应的槽,槽后面就是 Redis 的实例。比如 4 个槽: 1571748485305.png ​ Codis 的槽位映射关系是保存在 Proxy 中的,如果要解决单点的问题,Codis 也要做集群部署,多个 Codis 节点怎么同步槽和实例的关系呢?需要运行一个 Zookeeper (或者 etcd/本地文件)。 ​ 在新增节点的时候,可以为节点指定特定的槽位。Codis 也提供了自动均衡策略。Codis 不支持事务,其他的一些命令也不支持。 不支持的命令 https://github.com/CodisLabs/codis/blob/release3.2/doc/unsupported_cmds.md 获取数据原理(mget):在 Redis 中的各个实例里获取到符合的 key,然后再汇总到 Codis 中。 Codis 是第三方提供的分布式解决方案,在官方的集群功能稳定之前,Codis 也得到了大量的应用。 4.3 Redis Cluster https://redis.io/topics/cluster-tutorial/ Redis Cluster 是在 Redis 3.0 的版本正式推出的,用来解决分布式的需求,同时也可以实现高可用。跟 Codis 不一样,它是去中心化的,客户端可以连接到任意一个可用节点。 数据分片有几个关键的问题需要解决: 1、数据怎么相对均匀地分片 2、客户端怎么访问到相应的节点和数据 3、重新分片的过程,怎么保证正常服务 4.3.1 架构 ​ Redis Cluster 可以看成是由多个 Redis 实例组成的数据集合。客户端不需要关注数据的子集到底存储在哪个节点,只需要关注这个集合整体。 ​ 以 3 主 3 从为例,节点之间两两交互,共享数据分片、节点状态等信息。 1571748577107.png 4.3.2 搭建 首先,本篇要基于单实例的安装,你的机器上已经有一个Redis 博客 www.sundablog.com 为了节省机器,我们直接把6个Redis实例安装在同一台机器上(3主3从),只是使用不同的端口号。 机器IP 192.168.8.207 cd /usr/local/soft/redis-5.0.5 mkdir redis-cluster cd redis-cluster mkdir 7291 7292 7293 7294 7295 7296 复制redis配置文件到7291目录 cp /usr/local/soft/redis-5.0.5/redis.conf /usr/local/soft/redis-5.0.5/redis-cluster/7291 修改7291的配置文件 port 7291 dir /usr/local/soft/redis-5.0.5/redis-cluster/7291/ cluster-enabled yes cluster-config-file nodes-7291.conf cluster-node-timeout 5000 appendonly yes pidfile /var/run/redis_7291.pid 把7291下的redis.conf复制到其他5个目录。 cd /usr/local/soft/redis-5.0.5/redis-cluster/7291 cp redis.conf ../7292 cp redis.conf ../7293 cp redis.conf ../7294 cp redis.conf ../7295 cp redis.conf ../7296 批量替换内容 cd /usr/local/soft/redis-5.0.5/redis-cluster sed -i 's/7291/7292/g' 7292/redis.conf sed -i 's/7291/7293/