本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
本作品 (李兆龙 博文, 由 李兆龙 创作),由 李兆龙 确认,转载请注明版权。
引言
同一个问题在学习的不同阶段理解是不一样的。开始时对此领域了解尚欠,看问题的角度不但低,而且单一,就会导致很容易照着某种解去看待这个问题,就比如说这个"简单"的问题,即数据冗余
,在最开始的时候这个问题在我的眼中等同于一致性协议
,因为我们可以基于一致性协议去做数据冗余,当然最重要的是这是强一致性的,但是众所周知强一直性(服务器角度的强一直性,当然我们日常忽略掉网络时延)是极其影响性能的,所以大多数时候其实我们不会这样去做,可能退而且其次选择其他的一致性模型,这会导致数据冗余的策略发生变化,这是我在[1]这篇文章中没有发觉到的,当时忙着思考一致性,全然忘掉了一致性的展现的行为其实就是数据冗余。
再其次,这篇文章并不是像[2]那样非常笼统的下定义,而是要描述几种现实生活中使用的冗余策略,反推其一致性以及去匹配[2]中的概念。
文章中的资料大多来自于各大公司的论文,从正确性来说是没有任何问题的。
这篇文章我们只提及数据冗余策略,不提及其他任何东西。
一致性协议
强一致性协议可能是绝大多数学习分布式理论的朋友入行以来遇到的第一道坎了,先受Raft(multi)的虐待,再遭Paxos的蹂躏,最后发现还有ZAB这样的原子广播协议。加上拜占庭将军问题的困扰,PBFT,Pow,PoS,DPoS这样用于比特币的算法又不计其数;去掉强一致性的限制,还有诸如Gossip这样的非常弱的一致性协议(一般用来维护集群成员关系);限制再放弱一点还有 ViewStamped replication 这样的复制协议,当然这个我并不太了解,有兴趣的朋友可以看看[5]。
基于一致性协议去维护数据冗余就不说了,本质上就是结点之间同步日志的过程。
ceph
ceph中的事件分发是通过CRUSH
算法去做的,也就是通过从master
集群中获取的inode
我们可以计算出一个pgid
,通过pgid
可以得到一组OSD
的信息,此时就可以通过map得到它们的真实存储信息了。
OSD
列表的第一个结点为Primary
,剩下的都是Replica
。客户端完成所有的写到主OSD上的对象PG中(主host),这些对象和PG被分配新的版本号,然后写到副本OSD上,当每个复制都完成并响应给主节点,主节点完成更新,客户端写完成。这里是一个两阶段的提交,当副本都写到内存中时回复Ack
,主节点收到全部的Ack就可以回复,当副本都提交到磁盘时还会回复Commit
,主节点收到全部的Commit时还会给Client
一个回复,这样大大减小了客户端的响应速度。
客户端读就直接在主节点读,这个方法节省了客户端的副本之间的复杂同步和序列化。这种方法可见的W配置的就是全部结点,意味着错误恢复的时候都可可靠的保持副本一致性。当然倘若某个Replica
出现故障,操作就会失败了,因为没办法收到所有的回复。
论文中没提到此时该怎么办,我认为此时(超时以后)就可以重新计算OSD,进而重新复制,在OSD恢复时会加入原来的伙伴关系中,同时也会基于PG最近的变化日志去同步日志,这样就保证了一致性。
GFS
GFS的数据冗余来源于两个方面,一个是Master
结点的状态副本,一个ChunkServer
的数据副本。这其中又涉及到一个问题,即选主,前者通过Chubby进行可靠的选主(获取一把锁),后者通过Master
发放的leases
确定主节点,我们看到,两种方法都没有使用一致性协议,但都可以唯一的指定主节点。
其实对于master
的冗余paper中对于实现并没有太多的描述,但是提到了Master服务器所有的操作日志和checkpoint文件都被复制到多台机器上。且对Master服务器状态的修改操作能够提交成功的前提是,操作日志写入到Master服务器的备节点和本机的磁盘。这样的话当主服务器宕机时一旦通过Chubby选主成功,那么这些数据非常全的副本就可以很快的切换到正常的运行状态,因为其拥有原主节点的全部数据,且已经入盘。
初次之外,GFS中还有一种服务器称为影子服务器,这些影子服务器在“主”Master服务器宕机的时候提供文件系统的只读访问。它们是影子,而不是镜像,所以它们的数据可能比“主”Master服务器更新要慢,通常是不到1秒。这样的操作使得就算是“主”master下线也不会使得服务整体下线,因为影子还可以提供原数据的读取。对于那些不经常改变的文件、或者那些允许获取的数据有少量过期的应用程序,影子服务器能够提高读取的效率,且提升整个系统的可用性。
为什么一般影子的数据会慢一些呢?因为Chunk的信息是由影子自己与Chunk通信维护的,但是副本的创建和修改只能由master来完成,如果修改是选择先通知影子,收到回复后再更新可能会导致影子的数据新于master,这是不可忍受的,所以GFS中影子选择读取一份当前正在进行的操作的日志副本,并且依照和主Master服务器完全相同的顺序来更改内部的数据结构,一次保证一致性,不过当然会有一个同步的时间,导致数据稍微旧一点。
然后我们说说ChunkServer
的数据冗余,以下是更新流程:
每次看到这张图我都想说控制流与数据流的分离只能说巧妙。。
GFS使用租约机制来保持多个副本间变更顺序的一致性。Master节点为Chunk的一个副本建立一个租约,这个副本叫做主Chunk。主Chunk对Chunk的所有更改操作进行序列化,所有的副本都遵从这个序列进行修改操作。
可以在paper的3.1节中看出数据的拷贝是一个强一致性的过程,因为任何副本产生的任何错误都会返回给客户机,首先数据肯定在主chunk执行成功了,不然不会产生一个操作顺序,所以倘若有任何一个Replica
执行操作序列失败都会把这个消息传递给主chunk,此时客户请求被认为是失败的,且此时这些数据处于不一致的状态,客户端通过重复执行失败的操作来处理这样的错误。
至于为什么使用租约保证多个副本之间的一致性而使用租约,paper中给出的解释是这样的:设计租约机制的目的是为了最小化Master节点的管理负担。这个其实并不太好理解,我的想法是这样的,应该说是为了更为高效的管理chunk。因为在GFS眼中Chunk的管理是一个动态的过程,master对于chunk的管理包括但不局限于如下几点:
- master检查当前的副本分布情况,然后移动副本以便更好的利用硬盘空间、更有效的进行负载均衡
- 对于新Chunk的选择选择和创建时类似:平衡硬盘使用率、限制同一台Chunk服务器上的正在进行的克隆操作的数量、在机架间分布副本
- 当Chunk的有效副本数量少于用户指定的复制因数的时候,Master节点会重新复制它
如果把每一个Chunk配置成Paxos/Raft
组的话进行迁移(为了硬盘的利用率)会非常麻烦[9],且Chunk的开销也会变的比较大,因为强一致性协议本身的开销就是很大的。
当然以上只是我的个人想法。
Dynamo
Dynamo所描述的其实就是[2]中的无主结点部分,因为[2]这篇文章是我在阅读了DDIA以后写的文章,现在看来DDIA中那一节的内容其实也是针对与[10]这篇paper写的。
Dynamo中使用优化的一致性哈希做数据分布,显然数据冗余最直观的有两种方法,一个数每个结点跑主从(链式也可),或者像Dynamo这样为了极致的可用性而引入的无主结点写入。
Dynamo除了在本地存储其范围内的每个key外,还会复制这些key到环上顺时针方向的N-1后继节点。拿上图举例子,如果[A,B)区间插入一个key的话,BCD结点都会存储这个key,换句话说,节点D将存储落在范围(A,B],(B,C]和(C,D]中的所有key。
一个负责存储一个特定的键的节点列表被称为首选列表(preference list),且Dynamo中的任何存储节点都有资格接收客户端的任何对key的读写操作,也就意味着在这里可能会出现多主结点写入。为了保证一致性,一般配置W+R > N
,我们把处理读或写操作的节点被称为协调员,一次读写过程如下:
-
收到写请求时,协调员生成新版本向量时钟并在本地写入新版本。协调员然后将新版本(与新的向量时钟一起)发送给首选列表中的排名前N个的可达节点。如果至少W-1个节点返回了响应,那么这个写操作被认为是成功的。
-
收到读请求时,协调员为key从首选列表中排名前N个可达节点处请求所有现有版本的数据,然后等待R个响应,然后返回结果给客户端。如果最终协调员收集的数据的多个版本,它返回所有它认为没有因果关系的版本。不同版本将被协调,并且取代当前的版本,最后写回。
对于矢量时钟不了解的朋友可以查查资料。
这里还有一个问题,就是如何同步多个结点直接的值,因为写入是多节点的,我们根本不知道其他结点与自己有哪些键不一样,难道需要每次同步都需要发送全部的键吗?当然不需要,Dynamo使用MerkleTree
实现副本之间的同步,这里引出了带虚拟结点的一致性哈希的一个缺点,就是在新加入主节点的时候会导致很多MerkleTree
失效,而且是很难恢复的,只能通过现有数据重建树,因为key range
被破坏了,如何优化本文不谈。
Dynamo如何通过这样的无主结点写入实现极致的可用性本文不谈。
redis
Redis的数据冗余方案是主从复制,高可用方案为Sentinel
,哨兵充当选举结点。当使用Redis Cluster
时至少拥有一个slot
的主节点充当选举投票结点,从节点为选举结点,从节点选举成功后执行slave no one
,并撤销原主节点的槽指派,使这些槽都指向自己。上位后广播PONG
,这个Gossip包用于通知故障转移完成。
因为Redis实现的问题,其实主从复制没有丝毫的一致性可言,因为主更新成功以后才会更新从,并且这个更新的过程是异步的,而且没有Quorum
的概念,要发出的数据首先存在redisClient
结构中回复缓冲区buf
中,在收到此客户端的可写事件后才会进行同步,而这至少已经是下一次事件循环了。
这意味着Redis的数据冗余没有任何的一致性可言,用Redis做分布式锁从理论上讲就是扯淡,当然是理论上,毕竟主节点宕机,同时核心数据又恰好没同步到从服务器概率实在太低,但是基数大了以后,什么都可能发生。
在Redis Cluster
中数据冗余方案仍旧是主从复制,基本同步流程和上面一样,但是故障发现,故障转移的过程都不相同,发起选举的角色也不相同,本文不谈。
bigtale
Bigtable的数据冗余给我们一种新的思路,即借助其他组件进行数据冗余,Bigtable
采用GFS
来对Tablet
和Redo Point
进行数据冗余。
我想这才是比较常见的方法,即把一个成熟的分布式存储组件当做一个网络文件系统来用,这样可以给上层应用一个简单且强大的抽象,当然脑子里要清楚一件事情,这并不是分布式缓存,也就是说一次操作几百毫秒是很常见的,所以利用空间/时间局部性来在用户态对这些数据进行缓存(cache)是很有必要的,写入的时候也需要buffer
,即图中的memtable
,诚然这样可能造成数据丢失,毕竟数据没入盘,这里可以实施redo日志,通过Redo point
来恢复一个memtable
。
对于日志的提交bigtable做出了一个优化,见[11]commit log
部分。
因为一次GFS的操作是很昂贵的,用户态需要大量的缓存(cache),BigTable使用了两级缓存,扫描缓存是第一级缓存,主要缓存Tablet服务器通过SSTable接口获取的Key-Value对;Block缓存是二级缓存,缓存的是从GFS读取的SSTable的Block。对于经常要重复读取相同数据的应用程序来说,扫描缓存非常有效。
memcache
数据冗余的最高境界就是不需要数据冗余!
一个全内存的缓存怎么数据冗余嘛,冗余什么嘛你倒是说。就连路由信息都在客户端存着,当然如果为了数据安全当然就不需要数据冗余喽。但是可能会因为效率进行冗余。
但是显然在在新加入一个集群的时候,此时缓存命中率会很低,这样会削弱隔离后端服务的能力,这可以看成特殊的缓存雪崩,此时facebook团队的做法[12]是冷集群热身,即我们可以让这个新集群的客户端从已经运行了很长时间的集群客户端中检索数据,这样这个冷集群上升到满负载的时间将会大幅度缩短。
以下是Facebook团队使用memcache集群的架构:
前面刚说不需要冗余,为什么又出现了呢?前面说到了,为了效率,facebook团队不仅实现了region
间的数据冗余,还实现了region
内的数据冗余。
在[12]5中有如下描述:
- We designate one region to hold the master databases and the other regions to contain read-only replicas; we rely on MySQL’s replication mechanism to keep replica databases up-to-date with their masters.
- 我们指定一个region持有主数据库,别的region包含只读的副本;我们依赖MySQL的复制机制来保持副本数据库与主数据库的同步。
这样的话就可以利用上多数据中心的优势了,并且可以使得region内的读取操作,不管是memcache
还是Storage Cluster
延迟都很低。
其次region
内的复制,[12]内有如下描述:
我们使用复制(replication)来改善延迟和memcached服务器的效率。
显然当某个键的请求量超过单机负载的时候,我们需要进行一些优化,不然可能出现缓存击穿
的问题。此时一般来说有两种方法:
- 基于主键的划分(基于哈希或者数据特征)。
- 全量复制(优化读取操作)
后者就是一个数据冗余。那么如上两种方案如何选呢?我认为有两个因素:
- 热点键集合的大小
- 请求多键与请求单键的开销差值
前者很好理解,如何热点键只有一个,怎么划分都没用,此时全量复制才是王道,我想这也是阿里云Tair[13]选择多副本的方案来防止热点的原因。每次提到Tair我都要说一句,欢神牛逼!欢神永远的神!!
但是当热点比较均匀且没有超大热键的时候显然数据分片是一个很不错的方法。
单键与多键的开销问题则是[12]中描述的问题:
- Consider a memcached server holding 100 items and capable of responding to 500k requests per second. Each request asks for 100 keys. The difference in memcached overhead for retrieving 100 keys per request instead of 1 key is small. To scale the system to process 1M requests/sec, suppose that we add a second server and split the key space equally between the two. Clients now need to split each request for 100 keys into two parallel requests for 50 keys. Consequently, both servers still have to process 1M requests per second. However, if we replicate all 100 keys to multiple servers, a client’s request for 100 keys can be sent to any replica. This reduces the load per server to 500k requests per second
- 考虑一个包含100个数据项的memcached服务器,具有对每秒500K请求进行处理的能力。每一个请求查找100个主键。在memcached中每个请求查询100个主键与查询1个主键之间开销的差值是很小的。为了扩展系统来处理1M请求/秒,假如我们增加了第二台服务器,将主键平均分配到两台服务器上。现在客户端需要将每个包含100个主键的请求分割为两个并行的包含50个主键的请求。结果两台服务器都仍然不得不处理每秒1M的请求。然后,如果我们复制所以100个主键到两台服务器,一个包含100个主键的客户端请求可以被发送到任意副本(replica)。这样将每台服务器的负载降到了每秒500K个请求。
CRAQ
前面聊的都是某些工业软件如何运用数据冗余机制,但是无论再怎么变,基本逃不了主从复制的命运(除了无主),只不过因为一致性的不同而导致实现的策略不一样罢了,难道数据冗余只有主从负载吗?
当然不是,链式复制也是一种比较优秀的选择,更不必说链式复制的优化CRAQ了。
我们来看看[14]中的性能对比结果:
因为链式存储的读写在两个结点,这两个操作在保证强一致性的同时是可以并发的,而主从想要保证强一致性则需要读写都需要经过主节点,像zk这样客户端角度的FIFO一致性不算。所以链式存储的性能在更新操作在百分之零到百分之二十五之间效率都是高于写操作的。而且就算链式存储不保证强一致性,也是天然保证客户端角度的FIFO的,不需要像zk一样还需要维护一个zxid
。
而链式存储的优化CRAQ则可以在保证强一致性的同时极大幅度的提升读吞吐量。并且为了更高的写吞吐量,CRAQ也允许降低一致性的要求,即最终一致性。这意味着可能在一段时间内返回旧数据(即在写入被应用到所有节点之前)。
简单提一提,不细谈,具体可参考[14][15]。
总结
后面计划再写一篇聊聊分布式存储中的事件分发,那同样是一个很有意思的问题。
总结完这些顿时觉得脑子清晰了不少,像这样比较细致但是简单的描述此内容的文章我想网上是非常少的,这也算是我给这个领域的初学者一点小小的贡献了。
因为像这样内容总结是一个战线很长的过程,也恰好最近快春招,把分布式相关的知识复习了一下也才能写出这篇文章,所以这篇文章还没有结束,后面学到新的东西我还是会补充的。
作者水平有限,错误之处还请海涵并斧正。
参考:
- 《谈谈对分布式一致性的一点理解》
- 《节点间数据复制》
- 《一致性算法》
- 《Paxos算法概述与推导》
- 《ViewStamped replication revisited 解读》
- Ceph: A Scalable, High-Performance Distributed File System
- Google File System
- The Chubby lock service for loosely-coupled distributed systems
- 《Raft算法: 集群成员变更问题》
- Dynamo: Amazon’s Highly Available Key-value Store
- Bigtable: A Distributed Storage System for Structured Data
- Scaling Memcache at Facebook
- 《2017双11技术揭秘—分布式缓存服务Tair的热点数据散列机制》
- Chain Replication for Supporting High Throughput and Availability
- Object Storage on CRAQ