明智的开发,这里用到的策略可以归结为:程序员首先在本地主要采用线程来模拟不同的进程来开发这个应用。每个线程运行分布式状态机的一个部分,基本 上就是负责运行一段消息处理的循环。一旦这个应用是本地完整的且运行正确,就可以在远端的计算机上用真正的进程来取代线程。到这个阶段,除去网络中可能出 现的问题外,这个分布式应用已经可以正常工作了。到容错阶段时,可以通过配置每个分布式实体来正确反映故障的方式来达成,这种方式很直接。(我引述自“A Fault Tolerant Abstraction for Transparent Distributed Programming”) 因为分布式状态机的存在,局部性故障可以通过设计来解决。对于线程,其实也有很多种选择,但协程(coroutines)更适合(在各种不同的编程语言中,协程也被称为纤程fiber,轻量级线程,微线程或者就叫线程),因为协程允许我们对并发行为有更细粒度的控制。 结合“C代码并不会使网络变得更快”的论点,你可以转移到在语言级支持这种细粒度并发控制的编程语言中去。流行的选择如下(排名不分先后)注意,这些编程语言往往都是函数式的: 1. Mozart 2. Erlang 3. OCaml 4. Haskell 5. Stackless 6. Clojure 举个例子,下面让我们看看在Erlang中这种并发控制的代码看起来是怎样的(取自Erlang concurrent programming)
这看起来绝对是对旧有的RPC机制的一个重大提升。现在你可以推想一下,如果有消息没有到达时会发生什么事情了。Erlang还有附加的超时消息以及一个语言内建的“超时”组件,可以使你以一种优雅的方式来处理超时。 现在,你选择了你要采用的策略,选择了恰当的分布式算法以及合适的编程语言,然后就可以开干了。你很自信能驾驭分布式编程这头野兽了,因为你再也不是第一级的水平了。 哎呀,可惜的是这一路上并非风平浪静。过了一段时间,当第一个版本发布后,你将陷入泥潭之中。人们会告诉你,你的分布式应用有些问题。问题报告中的 主题全都是和变化有关的。开始时会出现“有时”或者“一次”这样的表示频率的词,之后的描述变成了:系统处于不期望的状态,卡住不动了。如果够幸运,你有 足够的log信息,可以开始着手检查这些日志。稍后,你发现是一系列不幸的事件序列造成了报告中所描述的情况。确实,这是个新的问题。你从来没有考虑过这 些,而且在你做大量的测试和模拟时问题从未出现过。所以,你修改代码以将这种情况也纳入考虑范围。 因为你试着要超前考虑,你决定构建一个“猴子”组件,它以伪随机的方式让你的分布式系统做些愚蠢的事情。“猴子”在笼子里使劲扑腾着,很快你会发现在很多场景下都会导致出现不期望的情况,比如系统卡住了,或者甚至更糟糕的情况:系统出现不一致的状态,而这在分布式系统中是永远也不应该发生的事情。 构建一个“猴子”是很棒的主意,而且它确实能减少遇到那些你从未在这个领域内碰到过的怪事的几率。因为你相信,修改一个bug必须和发现这个bug 的测试用例联系起来,现在需要回归测试这个用例,以证明bug的消除。你现在只需要再构建一次这个测试用例就可以了。可是现在的问题在于,如果说并非不可 能的话,要重现这个错误的场景起码是很困难的。你向上帝祈祷,得到的启示是:当心存疑虑时,就使用暴力法吧。因此,你构建一个测试用例,然后让它跑上无数 次,以此来弥补这极小的失败概率。这会使你解决bug的过程变得缓慢,而且你的测试套件会变得笨重。通过对你的测试集做分而治之的处理,你不得不再次做一 些补偿。无论如何,经过在时间和精力上的大量投入之后,你终于设法得到了一个较为稳定的系统。 你在第2级已经到顶了,如果没有新的启示,你将永远卡在这一级。 第3级:分布式算法 + 异步消息传递 + 纯函数式 我们需要花点时间才能意识到:长时间运行“猴子”以此发现系统中的缺陷然后再结合暴力法来重现它们,这种做法并不可取。使用暴力法重现只会显示出你 的无知。你需要的关键性的启示之一是,如果你可以只将等式中的不确定性拿掉的话,你就可以完美的对每一种场景做重现了。第2级分布式编程的一个重大的缺点 是:你的并发模型往往会成为你代码库中的病毒。你希望有细粒度的并发控制,好吧,你得到了,代码里到处都是。因此是并发导致了不确定性,而不确定性造成了 麻烦。因此必须得把并发给踢出去。可是你又不能抛弃并发,你需要它。那么,你一定要禁止把并发和你的分布式状态机结合在一起。换句话说,你的分布式状态机 必须成为纯函数式的。没有IO操作,没有并发,什么都没有。你的状态机特征看起来应该是这样的:
你传入一个消息和一个状态,你得到一个操作和一个结果状态。操作基本上就是任何试着改变外部世界的东西,需要一定的时间来完成,尝试的过程中可能会失败。典型的操作有:
这里要意识到的重要部分是:你只能通过一个新的消息来得到新的状态,再无其他。在这种严格的规定下所得到的好处是很多的。完美的控制,完美的重现能力以及完美的可追踪性。为此而得到的开销也同样存在,你将被迫使所有的操作都变得具体化。而这些基本上就是为了减少程序复杂性而附加的一层间接。你还需要将每一个你关心的外部世界变化都建模为一个消息。 相比第2级的分布式编程,另一个改变在于控制流。在第2级中,客户端会尝试强制更新并动态设置状态。而在这里,分布式状态机假定有完全的控制力,并且只有当它准备就绪,可以做些有用的事情时才会考虑客户端的请求。因此这些必须分离开来。 如果你把这些道理解释给一个2级的分布式系统架构师听,他可能或多或少的会把这个当成一种替代方案。然而,你需要经历足够多的痛苦之后才会意识到这是唯一可行的选择,我们姑且把这些痛苦称为经验吧。 第4级 对分布式系统领域的深刻理解:快乐,好心态,好好睡一觉 老实说,我现在只是第3级水平,我也不知道在这一级里有什么新鲜玩意。我深信,函数式编程和异步消息传递是分布式系统谜题的一部分,但这些还不够。 请允许我重申我所反对的东西。首先,我希望我的分布式算法实现能够涵盖到所有的可能情况。这对我而言是个大问题,我已经在系统部署的问题上牺牲掉了很多睡眠时间。大部分问题都是PEBKAC类的(Problem Exists Between Keyboard And Chair意指用户引起的错误),但有一些确是真正的问题,这给我造成了一些挫败感。知道自己实现的健壮性程度是很好的。我应该试试证明一下那些定理吗?我应该做更详尽的测试吗?我不知道。 附带提一下,GitHub上有一个称为baardskeerder的仅用于插入操作的B-树库,我们知道可以通过详尽的生成插入/删除排列并断言它们的正确性之后,我们就可以涵盖到所有的情况。但这里,并没有那么简单,而且我对于要对整个代码库做Coqify处理(Coq是一个正式的证明管理系统,它在一种半交互式的环境下提供了一个正式的语言用来编写数学定义、可执行的算法和定理,用计算机来做检查证明,这里作者生造出了Coqify这个词)还有些犹豫。 第二,为了保持清晰和简单,我决定不去碰其它一些正交性的需求。比如,服务发现、认证、授权、私密性以及性能。 说到性能,我们也许是幸运的,至少异步消息传递似乎与性能方面并不产生矛盾。安全性则完全是一个XX(作者真的爆粗口了…),因为它几乎切断了所有 你所做的事情。有些人把安全性看成是一种调味酱汁,你只要把它倒在你的应用程序上就可以保证安全了。哎,在这方面我从未取得过成功,而且现在我也认为这个 问题需要在设计的最初阶段从宏观的角度策略性的去分析解决。
结语 开发出健壮的分布式系统是个颇为棘手的问题,实际上根本没有完美的解决方案,或者说至少没有让我觉得完全满意的解决方案。我敢肯定分布式系统的重要性将随着处理器和其它一切事物之间的延迟增加而显著提高。这一结果使得这种类型的应用程序开发变得愈发繁荣。 至于分布式编程的第4级,也许我该去问问Peter Van Roy。这么些年来,我阅读了很多他写的论文,这些论文对于我自己的一些错误认识给了很多启示。关于这些启示的缺点嘛,你常常在大部分时间里看到别人在重复自己的错误,但我无法说服他们应该换种方式去做。 也许,这是因为我无法提供他们想要的那种灵丹妙药。他们就想要RPC,而且他们希望这样能搞定问题。这是固执的…就像宗教信仰一样。 |