随着接触的分布式系统(产品)越来越多,关于分布式系统的数据存储逐渐有了一些理解,进行统一整理和梳理。
在进行分布式系统设计时,面临的数据场景不同,因此对应的产品在进行架构设计时也采用了不同的存储策略。但是总的说来,主要包括如下两类。
小规模数据场景主要包括: 元数据、少量业务数据等。这种数据类型的特点是,存储的数据量一般都比较小(比如10G以内)。通常情况下,单个节点能够满足数据的存储、读写性能要求,因此该类型的分布式产品在设计时通常使用镜像模式进行存储。单节点保存所有数据(业务数据+元数据),通过多节点(主从模式,强同步或者半强同步)形成多副本,从而保证数据的一致性。主从模式能够避免数据不一致的问题,不同节点通过复杂的选主逻辑选择最合适的节点作为leader,leader节点响应客户端的写请求,从节点响应客户端的读请求(不同产品有差异)。该类型常见的产品如zk、etcd等。
参考zk的架构图如下
大规模数据场景主要包括:大数据、日志、图片、大量文档数据等,这种数据类型的特点是,存储的数据规模大(TB级别甚至更高)。通常情况下,单节点无法满足数据存储需求(单节点的磁盘、cpu内存等成为瓶颈),因此该类型的分布式产品在设计时通常使用分片模式进行存储。 将一个完整的业务数据(比如大索引)拆分成多个分片进行存储,不同分片分散在不同的节点上,从而规避单节点性能不足问题。这种类型的架构模式需要master角色,保存整个集群的元数据,从而形成上帝视角,用于进行数据分配、调度等职责。常见的产品如hdfs、hbase、es等
参考es的架构图如下
数据一致性和高可用是分布式场景下,面临的2个最核心问题。后续将重点开展镜像模式是如何实现数据一致性和高可用的。分片模式通常是在镜像模式下进行相应的组合而成。
镜像模式下,由于单节点已经保存了所有的数据(元数据+业务数据),因此在架构上只需要进行选主+多副本,就可以解决避免数据一致性和高可用问题。
数据一致性:通过选主选择出集群的leader节点,响应客户端的写需求,并且通过数据强同步(或者半强同步)将数据同步到其他的节点,从而保证数据一致性。
当客户端进行数据写入时,形成一个写入的事务,会面临多种情况。
1,集群的leader节点+所有(或者多个)节点完成数据写入,才能认为数据已经完成写入,并返回给客户端,此时客户端才能认为数据已经写入到集群中,客户端进行事务提交,一个完整的数据事务完成。此时集群需要承诺客户端,写入的数据不丢失。
2,如果客户端在发起写入事务后,集群内部不同节点出现了异常(比如一个非leader节点突然宕机),面对这种情况不同的产品的响应策略不同(一些产品直接返回失败,一些产品会跳过异常的节点继续写入其他节点尽可能保证写入成功)。但是整体上的一个策略是,如果不满足集群的leader节点+所有(或者多个)节点完成数据写入,会将返回结果返回客户客户端,由客户端决定这条数据的去留(重试或者丢弃)。这种模式下,数据的一致性需要客户端进行参与,如果客户端忽略了集群的异常返回(或者返回码),就会可能导致数据丢失。
相关的写入流程可以参考etcd的写入流程
3,不同的产品在设计上不同
有些产品把**集群的leader节点+所有(或者多个)**的决定权保留给客户端,客户端在写入的时候选择对应的模式,通常有如下几种模式
比如kafka在客户端写入数据是需要客户端指定acks策略。
有些产品则不允许客户端做出相关的选择,只有一种集群的内部定义的模式。
因此不同的客户端需要根据不同的产品特性进行相关的适配。
镜像模式下,通过leader+多节点的方式实现数据多副本,从而实现数据高可用,不同产品的选主有差异,通常的选主方式有2种
HA模式的实现方式有很多种,最常见的方式是,通过分布式锁实现,比如在zk中注册一个临时节点。
1,成功在zk中创建了临时目录的节点获取对应的分布式锁权限,从而成为leader,并且需要定期到zk中发送心跳,给临时目录续期。临时目录有一个特点,如果在ttl时间内没有被续期,就会自动消失。
2, 其余节点通过watch的方式监听这个临时目录,如果原leader在ttl时间内没有同步心跳,临时目录就会自动消失,zk给所有的watch客户端发送相关的事件
3, 收到zk的事件,集群的节点都抢锁(在zk中创建临时目录),抢到锁的节点升级为新leader
4,新leader发送广播告知所有的节点,新leader已经产生。
5,通常新leader产生后,老leader可能还存活,需要一定的策略将老leader降级,从能避免脑裂,比如hdfs的nn通过fencing策略将老leader降级(先rpc降级,如果rpc降级失败,则ssh到对应的机器上杀死进程)。
常见的选择主协议有Paxos,Raft,ZAB等多种协议进行选主逻辑,获取超过半数(n+1/2)
投票的节点成为leader。由于不同节点之间的数据可能有差异,因此选举出leader后,需要根据情况进行合并数据(将不同节点的事务进行合并,形成完整的数据)。
不同协议的选主逻辑差异,可以参考 Paxos,Raft,ZAB的差异对比
通常情况下,通过一致性选主协议进行数据选主的集群,需要获取超过半数节点投票才能成为leader,因此通常部署单数节点,能够容忍集群内部网络隔离异常。并且集群能够容忍的异常节点数量是(n-1)/2
,相对于双数节点部署,单数节点部署能够更节省资源,也更加合理。
raft的选主逻辑大致如下,其他协议的选主逻辑也跟raft大致差不多。
1,初始状态
Raft 每个节点初始化后的心跳超时时间都是随机的,如上所示,节点 C 的超时时间最短(120ms),任期编号都为 0,角色都是跟随者。
2,请求投票
此时没有一个节点是leader,节点等待心跳超时后,会推荐自己为候选人,向集群其他节点发起请求投票信息,此时任期编号 +1,自荐会获得自己的一票选票。
3,跟随者投票
跟随者收到请求投票信息后,如果该候选人符合投票要求后,则将自己宝贵(因为每个任期内跟随者只能投给先来的候选人一票,后面来的候选人则不能在投票给它了)的一票投给该候选人,同时更新任期编号。
4,当选leader
当节点 C 赢得大多数选票后,它会成为本次任期的leader。
5,leader与跟随者保持心跳
leader周期性发送心跳消息给其他节点,告知自己是leader,同时刷新跟随者的超时时间,防止跟随者发起新的leader选举。
分片模式在一定程度上是将大数据按照分片进行拆分,形成多个小分片,在每个小分片的整体数据一致性和高可用逻辑(主从多副本)跟镜像模式是一致的。
比如hdfs的架构如下
将hdfs进行拆分,主要分为2块
1,NN节点(2个nn),保存集群的元数据,构建集群的数据全局视角,使用HA模式进行实现高可用。
2,DN节点,将文件以block作为基础存储单元分散在不同的DN节点,单个block通常有3个副本,3个副本分散在不同机器/机架上,并进行内部选主。
如上2个模块, 均是将大系统拆分成小模块,利用镜像模式的基础方法实现数据一致性和高可用。
通常针对大规模集群,在系统架构的设计上主要是分2类
量者有比较大的区别,并且优缺点也比较明显
所谓中心化模式,就是在集群内部选择一些节点(角色)存储集群的元数据,从而构建集群的数据全局视角,不同产品的实现方式有差异。
例如
中心化模式
优点:集群变更客户端不感知,统一由master节点管控
缺点:master节点存储所有的元数据(通常通过HA模式选主),master需要响应大量的http、rpc请求进行数据读写请求,因此master节点容易成为性能瓶颈。
去中心化模式,是指集群中没有节点(角色)负责构建集群的全局视角,约定一些固定的数据存储算法。客户端如果需要进行数据读写,需要根据约定的数据存储算法(比如哈希)计算对应的数据以及存储数据所对应的节点。客户端在跟对应的节点进行通信,进行读写数据。
典型的产品是,redis-cluster。
去中心化的模式
优点:客户端自己完成数据查找逻辑,因而节省了数据查找环节,能够解决中心化模式的master节点性能压力
缺点:对于集群的扩、容规则有一定的限制,如果不合理的扩、缩容姿势、以及算法的变更,可能会导致客户端无法通过约定的算法找到准确对应的数据存储的节点。
在日常生产环境中,节点宕机/磁盘异常,是很常见的现象,因此分布式系统应当能够容忍常规的异常,不影响集群使用。不同产品在设计上有差异,通常有如下2种模式
自动隔离,应该都很容易理解,就是将读写从异常节点中摘除,避免客户端异常。差别主要在自动修复和人工修复上,两者各有优劣势。
咋一看,好像如果集群能够自修复当然是最好的,这样不用人工介入,只需要等自动修复完成就好。但是实际情况来看,效果可能并不好。
最主要的限制是来自2个: 生产环境复杂,自动修复的时机和自动修复的进度,难以掌握。
比如 hdfs集群,由于1个节点异常了,会触发自动修复逻辑。自动修复主要是拷贝异常的数据到新节点,在新的节点生成合适的数据副本,但是
1, 数据拷贝需要占用一定的磁盘读写和网络读写带宽,hdfs的数据量巨大,如果不能得到合理控制可能会把机器的磁盘io和带宽打满,从而影响线上业务。
2, 再比如,机器可能1h就完成修复,并且上线了,步骤1中完成的读写修复,似乎可能多余了。 并且由于节点重新上线,又会重新触发数据balance,又会导致集群的磁盘读写和网络读写的带宽,恢复过程也可能比较久。
看起来机器宕机1h在生产中并不是难以容忍的异常,并且也几乎很少有概率会导致数据丢失,但是却触发了2次的数据同步,占用了磁盘读写和网络带宽,并可能会影响生产任务。虽然最终能够完成修复,但是额外带来了风险,效率并不高。
人工修复,就是需要人工介入。 比如如上的hdfs集群异常,如果不进行自动修复,1h后机器修复正常开机并加入到集群,并不需要复杂的数据迁移和复制,由于磁盘数据都在只需要做简单的数据校验,就能够恢复正常,集群恢复的效率和速度要高很多。
但是代价就是,需要实时监控和人工介入,运维工作的量比较大,并且在一定时间内需要承担数据丢失的风险,毕竟副本少了一个。
不同产品的设计出发点不同,有些倾向于自动修复,有些倾向于人工修复,各有利弊。我个人倾向于做一个这种,设置自动修复的窗口期(比如12h),如果不能再窗口期内人工修复,就触发自动修复,取得数据风险和运维重量折中。
经常听到有人说,集群不是分布式吗,怎么单机故障了,集群就异常了?其实综合各种生产实践来看。常规情况下,如果单机设备异常的的比较干脆(机器宕机、磁盘不可读写)大多数的分布式产品是能够容忍的,并且效果还是有保证的。
但是分布式系统的故障容忍和转移,也是有限度的,并不是无限的单机故障(异常)都能容忍。主要的原因是来自,通常分布式产品内部的节点异常,是用过探活心跳来实现,如果探活能够成功,但是对应的节点性能很糟糕,难以满足读写的要求,就会导致由于一个慢节点,把集群完全拖垮的情况。成为单机故障的噩梦。
通常遇到的单机故障,容易导致集群被拖垮的现象不多,但是常见的有
如上2种都是日常生产常见的现象,很少有分布式组件能够逃过这两种因素,能够自动完成容灾剔除,成为单机故障的噩梦
为了尽可能的保证生产稳定性,应当在系统架构设计上做一些更好的可靠性建设,基于日常经验进行整理
一个分布式系统重,不同组件的重要度是不同的,核心链路涉及相关的组件和架构,是需要定期梳理和改进的。一个系统里面,几个不同的产品单独拆开用,没问题,但是合并在一起却可能出现不兼容的现象,因此在架构上需要进行梳理。
核心组件和链路,需要
区分核心链路和非核心链路
精力重点投入在核心链路上,特别是架构中的骨干环节。非核心链路可能会出现问题,但是不是大故障,通常还是能够容忍的。
合理的容量设计和压测
系统的容量模型和容量设计,是一个很复杂且很困难的事情,但是需要再内部测试环境尽可能的模拟和压测,能够得到一定的参考依据,生产上能够得到一定的参考数据。
核心链路混沌演练
只要真正生产的演练,才能发现问题,纸面上的理论验证都不如实际的操作演练,问题才能真正的暴露。
核心链路的数据、部署等,尽可能的做拆分,在系统架构上,我倾向于去中心化这个概念。在关键的容灾上,往往能够起到意想不到的效果
核心系统全链路监控和告警
需要包括核心链路的监控大盘和告警,特别是核心链路建议增加核心链路环节的全链路告警。
在告警的建设上要抓大放小,把重要的精力都放在重点问题的建设上,抓重点,多建设全链路监控
组件监控+iaas监控
深入业务逻辑, 构建业务监控大盘