设计原则和初衷
- 性能:这是Redis赖以生存的看家本领,增加集群功能后当然不能对性能产生太大影响,所以Redis采取了P2P而非Proxy方式、异步复制、客户端重定向等设计,而牺牲了部分的一致性、使用性。
- 水平扩展:集群的最重要能力当然是扩展,文档中称可以线性扩展到1000结点。
- 可用性:在Cluster推出之前,可用性要靠Sentinel保证。有了集群之后也自动具有了Sentinel的监控和自动Failover能力。
redis-cluster设计
Redis-Cluster采用无中心结构
,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。
其结构设计:
- Redis Cluster中节点负责存储数据,记录集群状态,集群节点能自动发现其他节点,检测出节点的状态,并在需要时剔除故障节点,提升新的主节点
- Redis Cluster中所有节点通过PING-PONG机制彼此互联,使用一个二级制协议(Cluster Bus) 进行通信,优化传输速度和带宽。发现新的节点、发送PING包、特定情况下发送集群消息,集群连接能够发布与订阅消息。
- 客户端和集群中的节点直连,不需要中间的Proxy层。理论上而言,客户端可以自由地向集群中的所有节点发送请求,但是每次不需要连接集群中的所有节点,只需要连接集群中任何一个可用节点即可。当客户端发起请求后,接收到重定向(MOVED\ASK)错误,会自动重定向到其他节点,所以客户端无需保存集群状态。不过客户端可以缓存键值和节点之间的映射关系,这样能明显提高命令执行的效率。
- Redis Cluster中节点之间使用异步复制,在分区过程中存在窗口,容易导致丢失写入的数据,集群即使努力尝试所有写入,但是以下两种情况可能丢失数据:
- 命令操作已经到达主节点,但在主节点回复的时候,写入可能还没有通过主节点复制到从节点那里。如果这时主节点宕机了,这条命令将永久丢失。以防主节点长时间不可达而它的一个从节点已经被提升为主节点。
- 分区导致一个主节点不可达,然而集群发送故障转移(failover),提升从节点为主节点,原来的主节点再次恢复。一个没有更新路由表(routing table)的客户端或许会在集群把这个主节点变成一个从节点(新主节点的从节点)之前对它进行写入操作,导致数据彻底丢失。
- Redis集群的节点不可用后,在经过集群半数以上Master节点与故障节点通信超过cluster-node-timeout时间后,认为该节点故障,从而集群根据自动故障机制,将从节点提升为主节点。这时集群恢复可用。
redis cluster 数据分片
Redis Cluster在设计中没有使用一致性哈希(Consistency Hashing),而是使用数据分片(Sharding)引入哈希槽(hash slot)来实现;一个 Redis Cluster包含16384(0~16383)个哈希槽,存储在Redis Cluster中的所有键都会被映射到这些slot中,集群中的每个键都属于这16384个哈希槽中的一个,集群使用公式slot=CRC16(key)/16384来计算key属于哪个槽,其中CRC16(key)语句用于计算key的CRC16 校验和。
集群中的每个主节点(Master)都负责处理16384个哈希槽中的一部分,当集群处于稳定状态时,每个哈希槽都只由一个主节点进行处理,每个主节点可以有一个到N个从节点(Slave),当主节点出现宕机或网络断线等不可用时,从节点能自动提升为主节点进行处理。
现在我们是三个主节点分别是:A, B, C 三个节点,它们可以是一台机器上的三个端口,也可以是三台不同的服务器。那么,采用哈希槽 (hash slot)的方式来分配16384个slot 的话,它们三个节点分别承担的slot 区间是:
节点A覆盖0-5460;
节点B覆盖5461-10922;
节点C覆盖10923-16383.
获取数据:
如果存入一个值,按照redis cluster哈希槽的算法: CRC16(‘key’)384 = 6782。 那么就会把这个key 的存储分配到 B 上了。同样,当我连接(A,B,C)任何一个节点想获取’key’这个key时,也会这样的算法,然后内部跳转到B节点上获取数据 ,如图:
**新增一个主节点:**
新增一个节点D,redis cluster的这种做法是从各个节点的前面各拿取一部分slot到D上,我会在接下来的实践中实验。大致就会变成这样:
节点A覆盖1365-5460
节点B覆盖6827-10922
节点C覆盖12288-16383
节点D覆盖0-1364,5461-6826,10923-12287
同样删除一个节点也是类似,移动完成后就可以删除这个节点了。
优势
无中心架构 数据按照slot存储分布在多个节点,节点间数据共享,可动态调整数据分布 可扩展性,可线性扩展到1000个节点,节点可动态添加或删除 高可用性,部分节点不可用时,集群仍可用。通过增加Slave做standby数据副本,能够实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成Slave到Master的角色提升 降低运维成本,提高系统的扩展性和可用性
不足
Client实现复杂,驱动要求实现Smart Client,缓存slots mapping信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性。目前仅JedisCluster相对成熟,异常处理部分还不完善,比如常见的“max redirect exception” 节点会因为某些原因发生阻塞(阻塞时间大于clutser-node-timeout),被判断下线,这种failover是没有必要的 数据通过异步复制,不保证数据的强一致性 多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况 Slave在集群中充当“冷备”,不能缓解读压力,当然可以通过SDK的合理设计来提高Slave资源的利用率
Redis 主从模式
redis cluster 为了保证数据的高可用性,加入了主从模式,一个主节点对应一个或多个从节点,主节点提供数据存取,从节点则是从主节点拉取数据备份,当这个主节点挂掉后,就会有这个从节点选取一个来充当主节点,从而保证集群不会挂掉。
上面那个例子里, 集群有ABC三个主节点, 如果这3个节点都没有加入从节点,如果B挂掉了,我们就无法访问整个集群了。A和C的slot也无法访问。所以我们在集群建立的时候,一定要为每个主节点都添加了从节点, 比如像这样, 集群包含主节点A、B、C, 以及从节点A1、B1、C1, 那么即使B挂掉系统也可以继续正确工作。B1节点替代了B节点,所以Redis集群将会选择B1节点作为新的主节点,集群将会继续正确地提供服务。 当B重新开启后,它就会变成B1的从节点。不过需要注意,如果节点B和B1同时挂了,Redis集群就无法继续正确地提供服务了
redis主从复制的一些特点:
1.master可以有多个slave
2.除了多个slave连到相同的master外,slave也可以连接其他slave形成图状结构
3.主从复制不会阻塞master。也就是说当一个或多个slave与master进行初次同步数据时,master可以继续处理client发来的请求。相反slave在初次同步数据时则会阻塞不能处理client的请求
4.主从复制可以用来提高系统的可伸缩性,我们可以用多个slave专门用于client的读请求,比如sort操作可以使用slave来处理。也可以用来做简单的数据冗余
5.可以在master禁用数据持久化,只需要注释掉master配置文件中的所有save配置,然后只在slave上配置数据持久化
redis的主从复制分为两个阶段:
1.同步操作:将从服务器的数据库状态更新至主服务器当前所处的数据库状态 2.命令传播:在主服务器的数据库状态被修改,导致主从服务器的数据库状态出现不一致时,主服务器会将自己执行的写命令送给从服务器执行
同步操作的过程:
1)设置主服务器地址和端口,通过调用SAVEOF
2.8版本之后 同步操作PSYNC。自行判断 是全量同步 还是 增量同步 效率比较高
部分重同步功能由下面几个部分构成
主服务器的复制偏移量和从服务器的复制偏移量: 当主服务器在向从服务器进行命令同步时,主服务器和从服务器会各自记录一个复制偏移量,当主从服务器的数据库状态一致时这两个复制偏移量是相同的,如果这两个偏移量不一致说明当前主从服务器的状态不一致 主服务器的复制积压缓冲区: 复制积压缓冲区是一个固定大小的FIFO队列,当队列已满时会弹出最早插入的数据,在主服务器进行命令传播时会同时把命令放到缓冲区中,缓冲区包含两部分数据,偏移量和字节。在进行复制时从服务器会将偏移量上报到主服务器,主服务检查当前偏移量是否还存在缓冲区中,如果存在进行部分重同步,如果不存在进行完整重同步。因为这个积压缓冲区是一个固定大小的队列,所以当从服务器长时间断线时,从服务器的复制偏移量很可能已不再缓冲区中,这时候只能进行完整重同步 服务器的运行ID: 初次同步时主服务器会把ID发给从服务器,从服务器保存主服务器ID,当断线重连后,会把之前保存的主服务器ID上报给主服务器,主服务器检查从服务器之前复制的主服务器ID是否和自己的ID相同,如果相同,执行部分重同步,如果不同说明从服务器之前记录的状态不是当前主服务器,这时候需要执行完整重同步 PSYNC命令实现
1)初始复制或者之前执行过SLAVEOF no one命令,执行完整重同步:发送PSYNC ? -1命令到主服务器
2)如果从服务器已经复制过某个主服务器,在开始新复制时向主服务器发送PSYNC
回复+FULLRESYNC
心跳检测
在命令传播阶段,从服务器默认每秒一次的频率向主服务器发送命令:REPLCONF ACK
1)检测从服务器的网络连接状态,检测主从服务器连接是否正常,如果主服务器超过一定时间没有收到从服务器的REPLCONF ACK 命令,那么它们的连接可能出了问题 2)辅助实现min-slaves选项,min-slaves-to-write和min-slaves-max-lag两个选项可以防止主服务器在不安全的情况下执行写命令,min-slaves-to-write 3 min-slaves-max-lag 10 表示如果从服务器少于3个,或者3个从服务器的延迟都大于10秒时,主服务器拒绝写命令 3)检测命令丢失,主服务器接收到从服务器的REPLCONF ACK 命令之后会检查从服务器的偏移量是否和主服务器的一致,如果不一致会把积压缓冲区中的从服务器偏移量后面的命令发送到从服务器
关闭主服务器持久化时,复制功能的数据安全
当配置Redis复制功能时,强烈建议打开主服务器的持久化功能。 否则的话,由于延迟等问题,部署的服务应该要避免自动拉起。为了帮助理解主服务器关闭持久化时自动拉起的危险性,参考一下以下会导致主从服务器数据全部丢失的例子: 假设节点A为主服务器,并且关闭了持久化。 并且节点B和节点C从节点A复制数据 节点A崩溃,然后由自动拉起服务重启了节点A. 由于节点A的持久化被关闭了,所以重启之后没有任何数据 节点B和节点C将从节点A复制数据,但是A的数据是空的, 于是就把自身保存的数据副本删除。 在关闭主服务器上的持久化,并同时开启自动拉起进程的情况下,即便使用Sentinel来实现Redis的高可用性,也是非常危险的。 因为主服务器可能拉起得非常快,以至于Sentinel在配置的心跳时间间隔内没有检测到主服务器已被重启,然后还是会执行上面的数据丢失的流程。无论何时,数据安全都是极其重要的,所以应该禁止主服务器关闭持久化的同时自动拉起
主服务器只在有至少 N 个从服务器的情况下,才执行写操作
从 Redis 2.8 开始, 为了保证数据的安全性, 可以通过配置, 让主服务器只在有至少 N 个当前已连接从服务器的情况下, 才执行写命令。不过, 因为 Redis 使用异步复制, 所以主服务器发送的写数据并不一定会被从服务器接收到, 因此, 数据丢失的可能性仍然是存在的。以下是这个特性的运作原理: 从服务器以每秒一次的频率 PING 主服务器一次, 并报告复制流的处理情况。 主服务器会记录各个从服务器最后一次向它发送 PING 的时间。 用户可以通过配置, 指定网络延迟的最大值 min-slaves-max-lag , 以及执行写操作所需的至少从服务器数量 min-slaves-to-write 如果至少有 min-slaves-to-write 个从服务器, 并且这些服务器的延迟值都少于 min-slaves-max-lag 秒, 那么主服务器就会执行客户端请求的写操作。你可以将这个特性看作 CAP 理论中的 C 的条件放宽版本: 尽管不能保证写操作的持久性, 但起码丢失数据的窗口会被严格限制在指定的秒数中。 如果条件达不到 min-slaves-to-write 和 min-slaves-max-lag 所指定的条件, 那么写操作就不会被执行, 主服务器会向请求执行写操作的客户端返回一个错误
Redis可扩展集群搭建
1. 主动复制避开Redis复制缺陷
既然Redis的复制功能有缺陷,不妨放弃Redis本身提供的复制功能,我们可以采用主动复制的方式来搭建我们的集群环境。所谓主动复制是指由业务端或者通过代理中间件对Redis存储的数据进行双写或多写,通过数据的多份存储来达到与复制相同的目的,主动复制不仅限于 用在Redis集群上,目前很多公司采用主动复制的技术来解决MySQL主从之间复制的延迟问题,比如Twitter还专门开发了用于复制和分区的中间件gizzard(https://github.com/twitter/gizzard) 。
主动复制虽然解决了被动复制的延迟问题,但也带来了新的问题,就是数据的一致性问题,数据写2次或多次,如何保证多份数据的一致性呢?如果你的应用 对数据一致性要求不高,允许最终一致性的话,那么通常简单的解决方案是可以通过时间戳或者vector clock等方式,让客户端同时取到多份数据并进行校验,如果你的应用对数据一致性要求非常高,那么就需要引入一些复杂的一致性算法比如Paxos来保证 数据的一致性,但是写入性能也会相应下降很多。 通过主动复制,数据多份存储我们也就不再担心Redis单点故障的问题了,如果一组Redis集群挂掉,我们可以让业务快速切换到另一组Redis上,降低业务风险。
2. 通过presharding进行Redis在线扩容
通过主动复制我们解决了Redis单点故障问题,那么还有一个重要的问题需要解决:容量规划与在线扩容问题。我们前面分析过Redis的适用场景是全部数据存储在内存中,而内存容量有限,那么首先需要根据业务数据量进行初步的容量规划,比如你的业务数据需 要100G存储空间,假设服务器内存是48G,至少需要3~4台服务器来存储。这个实际是对现有 业务情况所做的一个容量规划,假如业务增长很快,很快就会发现当前的容量已经不够了,Redis里面存储的数据很快就会超过物理内存大小,如何进行 Redis的在线扩容呢?Redis的作者提出了一种叫做presharding的方案来解决动态扩容和数据分区的问题,实际就是在同一台机器上部署多个Redis实例的方式,当容量不够时将多个实例拆分到不同的机器上,这样实际就达到了扩容的效果