保障一个在线系统的可靠性和可用性的常用基本手段是多副本和主-从两种方案。这两种方案都的核心目的是消除单点。单点就是在一个系统中,某一个服务,或者功能模块,只有一个实例在运行。造成的问题就是,一旦这个实例下线,那么整个系统将会宕机;一旦这个实例丢失数据,那么整个系统将丢失数据。
消除单点的手段不外乎增加实例数,也就是我们常说的“冗余”。但是冗余并没有那么简单。有些服务或模块没有持久化的状态(通俗地讲,不保存数据),增加冗余很容易,按要求多重部署即可。但如果服务或模块需要保存数据,那么问题就复杂了。人们在实践中发展出两种常用的冗余方案:主-从和多副本。
多副本方案很直观:不想要单点,那就增加数据的份数,坏了一个,还有其他的。但是,考虑到服务下线或者损坏,多个副本之间数据可能不一样。我们无法确定从一个副本上读到的数据是最新的。因为任何一次对多个副本的数据写入,都无法保证都成功。这就是我们常说的"一致性"问题。要得到正确的数据,必须整合所有的副本。通常的处理手段是规定成功写入的副本数必须超过某个数量,成功读取的份数也必须超过某个数量。只要两个数值设定合理,就能确保读取到最新的数据。
主-从方案更容易理解:用额外的从服务器不停地备份主服务器的数据,主服务器宕了或者数据丢了,从服务器可以接替主服务器,继续工作。主从之间的数据备份有两种方式:异步备份和同步备份。异步模式下,数据写入主服务器后,随即向客户端反馈成功。随后在恰当的时候,数据被同步到从服务器中。同步备份是说数据写入主服务器后,主服务器将数据转发至从服务器,当从服务器也写入成功,再向客户端反馈成功。
显而易见,异步主从备份无法保证可靠性。因为主服务器丢失数据的时候,总会有一些数据没有来得及备份到从服务器,这些数据就永远丢失了。
因此,同步数据备份比一部数据备份更好地保证可靠性。每次正确的写入都会有两份数据存在。主服务器宕机,从服务器那边还有另一份。(当然,实际情况远比这复杂得多,这里就不展开了)。同步数据备份的一大问题是性能。数据写入请求要通过主服务器转发到从服务器,并获得反馈,才能响应用户。大幅增加了请求的延迟。
现在,我们先抛开性能问题,看看其他方面。可靠性之后,我们还得考虑可用性。同步写的模式要求主和从服务器都写入成功,才能告诉客户成功。因此,当主从两台服务器有任何一台下线,系统就不可用。如果放松要求,从服务器不一定要写成功,那么有些数据只有主服务器上的一份数据,依然会丢失数据。
为了保证可靠性的同时,提高可用性,一个合理的选择是增加从服务器的数量,比如2台、3台,甚至5台。不一定要每台从服务器都写成功,只要保证有1台,保险点2台从服务器写入成功,便反馈客户数据以妥善保存。如此。可以保障足够的可靠性,而可用性也不会受到单台服务器下线的影响。
(Bad feelings I have about this, huh)
接下来的问题是一致性。主从模式最大的好处就是不用操心一致性问题,因为主服务器的数据都是成功写入的。同用户的视角一致。
真的是这样么?一般情况下,或者说没有异常情况的时候是这样。当主服务器出现问题,比如硬件失效,或者系统崩溃,那么就需要将一台从服务器升格为主服务器,以确保系统可用。
但是,前面说过了,从服务器并不一定要每次都写成功。于是,从服务器升格而来的主服务器可能数据不完整。相比原来的主服务器少了数据。在用户眼里,此时的系统丢了数据。尽管实际上数据还在,只是不在当前的主服务器上。
现在我们来解决这个问题。问题的根源是从服务器的数据和主服务器不一致,最直接的解决方法是设法让它们一致。主服务器在向从服务器作同步写的时候,可以多试几次,提高成功率。但是,终究还是有怎么重试也无法成功的时候。如果就此作罢,那么数据不一致的情况依然存在。如果一直这么重试下去,就无法响应用户。唯一的做法是反馈用户,同时接着重试。为减少系统的复杂性,通常会将这后续的重试操作放在一个单独的服务模块里异步地执行。但是,重试这个步骤无论在主逻辑,还是在异步的独立模块中执行,都无法保证完全成功。系统各类异常会打断重试的努力。甚至会丢失重试任务。重试这个failover过程本身会fail。那么如果要保证failover能够正确及时的执行,那么就必须处理好failover的failover。按这个逻辑下去,还有failover的failover的failover...。没底了。所以,实际上是无法保证主从服务器的数据完全一致,而不影响可用性。这条路是走不通的。
那么,我们退而求其次,容忍主从服务器的数据不一致,寻找消除数据不一致的问题。前面提到,我们不一定要每台从服务器的每次写入都成功,只需要保证有一定数量的从服务器成功即可。所以,多台从服务器的数据整合起来,就可以确保同主服务器一致。这样的话,一旦主服务器宕机,从服务器升格为主服务器,新的主服务器每收到一个数据读取的请求,就去从服务器那里把相应的数据都取来,同自己的数据一起比对,找出正确的那份数据,返还给用户。
等等,这不就变成了多副本的方案么?的确如此。根本而言,主-从方案同时满足可靠性、可用性和一致性的前提是没有服务器宕机。这本身就违背了“任何东西都会失效”这个基本事实。(更血腥的事实是“任何不可能发生的事情也会发生”,而且总是在你认为不会发生的时候发生)。而且,主-从方案的一致性基准是建立在主服务器上的,在发生了主从服务器切换之后,整个集群就失去了一致性基准,从而被迫在所有主-从服务器上寻求一致性。而多副本方案一开始就明确服务器是不可靠的,单台服务器的数据是不可能完整和准确的。整个系统的可靠性、可用性和一致性建立在所有服务器数据整合之上。
在接受了“任何东西都会失效”这个前提之后,我们看到主-从方案也不得不综合所有服务器的数据,以便获得一致性。这也等同于多副本方案,但却要付出更多的代价,比如主-从切换逻辑,多一次主从转发的数据访问延迟。
但是,多副本方案并非完美。通常它只能用于非常简单的数据存储结构和数据操作逻辑,比如map和插入/删除等简单操作。涉及多个数据的操作,非幂等的操作,或者带有事务的操作,使得各副本间的一致性比对非常困难。
对于可靠性可用性要求不高的场景,我们往往只能退而求其次,使用主-从模式,以牺牲可靠性、可用性为代价,确保业务逻辑得以实现。在使用主-从方案时,我们通常会设法提高设备的可靠性,比如使用RAID,减少出错的概率。
有时,我们对于可靠性、可用性和一致性的要求非常高,主-从方式无法满足。那么,只能改变业务逻辑,将其简化成能够适用于多副本架构的形式。大型对象存储对可靠性、可用性和一致性有极高的要求,(它是在线服务的服务,依赖可靠性和可用性赚钱,此处绝对马虎不得)。因而不得不放弃任何复杂的数据操作,将其简化到适应多副本存储的形态。所以说大型对象存储系统采用<key,value>的形式,并非是自愿,或者睿智的,完完全全是被迫的。云存储系统不可避免地采用多副本的方案。
多副本和主-从方案之间的比较问题是非常典型云计算问题(或者说“坑”)。很多方案在单独考虑某一种需求,比如可靠性,或者可用性,都可以应付。但一旦将这些需求综合在一起考虑,这些方案将会遇到各种障碍。另一方面,一个在线系统的方案,不仅仅考虑在一般情况下的功能实现,必须时刻考虑在异常情况出现时,可能遇到的各种问题。后者占据了云计算系统架构和设计的7、8成工作量。换句话说,云计算系统成功的难易程度,取决于你躲过了多少"坑"。