一、多形态的存储型集群
在上阶段,我们简单聊了下集群的基本知识,以及快速过了一下逻辑处理型集群的内容,下面重点来看看存储型集群,毕竟这块才是重头戏,集群的形态在其中有着多种多样的变化。
逻辑处理型的应用,部署集群架构是为了解决单点故障、获得更高的吞吐量,集群内各节点之间没有依赖关系,同时遵循着“去中心化思想”,即多个节点里没有所谓的“老大”。
反观存储型的应用,需要保存数据、记录会话数据、用户认证信息、事务状态等信息,如果要搭建集群架构,则需要考虑各节点之间的数据一致性和完整性,来看个例子:
假设现在数据库使用三个节点组成集群,如果和业务系统集群一样,多个节点之间地位平等,此时客户端读写数据时,究竟该去到哪个节点呢?包含多个操作的事务如何保证落到同一节点?用户在A
节点登录后,如何把登录态(连接信息)同步给B、C
节点?
正是因为上述一堆的问题,存储型集群里的节点必须要划分等级,一定要有一个等级更高的节点,来承担客户端的写操作。因此,在很长一段时间内,涉及到数据存储的应用,集群方案都是以主从模式为主。
趣事:主从模式对应的英文是
Master/Slave
,不过大家会发现,后来许多技术的主从模式,都叫Leader/Follower
,这是为啥?因为Master/Slave
代表的是“主人/奴隶”,再加上集群存在选举机制,所以在西方被痛斥为“此命名的政治思想不正确”,于是许多技术栈都经历过“语言净化”(如Redis)……
好了,上面提到主从集群,下面来展开聊聊存储型应用的集群方案,粗分下来也是两大类:
主从模式:集群内的节点区分等级,可以有一或多个主节点,具备完整的读写能力;也允许拥有多个从节点,但能力方面有所阉割,只具备处理读取请求的能力。
分片模式:集群内节点地位平等,各节点都具备数据读写能力,但每个节点只负责一部分数据,完整数据分散在各节点上。
1.2、主从架构
主从集群,这个概念所有人都熟悉,它的身影在各种技术栈随处可见,如主从集群、镜像集群、复制集群、副本集群……,尽管在不同技术栈的叫法不同,可本质上都是同一个东西。
上图是经典的一主多从架构,客户端的写请求,都会落到主节点A
上,而后再给同步给B、C
这两个从节点,这就是大名鼎鼎的“主从复制技术”,数据同步有三种方案:
- 同步复制:等数据写入集群所有节点后,再给客户端返回写入成功;
- 半同步复制:等集群内一半数量以上的节点写入数据成功后,给客户端返回写入成功;
- 异步复制:数据在主节点上写入成功后,立即给客户端返回写入成功。
大多数支持主从复制的技术栈,几乎都上面三种同步方案,从数据一致性角度看:同步复制>半同步复制>异步复制,而从性能角度出发则完全相反:异步复制>半同步复制>同步复制。
如今大多数技术栈默认的复制方案都是第三种,虽然有数据丢失的风险,可是它的性能最好,何况“主节点写入完数据,正好就发生故障”的这种几率很小。当然,如果你的系统更在乎数据一致性和安全性,那就可以选择半同步模式,如果完全不在乎性能,则可以切换成同步复制模式。
1.2.1、读写分离
在主从架构中,因为从节点被阉割掉了处理写请求的能力,所以在大部分时间里,如果从节点只作为数据副本而存在,显然会造成很大的资源浪费。为了充分利用从节点的资源,一种名为“读写分离”的技术曾风靡一时。
虽然从节点没有处理写请求的能力,可是它会把主节点写入的所有数据都同步过来,为此,它具备“一定程度”的读请求处理能力,此时就可以将读取数据的请求分发给从机,既能充分利用空闲资源,又能减轻主节点的访问压力。
PS:从节点只具备“一定程度”的读请求处理能力,注意里面用引号标出的“一定程度”,这啥意思?
因为主从复制技术,都依靠网络来同步数据,而网络并不可靠,也必然存在延迟性。在异步复制的模式下,当一个数据写入到主节点,立马出现读请求到从节点读取,此时有可能读不到最新的数据。
1.2.2、故障转移
本文开篇提到的集群四大优势,其中一条则是保证了高可用,解决了单点故障问题,可实际上部分技术栈,虽然支持主从集群,但却只提供了基本的主从复制功能(如MySQL
),并不具备故障转移能力。
所谓的故障转移,通常是针对主节点而言的,即:当主节点发生故障时,集群能自动推选出新主节点,并将流量自动转移到新主处理。大家熟知的MySQL
,它并不具备自动故障转移功能,当原本设定的主节点故障后,必须通过人工介入、或第三方工具(如MHA
),又或者自己研发的组件实现故障转移。
当然,也有很多技术栈,官方就实现了故障转移机制,比如Redis
的Sentinel
哨兵、MongoDB
的Arbiter
仲裁者、Kafka
的Controller
控制器……,这些中间件的主从集群,在主节点发生故障时,都能实现无人力介入、自动转移效果。
这种自动故障转移机制,它们内部是如何实现的?其实原理大同小异,故障转移的第一步,是需要能检测出故障的节点,有两种方案可选:
- ①探测模式:负责故障处理的节点,每隔一段时间向集群所有节点发送探测包(
ping
),没有回复的节点说明已经故障; - ②上报模式:集群内所有节点,主动向负责故障处理的节点发送存活信息(心跳),当一个节点没有心跳时说明陷入故障。
不过为了兼容网络分区问题,通常会组合起来使用,正常情况下,集群节点主动发送心跳包,当某个节点没有心跳后,故障处理节点会主动发起探测请求,如果还是没有回复,则会认为该节点陷入故障。考虑到网络分区的影响,故障处理节点也可能陷入了分区,因此也会向其他故障处理节点,发出二次确认的请求,以此进一步确认对应节点是否故障。
PS:上述概念类似于
Redis
哨兵里的主观下线、客观下线,同时也不仅仅只有故障处理节点,具备故障检测能力,集群内的节点也拥有该能力,如从节点去拉数据时,也可以检测出主节点的健康状态。
如果本次检测出的故障节点是主节点,则会进入新主选举流程,选举一般是通过投票机制实现,新主上任的前提是“节点数一半以上的票数”;而投票机制的内在逻辑则是数据POS
点,即谁的数据更新则更有机会成为新主,总体流程如下:
- ①故障检测:当感知到某个节点不可用时,会先发起通信向其他可用节点进行二次确认;
- ②故障处理:如果检测到主节点不可用,从节点会将自己转换为候选人,并向其他成员宣布;
- ③选举开始:轮次号加一,开启一轮新的选举,每个候选人节点开始向其他成员发送拉票请求;
- ④投票开始:在一个新的选举轮次中,每个节点只能投一票,可以投给自己或者其他节点;
- ⑤投票结束:所有节点已投票,或抵达本轮选举的时间限制后,将获得大多数投票的节点立为新主;
- ⑥主从切换:新主会向其他节点发送“上位”消息,其他节点更新自己的配置,接受新主上位;
- ⑦数据同步:完成主从切换后,从节点以新主为数据基准,校验自身数据是否完整,有缺失则同步。
上面只是大概流程,但几乎所有技术栈实现的思路都是这样,因为它们都遵循着Paxos、Raft
等协议制定的标准,本文对此不做展开,后续会单开篇章来聊。
1.2.3、级联复制
级联复制是主从架构的变种,其本质还是主从模型,主要用于一主多从的结构中,如:
在主从架构中,想要实现数据同步有两种方式:
- 主节点推送:当主节点出现数据变更时,主动向自身注册的所有从节点推送新数据写入。
- 从节点拉取:从节点定期去询问一次主节点是否有数据更新,有则拉取新数据写入。
但不管是哪种方式,都会存在一个问题:主节点同步数据的压力,会随着从节点的数量呈线性增长!啥意思?因为不管是推还是拉,数据都需要从主节点出站。
对主节点来说,数据同步会对磁盘、内存、带宽、CPU
带来不小压力,此时多一个从节点,自然多出一倍的压力,数据量较大时,特别容易把主节点的资源占满,从而造成主节点无法处理客户端的请求,级联复制的出现,就是为了解决此问题。
级联复制模式中,只有一个从节点B
挂在主节点A
下面,其余的从节点都会成为B
的从节点,数据由B
先同步到自身,其余节点再从B
去同步数据,这样就解决前面的问题,可此时又会带来新问题:
- 数据延迟:本身主从复制就存在延迟性,而新加入一层后,数据延迟性会更高;
- 级联节点故障:负责同步主节点数据的
B
一旦故障,会导致其余从节点无法同步数据。
凡事有利必有弊,虽然级联复制能解决原本的问题,可是也会带来新问题,这时就需要处理新的问题,或者把新问题的影响降到最低。
PS:用来同步数据的
Canal
中间件,就类似于级联复制的思想。当然,除开小部分特殊的项目,一般很少用到级联复制模式。
1.2.4、多主热备
虽然可以通过读写分离、级联复制,减轻主节点的部分压力,可对于写请求,都必须得落到主节点上处理,而主节点再强悍,总有扛不住的时候,这时又出现了变种集群:双主/多主模式。
多主模式也是建立在主从复制的基础之上,比如双主模式时,两个节点互为主从,各自都具备完整的读写能力,客户端的写请求,可以落入任意节点上处理。不管数据落到哪个节点,另一个节点都会将数据同步过去,此时客户端的读取请求,在任意节点上都能读到数据。
通过这种方案,可以将写入性能翻个倍,并且可以继续拓展出更多的主节点,组成多主集群。不过一般是双主,因为大部分技术栈支持的主从集群,仅允许为一个从节点配置一个主节点,在这个限制下,想要实现多主,则只能组成环形多主集群:
这种环形多主架构,由于每个节点都支持写入数据,所以能极大提升写入吞吐量,但成也萧何败也萧何,一个节点写入新数据后,需要经过N
次(节点数量-1)复制,才能将数据同步给所有节点,集群内的延迟性很高,这也是为什么一般只搭建双主的根本原因。
PS:多主集群除开能提升写入吞吐量外,只要客户端略加适配,就可以实现“客户端版故障转移”,即客户端检测到一个节点不可用后,自动切换到其他节点上读写数据。
1.3、分片架构
不管是开始提到的横向集群,还是上面聊到的主从集群,本质上都是“克隆”多个具备完整功能的单机节点组合部署,尽管主从里的从节点功能有所阉割,可整体上还是属于主节点的复制体。
对业务系统这类应用而言,用多个克隆体组成集群没任何问题,这也是集群概念的标准思想,但对于存储型应用来说,就会出现三个致命问题:
- 木桶效应:三个节点的磁盘容量为
512G、1T、1T
,集群最大容量则为512G
; - 写入受限:传统的主从集群,写入的吞吐量受单个主节点限制,写入的并发很难提升;
- 容量危机:主从集群通过升配存储
GB、TB
级数据,但上升到PB
甚至更高量级时无力。
为了解决木桶效应,必须保证集群所有节点的资源配置一致,否则同步数据时存不下;为了解决写入受限,可以搭建另类的多主集群,可这种模式数据延迟太大;为了存储海量数据,只能不断加硬件资源带来更大的存储空间……
主从集群有着天然的弊端,虽然可以通过特殊手段环境,但无法根治“先天性的劣势”,这种传统型的集群方案,不再适用于增长迅猛的系统(无法处理高并发与海量数据存储)。正因如此,存储型应用在设计集群模式时,做出了一个违背祖宗的决定:将数据分区存储,既然一台机子存不下,那我就用多台机子存!
1.3.1、分片式集群
分区存储,就是目前大数据存储领域大名鼎鼎的分布式存储技术,也叫:分片式集群,我们再来好好聊聊这块知识~
之前的单体模式也好,主从集群也罢,数据其实都是集中式存储,即所有数据都存储在一起。分区存储,就相当于把原本的一缸水,分到不同的桶子里,如下:
每个分片节点,都具备读写数据的能力,这样就从根本上解决了写入瓶颈,并且数据分散到多个节点独立存储,这并不受木桶效应的影响,解决了PB
级数据存储造成的容量危机。
分片集群的好处很多,但想要实现分片式存储,首先要解决的就是数据路由问题,写入时能自动根据规则落到某个分片节点存储,读取时能精准找到存储的分片节点获取数据。这和前面聊的请求分发算法类似,只不过数据分发时,需要保证同一数据的写入/读取落入相同的节点。
数据路由到具体的分片节点,有两个核心点,一是分片(路由)算法,二是分片(路由)键。数据分发和请求分发不同,因为要保证同一数据读写都在相同节点操作,光靠分发算法无法实现,所以每条数据需要有个标识,这个标识就是路由键,以ID
字段作为路由键来举例:
集群有四个节点,选用取模分发算法,根据ID
值取模节点数量,计算出数据最终要落入的节点。来看实际过程,假设要操作ID=1
这条数据:
- 写入:路由键
ID
取模四:1%4
,结果为1
,数据落入分片节点2
; - 读取:路由键
ID
取模四:1%4=1
,从分片节点2
中读取数据。
通过这种方式,就保证了读写数据的节点一致,避免“写入成功,无法读取”的尴尬现象发生。不过除了取模分发外,还有其他一些分发算法,如范围分发、哈希取模、一致性哈希、哈希槽……,像Redis-cluster
集群中,就采用CRC16
算法计算Key
(Key
即路由键),并结合哈希槽模式实现数据分发。
解决了数据分发问题后,分片架构其实还存在一系列问题,好比多节点的数据如何聚合等等。如果当前分片的存储组件属于关系型数据库,如何兼容事务机制?多表联查如何关联?如何实现多字段维度查询?当然,其实还有一系列头疼的问题,这里不做展开,感兴趣可参考《分库分表副作用篇》。
经过上面的阐述后,大家一定对分片式集群有了基本认知,现在来聊聊分片集群的类型,总体可分两种:中心代理分片集群、去中心化分片集群。
1.3.2、中心化分片集群
中心化分片集群,意味着集群中又存在不同的角色节点,最少有两种:
- 中心节点:负责路由规则、数据节点的管理工作,以及处理数据分发请求,自身不存储分片数据;
- 数据节点:具体存储数据的分片节点,负责处理中心节点分发过来的写入/读取数据请求。
上面的中心节点,就类似于之前的”负载均衡器“,负责管理集群所有节点,以及具体的分发工作,这种模式也是早期最主流的分片集群方案,即:代理式分片集群。
代理式分片,可以在官方不支持分片集群的情况下,自己来搭建分片集群,以传统关系型数据为例,MySQL
官方并不支持分片存储,怎么办?如果工作年限较长的小伙伴一定清楚,之前有个大名鼎鼎的中间件叫:MyCat
,示意图如下:
尽管MySQL
不支持分片架构,但通过上述方案,可以将一个个独立的MySQL
节点,组合成一个逻辑上的大整体,每个独立的MySQL
节点,则代表着具体要存储数据的分片节点。MyCat
则是负责节点管理与数据分发的中心节点,所有要操作MySQL
的客户端,都会连接到MyCat
,读写请求交给MyCat
做具体分发。
PS:实际
MyCat
就是伪装成了一台MySQL
,业务系统依旧和往常一样连接即可,屏蔽了客户端对分片集群的感知。
除开MyCat
外,如同类型的Sharding-Proxy
,又或者早期Redis
的TwemProxy、Codis
等等,这都是代理式分片理念的产物。究其根本,还是由于早期官方并不支持分片式架构,因此部分业务庞大的企业迫于无奈,只能在更高的维度上,架设中心节点来分发数据到不同的节点中存储。
这种中心化分片集群的思想,直到现在依旧在使用,只不过如今很少有代理中间件,基本都由官方自己实现,如MongoDB
分片集群,依靠路由节点mongos
进行数据分发。
为啥我称呼这类分片集群为“中心化分片集群”呢?其实站在数据/分片节点的角度出发,节点之间相互平等,不存在之前主从架构里的Master
概念。可是,虽然没有了Master
,但在数据节点之上有了一种更高维度的节点,所有数据节点都得“听”它安排,这就是一种另类的中心化体现。
中心化分片集群有何劣势?特别明显,不管是第三方实现的代理中间件,还是官方自身研发的“高等级节点”,毕竟所有请求都需经过“中心节点”,为此,一旦这类节点故障,必然会造成整个分片集群不可用。
中心化分片集群,为了解决中心节点的单点故障问题,一般都需要对中心节点做高可用建设,如
mongos
集群式部署、MyCat
做热备等等。
1.3.3、去中心化分片集群
与上阶段提到的中心化分片集群相反的,则是去中心化分片集群,典型的例子就是Redis3.x
推出的Redis-Cluster
集群,这是一种完全意义上的去中心化集群,所有负责读写数据的节点地位平等,并且没有“中心化”的路由节点。
相反,Redis-Cluster
中采用了哈希槽的概念,总计16384
个槽位,集群初始化时会平均分给每个节点负责管理(支持手动设置),比如现在有四个节点,哈希槽的默认分配如下:
A
节点:0~4095
槽位;B
节点:4096~8191
槽位;C
节点:8192~12287
槽位;D
节点:12288~16384
槽位;
类似于之前0~15
这16
个默认的库一样,每个槽中都可以存放多个Key
,同时配备CRC16
哈希算法,最终实现数据的分发,例如下述命令:
set name zhuzi
则会使用CRC16
算法对name
这个键名进行哈希,接着取模总槽数16384
,从而得出具体要落入的槽位。假设name
对应的哈希值是185272
,最终落入槽位则是5047(185272%16384-1)
,即落入到B
节点中存储。
既然没有中心化的路由节点,那CRC16
+取模运算的工作谁来完成?集群内的所有节点!在Redis-Cluster
中,任意节点都具备CRC16
取模的能力,来看个场景:
如果一个本该落入
A
节点的Key
,在set
时去到了C
节点,那么C
节点在经过CRC16
计算后,会不会把该Key
转发到A
节点去呢?
答案是不会,而是会向客户端会返回一个重定向错误的消息,其中指示了正确的节点位置,告诉客户端去连接对应节点写入数据即可。
PS:实际项目中,并不会把
CRC16
+取模的工作交给Redis
来做,因为这样有可能会出现一次重定向,通常都是客户端计算,而后直连正确的节点写入数据(如Java
中主流的Redis
客户端框架,都有实现CRC16
的逻辑)。
好了,上述便是去中心化的大体逻辑,相信大家一定明白为啥叫去中心化分片集群,因为不存在中心节点,各个节点自身就具备数据分发计算的能力,即使集群内部分节点宕机,也只会影响部分数据,保证了BASE
理论中的基本可用思想,并不会像中心化分片集群那样,一旦中心节点故障,就会造成分片集群不可用。
PS:分库分表领域的
Sharding-JDBC
,也是一种去中心化分片存储的体现,即各个客户端具备数据分发的能力,不需要依赖MyCat
这类中间件实现数据路由。
相较于传统的主从集群,分片式集群的维度显然更高,当应对数据量级、并发规模非常大的系统时,分片式集群能够利用多台机器的共同满足数据需求,它能充分利用集群中每一台机器的资源,实现数据读写的并行处理,以及海量数据的分区存储。也正因如此,早期想实现分片式存储,需要自己研发中间件,反观如今,随着分布式各领域技术的发展,分片式集群成为了新趋势,官方基本都开始支持分片架构。
二、集群篇总结
OK,看到这里,我们一步步阐述了集群中的常用知识,也对集群的形态有了全面认知,但本文并没有涵盖集群的方方面面,实际上,不同类型的集群架构,总会带来各种各样的问题,而这些问题并未做展开,也包括集群与云平台的结合,本文也未曾说明,这些就留给诸位自行去探索啦~
相信看到这里的小伙伴,对集群方面的知识应该建立出了系统化、结构化的认知体系,不过经常阅读我文章的小伙伴会发现,本文中的很多概念,在以往的文章里都有提及,看起来会感觉异常熟悉,为啥?这就是我经常说的那句话:技术学到最后都是共通的,不同技术之间最后都能串起来。
不管是业务系统,还是注册中心、定时任务、Redis、MQ、MySQL、ES……
等各种组件,在实现集群方案时,底层逻辑都大差不差,本文则是各种技术栈集群方面的共通知识,当你在学不同技术栈的集群技术时,就会发现这些技术之间的共通性。
最后,也希望大家不管在学习什么技术,不要只把目光集中在表层的应用,闲暇之余,记得看看内层的实现思想,这样方能做到一通百通,才能让大家真正成为技术领域的“高手”。