解决事件驱动型微服务中的并发问题

2022/5/15 来源:不详

视频网站免费vip账号 http://m.360xh.com/xinwen/kjzh/66346.html
作者

HugoRocha译者

平川策划

闫园园

富兰克林·罗斯福曾经说过,我们往往过多地考虑了早起的鸟儿运气好,却不怎么想早起的虫子运气差。我从来不玩彩票。彩票的失败率大到惊人;实际上,成为圣人或美国总统的可能性都比赢得彩票(例如欧洲的EuroMillions或美国的Powerball)大。

事件驱动型服务的并发常常是一种有保障的反面的彩票中奖,虽然对于特定的并发问题可能概率很低。然而,一切都归结于尝试次数,由于服务所处理的事件量非常大,所以一个不大可能的事件几乎变成了一定会发生的事情。例如,我们曾经遇到一个问题,其发生的概率大约为百万分之一。该服务每秒处理约一百条信息,这意味着该问题每小时会发生三次左右。根据设计,事件驱动型服务需要应对巨大的规模和吞吐量,使得并发问题特别容易发生。

并发问题,或称竞态条件,是指当某行代码并行运行时所产生的意想不到的行为,如果代码单线程运行,就不会出现这种情况。对程序员来说,处理并发问题往往不是自然而然的事情,我们习惯于以单线程的方式来考虑我们的代码。检测并确保代码并行运行的安全,往往需要一个有丰富经验、接受过专门训练的人。而且,并发问题并不明显,往往只在生产环境中才会暴露出来,因为本地或开发环境与实际环境的吞吐量有很大的差别。

火星漫游者

例如,美国国家航空航天局(NASA)有非常严格的编码准则,以及一个非常详尽、细致的质量保证过程。毕竟,调试地球以外的东西与分析大多数生产问题不太一样(虽然有时会觉得有异样的事情在发生)。一个短暂出现的错误,很可能会被大多数开发人员所忽略,但却往往是一个竞态条件的症状。NASA可不会放过类似的问题,它甚至可能追踪到应用程序之外,开发人员甚至可能要深挖到操作系统层才能找出根本原因。事实上,那是几百万美元的风险。但是,即使有这样孜孜不倦的过程,竞态条件往往还是不可避免。例如,我还记得美国国家航空航天局(NASA)因竞态条件而与火星车失去联系的那段插曲。

并发问题的不可避免性和事件驱动型服务的高吞吐量,使得制定一个深思熟虑的策略来从根本上解决并发问题的需求变得尤为迫切。事件驱动型服务的一个重要属性是能够通过添加同一服务的多个实例来进行横向扩展。这种方法使传统的并发处理方式失效,因为不同的请求可能会被发送到不同的实例上,所以要做一个内存锁,如互斥量、锁或信号量。通常,分布式系统采用外部工具来管理分布式并发,如Consul或Zookeeper。然而,对于事件驱动型服务,可以引入一个本质上完全不同的概念来处理并发。端到端消息路由是一种非常有效并且可扩展的方法,它是通过设计(使用架构解决方案)来处理并发问题,而不是实现(求助于外部工具或在服务实现中)。

多年来,我们借助RabbitMQ和Kafka,在多个不同的生产用例中尝试了几种不同的方法。我们最终决定在可能的时候通过设计来处理并发问题,而不是通过实现。以下是我们在生产中全面使用的一些解决方案,希望可以为你处理并发问题带来一些灵感。

1并发问题示例

让我们用一个例子来说明。想象一下,我们有一个产品在线销售平台,用户可以订阅“新进”和“热销补货”产品的通知。每当所需产品的库存增加时,用户可以通过邮件、短信等方式接收通知。持有产品和库存信息的服务在每次库存发生变化时都会发送一个事件。订阅服务必须知道产品库存何时从0变为1,并在变化时发送通知。下图说明了这种情况。

订阅服务处理ProductStockIn事件,在产品库存改变时作出反应。因为只有当库存从0变为1时,订阅才有价值,该服务在内部状态中保存每个产品的当前库存。ProductStockIn事件流包括以下动作:

1.产品服务发布事件;

2.订阅服务处理事件;

3.获取本地库存,检查库存是否从0变为1;

4.获取当前的订阅信息;

5.针对每条订阅发送通知;

6.更新本地库存数据。

在单线程思维模式下,这种方法讲得通,不会产生任何问题。然而,为了充分优化服务资源并达到合理的性能,我们应该给服务添加并行性。如果服务处理两个或两个以上的事件会发生什么?一个竞态条件会使服务把同一个订阅发布两次。如果服务处理两个库存变化事件(例如,库存从0到1和从1到2),并同时运行步骤3的验证,那么它将传入两个事件,产生一个竞态条件,并因此把相同的通知发送两次。

要处理这个问题,只需简单地用传统的并发处理方法(如锁、互斥量、信号量等)锁定线程执行。然而,传统方法只适用于单实例服务,如下图所示。

由于内存中的锁只被做锁的实例共享,其他实例仍然能够同时处理其他事件。同一产品的两个库存变化事件可以由不同的实例来处理,即使两个实例都锁定了它们的执行,也只在它们各自的实例内有效,没有什么可以防止两个实例之间产生并发问题。由于事件驱动型服务的一个重要属性是水平扩展的能力,这类传统的方法在这种情况下可以说相当不充分。

本地锁的一个替代方法是使用数据库来防止并发问题。处理货币时有一个典型的悲观方法(下文会介绍更多关于悲观方法和乐观方法的内容),就是将操作包裹在一个事务中。然而,通常来说,没有一种简单直接的方法可以保证存在外部依赖时的交易一致性,而又不涉足我们最想也应该避免的分布式交易领域。使用事务性一致性也受限于支持它的技术,许多NoSQL数据库并不提供与传统关系型数据库相同的保证。

2悲观方法vs乐观方法

有两种处理并发的方法:悲观方法和乐观方法。

悲观的并发策略通过阻止对所需资源的并行访问来防止并发。这类策略假设存在并发,并因此预先限制了对资源的访问。这类策略适用于高并发的用例,即两个进程很可能同时访问同一资源。

乐观并发策略假设不存在并发。这类策略是在并发问题发生时,提供一个策略来处理失败的操作,抛出一个错误或是重试该操作。乐观并发在并发几率较低的环境中最有效。

悲观并发会影响性能,并且限制了解决方案的整体并发性。乐观并发可以提供很好的性能,因为它不锁定任何东西,只是对失败做出反应。在低并发环境中,几乎就像没有并发处理策略一样。然而,当并发的可能性很高时,与限制对资源的访问相比,重试操作的成本通常要高很多。在这些情况下, 使用悲观并发。

3Kafka主题剖析

Kafka是一个流行的事件流平台。如果你用它来实现简单的发布-订阅和事件流用例,并且不太

转载请注明:
http://www.3g-city.net/gjyby/220.html
  • 上一篇文章:

  • 下一篇文章:
  • 网站首页 版权信息 发布优势 合作伙伴 隐私保护 服务条款 网站地图 网站简介

    温馨提示:本站信息不能作为诊断和医疗依据
    版权所有2014-2024
    粤ICP备19025322号-1

    今天是: