公告公告
同事们,我计划在仲夏时发布有关排队系统设计的另一系列文章:“ VTrade实验”-尝试编写交易系统框架。 该周期将分析建立交易所,拍卖和商店的理论和实践。 在本文的结尾,我建议对您最感兴趣的主题进行投票。

这是Erlang / Elixir分布式反应式应用程序周期中的最后一篇文章。 在第一篇文章中,您可以找到反应式体系结构的理论基础。 第二篇文章说明了构建此类系统的基本模式和机制。
今天,我们将提出有关代码库和一般项目的开发问题。
服务机构
在现实生活中,开发服务时,通常必须在一个控制器中结合多种交互模式。 例如,解决了管理项目用户配置文件的任务的用户服务应响应req-resp请求并通过pub-sub报告配置文件更新。 这种情况非常简单:在消息传递之后,有一个控制器实现服务的逻辑并发布更新。
当我们需要实现容错分布式服务时,情况就很复杂。 假设用户要求已更改:
- 现在该服务应该在集群的5个节点上处理请求,
- 能够执行后台处理任务,
- 并能够动态管理您的个人资料更新订阅列表。
注意:我们不考虑数据的一致存储和复制的问题。 假设这些问题已较早解决,并且系统已经具有可靠且可扩展的存储层,并且处理程序具有与其进行交互的机制。
用户服务的形式描述变得更加复杂。 从程序员的角度来看,使用消息传递更改最少。 为了满足第一个要求,我们需要调整req-resp交换点上的平衡。
经常需要处理后台任务。 在用户中,这可以是检查用户文档,处理下载的多媒体或将数据与社交服务同步。 网络。 这些任务需要以某种方式分布在集群中并控制进度。 因此,我们有两个解决方案:要么使用上一篇文章中的任务分发模板,要么(如果不合适)编写一个自定义任务计划程序,这对于我们管理处理程序池是必需的。
第3点要求对pub-sub模板进行扩展。 为了实现,在创建pub-sub交换点后,我们需要另外启动该点的控制器,作为我们服务的一部分。 因此,我们似乎采用了处理订阅和从消息传递层退订到用户实现中的逻辑。
结果,任务分解表明,为了满足要求,我们需要在不同的节点上运行5个服务实例,并创建一个额外的实体-负责订阅的pub-sub控制器。
要运行5个处理程序,您无需更改服务代码。 唯一的附加操作是在交换点上建立平衡规则,稍后我们将讨论。
还存在其他复杂性:pub-sub控制器和自定义任务计划程序应在单个副本中工作。 同样,消息传递服务作为基础,应该提供选择领导者的机制。
领导者的选择
在分布式系统中,领导者的选择是指定负责计划负载的分布式处理的唯一过程的过程。
在不倾向于集中化的系统中,使用了通用共识算法,例如paxos或raft。
由于消息传递是代理程序和中心元素,因此他了解所有服务控制者-领导者的候选人。 消息传递可以任命一位领导人而无需表决。
启动并连接到交换点后,所有服务均收到系统消息#'$leader'{exchange = ?EXCHANGE, pid = LeaderPid, servers = Servers}
。 如果LeaderPid
与当前进程的pid
匹配,则将其分配为领导者,并且“ Servers
列表将包括所有节点及其参数。
当出现新的群集节点并断开连接时,所有服务控制器都会收到#'$slave_up'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts}
和#'$slave_down'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts}
。
因此,所有组件都知道所有更改,并且在集群中的任何给定时间都可以保证一位领导者。
中介人
为了实现复杂的分布式处理过程以及优化现有体系结构,使用中介很方便。
为了不更改服务代码并解决例如其他处理,路由或日志记录消息的任务,可以在服务之前启用代理处理器,这将执行所有其他工作。
发布订阅优化的经典示例是具有业务核心的分布式应用程序,该业务核心生成更新事件(例如,市场价格变化)和访问层-N个服务器,这些服务器为Web客户端提供WebSocket API。
如果您决定“先行”,那么客户服务如下:
- 客户端与平台建立连接。 在服务器端,终止流量,开始为该连接提供服务的进程。
- 在服务流程的上下文中,进行更新的授权和订阅。 该过程调用主题的subscribe方法。
- 在内核中生成事件后,该事件将传递给服务于连接的进程。
想象一下,我们有50,000名订阅“新闻”主题的用户。 订户平均分布在5台服务器上。 结果,到达交换点的每个更新将被复制50,000次:根据每个服务器上的订户数量,向每个服务器复制10,000次。 不是很有效的方案,是吗?
为了改善这种情况,我们引入了一个与交换点同名的代理。 全局名称注册商应该能够按名称返回最接近的进程,这一点很重要。
在访问层的服务器上运行此代理,我们为websocket api服务的所有进程都将订阅该代理,而不是内核中原始的pub-sub交换点。 代理仅在唯一订阅的情况下订阅内核,并将接收到的消息复制到其所有订阅者。
结果,内核和访问服务器之间将发送5条消息,而不是50,000条。
路由和平衡
要求
在当前的消息传递实现中,有7种查询分配策略:
default
。 该请求将传递到所有控制器。round-robin
。 遍历控制器之间的请求并循环分发。consensus
。 服务该服务的控制者分为领导者和跟随者。 请求仅传递给领导者。consensus & round-robin
。 该组中有一个领导者,但是请求在所有成员之间分配。sticky
。 计算哈希函数并将其分配给特定的处理程序。 具有此签名的后续请求将转到同一处理程序。sticky-fun
。 当交换点初始化时,用于sticky
平衡的哈希计算功能会额外转移。fun
。 它类似于粘黏的乐趣,除了除此之外,您还可以重定向,拒绝或预处理它。
初始化交换点时设置分发策略。
除了平衡消息传递之外,还可以标记实体。 考虑系统中标签的类型:
- 连接标签。 使您能够了解事件通过哪个连接发生。 当控制器进程连接到同一交换点但使用不同的路由密钥时使用。
- 服务标签。 允许使用一项服务来对处理器进行分组并扩展路由和平衡功能。 对于req-resp模式,路由是线性的。 我们将请求发送到交换点,然后将其传递给服务。 但是,如果需要将处理程序划分为逻辑组,则可以使用标签进行划分。 指定标签时,请求将定向到特定的控制器组。
- 请求标签。 允许区分答案。 由于我们的系统是异步的,因此要处理服务响应,您必须能够在发送请求时指定RequestTag。 从中我们可以了解提出请求的答案。
酒馆子
对于pub-sub,事情要容易一些。 我们有一个发布消息的交换点。 交换点在订阅他们所需的路由密钥的订户之间分发消息(我们可以说这与那些相似)。
可扩展性和弹性
整个系统的可伸缩性取决于系统各层和组件的可伸缩性程度:
- 通过使用此服务的处理程序将其他节点添加到群集中来扩展服务。 在试运行期间,您可以选择最佳的平衡策略。
- 通常,可以通过将特别加载的交换点移动到各个群集节点,或通过将代理进程添加到群集的特别加载的区域来扩展单个群集中的消息传递服务本身。
- 整个系统的可伸缩性作为一个特征,取决于体系结构的灵活性以及将各个群集组合成一个公共逻辑实体的可能性。
扩展的简单性和速度通常决定了项目的成功。 即时消息的性能随着应用程序的发展而增长。 即使我们缺乏50-60辆汽车的集群,我们也可以诉诸联邦。 不幸的是,联邦的主题超出了本文的范围。
订座
在负载平衡分析中,我们已经讨论了服务控制器的保留。 但是,消息传递也应该保留。 在节点或计算机崩溃的情况下,消息传递应自动并尽快恢复。
在我的项目中,我使用其他节点来承担负载,以防万一。 Erlang具有用于OTP应用程序的标准分布式模式实现。 实际上,分布式模式通过在另一个先前启动的节点上启动崩溃的应用程序来在发生故障时执行恢复。 该过程是透明的,发生故障后,应用程序将自动移至故障转移节点。 您可以在此处阅读有关此功能的更多信息。
性能表现
让我们尝试至少大致比较Rabbitmq和自定义消息传递的性能。
我从openstack团队中获得了Rabbitmq 官方测试结果。
在第6.14.1.2.1.2.2节中。 原始文档介绍了RPC CAST的结果:

以前,我们不会对OS内核或erlang VM进行任何其他设置。 测试条件:
- 错误选择:+ A1 + sbtu。
- 单个erlang节点中的测试可以在笔记本电脑上运行,而笔记本电脑上的i7则具有更高的移动性能。
- 群集测试在具有10G网络的服务器上进行。
- 该代码在Docker容器中工作。 NAT模式下的网络。
测试代码:
req_resp_bench(_) -> W = perftest:comprehensive(10000, fun() -> messaging:request(?EXCHANGE, default, ping, self()), receive #'$msg'{message = pong} -> ok after 5000 -> throw(timeout) end end ), true = lists:any(fun(E) -> E >= 30000 end, W), ok.
方案1:测试在具有旧i7移动执行功能的笔记本电脑上运行。 测试,消息传递和服务在一个Docker容器中的一个节点上执行:
Sequential 10000 cycles in ~0 seconds (26987 cycles/s) Sequential 20000 cycles in ~1 seconds (26915 cycles/s) Sequential 100000 cycles in ~4 seconds (26957 cycles/s) Parallel 2 100000 cycles in ~2 seconds (44240 cycles/s) Parallel 4 100000 cycles in ~2 seconds (53459 cycles/s) Parallel 10 100000 cycles in ~2 seconds (52283 cycles/s) Parallel 100 100000 cycles in ~3 seconds (49317 cycles/s)
场景2 :3个节点在docker(NAT)下的不同计算机上运行。
Sequential 10000 cycles in ~1 seconds (8684 cycles/s) Sequential 20000 cycles in ~2 seconds (8424 cycles/s) Sequential 100000 cycles in ~12 seconds (8655 cycles/s) Parallel 2 100000 cycles in ~7 seconds (15160 cycles/s) Parallel 4 100000 cycles in ~5 seconds (19133 cycles/s) Parallel 10 100000 cycles in ~4 seconds (24399 cycles/s) Parallel 100 100000 cycles in ~3 seconds (34517 cycles/s)
在所有情况下,CPU利用率均不超过250%
总结
我希望这个周期看起来不像是一堆意识,我的经验将为分布式系统的研究人员和从业人员带来真正的好处,这些人员正处于为他们的业务系统构建分布式体系结构之初,并且对Erlang / Elixir感兴趣。值得吗...
图片来自@chuttersnap