首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Zocdoc的事件驱动架构实践

本文最初发布于Zocdoc技术博客,经原作者授权由InfoQ中文站翻译并分享。

ZocDoc成立于2007年,是一家在线医生预约平台。它根据地理位置、保险状态及医生专业为患者推荐医生,并可在平台上直接完成预约。Zocdoc采取对患者免费,向医生收费的商业模式。

在Zocdoc,我们提供一个连接病人和医疗服务提供商的平台。虽然,大多数访问Zocdoc的人都熟悉我们面向病患提供的产品,这些产品确保人们能在网上找到医生并在线预约,但他们可能不太熟悉我们提供的面向服务提供商的工具。

针对服务提供商,我们负责构建和维护这些系统,让医生能执行各种任务,如确认预约、更新他们的就诊原因或接受的保险,并跟踪他们的表现。我们的目标是帮助医生最大限度地利用Zocdoc的市场。

微服务之旅

从2015年开始向AWS过渡以来,Zocdoc的大多数团队已经从单体应用程序迁移到全新的微服务架构(这里列出了单体应用的缺点)。

然而,由于我们拥有大量的工具以及单体应用中领域之间的紧密耦合,提供商方面的迁移已经滞后。

这种新架构在可伸缩性、敏捷性和灵活性方面给我们带来很多好处,因此,今年,我们共同努力加快了微服务的采用。这样,随着不断摆脱单体(我们亲切地称之为OOM项目),我们最终实现了所提出的“微服务优先”策略。

Martin Fowler将其描述为,“从几个粗粒度的服务开始,比预期的最终服务要大。然后,随着边界的稳定,分解为更细粒度的服务。“这个策略很有效,因为它使我们能快速构建新的试验工具和功能,首先从一个简单的版本开始,看看如何回应,然后再致力于开发一个完备的、可扩展的产品或微服务。”

分布式数据的困境

我们继续发展这个微服务架构,但由于其数据访问的分布式特性,遇到了一些复杂性,比如在预约时,我们跟踪的核心数据是归提供商所有,比如医生的网站。正确地归因这些数据,以确定会话是从哪里产生的,这对我们来说至关重要。无论它是一个医生工作地点的网站还是通过Zocdoc提供的像SEO这样的渠道,所以我们需要一种方法来将我们的appointment-attributionpractice-website的数据串联起来。

然而,在微服务世界中,每个服务拥有的数据是该服务的私有数据,只能通过服务的API访问,就像我们的appointment-attribution-servicepractice-website-service一样。(注意:这种封装是必要的,可以确保微服务是松耦合的,并且可以独立开发。否则,如果多个服务共享同一个数据库,那么任何模式更新都需要对所有服务进行耗时的协同更新——这是我们过去的惨痛教训)。

此外,我们在AWS上运行的不同服务经常使用不同类型的数据库,如SQL或类似于SQL的Aurora、DynamoB、ElasticSearch等,这让数据访问变得更加复杂。最后,这导致了两个主要的分布式数据挑战。

1.分布式事务

第一个挑战是维护跨多个服务的事务的数据一致性。与使用ACID数据库的单体应用不同,我们不能简单地使用本地数据库事务。在分布式系统中实现一致性的一种方法是使用分布式事务协议,比如两阶段提交(2PC)。

然而,2PC通常是非常复杂的,不是现代Web服务的一个可行选项。本质上,CAP定理强行规定了你要在可用性和一致性之间进行选择。在我们的用例中,可用性是更好的选择,因为我们无法承受由于次要操作(如预约归因)而导致的预订流中断。

2. 查询

第二个挑战出现在实现从多个服务检索数据的查询时。如果这些服务中的任何一个需要来自多个API的数据来进行操作,则可能导致复杂的应用程序端连接,通常涉及许多同步请求/响应调用。

当微服务像这样根据需要自由地调用其他微服务时,它会创建一个紧耦合的复杂的依赖关系图**,很难进行推理和扩展(这也是我们过去一直在解决的问题)。此外,如果任何依赖的服务API发生故障,在处理时都需要仔细考虑,理清回退逻辑。

进化为事件驱动

对于许多像我们这样的应用程序,解决方案是使用事件驱动架构。这种架构中,在实体或域模型变化时,微服务会异步发布事件,而不是同步调用API。其他微服务订阅这些事件并更新它们自己的实体,从而发布后续事件。这通常被称为基于编排的Saga模式。

它支持跨多个服务的数据一致性,而不使用分布式事务,但提供的保证更弱,如最终一致性(通常称为BASE模型)。由于事件驱动的系统本质上是异步的,因此,它们通常比传统的REST(或API)架构响应更快,并且可以通过传入事件触发器来激活它们。

这促成了服务之间的松耦合(和易伸缩),因为事件生产者不需要了解使用者以及它如何处理事件。它还消除了典型的请求-响应风格的API的阻塞/等待,释放了服务资源。

事件使用者还可以维护自己的“物化视图”或数据副本,以供临时查询或与自己的数据进行连接。例如,我们的预约归因服务接收到一个PracticeWebsiteAdded事件,通知它新添加了url,它会更新自己的PracticeUrl数据存储副本,以便和Booking数据进行连接。

虽然通常有许多模式属于事件驱动的范畴,但是这个特定的模式与Martin Fowler所说的事件传递状态转移是一致的。这种模式会生成大量重复的数据,但是由于目前存储成本较低,通常不需要考虑这个问题。另外,减少耦合和更好的弹性所带来的好处超过了对数据冗余的关注。

事件流和原子性

然而,要使这些事件驱动的系统可靠,必须满足一个核心条件——服务必须原子地写入其数据库并发布事件。这通常会导致以下基于事务的模式。

1. 事务性发件箱模式

在这种方法中,你将在服务的数据库中创建一个额外的“发件箱”表。在接收到修改/创建业务实体的请求时,你必须更新你的实体表,并且,作为同一数据库事务的一部分,还必须在表示待发布事件的发件箱表中插入一条记录。然后,一个异步进程会监视该表中的新条目,并将这些事件发布到数据流或消息代理。

2. 事务日志跟踪或更改数据捕获(CDC)

这里的思想是跟踪数据库的事务日志,并将实体表中的每个更改作为事件发布。与基于轮询的方法相反,这种基于日志的更改数据捕获(CDC)几乎是实时的,而且开销非常小。

Debezium是一个流行的分布式平台,它为MySQL、Postgres和SQL Server等多个数据库提供了CDC连接器。如果你使用DynamoDB数据库的话,AWS以DynamoDB流的形式为CDC提供了一种更简单的机制。在任何DynamoDb表中,这些流都提供按时间顺序排列的条目级修改序列,并将这些信息存储在日志中长达24小时。

在Zocdoc,我们大量使用了DynamoDB,因为它是一个完全托管的无服务器NoSQL数据库,提供完美的可伸缩性和性能,并且免费提供高达25GB的存储空间。考虑到DynamoDB流的便利,它是基于CDC的事件驱动服务的完美选择。设置完成后,这些流可以调用其他AWS服务,如简单通知服务(SNS)或Lambda。

然后,事件可以扇出到任意数量的订阅者。一个非常有用的AWS无服务器模式(我们在Zocdoc大量使用)是使用SNS将事件发布到一个或多个SQS队列。这为我们提供了一种可靠的方法,可以通过基本可靠的传递异步发送事件,并且还提供了消息限流的好处

不过,这种方法有一个缺点。虽然我们在事件流中发布数据存储的所有更改,但数据存储本身只捕获数据的最新状态。结果,我们从设计上就丢了数据。不可重放,不能审计,也没有方法能查询特定历史点的数据状态。这对我们来说是一个明显的限制,因为我们不仅需要动态地确定预约归属,而且还需要能够追溯(以防需要修正)。

因此,对我们来说,了解执行更改的时间和详细信息以及用户标识是非常重要的。我们还考虑把生成的CDC事件(如插入、更新、删除,并做少量的转换)存储到我们的Redshift分析仓库中,但这意味着我们的OLAP数据会背离我们的OLTP数据,在一个有很多步骤的数据工程管道里,如果数据在其中的任何一个步骤里丢了,都没有办法恢复。

以事件为事实来源

这把我们导向了一个越来越流行的模式,该模式已经成为传统CRUD应用程序的替代方案——事件源。

事件源

事件源是将实体状态的更改建模为不可变的事件“日志”。然后,这个“事件日志”或“事件存储”就成了事实的来源,而系统状态纯粹是从它派生出来的。由于保存事件是一个操作,因此,该模式本质上是原子性的,并且最小化了数据更新冲突的可能。这里的一个事件代表了一些在该领域发生的某件事,如:PracticeWebsiteAddedPracticeWebsiteModifiedPracticeWebsiteStatusChangedAppointmentConfirmed等。

通常,事件存储将这些事件发布到事件流(或消息代理),使用者订阅这些事件并根据需要处理它们。这可以使用前面提到的CDC来实现。如果事件源中的事件语义级别太低,可以考虑发布更高级的域事件,而不是使用另外的事件处理程序。

CQRS

不幸的是,一旦应用了事件源模式,就无法再轻松地查询数据了。这将我们引向一个不同但密切相关的模式,称为命令查询责任隔离或CQRS。CQRS的思想是隔离命令(写请求)和查询(读请求)之间的职责,并在应用程序中以不同的方式处理它们。你甚至可以分割数据存储,创建单独的读和写数据存储,从而实现更好的隔离和独立扩展。

因此,在一个典型的事件源+ CQRS应用程序中,最终你会得到一个“事件”表(命令会以追加的方式写入)和“投影”表(也称为“状态表”或“物化视图”或“持久化读取模式”),通过一个灵活的模式来支持快速高效的查询。服务通过订阅由事件日志发布的域事件来更新该投影。在Zocdoc,我们过去已经成功地实现了这些类型的投影存储,例如我们的wasabi基础设施

在AWS的生态下

结合DynamoDB灵活的基于键值的数据模型和条目级活动的流式推送,我们能够将事件源+ CQRS应用到我们的practice-website-service。最后,我们得到两个不同的DynamoDB表,它们支持不同的查询模式,并有一个DynamoDB流将这两个表连接起来。对于状态表,我们的模式相当简单。我们使用practiceId作为散列(分区)键,使用Url作为范围(排序)键,因为我们必须保证(practiceId,Url)的值唯一。

对于事件,我们不能使用相同的主键,因为对于给定的组合(practiceIdurl)可能有多个事件,而(practiceIdEventType)也不太合适,因为单个条目可能有多个类型为UPDATED的事件。很自然地,我们就选择使用(practiceIdEventId)作为主键。我们选择用GUID(保证惟一性)和时间戳(允许按EventId排序)的组合作为Event ID。

然后,我们使用AWS Lambda创建触发器,响应事件表DynamoDB流中的事件。一个触发器处理这些事件并更新投影表以反映最新的状态。另一个触发器将这些事件发布到SNS主题,以流的形式发送到订阅服务。

接收触发器事件:

{
   "Records":[
      {
         "EventSourceArn":"arn:aws:dynamodb:us-east-1:1234:table/practice-website-events/stream/2019-09-05T19:28:23.205",
         "AwsRegion":"us-east-1",
         "Dynamodb":{
            "ApproximateCreationDateTime":"2019-12-19T21:05:23Z",
            "Keys":{
               "PracticeId":{
                  "S":"noj6XuZmMU6aq-7Pg9m5sB"
               },
               "EventId":{
                  "S":"2019-10-02T16:51:39Z_1234567-890a-bcde-fghi-123456789012"
               }
            },
            "NewImage":{
               "DomainType":{
                  "S":"ProviderOwned"
               },
               "Version":{
                  "S":"1.0.0"
               },
               "PracticeId":{
                  "S":"noj6XuZmMU6aq-7Pg9m5sB"
               },
               "EventId":{
                  "S":"2019-10-02T16:51:39Z_1234567-890a-bcde-fghi-123456789012"
               },
               "TimestampUtc":{
                  "S":"2019-06-13T17:05:00Z"
               },
               "Url":{
                  "S":"realdoctorwebsite.com"
               },
               "Name":{
                  "S":"PracticeWebsiteAdded"
               },
               "InitiatedBy":{
                  "S":"johndoe@mail.com"
               }
            },
            "OldImage":{
            },
            "SequenceNumber":"499848100000000006586459546",
            "SizeBytes":307,
            "StreamViewType":{
               "Value":"NEW_IMAGE"
            }
         },
         "EventID":"f00000ffe0000e448db06a1abcdefghij",
         "EventName":{
            "Value":"INSERT"
         },
         "EventSource":"aws:dynamodb",
         "EventVersion":"1.1",
         "UserIdentity":null
      }
   ]
}

优缺点

事件源有几个好处。首先,它提供了可靠的数据审计日志,是审计、遵从性、数据治理和数据分析系统的最佳选择。它让实现临时查询成为可能,从而可以确定业务实体在任何时间点的状态。如前所述,它还解决了实现事件驱动的架构原子性和数据一致性中的一些关键问题。

另外,因为它持久化事件而不是域对象,所以它基本上避免了对象-关系阻抗不匹配的问题。当与CQRS相结合时,它提供了一种方便的方式来独立地扩展读和写。

不过,它也有一些不足:

  1. 编程模型更复杂,学习曲线更高。我们团队花了相当多的时间来适应建模和持久化事件的思想,那不同于CRUD应用程序中的实体状态。
  2. 你的应用程序现在必须处理最终一致的数据。使用CQRS,如果应用程序从尚未更新的投影中读取数据,那么它可能会遇到过期数据。这种情况可能会发生,例如,如果更新投影的Lambda出现了冷启动问题,这是我们自己遇到过的问题。此外,我们提供的医生网站的用户界面在一个旧网页上,每次编辑后都会重新加载,使得最终一致性有时有点太……最终。作为一个解决方案,我们设置了一个Lambda预热,它每5分钟调用一次我们的Lambda,这样就减了少启动时间,将延迟问题减少到一个很小的可以忽略的百分比。
  3. 你必须为建模的域事件选择正确的粒度级别。一个事件粒度太大(比如PracticeWebsiteEvent),所有的消费者都必须监听这些事件,并弄清楚它是否会影响它们。如果一个事件粒度太小(如PracticeWebsiteUrlChanged & PracticeWebsiteDomainTypeChanged),消费者就必须监听多个事件,以便在采取行动之前将它们组合起来。此外,事件永远不会被删除。即使发生了应用程序级的故障或不一致,你也要运行补偿事务,创建更多事件。如果事件范围太小,还会产生大量的数据。通常,事件粒度是一个需要解决的棘手问题,需要投入大量时间来理解域并与域专家进行交谈。新近的热门技术事件风暴可以帮助使用更多领域驱动的设计方法解决这个问题。
  4. 另一个缺点是使用服务必须检测和处理无序、重复或丢失的事件。虽然DynamoDB流结合Lambda、SNS和SQS可以在很大程度上缓解这些问题,但是仍然存在差距(特别是因为SNS目前不支持将消息转发到SQS FIFO队列)。

其他可选方法

如果你特别关注最终一致性,那么你可以采用另一种方法。与使用Lambda侦听DynamoDB流并更新投影表不同,你可以在接收请求时以事务方式同时写入事件存储和状态表,从而消除读取存储更新的任何延迟。这是事务性发件箱模式事件源模式的混合,后者提供了额外的好处,允许你在持久化任何更改之前使用状态表验证命令。

虽然DynamoDB确实提供了API以事务方式更新多个表,但是你必须使用它们的底层API,而不是更流畅的文档或对象持久性模型。使用这种方法,还会失去读写独立伸缩的能力,并可能增加写API的整体延迟。因此我们发现,更好的方法是使用CQRS方法,但是在将来可能会考虑转向其他方法(特别是当AWS SDK开始使用对象持久性API支持多个事务时)。

还有其他解决最终一致性的方法,它们也可能适合你的用例,这取决于你的需要,比如维护数据的内存缓存和在读取期间检查事件存储。

小结

事件驱动架构是分离微服务并获得更好的可伸缩性的最佳选择。在设计事件驱动的系统时,我们需要对导致域实体更改的事件建模,并自动将它们发布到事件流。在AWS中,DynamoDB流为我们提供了一种很好的方式来实现这一点。

当我们希望永远保留这些事件并将它们用作事实来源时,我们可以采用事件源模式。在此模式中,事件被持久化到充当记录系统的事件存储中。它通常与CQRS模式结合使用,从存储的事件创建物化视图。虽然这种方法有几个好处,比如完全可重放性、可审计性和原子性,但它不是银弹。你最终会创建更复杂的系统,并被迫处理最终一致性。

关于作者

Anwesha Das是Zocdoc提供商团队的一名工程师。此前,她曾在金融行业开发实时交易应用程序。她有物理学背景,对云计算、系统架构和数据工程有着浓厚的兴趣。

英文原文:

Breaking Down the Hype: Promises and Pitfalls of Event Driven Architecture

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/HRzzfPbQQvv18kbBVhkB
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券