这里主要记录自己最近思考的一些分布式系统相关的知识。

基础问题

设计分布式系统时往往要考虑系统的可扩展性,可用性,一致性,可靠性,负载均衡,数据分布,自动化容错和数据恢复,数据迁移,数据分裂等等。每一部分都需要认真的思考。在考虑分布式系统的时候,也要考虑好单机系统部分。如系统的网络I/O,磁盘I/O,内存大小,磁盘故障等等。 可扩展性是分布式系统的基础。可扩展性要求系统能够往系统中添加和删除机器,相关的存储能力等也是线性增加的。可扩展性主要受限于元数据和真实数据存储,在常见的分布式系统中,主要是元数据的存储能力受限制。如GFS系统的单节点中master节点往往会成为瓶颈。可用性指的的系统的对外服务能力,即使机器出现了宕机也能够及时的恢复对外提供服务。可用性不要求机器100%的可靠,但是要求出现错误后能够快速恢复或选取新的节点代替故障节点并对外提供服务,在真实环境中,机器在秒级别时间内能够恢复服务即可。一致性主要针对于存储的数据来说,在分布式系统中,系统往往要存储多份,这样就很容易导致一致性问题,根据应用场景要确定一致性的级别:弱一致性,最终一致性还是强一致性。一致性可以有master节点控制,或者运行paxos或者raft算法。可靠性指的是系统可靠,可以保证数据不会丢失,即使在磁盘故障,断点,网络故障等情况下仍然能够保证。负载均衡指的是要保证系统中的节点的负载基本相同,不会出现某些节点负载过重而某些节点负载太轻的情况。数据分布指的是将数据分布在那些机器上,有简单的数据分布,哈希分布,还有复杂的一些的如ceph系统中的CRUSH算法解决数据分布的问题。自动化容错和数据恢复时分布式系统的重要特征,可靠地系统要能够自动恢复一些数据,能够容忍错误,而不需要人工的干预,这样的系统才能更好地运行,更加的可靠。数据迁移往往为了解决负载均衡问题和热点问题。数据分裂存在于少量的系统中,往往是由于单机无法存储所有的数据,如BigTable中的表,某些分布式系统桶的概念。 单机问题指的是要好好考虑单机的设计和实现。如网络通信的实现,往往采用现成的库如libevent和muduo,考虑单机系统的网络吞吐,网络吞吐和磁盘吞吐能力,如果网络吞吐能力大于磁盘,那么分布式系统中就可以把数据分布在多台机器上,实现并发写,而不需要出现单机瓶颈问题。考虑利用单机系统的CPU资源,内存资源和磁盘资源。内存使用往往和实现有关,真实实现中往往会出现很多问题,很考验开发者的工程能力。有时候会出现写入性能大于读取性能,这主要是因为缓存的原因。

系统架构

分布式系统主要功能包括元数据存储,数据存储,客户端请求处理,全局任务,集群监控等方面。实现分布式系统主要考虑如何实现这些功能和实现方式。这些问题也往往决定了系统架构。常见的系统架构有两种:P2P结构和master-slave结构。它们的核心问题主要是数据存储和数据索引的问题。具体可以参考Dynamo和GFS的设计与实现,里面有很多可以学习的东西。

租约

租约指的是某节点(或系统)赋予一个节点的权利。租约往往有时间限制,在规定时间内,被赋予权利的节点负责行使使命,具有一切生杀大权。但是,租约容易出现的一个问题就是被赋予权利的节点出现故障时,不会有新的节点被赋予权利,在分布式系统中,就会产生可用性的问题,但是这是必须付出的代价。租约的实现有多种形式,既有GFS中master节点赋予chunkserver节点处理数据的权利,也有raft中,为了防止中有两个leader(新旧leader),Follower节点向leader节点保证规定时间内不会进行选举操作的租约。

NWR模型

NWR模型是Dynamo系统中提出的一个概念,非常的有意思,也值得学习分布式系统的人进行好好的思考。所谓NWR模型。N代表N个备份,W代表要写入至少W份才认为成功,R表示至少读取R个备份。配置的时候要求W+R>N。因为W+R>N,所以R>N-W,这指的是读取的份数一定要比总备份数减去确保写成功的倍数的差值要大。也就是说,每次读取,都至少读取到一个最新的版本。当我们需要高可写的环境的时候,我们可以配置W=1,这个时候只要写任何节点成功就认为成功,但是读的时候必须从所有的节点都读出数据。如果我们要求读的高效率,我们可以配置W=N,R=1。这个时候任何一个节点读成功就认为成功,但是写的时候必须写所有三个节点成功才认为成功。NWR模型的一些设置会造成脏数据的问题,因为这很明显不是像Paxos一样是一个强一致的东西,所以,可能每次的读写操作都不在同一个结点上,于是会出现一些结点上的数据并不是最新版本,但却进行了最新的操作。所以,Amazon Dynamo引了数据版本的设计。也就是说,如果你读出来数据的版本是v1,当你计算完成后要回填数据后,却发现数据的版本号已经被人更新成了v2,那么服务器就会拒绝你。版本这个事就像“乐观锁”一样。但是,对于分布式和NWR模型来说,版本也会有恶梦的时候——就是版本冲的问题,比如:我们设置了N=3 W=1,如果A结点上接受了一个值,版本由v1 -> v2,但还没有来得及同步到结点B上(异步的,应该W=1,写一份就算成功),B结点上还是v1版本,此时,B结点接到写请求,按道理来说,他需要拒绝掉,但是他一方面并不知道别的结点已经被更新到v2,另一方面他也无法拒绝,因为W=1,所以写一分就成功了。于是,出现了严重的版本冲突。Amazon的Dynamo把版本冲突这个问题巧妙地回避掉了——版本冲这个事交给用户自己来处理。于是,Dynamo引入了Vector Clock(矢量钟?!)这个设计。这个设计让每个结点各自记录自己的版本信息,也就是说,对于同一个数据,需要记录两个事:1)谁更新的我,2)我的版本号是什么。 下面,我们来看一个操作序列:

  1. 一个写请求,第一次被节点A处理了。节点A会增加一个版本信息(A,1)。我们把这个时候的数据记做D1(A,1)。 然后另外一个对同样key的请求还是被A处理了于是有D2(A,2)。这个时候,D2是可以覆盖D1的,不会有冲突产生。

  2. 现在我们假设D2传播到了所有节点(B和C),B和C收到的数据不是从客户产生的,而是别人复制给他们的,所以他们不产生新的版本信息,所以现在B和C所持有的数据还是D2(A,2)。于是A,B,C上的数据及其版本号都是一样的。
  3. 如果我们有一个新的写请求到了B结点上,于是B结点生成数据D3(A,2; B,1),意思是:数据D全局版本号为3,A升了两新,B升了一次。这不就是所谓的代码版本的log么?
  4. 如果D3没有传播到C的时候又一个请求被C处理了,于是,以C结点上的数据是D4(A,2; C,1)。
  5. 如果这个时候来了一个读请求,我们要记得,我们的W=1 那么R=N=3,所以R会从所有三个节点上读,此时,他会读到三个版本:A结点:D2(A,2);B结点:D3(A,2; B,1);C结点:D4(A,2; C,1)
  6. 这个时候可以判断出,D2已经是旧版本(已经包含在D3/D4中),可以舍弃。
  7. 但是D3和D4是明显的版本冲突。于是,交给调用方自己去做版本冲突处理。就像源代码版本管理一样。