你好,我是徐文浩。
过去的两讲里,我们都在尝试做一件事情,就是在Master和Backup Master之间保持数据的同步复制。无论是通过分布式事务的两阶段提交算法,还是通过分布式共识的Paxos算法,都是为了做到这一点。
而我们要去保障Master和Backup Master之间的同步复制,也是为了一个小小的目标,那就是整个系统的高可用性。因为系统中只有一个Master节点,我们希望能够在Master节点挂掉的时候,快速切换到另外一个节点,所以我们需要这两个节点的数据是完全同步的。不然的话,我们就可能会丢失一部分数据。
不过,无论是GFS也好,Bigtable也好,我们能看到它们都是一个单Master系统,而不是有多个Master,能够同时接受外部的请求来保持高可用性。所以,尽管在论文里面,Google没有说GFS在Master和Backup Master之间数据的同步复制是怎么进行的,但是根据我的推测,采用一个两阶段提交的方式会更简单直接一点。
那么,现在你可能就会觉得有问题了:如果还是使用两阶段提交这样的方式,我们不还是会面临单点故障吗?而且,我们上一讲所说的Paxos算法也用不上啊?
要回答这个问题,就请你一起来和我学习今天这一讲,也就是Chubby这个系统到底是怎么一回事儿。通过这一讲,我会让你知道:
好,下面就让我们一起来看看,Chubby这个系统是怎么一回事情吧。
无论是GFS还是Bigtable,其实都是一个单Master的系统。而为了让这样的系统保障高可用性,我们通常会采用两个策略。
第一个,是对它进行同步复制,数据会同步写入另外一个Backup Master,这个方法最简单,我们可以用两阶段提交来解决。第二个,是对Master进行监控,一旦Master出现故障,我们就把它切换到Backup Master就好了。
但我们之前也说过,这里面有两个问题,首先是两阶段提交也有单点故障,其次是监控程序怎么去判断Master是真的挂掉了,还是只是监控程序和Master之间的网络中断了呢?
其实,解决这两个问题的答案,就是Chubby,也就是Paxos算法。Chubby具体的技术实现并不简单,但是思路却非常简单。那就是,我们的“共识”并不需要在每一个操作、每一条日志写入的时候发生,我们只需要有一个“共识”,确认哪一个是Master就好了。
这样,系统的可用性以及容错机制,就从原先的系统里被剥离出来了。我们原先的系统只需要Master,即使是做同步数据复制,也只需要通过两阶段提交这样的策略就可以了。而一旦出现单点故障,我们只需要做一件事情,就是把故障的节点切换到它同步备份的节点就行。
而我们担心的,系统里会有两个Master的问题,通过Paxos算法来解决就好了。
只要通过Paxos算法,让一个一致性模块达成共识,当前哪一个是Master就好,其他的节点都通过这个一致性模块,获取到谁是最新的Master即可。而且,这个一致性模块本身会有多个节点,比如5个节点,另外我们在部署的时候,还会把它们放到不同的机架上,也就是不同的交换机下。这样一来,一致性模块里单个节点出现故障,也并不会影响这个一致性模块对外提供服务。
那么,在Chubby这个系统里,它其实针对Paxos做了封装,把对外提供的接口变成一个锁。这样,Chubby就变成了一个通用的分布式锁服务,而不是一个Paxos的一致性模块。在锁服务下达成的共识,就不是谁是Master了,而是哪一台服务器持有了Master的锁。对于应用系统来说,谁持有Master的锁,我们就认为这台服务器就是Master。
事实上,把Chubby对外暴露的接口,变成一个分布式锁服务,Google是经过深思熟虑的。对于锁服务来说,大部分工程师都是非常熟悉的,无论是MySQL这样的关系数据库,还是Redis这样的KV数据库,或者是Java标准库里的Lock类,都是我们经常使用的开发工具。
但是Paxos算法,大部分工程师可能都没有听说过。我们之前在MapReduce的论文里就说过,一个好的分布式系统的设计,就是要让使用系统的开发人员意识不到分布式的存在。那么,通过把Paxos协议封装成一个分布式锁,就可以让所有开发人员立刻上手使用,而不需要让每个人都学习Paxos和共识问题,从而能够更容易地在整个组织的层面快速开发出好用的系统。
而且,Chubby这个锁服务,是一个粗粒度的锁服务。所谓粗粒度,指的是外部客户端占用锁的时间是比较长的。比如说,我们的Master只要不出现故障,就可以一直占用这把锁。但是,我们并不会用这个锁做很多细粒度的动作,不会通过这个分布式的锁,在Bigtable上去实现一个多行数据写入的数据库事务。
这是因为,像Master的切换这样的操作,发生的频率其实很低。这就意味着,Chubby负载也很低。而像Bigtable里面的数据库事务操作,每秒可以有百万次,如果通过Chubby来实现,那Chubby的负载肯定是承受不了的。要知道,Chubby的底层算法,也是Paxos。我们上一讲刚刚一起来了解过这个算法,它的每一个共识的达成,都是需要通过至少两轮的RPC协商来完成的,性能肯定跟不上。
事实上,在Bigtable里,Chubby也主要是被用来做四件事情,第一个是Master的高可用性切换;第二个是存储引导位置(Bootstrap Location),让客户端能够找到METADATA数据的存储位置;第三个是Tablet和Tablet Server之间的分配关系;最后一个是Bigtable里表的Schema。可以看到,Chubby存储的这些数据都是很少变化,但是一旦丢失就会导致数据不一致的元数据。
那么相信到这里,你对Chubby在整个分布式系统中的作用应该就弄明白了。Chubby并不是提供一个底层的Paxos算法库,然后让所有的GFS、Bigtable等等,基于Paxos协议来实现数据库事务。而是把自己变成了一个分布式锁服务,主要解决GFS、Bigtable这些系统的元数据的一致性问题,以及容错场景下的灾难恢复问题。
GFS和Bigtable这些系统,仍然会采用单个Master,然后对数据进行分区的方式,来提升整个系统的性能容量。但是在关键的元数据管理,以及Master节点挂掉切换的时候,会利用Chubby这个分布式锁服务,来确保整个分布式系统是有“共识”的,避免出现多个人都说自己是Master这样“真假美猴王”的情况出现。
理解完了Chubby在整个分布式系统中的作用,我们下面就来深入看一下,整个Chubby的系统是怎么样的。我们会一起来了解下Chubby的系统架构、Chubby对外提供的接口,以及看看具体这个分布式锁是如何使用的。
首先,Chubby这个系统也是有Master的。
在Chubby里,它自己的多个节点,会先通过“共识”算法,确认一个Master节点。这个Master节点,会作为系统中唯一的一个提案者(Proposer),所有对于Chubby的写入数据的请求,比如获取某个锁,都会发送到这个Master节点,由它作为提案者发起提案,然后所有节点都会作为接受者来接受提案达成共识。
只有一个提案者带来的好处就是,大部分时间,我们不太会因为两个Proposer之间竞争提案,而导致需要很多轮协商才能达成一致的情况。
那看到这里你可能会问了,如果Chubby的Master挂掉了怎么办呢?
不要紧,我们可以通过剩下的节点,通过共识算法再找一个Master出来。而且如果是因为网络故障,导致有两个Master的话,也会很快通过共识算法确定一个Master出来。另外,两个Master其实只是一致性模块里的两个提案者,即使两边都接受外部请求,也都会通过共识算法,只选择一个值出来。
在论文里面,Master的生命周期被称之为租期(lease)。也就是说,Master发起的共识算法达成的共识,是在一段时间T之内,它是Master。而只要Master不崩溃,一般它都会在这个T的时间到期之前,进行续租。而如果Master崩溃了,当T的时间到了,那么所有节点都可以发起自己是Master的一次提案,最终确认一个新的Master。
可以看到,虽然Paxos这样的共识算法其实是不需要单一的Master节点的。但是为了实际应用中的效率问题,我们会采用选举出一个Master的办法,来让整个系统更加简化。
对于Chubby的整个服务器端来说,我们可以把它看成一个三层的系统。最底层,是一个Paxos协议实现的同步日志复制的系统,也就是我们上一讲所说的状态机复制的系统。上面一层,就是通过这个状态机实现的数据库了,Google是直接采用了BerkeleyDB作为这个数据库。换句话说,Chubby是通过Paxos在多个BerkeleyDB里,实现数据库的同步复制。在BerkeleyDB之上,才是由Chubby自己实现的锁服务。
除了服务器端,Chubby还给所有想要使用Chubby的应用提供了一个客户端。客户端会通过DNS拿到所有的Chubby的服务端的节点,然后可以去里面任何一个节点询问,哪一个是Master。无论是读还是写的请求,客户端都是通过访问Master来获取的。
对于数据写入的请求,Master会作为刚才我们说过的提案者,在所有的Chubby服务器节点上通过Paxos算法进行同步复制。而对于读请求,Master直接返回本地数据就好,因为所有服务器节点上的数据是有共识的。
既然Chubby的底层存储系统,是BerkeleyDB这样一个KV数据库,那么我们就可以通过它做很多事情了。Chubby对外封装的访问接口,是一个类似于Unix文件系统的接口。使用这个形式,同样也降低了使用Chubby的用户的门槛。毕竟每个工程师都熟悉用ls命令,去查询目录下的子目录和文件列表。
Chubby里的每一个目录或者文件,都被称之为一个节点(node)。外部应用所使用的分布式“锁”,其实就是锁在这个节点上。哪个客户端获得了锁,就可以向对应的目录或者文件里面写入数据。比如谁是真正的Master,就是看谁获得了某个特定的文件锁。
举个例子,我们可以定义/gfs/master这个命名空间,就用来存放Master的相关信息。这样,Master服务器会通过RPC锁住这个文件,然后往里面写下自己的IP地址以及其他相关的元数据就好了。而其他客户端在这个时候,就无法获得这个锁,自然也就无法把Master改成自己。所有想要知道谁是Master的客户端,就只需要去查询/gfs/master这个文件就行。
而所有的这些其实是目录和文件的“节点”,在Chubby中会分成永久(permanent)节点和临时(ephemeral)节点两种。
对于永久节点来说,客户端需要显式地调用API,才能够删除掉。比如Bigtable里面,Chubby存放的引导位置的信息,就肯定应该使用永久节点。而临时节点,则是一旦客户端和服务器的Session断开,就会自动消失掉。一个比较典型的使用方式,就是我们在Bigtable中所说的Tablet Server的注册。
我们可以用一个Chubby里面的目录/bigtable/tablet_servers,来存放所有上线的Tablet Server,每个Tablet Server都可以在里面创建一个文件,比如/bigtable/tablet_servers/ts1、/bigtable/tablet_servers/ts2… /bigtable/tablet_servers/ts100这样排列下去。
Tablet Server会一直和Chubby之间维护着一个会话,一旦这个会话结束了,那么对应的节点会被自动删除掉,也就意味着这个节点下线无法使用了。这样,由于网络故障导致的Tablet Server下线,也会表现为会话超时,由此一来,它就很容易在Chubby这样的服务里面实现了。
回顾一下我们之前讲过的Bigtable的论文,Bigtable的Master一旦发现这种情况,就会尝试去Chubby里面获取这个节点对应的锁,如果能够获取到,那么说明Master到Chubby的网络没有问题,Master就会认为是Tablet Server节点下线了,它就要去调度其他的Tablet Server,去承接这个下线了的服务器之前服务的那些tablet。Master只需要监控/bigtable/tablet_servers这个目录,就能够知道线上有哪些Tablet Server可供使用了。
而为了减少Chubby的负载,我们不希望所有想要知道Chubby里面节点变更的客户端,都来不断地轮询查询各个目录和文件的最新变更。因为Chubby管理的通常是各种元数据,这些数据的变更并不频繁。所以,Chubby实现了一个事件通知的机制。
这是一个典型的设计模式中的观察者模型(observer pattern)。客户端可以注册它自己对哪些事件感兴趣,比如特定目录或者文件的内容变更,或者是或者是某个文件或者目录被删除了,一旦这些事件发生了,Chubby就会推送对应的事件信息给这些应用客户端,应用客户端就可以去做类似于调度Tablet Server这样的操作了。
现在我们知道,每一个Chubby的目录或者文件,就是一把锁。那么是不是我们有了锁之后,分布式共识的问题就被解决了呢?如果你是这么想的,那么你肯定还没有在网络延时上吃到足够的亏。
首先,作为分布式锁,客户端去获取的锁都是有时效的,也就是它只能占用这个锁一段时间。这个和我们前面提到的Chubby的Master的“租约”原理类似,主要是为了避免某个客户端获取了锁之后,它因为网络或者硬件原因下线了。
这样乍一听起来,我们只要给锁的时间设置一个时效就好了。不过,一旦涉及到不可靠的网络,事情就没有那么简单了。
Chubby的论文里给出了这样一种情况,我们可以一起来看一下:
这个就好像你租了一个仓库,租期是一年。你呢,在租期快要到期的时候,向仓库里发了一批货。但是因为物流延误,货在路上耽搁了。当你的仓库租期到期的时候,房东把仓库租给了别人,别人也已经往仓库里面放了他自己的货物。而这个时候,你的货到了,但是因为仓库的门卫并不知道房东把仓库租给了谁,它不会检查你是不是还租着仓库,就直接把你的货物也入库了,而把新的租客的货物给“覆盖”掉了。
当然,Chubby也解决了这个问题,它主要是通过两种方式来解决的。
也就是当客户端A的“租约”不是正常到期由客户端主动释放的话,它会让客户端继续持有这个锁一段时间。这很好理解,如果是客户端主动释放的话,意味着它已经明确告诉Chubby,我不会再往里面写入数据。而没有主动释放,很有可能是还有请求在网络上传输,我们就再稍微等一会儿。
而如果等一会儿还是没有过来,那么Chubby就会再把锁释放掉。这个就好像你在现实生活中租房子,租约要到期了,如果你是主动和房东说你不再续租了,房东自然可以立刻租给别人。但是可能你因为出差或者疫情隔离,没有来得及和房东沟通,房东也会善意地多等你几天,直到一段时间之后你还是失去联系了,才再会去租给别人。
它本质上是一个乐观锁,或者在很多地方也叫做Fencing令牌。这种方式是这样的:客户端在获取Chubby的锁的时候,就要拿到对应的锁的序号,比方说23。在发送请求的时候,客户端会带上这个序号。而当Chubby把锁给了别的客户端之后,对应的锁的序号会变大,变成了24。而我们对应的业务服务,比如Bigtable呢,也要记录每次请求的锁序列号,通过对比锁序列号来确定是否会有之前的锁,尝试去覆盖最新的数据。当遇到这种情况的时候,我们姗姗来迟的来自上一个锁的客户端请求,就会被业务服务拒绝掉。
所以你会看到,Chubby的每个锁,除了文件、目录本身,以及ACL权限这样的元数据之外,还有这样四个编号。
这样,通过锁编号,我们就很容易实现前面所说的锁序列器的功能。其他的编号,也都是实现了数据的“版本”功能。这个也使得我们在不确定的网络情况下,能确保写入的数据是按照我们期望的顺序。如果我们尝试拿过时“版本”的锁来更新最新的数据,那么更新就不会成功。
最后,和其他的分布式系统一样,为了提升性能,Chubby也会在客户端里维护它拿到的数据缓存。Chubby也有像代理、分区等等其他一系列的机制,来让整个系统更容易扩展,不过这些,就不是Chubby在整个大数据领域的核心和重点功能了,我把这部分内容留给你自己去好好研读啦。
我们漫长的旅程终于告一段落了。在过去三讲里,我们从GFS的Master的“同步复制”这个需求出发,逐步了解了两阶段提交、三阶段提交,以及Paxos算法,并且最终在今天一起学习完了Chubby的论文。
我们能看到,Google并没有非常僵化地在所有的分布式系统里面,都简单通过实现一遍Paxos算法,来解决单点故障问题,而是选择通过Chubby实现了一个粗粒度的锁。这个锁,只是帮助我们解决大型分布式系统的元数据管理的一致性,以及Master节点出现故障后的容错恢复问题。
因为大型分布式系统,并不是时时刻刻都会出现数据不一致的风险的。把“哪一个是Master”这个问题通过共识算法来解决,我们在系统的容错恢复上,就避免了出现两个Master的情况。而Chubby也是一个非常适合拿来管理非常重要的元数据的地方,这一点我们在Bigtable的论文里,其实已经看到了。
Chubby的系统本身,其实是通过Paxos实现了多个BerkeleyDB之间日志的同步复制。Chubby相当于是在BerkeleyDB之上,封装好了一个Unix文件系统形式的对外访问接口。并且,为了减轻自己的负载,Chubby还实现了一个观察者模式,外部的客户端可以监听Chubby里的某一个“目录”或者“文件”,一旦内容变更,Chubby会通知这些客户端,而不需要客户端反复过来轮询。这个也是为了提升系统的整体性能。
另外,由于网络延时的存在,即使我们的客户端获取到了锁,当写入请求到达锁对应的业务系统的时候,可能这个锁已经过期了。这个会导致我们错误地用旧数据去覆盖新数据,或者说,在没有获取对应资源的锁的情况下,写入了数据。不过,Chubby通过锁延迟和锁序列器这两种方式解决了这个问题。
可以看到,和所有之前Google发布的系统一样,Chubby并没有发明什么新的理论,而是巧妙地通过系统工程,来保障系统的可用性。并且在这个过程中,Google选择了尽可能使用各种一般工程师都熟悉的编程模型。Google没有开发一个Paxos库,让所有工程师去学习分布式共识算法,而是提供了一个分布式锁服务Chubby。而在Chubby里,提供的也是我们熟悉的类似Unix的文件系统、观察者模式这样,普通工程师耳熟能详的编程模型。
这一点,在大型组织中设计基础设施的时候非常关键。我们设计系统的时候,不能光考虑对应系统的功能,如何让整个系统对于其他团队的开发者和使用者易用,也非常关键。我们不仅在Chubby这个系统里看到了这一点,从GFS、MapReduce、Bigtable这些系统里面也能看到这个设计思路,其实这是一个一以贯之的设计思想。
到这里,我们对于大数据论文的基础知识部分就已经学习完了。接下来,我们就要迈入对于大数据论文里面关于数据库部分的学习啦。
市面上,对于Paxos算法的教程和分析着实不少,但是对于Chubby这篇论文的讲解反倒是不太容易找到。如果你希望阅读中文资料,我推荐你去读一读《从Paxos到Zookeeper-分布式一致性原理与实践》这本书的第三章,里面有对Chubby系统比较详细的讲解。如果你习惯于看视频的话,那么这个Youtube上,Data Council社区放出来的关于Chubby的讲解视频,值得去好好看一看。
最后,给你留一道思考题。
我们之前提到过分布式系统的CAP三者难以同时满足的问题,并且我们也看到,两阶段提交是一个CP系统,保障一致性但是会牺牲可用性。而三阶段提交则是一个AP系统,提升可用性但是会牺牲一致性。
那么,在使用Chubby帮助我们解决了Master的容错切换问题之后,我们的系统在CAP上是否都满足了呢?有人说,CAP之间难以满足是一个伪问题,在学完了过去这几讲之后,你对这个问题的看法是什么呢?
欢迎在留言区分享下你的观点,和其他同学共同讨论学习。