Erlang 分布式系统(1)

一. 分布式计算的八大谬论

1. 网络总是可靠的

在分布式系统中,你可能最容易犯下的错误就是认为远程节点是永远可用的,尽管这可以通过添加更多的硬件(比如主从)来实现,但这也带来了冗余性。网络可能随时因为断电,硬件故障,自然因素,人为因素等不可用,在这种情况下,保证你的程序能够在远程节点或者第三方服务不可用时,能够正常运行是非常重要的。Erlang除了能够监测到外部服务失去连接(或者不能响应)之外,没有任何的其它措施,毕竟,除了你自己以外,谁也不知道某个组件有多重要。

注意,Erlang跨节点的monitor和link操作是比较危险的,因为一旦远程节点不可用,将触发关于该节点所有的远程monitor和link,这可能会引起一场网络风暴,给系统带来意料之外的高负载。在不可靠的网络上构建可靠的分布式应用,需要你随时准备面临这种突发状况,并且保证系统能够继续正常稳定地工作。

2. 可忽略的延迟

一个好的分布式系统的特性之一就是隐藏函数调用为远程调用的事实,然而这是一把双刃剑,一些本地上执行非常快的函数,通过网络调用时,会与预期大不相同。在这一点上,Erlang的通信模型处理得很好,Erlang的每个进程是孤立的,通过异步消息进行通信,这使得我们考虑超时,以及外部服务不可用的情况。将Erlang程序改造为分布式程序只需要做很少的工作,异步通信模型,超时,监视,链接等都可以保留。因此,Erlang在设计之初,并不忽略延迟的存在。但是作为设计者,在设计上,你需要对此留意。

3. 带宽是无限的

尽管现在网络越来越快,每个字节在网络上传输的成本也越来越便宜,但是对网络负载过于乐观的假设仍然是具有风险的。关于这一点的一个小技巧是发送当前的事件,而不是当前的整个状态。如果在一些情况下,你不得不发送大消息,一定要小心,由于Erlang保证两个进程间的消息次序(即使是通过网络),因此一个大消息可能会阻塞该通道上的其它消息。更糟糕的是,这可能会阻塞节点间的正常心跳,导致节点误以为对方节点无法响应并且断开连接。要避免这种情况发生的唯一方案就是控制消息的大小,这也是一条好的Erlang设计实践。

4. 网络是安全的

当你在分布式环境下,信任你所接收到的任何消息是非常危险的,消息可能被刻意伪造或抓包改写,甚至外部节点可能已经完全被其他人控制。遗憾的是,Erlang做了这种假设,Erlang分布式系统的网络安全是非常薄弱的,这和它的历史有关。这意味着Erlang程序很少被部署在不同的数据中心,Erlang也不建议你这么做。你可以将你的系统分为很多小的,相互隔离的节点,并且部署在可靠的地方(一台物理机,或者安全的局域网)。但任何除此之外的东西,都需要开发者自己去实现,例如切换到SSL,或者实现你自己的更高层次的通信协议,或重写节点之间的通信协议等。关于这些主题可以参见How to implement an alternative carrier for the Erlang distributionDistribution Protocol。即使做了更多的安全工作,也要非常小心,因为一旦有人获取了你的远程节点的访问权限,他就可以获取到节点上的一切,并且在该节点上执行任何命令。

5. 网络拓扑是不变的

在分布式系统构建之初,你的系统原型可能有指定数量的节点并且确定了它们的网络位置(IP+Port),但硬件错误,人为部署,负载均衡等,都会导致节点的动态添加和删除,这时集群的网络拓扑结构就会变化,如果你在程序中对集群节点位置有任何依赖,都将很难适应这种变化。在Erlang中,每个节点都有其名字(相当于IP+Port),如果在程序中对这些节点地址硬编码,将会使程序很难动态扩展。

6. 只有一个管理员

这是从系统运维上来说的,你可能只管理着整个系统的一部分,一方面你需要诊断系统问题的工具,这一点上来说,Erlang提供了比较完善的调试诊断系统,并且支持热更。另一方面,你需要做好系统各个部分间的通讯协议,以管理各个子系统的不同版本的兼容问题。

7. 数据传输代价为零

这里的代价包括时间代价和金钱代价,前者指数据的序列化和反序列化时间,后者指数据对带宽的占用,从这个角度来看,将消息优化得小而短又一次被证明是很重要的。基于Erlang的历史,Erlang并没有对网络传输数据做任何压缩处理(尽管提供了这样的函数),Erlang的设计者选择让开发者自己按需实现其通讯协议,减小数据传输的代价。

8. 集群是同质的

这里的同质(Homogeneous)指的是同一种语言(或协议),在分布式集群中,你不能对所有节点都是同质的作出假设,集群中的节点可能由不同的语言实现,或者遵从其它通讯协议。你应该对集群持开放原则而不是封闭原则。对Erlang来说,分布式协议是公开的,但所有的Erlang节点都假设其它节点都是同质的,外部节点想要将自己整合在Erlang集群中,有两种方式:

第一种方式是使自己看起来像Erlang节点,实现Erlang节点的通讯协议,比如Erlang C-nodes,这样其它语言实现的节点也能像Erlang节点一样运行于Erlang集群中。

另一种方式是使用其它的数据交换协议,如BERT-RPC,一种类似于XML或JSON的数据交换格式,但与Erlang External Term Format更为契合。

关于以上8点更多请参考:Fallacies of Distributed Computing Explained

二. 节点检活

对于分布式系统来说,其中最让人头疼的事情之一就是当节点不可响应(或网络错误)时的处理流程。一个节点不可响应的因素有很多:网络故障,网络拥塞,硬件错误,应用崩溃,节点忙碌等,几乎没有方式能够对问题节点当时的状态进行确认和诊断,这个时候,其它节点有几种处理方式:继续等待响应,再次发起请求,或者假设问题节点已经挂掉并且继续后续事宜。如果真是问题节点挂掉了,那你可以忽略这个节点,整个集群继续运转。而更差的情况下,问题节点此时仍然运行于孤立的环境中,从该节点的视角来看,其它节点都挂掉了,整个集群只剩自己一个节点。

Erlang集群默认即视不可达的节点为死节点,这是一种悲观的方案,这可以让我们对灾难性故障迅速作出反应。Erlang假设网络故障的概率比硬件或应用故障的概率低,Erlang最初就是这么设计的(为电信通讯平台服务)。另一种乐观的方案(假设失连节点仍然存活)可能或延迟故障恢复相关的处理,因为它假设网络故障的可能性要比硬件或应用故障的可能性更大,因此它会重试等待更长的时间,以便失连节点重返集群。

那么问题来了,在悲观的解决方案中,如果失连节点突然回归集群(比如网络恢复)又会怎么样呢?此时失连节点可能已经运行在孤立的环境数日,有自己的数据,连接,状态等。这个时候想要协调数据和状态的一致性是很困难的,并且你的集群可能已经启动了失连节点的备用节点,而直接忽略节点也不可取,也许该节点已经处理了很多外部请求,甚至向DB写入了数据。总之,会有一些非常奇怪的事情发生。

那么,是否有一个方案可以在节点失连的时候保持应用正常运行,并且不会出现数据丢失或不一致呢?

三. CAP理论

在CAP理论中,关于上一个问题的答案是”没有”,你没有办法让一个分布式系统在网络断开的时候保持正常运行并且正确地运行。CAP理论在Wiki中解释:

  • 一致性(Consistence) (等同于所有节点访问同一份最新的数据副本)
  • 可用性(Availability)(对数据更新具备高可用性)
  • 容忍网络分区(Partition tolerance)(以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择)

根据定理,分布式系统只能满足三项中的两项而不可能满足全部三项。理解CAP理论的最简单方式是想象两个节点分处分区两侧。允许至少一个节点更新状态会导致数据不一致,即丧失了C性质。如果为了保证数据一致性,将分区一侧的节点设置为不可用,那么又丧失了A性质。除非两个节点可以互相通信,才能既保证C又保证A,这又会导致丧失P性质。

关于CAP理论的另一篇很有意思的文章:CAP理论十二年回顾:”规则”变了

从CAP理论来看,我们只有三种选择: CA, CP, AP,通常情况下,CA是我们无需考虑的,除非你可以保证你的网络不会出现故障,抑或集群是部署在同一台主机上。剩下的CP, AP需要根据你自己的系统进行取舍。

四. 待续

尽管在Erlang中构建分布式系统是非常Easy的一件事情,但是分布式系统本身的复杂度,需要你根据系统需求,从多个维度去设计,考量和评估整个分布式系统,同时理解分布式系统的常见误区,注重底层细节。本文内容参考:http://learnyousomeerlang.com/distribunomicon

之后可能详细整理一下Erlang本身在分布式方面提供的具体支持,并且评估一下当前项目用到的服务器集群系统。