使用 ZeroMQ 实现分布式消息系统
“分布式系统是你甚至不知道的一台计算机上的故障可以使您自己的计算机不可用。”-Leslie Lamport
随着云计算的普及和可用性,分布式系统架构已很大程度上取代了更多的整体构建。当然,使用面向服务的架构,言外之意,就是你现在必须面对无数的以前从未存 在过的困难,如容错性,可用性和水平缩放。复杂性的另一层有趣的问题是提供一致性的节点,这本身就是一个未完全研究明白的问题。像 Paxos 和 Raft 算法试图提供管理数据复制的方案,而其他解决方案提供最终一致性。
构建可扩展的分布式系统,是一个不小的壮举,但它与构建相似的实时系统比较的话,这算不上什么。分布式体系结构是一个很好理解的问题,事实上,大多数应用 对延迟的容忍度很高。有些系统有明显的实时通信的需要,但对于开发者来说这是一个有趣的挑战。在这篇文章中,我探讨用 ZeroMQ 来以可扩展的方式处理分布式实时消息传送,同时也考虑到最终一致性的概念。
智能传输层
ZeroMQ 是一个由C++编写的高性能异步消息库。它不是一个专用的消息 broker,而是一个可嵌入的并发框架,支持通过多种传输的直接和扇出短点。ZeroMQ 实现了许多不同的通讯模式,如通过 TCP 协议请求-应答(request-reply),发布-订阅(pub-sub)和推送-拖拉(push-pull),PGM(多点广播),进程内和进程间 的通道。或多或少在设计的时候缺乏UDP协议的支持,因为 ZeroMQ 原本构想能够提供一个原子消息的 guaranteed-ish 传输。这个库没有实际的传输保证,但它却是做出了最大的努力。然而,ZeroMQ 所能确保的是,你不会收到一个部分消息,消息会按照顺序被接收。这个很重要,因为 UDP 协议的性能在损耗和拥挤的环境中才能真正体现出来。
单单就消息传递模式和传输(协议)的综合列表,就让 ZeroMQ 成为构建分布式应用程序的一个有吸引力的选择,但它的特殊优势是其可靠性,可扩展性和高吞吐量。ZeroMQ 和相关技术广泛地应用于高频交易应用中,这些应用一般不允许金融数据包丢失1。在2011年,CERN 进行的一项研究,在粒子加速器中比较了 CORBA,Ice, Thrift, ZeroMQ 和其他几个协议,ZeroMQ 排名最高。
在吞吐量上,ZeroMQ 比 TCP sockets 表现更好,因为它使用一些技术:如智能消息批处理,最小化网络堆栈遍历,并禁用 Nagle 算法。 默认情况下(尽可能),消息都是排队等待订阅方,并试图避免订阅过慢的问题。然而,当这样做还不能避免的话,ZeroMQ 会采用一种称为“自杀的蜗牛”的模式。 当一个订阅方运行缓慢并且不能赶上进入消息的速度,ZeroMQ 就会说服订阅方来杀死自己。“慢”是由一个可配置的高水位线来决定的。这里的想法是,最好是快速的失败使问题很快得到解决而不是潜在地允许过期的数据流入 下游。再次考虑高频交易使用案例。
一个分布式,可伸缩和快速消息架构
ZeroMQ作为一个通讯层使用,它可以成为一个可以信服的案例。让我们探索更深一点,看看它作为一个消息框架在一个实时系统里是如何被使用的。ZeroMQ可以直观地使用并提供了很多语言支持,因此,我们将会更多聚焦在架构上和消息传递的范例上,而不是实际的代码。
大约一年前,当我第一次研究ZeroMQ,我建立了一个框架去演示实时消息并且具备文档同步,它被叫做Zinc。从“文档”,在这层意义上说,任何有良好结构的和为可变数据考虑的文本文档,电子表以及画布(canvas)块等等。从纯学术角度来说,目标是给开发提供一个可建构的丰富框架,应对在分布式环境下的协作体验。
这个框架实际上有两种实现,第一个由原生的 ZeroMQ 所支持,另一个是由纯 Java 实现 JeroMQ 2所支持。这专未允许使用任何传输层而设计的。
Zinc 围绕几个核心概念而设计:Endpoints(端点), ChannelListeners(通道监听者), MessageHandlers(消息处理者) 和 Messages(消息)。在应用程序集群中,端点代表一个单一的节点,提供发送消息到其他端点和从其他端点接收消息的功能。它具有分别传输给同类组件和 从其他相同组件接收消息的输出和输入通道。
当输入通道在一个端点被打开时,ChannelListeners (通道监听者)本质上扮演输入信息的守护监听。当消息被接收时,它会被传递到一个线程池然后被 MessageHandler (消息处理者)处理。因此如上所述,消息都是根据他们接收的顺序被异步处理的。此外,这是在我学习 Go 之前,它成为 Java 的理想代替品,因为它相当适合解决这个问题。
消息是端点间的数据交换,来自我们可以建立的文档与文档间片段。文档是通过应用被结构化定义的数据,而文档片段是根据粗的或细的需求来确定粒度所表示的部分文档,或者增量。
Zinc是围绕着发布-订阅(publish-subscribe)和推送-拉取(push-pull)的消息模式建立的。一个端点将会作为集群的主机, 其他主机作为客户端。使用这样的架构,主机作为发布者而客户端作为订阅者。因此,主机发布一条消息,它就会发布到每一个订阅的客户端,这类似多播的方式。 相反地,客户端一直扮演着“推送(push)”端点,而主机是“拉取(pull)”端点。这些客户端可以把消息推送到主机的消息队列,主机以先进先出方式 从队列中拉取。
这种结构允许消息被传送到整个集群----客户端修改使其发送给主机,主机发送增量给所有的客户端。这也就意味着引起变化的客户端将受到一个“回应”变 量,但是在检查消息源的时候它将被丢弃,消息源是一个能特殊标识端点的UUID。客户端然后在必要的情况下负责数据的一致性,或许通过操作转换或保持一个客户端能综合的单一来源的事实。
这种结构的一个优点就是它的规模合理,因为它的可组合性。具体来说,我们可以建构我们的集群客户提供任意广度和深度的树。显而易见,越从水平和垂直上扩大规模,越容易带来边缘结点之间的延迟。再加上最后的一致性,这可能会造成一些应用的问题,但是或许会被接受。
缺点是这种固有的客户端--服务器模型引入了单点故障的特征。一种解决办法是当主机有问题时提升其他的结点而且平衡整个树。
再次,这个框架主要作为我的学术支持和测试驱动 ZeroMQ 的方式,虽然已经存在其他一些有趣的类似的应用。本框架支持通过推送或发布订阅机制的多播消息的传递,下面是一个自主负载均衡的用例。
匹配像 Zoomkeeper, 或其他一些服务发现协议,客户机将能够发现主机,主机将作为负载平衡器。一旦客户机发现了一个主机,它可以请求成为主机的群集的一部分。如果主机接受请 求,客户端可以发送消息给主机(并且,因此,建立一个集群),同样,从主机接收消息(和集群休息)。这使客户机和主机成为提交工作的集群,通过这样一个均 匀分布的方式进行处理,并且可决定是否将进一步扩展这个树或使其处理这个工作。客户机可以自己决定是否参与负载均衡集群,当他们成为可用的,他们大多是有 自主权。客户可以快速运作,并且向下运作使用,比如像,Docker 容器。
ZeroMQ 在实现可靠、快速和可拓展的分布式消息上是很不错的,但它在一个单一的机器或几个局部网络中,通过跨进程通信使用相同的模式所进行并行计算的同样有用。它 还表明可以毫不费力地在每台机器上进行多核操作。ZeroMQ 是一个不可替代的消息代理,但它可以和传统的面向消息的中间件协调一致的工作。结合协议缓冲区和其他序列化方法,ZeroMQ 可以很容易建立非常高通量信息框架。