一个有趣的观点是Erlang并非真正的函数式编程。从另外的角度来看,Erlang编程的内部进程正是通过函数式编程实现的,但是外部用户却看不到,这看起来非常有意思。
对于这个问题,程序是当务之急。事实证明,进程和进程之间的通信消息的独立性是Erlang的核心。这样的进程是一个角色的事实,并不那么吸引人。你可能做了一个基于通道的信息传递系统,继承了更多的Pi-calculus(在这之前是CSP)的特性.我不认为这对于外部来说有多大改变。在内部,为了使Erlang的一个进程的编程变得简单,有多种原因支持确定一个特定的编程风格。但是我们并不能看到,除非我们知道这个进程的具体实现。
很明显,Erlang也不是使用RPC协议的,在2012年的Erlang技术大会上我从Steve Vinoski那得知要及时回顾一些最初的RFCs(收录互联网通信协议的文件集)-707文件和708文件。这2个都是由J.E拟定的文件需要我们了解的比期望中的要深层次一些。它们还都是在1975和1976年间发布的.看来有时候我们忘记了过去和历史了。
因为这些,我同Joe Armstrong讨论了下关于协议的问题,我和他都认为那是系统里面很重要的一部分,不仅仅是在用Erlang写的系统中,而且我还要强调下这点。当你在Erlang程序中有2个或者更多的进程进行通信时,你已经在这些进程之间定义了一个协议。就像是在RFC 707文件协议中,2边都可以扮演客户端或者是服务端-包括发送请求,接收同它通信的那端的回应。在Erlang的消息传递和这些有趣的RFCs文件协议之间是有一些相似之处的。清晰的了解你的协议是什么含义对设计自己的系统有很重要的价值。系统中的这些进程并不和协议一样有趣.当进程开始工作的时候,这些协议就开始协调起来了。
自己设计的协议包括两部分。1、协议的语法。这包括消息的内容、格式及返回内容。2、协议的语义,这用来确定消息内容是否合法和消息内容的含义。许多协议只注重消息的语法却忽略消息的语义。为了internet的健康考虑,我们需要改变这种做法。这样我们就可以开始我们的Erlang编程了。在保证协议的语义同时,使用较少的动态部分,能够我们的协议更加的简洁。这样就更加的容易实现和扩展。简单的结构能够减少代码的错误。
协议的重点性体现在它的标准性和抽象性。首先,他为其他应用奠定一个标准的基础,例如:HTTP, TCP/IP, bt协议, DNS协议。标准的好处就是我们可以使用Erlang编写服务端应用,然后使用其他语言编写的浏览器来访问我们的应用。我们完全不用担心浏览器的实现语言和具体细节。这都归功于协议的标准性。然后,协议为我们抽象具体细节。事实是我们刻意把具体实现细节隐藏起来才使得互操作这么的简洁。也正因为internet的抽象才使它无处不在。
一个特别容易让人生畏的协议就是IP协议(版本4和版本6)。虽然其可以通过大概500行Erlang代码得以实现,但该协议却是因特网上进行所有通信的基础。那500行代码的成本/收益比是低的,可以说是相当低的。IPv4,IPv6 - 500行Erlang代码足矣。如今因特网上各种协议都使用该协议作为基础 - 可以认为我们对该协议已做了恰到好处的抽象,所以才获得如此成功。值得注意的是,IP协议还是选择遗留下一些问题不进行解决,取而代之的是对其能够解决的层面进行标准化处理。如果你采用Erlang语言实现的协议版本是精炼且简单的,那么你已然获得构建优秀的可重用系统的良好基础。
研读Erlang程序的一种好方式是忘记进程内部的细节。你所需要做仅是如何描述通信协议自身。你可以描述出当某个进程收到特定消息时将会发生什么。例如,BitTorrent客户端可能有一条要发送到IO层的消息`{read, K, Tag}`。其语义是IO子系统将从底层稳定存储系统中读取分片`K`,并以`{ok, Tag, Data}`或者`{error, Tag, Reason}`消息进行应答。tag作为唯一标识符使用以将特定的读和应答进行匹配。并且这种tag的用法也是标准OTP `gen_server`行为模式中call的语法形式。
需注意到的是我们并未讲到任何关于实施读取稳定存储的事。我们仅仅描述了对外部世界的处理行为。由于现在我们不用再改变任何协议便实施或再实施IO子系统,这是一个好抽象。另外注意到,我们没有用函数调用,也没有像它变得那样调用"RPC"。此IO子系统可我们的之前随时执行其他请求。同时它也允许在我们的请求和回复之间服务另一个进程。或者它可能选择交替几个这样的请求。或者也许,可能的问题是复制读请求然后等待它们一个个到达。IO子系统可能会发送一条消息{cached,K}表示刚刚存储在内存的一条信息,这样就不需要磁盘寻道。IO子系统也可能在没有任何回复的情况下崩溃,这种情况我们必须用超时或监视器解决。这些例子没有封装入函数调用语义。
一个协议应当有供修改的余地。看看TCP协议的定义,就会明白它定义得恰到好处。它并非定义不足,所以不同的实现能够使用它作为通讯介质。但是它也不是定义过度,这样实现时就可以不同方式解读协议。一个好的例子是:可以在没有windows的情况下如一个stop-n-go协议一样实现TCP。任何一支的TCP实现能够理解这个。它使我们能够建立一个简单版本的TCP,使它能够运行,然后改进它。或者它使我们能够为嵌入式设备(对代码大小要求很高)写一个简单的TCP/IP栈。
我们可以使用Erlang协议达到同样的效果。建立简单的协议,但允许在实现中有一定的活动性。从长远来看,它让我们的系统更具可扩展性,而且为以后的改进实现提供空间,不需要重新定义整个系统。可扩展性通常是通过协议复合来完成的。JSON取胜的原因是简单且定义在其上的协议是自动扩展的。在Erlang中也是如此,因为Erlang术语是定义可扩展的。
一个糟糕扩展设计的好例子是最初的BitTorrent协议。在这一握手协议里有一个64bit的值,这个值是一个有64种可能扩展的位串。现在的问题是,一个扩展需要中间协调因为每个人都必须同意bit X是指Y。通过使用一个bit表示一种新的消息是有效的,这个问题得以解决。这个新的消息包含一个类似JSON的结构体,他反过来描述什么扩展可以被解析。现在每个人都可以用他们喜欢的特定方式添加扩展。
比较糟糕的设计还有一个很好的例子就是Minecraft游戏中Minecraft服务器所使用的协议。该协议已经经过了多轮的迭代设计,但最新版中有一个专门的数据包(0x11)表示“Use Bed(使用床)”,另一个数据包(0x47)用于游戏里的雷电打击行动。协议中所有的消息都是扁平式的,其实本来应该使用树形结构。而且数据包竟然没有“长度”字段,因此,为了判定数据包的长度,不得不对数据包里的内容先进行部分解码。刚说的这一点还有一个值得注意的地方,就是如果你无法理解一个数据包的内容,就可以简单地直接退出通信 —— 因为在数据包的流中你无法跳过一个数据包然后接着再处理下一个数据包。
上一段讲了Joh Postel原理/定理的一个部分:在发送数据时要保守,在接收数据时应激进。这也就是说,要永远遵守协议 —— 特别是在你发送数据时,但在你接收到你无法理解的数据时 —— 那么请忽略它吧。这里有个假设是,和你进行通信的另一方所用的协议的版本可能比你所用的协议新,你应该按此假设进行设计。但有个出格的例子,就是,浏览器在解析HTML时显得太宽容了。如果HTML里有个显然的错误,浏览器为此HTML产生的不是一个空页面,而是会尝试修复这类错误。这里的关键在于,只有在数据包里的消息对你来说仍旧有意义的情况下,才应该做出冒进的行为。错误的HTML可没有意义。就是因为这一点以及HTML这种媒介是一种不具扩展性的协议这个事实,才造成今天的许多问题。
设计协议的另一个重点就是可靠性。当我们发送一个消息后,我们是否需要确保这条消息能够到达?我设计协议时经常会考虑到传输失败的问题。消息传输是不可靠的。在本机上我们认为接收消息的Erlang处理程序不会出现问题。但是如果这个处理程序确实挂了,这样就没有哪个处理程序来接收我们的消息了。但是问题是:如果处理程序接收到了我们的消息,在处理的时候崩溃了,我们就不知道我们的消息是否被处理过了。通常来说,我可以从IP协议的规范上得到启发:IP协议应用于网络传输,就让我也沿用这种规范来设计协议。
解决这个问题的方法就是遵照“最有效传输”的原则。如果`{read, K, Tag}`命令在十秒钟内没有返回结果,我们就可以假设网络崩溃了并重新发送命令。设计协议的技巧就是允许失败。
分布式系统中还有其他很多问题需要解决——例如网络会瘫痪,这导致我们无法确信另一端的机器工作正常,所以我们必须为这种问题设计协议。我们别无他法,因为几乎所有现代的系统都是分布式的。我们无法确保错误不会发生,因此必须确保出错的几率很低。我们的设计必须确保类似1%这样的低出错率。这就是说,在错误处理的代价非常昂贵的条件下,只有让错误极少发生才是可以接受的。从实践中的经验来看,分布式与不可靠总是如影随形的。就我而言,任何假设网络或子系统不会出错的并发式设计都是非常危险的。某个模块或许可以实效,但是这不能影响整个系统的可用性。我们或许面对这样的窘境:随着系统复杂性提高,其运行的风险也不断增大。
在分布式的王国中,不可靠性才是在位的国王。任何一个试图摆脱他的人都会发现根本无法与他分离。随时有某个细节实效的风险,而产生这种风险的因素又很模糊。在设计本地或分布式协议时,必须考虑这些问题。在Erlang中,能驯服这头野兽的,是可容错的代码。监督工具箱、链接进程和有效的状态隔离,这些都可以在设计协议的时候帮助我们处理不可靠性。
如同我们所知晓的,模糊性正改变着数据运算的世界。多核只是我们多面临的一个问题。分布和出错则是另一个亟需解决的问题。例如 CAP 定理就是一个直接的推论:一致性不再独立存在。
你必须在一个充满了分布式运算和协议的现代世界中不断容错。没有第二条路。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。 2KB翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务