第七章 使用zmq高级框架
·如何安全地从创意过渡到能工作的原型(MOPED模式)
·将的数据作为zmq消息序列化的不同方式
·如何用代码生成二进制序列化的编解码器
·如何使用GSL工具来建立自定义的代码生成器
·如何撰写和许可一个协议规范
·如何在zmq上进行可快速重新启动的文件传输
·如何实现基于信用的流量控制
·如何将协议的服务器和客户端构建为状态机
·如何制作一个在zmq之上的安全协议
·一个大型的文件发布系统(FileMQ)
MOPED的目标是定义一个过程,通过它可以取得针对一个新的分布式应用程序的粗略用例。使用MOPED,通过专注于合同而不是实现,就避免了过早优化的风险。通过短的基于测试的迭代来驱动设计过程,可以在添加更多功能之前对已有什么作品更加肯定。
将其分布五个具体步骤:
1.内部化zmq语义。
2.描绘一个粗略的架构。
3.决定合同。
4.编写一个最小的端到端解决方案。
5.解决一个问题,然后重复。
1.1 第一步:内部化zmq语义
学习和消化套接字模式以及它们的工作方式,学习一门语言的唯一方法是使用它。(增加代码量)
1.2 第二步:描绘一个粗略的架构
隔离各个层,可以廉价地替换整个层。选择所要解决的核心问题,忽略任何不必要的问题:以后在添加它。为了简约化,让架构能随时间的推移渐趋完整和逼真:例如,添加多个工人、增加客户端和API,处理故障等等。
1.3 第三步:决定合同
两种类型的分布式系统合同:
·针对客户端应用程序的API,API必须尽量绝对简单、一致和为人熟知。
·连接部件的协议(一个简单的技巧)称为“unprotocols”(反协议)。
1.4 第四步:编写一个最小的端到端解决方案
若希望把代码编写出来时能够对其进行测试,官方意思是编写一个最小的骨架系统应用程序对其进行测试。目标是让最简单的测试案例能够工作,没有任何多余的功能。在要做的事情列表中,砍掉一切可以砍掉的任务。可以随时添加功能这是比较容易的,但目标是将整体规模保持在最小值。
1.5 第五步:解决一个问题然后重复
可以开始解决有形的问题,而不是增加功能。编写清楚说明问题的议题,并为每个议题提出解决方案。当设计API时要记住命名标准、一致性和行为。用简单的文字写下这些内容有助于保持它们的清晰。从这里对架构和代码的每一处更改都可以通过运行测试案例来验证,如果它不能正常工作则进行修改,如此往复,直到它可以正常工作为止。
这样就可以遍历整个周期(根据需要扩展测试案例、修正API、更新协议、扩展代码),每次选取其中一个问题并单独测试其解决方案。
2 协议
zmq提供了成功的协议抽象层,它使用“通过随机传输进行多部分消息传递”的工作方式。因为zmq默默地处理组帧、连接和路由,所以在zmq之上编写完整的协议规范非常容易,已经在第四章和第五章展示了如何做到这一点。
2.2 合同是艰难的
编写合同,也许是大规模架构中最困难的部分。使用unprotocol能尽可能多地消除不必要的摩擦。但剩下的仍然是需要解决的很难的一系列问题。好的合同无论是一个APl、协议或租赁协议必须简单、明确、技术上可靠,同时易于执行。
编写协议总结:
·从简单开始,并逐步开发的需求。不去解决还没有遇到的问题。
·使用非常明确和一贯的语言。协议往往分解成命令和字段为这些实体使用清晰而简短的名称。
·尽量避免发明概念。从现有的规范重用一些东西就可以了。
·不做不能证明有迫切需要的任何东西。的规范能解决问题,它不提供功能要为确认的每个问题做出最简单可行的解决方案。
·在构建它的同时实现的协议,以便了解每个选择的技术后果。使用一种使得它很难实现的语言(如C),而不是一种使它很容易实现的语言(如Python)。
·在构建它的同时测试的规范。针对一个规范的最好的反馈是在别人不具备的头脑中的假设和知识时,试图实现它。
·快速、一致地交叉测试,用别人的客户端连接的服务器反之亦然。
·准备把它抛出来,并根据需要随时重新启动。要对这一点做出规划,举例来说,通过对架构进行分层以便可以保留一个API,但改变底层协议。
·只使用独立于编程语言和操作系统的结构。
·分层解决大问题,使得每一层都是独立的规范。谨防创建庞大的整体协议。考虑如何重用每一层。思考不同的团队如何可以在每一层构建竞争性的规范。
最重要的是把它写下来。代码不是一种规范。通过写下一个规范,将能够发现灰色地带这些东西都是不可能在代码中看到的。
使用zmq的一种不太明显的好处是,它减少了编写一个协议规范大约90%以上的必要努力,因为它已经处理了组帧、路由、排队,等等。这意味着可以快速尝试,低成本地犯错,从而迅速学习。
·封面部分:带有写在一行中的摘要、规范的URL、正式的名称、版本、负责人。
·正文的许可证:对于公共规范是绝对需要的。
·变更过程:怎么解决规范中的问题?
·语言的使用:必须、可以、应该等,具有对RFC2119的引用。·成熟度指标:这是一个实验、草稿、稳定、遗产,还是已退休的版本?
·协议目标:它试图解决什么问题?
·正式语法:防止由于文本的不同解释导致的争论。
·技术说明:每个消息的语义、错误处理等。
·安全讨论:明确的,协议的安全程度。·参考文献:对其他文件、协议等的引用。
以下是有关协议的一些关键点:
·只要过程是开放的,就不需要一个委员会:只是做干净、简单的设计,确保任何人都可以自由地改进它们。
·如果使用现有的许可证,就不会有法律的后顾之忧。建议公开规范使用GPLv3。
·形式是有价值的。也就是说,学会写正式的语法,如ABNF(增广巴科斯-诺尔范式)并用它来完全记录的信息。
·使用类似Digistan的COSS的一种市场驱动的生命周期过程,以使人们当他们成熟(或不成熟)时,正确地把握的规范。
当实现一个GPLv3的规范时,它们可以按喜欢的任何方式来许可。但可以肯定两件事情。首先,该规范将永远不会被纳入和扩展为专有的形式。本规范的任何派生形式也必须是GPLv3的。第二,曾经实现或使用这个协议的人永远不会对它涵盖的任何东西发动专利攻击。
官方文档表示最喜欢的语法是由RFC2234定义的ABNF,因为它可能是用于定义双向通信协议的最简单和最广泛使用的正式语言。大多数IETF(互联网工程任务组)规范都使用ABNF,这是一个值得合作的好伙伴。
给出一个编写ABNF的速成教程。把语法编写成规则。每个规则的形式为“名称=元素”。一个元素也可以是其他规则(在下面定义为另一规则的东西),或预先定义的“终端”(如CRLF,八位字节),或一个数字。RFC中列出了所有的终端。要定义可选元素,用“元素/元素”的形式。要定义重复,用“*”(请参考RFC,因为它不直观)。要对元素分组,则使用括号。
这是一个称为NOM的协议的ABNF:
nom-protocol = open-peering *use-peering
open-peering = C:OHAI ( S:OHAI-OK / S:WTF )
use-peering = C:ICANHAZ
/ S:CHEEZBURGER
/ C:HUGZ S:HUGZ-OK
/ S:HUGZ C:HUGZ-OK
用传统的客户端/服务器用例来解释。客户端连接到服务器并进行身份验证。然后,它会要求得到一些资源。服务器做出回应然后开始发送数据返回给客户端。最终客户端断开连接或服务器完成,对话就结束了。
在设计这些消息之前对控制对话和数据流加以比较:
·控制对话持续时间短,涉及非常少的信息。数据流可能会持续数小时或数天,并涉及数十亿个消息。
·在控制对话中会发生所有“正常”的错误,例如,未通过身份验证,没有找到,需要付费,审查,等等。数据流过程中所发生的任何错误都是例外(磁盘已满,服务器崩溃)。
·添加更多的选项或参数等的时候,控制对话里的东西会随着时间的推移而改变。数据流几乎不应随时间而改变,因为一个资源的语义在一段时间内是相当恒定的。
·控制对话本质上是一个同步的请求-应答对话。数据流基本上是单向的异步流。
这些差异是重要的。因此当谈论性能时它仅适用于数据流。将一次性控制对话设计为快速的。当谈论序列化的成本时,这仅适用于数据流。对控制流进行编码/解码的成本可能是巨大的,而在许多情况下,它不会改变任何事情。因此利用廉价的方式对控制编码,并且利用讨厌的方式对数据流编码。
廉价方式本质上是同步、详细、描述性和灵活的。一个廉价的消息充满了可以针对每个应用程序改变的丰富的信息。设计目标是让这些信息易于编码和解析,对于实验或增长易于扩展,并且针对变化非常健壮,同时向前和向后兼容。协议的廉价部分看起来像这样:
·它对数据使用简单的自描述的结构化编码,无论是XML、JSON、HTTP式的标题,或者是一些其他的编码。任何编码方式都是好的,只要的目标语言对它有标准简单的解析器。
·它采用的是直白的请求-应答模型,其中每个请求都有一个成功/失败的应答。这使得它容易编写正确的廉价对话客户端和服务器。
·它不会尝试快速,甚至连轻微的尝试都没有。当做一些只有一次性或每个会话几次的事情时,性能是无所谓的。
廉价方式的解析器是从架子上取下来后就将数据丢给它的东西。它不应该崩溃,应该不会出现内存泄漏,应该高度宽容,并且应该是比较易用的。这就行了。
但是讨厌方式本质上是异步、简洁、沉默和不灵活的。一个讨厌方式的消息中携带几乎从不改变的最精简的信息。设计目标是让这些信息得到超快的解析,甚至可能无法扩展和试验。理想的讨厌模式看起来像这样:
·它对数据使用一个手工优化的二进制布局,其中每个二进制位都是精心设计的。
·它采用纯异步模型,其中一个或两个对等节点发送数据而无须确认。
·它不会尝试对人友善甚至连轻微的尝试都没有。当正在做每秒执行数百万次的东西时性能是一切。
讨厌方式的解析器是手工编写的,它单独和精确地写入或读取二进制位、字节、字和整数。它拒绝任何它不喜欢的东西,根本不执行内存分配,永不崩溃。
廉价或讨厌不是一个普遍的模式不是所有的协议都具有这种二分法。此外,如何使用廉价或讨厌的方式将取决于的具体情况。在某些情况下它可以是一个单独的协议的两个部分。在其他情况下它可以是一个层叠在另一个之上的两个协议。
使用廉价方式或讨厌方式做错误处理相当简单。它具有两种命令和两种引发错误信号的办法:
1)同步控制命令
错误是正常的:每个请求都有一个响应,要么是OK,要么是错误响应。
2)异步数据命令
错误是异常的:坏的命令要么被悄悄丢弃,要么导致整个连接被关闭。
区分几种错误通常是很好的,但要始终使它保持最精简,并只添加所需要的东西。。
3 序列化数据
3.1 zmq组帧
用于zmq应用程序的最简单、最广泛使用的序列化格式是zmq自己的多部分组帧。
例如,下面是管家协议定义请求的方式:
第0帧:空帧
第1帧:“MDPW01“(6个字节,相当于MDP/工人V0.1)
第2帧:0X02(一个字节,相当于REQUEST)
第3帧:客户端地址(封包栈)
第4帧:空(零字节,封包分隔符)
第5帧以上:请求正文(不透明的二进制码)
在代码中读取和写入这些是很容易的。但是这是控制流的一个经典例子(整个MDP是一个经典的例子,因为它是一个同步交互的请求-应答协议)。当改善MDP的第二个版本时必须改变这种组帧。向后兼容性是很难的,但将zmq组帧用于控制流并没有好处。下面是官方意见,应该怎么设计这个协议。它分解成一个廉价的部分和一个讨厌的部分,并且它使用zmq组帧来区分这些:
第0帧:“MDP/2.0"的协议名称和版本
第1帧:命令标头
第2帧:命令正文
期望在各种中介(客户端APl、代理以及工人APl)中解析命令标头,并将命令正文体原封不动地从一个应用程序传递到另一个应用程序。
3.2 序列化语言
各种序列化语言都有自己的风格。XML在流行的时候曾经是大型的,然后陷入了“企业信息架构师”之手,从那以后它就不再有活力了,今天的XML是“小型、优雅的语言试图逃脱的某处困境”的缩影。
尽管如此XML仍是比它的前辈更好的方法。因此序列化语言的历史似乎是一个逐渐显现理智的过程, JSON从JavaScript的世界杀出来。JSON只是表达得像JavaScript源代码的最精简的XML。
这是在Cheap协议中使用JSON的简单示例:
"protocol": {
"name": "MTL",
"version": 1
},
"virtual-host": "test-env"
XML中的数据将是相同的(XML迫使发明单个顶级实体):
<command>
<protocol name = "MTL" version = "1" />
<virtual-host>test-env</virtual-host>
</command>
这里使用的是普通的HTTP样式的标头:
Protocol: MTL/1.0
Virtual-host: test-env
这些都是等价的,只要不针对验证解析器、模式走极端。一种廉价方式的序列化语言所提供自由实验的空(忽略任何不认识的元素/属性/标头),并且容易编写通用的解析器,例如将一个命令转换到一个散列表或反向转换。
但是这不是完美的,虽然现代的脚本语言能足够轻松地支持JSON和XML,但旧的语言不能。如果使用XML或JSON,就产生了不寻常的依赖。这也是一个有点像在C语言中处理树形结构数据时的痛苦。
所以可以根据的目标语言驱动的选择,如果的环境是一种脚本语言那么去用JSON。如果的目标是建立更广泛的系统中使用的协议,那就让事情对C语言开发者保持简单,并坚持采用HTTP风格的标头。
3.3 序列化库
这就像JSON既快速又小巧。MessagePack序列化库是一种高效的二进制序列化格式。它允许将数据在类似JSON的多种语言之间交换,但它的速度更快、更小巧。
这是使用MessagePack接口定义语言(IDL)描述典型消息的方式:
message Person {
1: string surname
2: string firstname
3: optional string email
}
现在,使用Google协议的同一条消息将缓冲IDL:
message Person {
required string surname = 1;
required string firstname = 2;
optional string email = 3;
}
它可以工作但是在大多数实际情况下,以手工编写或机械生成的适当规范为后盾的序列化语言几乎不会带来什么好处。将要付出的代价是一个额外的依赖性而且很可能会比使用Cheap或Nasty时的整体性能更差。
3.4 手写的二进制序列化
当关心序列化的速度和/或结果的大小(通常这些是相互矛盾的)时,需要手写的二进制序列化。
编写一个高效讨厌的编码器/解码器(CODEC)的基本流程是:
·构建有代表性的数据集和测试应用程序,它们可以对编解码器进行压力测试。
·编写编解码器的第一个哑巴版本。
·测试、测量、改进和重复,直到用完了时间和/或金钱。。
下面是一些用来改善编解码器的技术:
·使用剖析器。原因很简单,没有办法知道的代码在做什么,除非已经剖析出它的函数执行次数和每个函数的CPU开销。
·消除内存分配。在现代的Linux内核中堆是非常快的,但它仍然是最简陋的编解码器的瓶颈。在较旧版本的内核中堆可能会非常慢,需要在代码中尽可能使用局部变量(栈)来取代堆。
·在不同的平台上用不同的编译器和编译器选项测试。除了堆之外还有许多其他的差异。
·使用状态来更好地压缩。如果担心编解码器的性能那么几乎肯定是将相同类型的数据发送了很多次。数据实例之间将有冗余。可以检测这些冗余并用它来压缩。
·了解的数据。最好的压缩技术(在紧凑性的CPU的成本方面)需要知道数据的相关信息。例如,用于压缩一个单词列表、视频和股票市场数据流的技术都不同。
·准备好打破规则。真的需要用大端网络字节顺序对整数编码吗?×86和ARM占了几乎所有现代的CPU的数量,但它们使用小端字节顺序(ARM实际上是双端的,但Android与Windows和iOS一样,是小端的)。
3.5 代码生成
所有这些都将从代码生成中受益,但是没有通用模型。因此诀窍是根据需要设计自己的模型,然后使代码生成器成为该模型的廉价编译器。
编写GSL模型时可以使用任何喜欢的语义,换句话说可以当场发明特定领域的语言。这里将发明一对夫妇-看看是否能猜出它们代表什么:
slideshow
name = Cookery level 3
page
title = French Cuisine
item = Overview
item = The historical cuisine
item = The nouvelle cuisine
item = Why the French live longer
page
title = Overview
item = Soups and salads
item = Le plat principal
item = Béchamel and other sauces
item = Pastries, cakes, and quiches
item = Soufflé: cheese to strawberry
还有这个:
table
name = person
column
name = firstname
type = string
column
name = lastname
type = string
column
name = rating
type = integer
可以将第一段汇编成演示文稿。第二段可以编译成SQL以创建和使用数据库表。因此对于本练习模型由“类”组成,这些“类”包含“消息”,这些“消息”包含各种类型的“字段”。这是故意设计成熟悉样子的。这是MDP客户端协议:
<class name = "mdp_client">
MDP/Client
<header>
<field name = "empty" type = "string" value = ""
>Empty frame</field>
<field name = "protocol" type = "string" value = "MDPC01"
>Protocol identifier</field>
</header>
<message name = "request">
Client request to broker
<field name = "service" type = "string">Service name</field>
<field name = "body" type = "frame">Request body</field>
</message>
<message name = "reply">
Response back to client
<field name = "service" type = "string">Service name</field>
<field name = "body" type = "frame">Response body</field>
</message>
</class>
这是MDP工作程序协议:
<class name = "mdp_worker">
MDP/Worker
<header>
<field name = "empty" type = "string" value = ""
>Empty frame</field>
<field name = "protocol" type = "string" value = "MDPW01"
>Protocol identifier</field>
<field name = "id" type = "octet">Message identifier</field>
</header>
<message name = "ready" id = "1">
Worker tells broker it is ready
<field name = "service" type = "string">Service name</field>
</message>
<message name = "request" id = "2">
Client request to broker
<field name = "client" type = "frame">Client address</field>
<field name = "body" type = "frame">Request body</field>
</message>
<message name = "reply" id = "3">
Worker returns reply to broker
<field name = "client" type = "frame">Client address</field>
<field name = "body" type = "frame">Request body</field>
</message>
<message name = "hearbeat" id = "4">
Either peer tells the other it's still alive
</message>
<message name = "disconnect" id = "5">
Either peer tells other the party is over
</message>
</class>
GSL使用XML作为其建模语言。XML的声誉很差,它被太多的企业下水道所吸引,难以闻到香甜,但是只要保持简单,它就会带来一些积极的影响。编写项目和属性的自描述层次结构的任何方法都是可行的。
现在这里是用GSL编写的简短IDL生成器,它将协议模型转变为文档:
.# Trivial IDL generator (specs.gsl)
.#
.output "$(class.name).md"
## The $(string.trim (class.?''):left) Protocol
.for message
. frames = count (class->header.field) + count (field)
A $(message.NAME) command consists of a multipart message of $(frames)
frames:
. for class->header.field
. if name = "id"
* Frame $(item ()): 0x$(message.id:%02x) (1 byte, $(message.NAME))
. else
* Frame $(item ()): "$(value:)" ($(string.length ("$(value)")) \
bytes, $(field.:))
. endif
. endfor
. index = count (class->header.field) + 1
. for field
* Frame $(index): $(field.?'') \
. if type = "string"
(printable string)
. elsif type = "frame"
(opaque binary)
. index += 1
. else
. echo "E: unknown field type: $(type)"
. endif
. index += 1
. endfor
.

文章探讨了使用ZMQ高级框架进行高效协议设计的方法,包括MOPED模式、协议抽象层、协议设计原则及二进制序列化编解码器的生成。还介绍了如何在ZMQ上实施快速重启的文件传输和信用流量控制,以及构建协议服务器与客户端为状态机的策略。特别强调了分离协议的控制流与数据流,分别采用‘廉价’与‘讨厌’方式处理,确保性能与灵活性。
3595

被折叠的 条评论
为什么被折叠?



