Categories
程式開發

Zocdoc的事件驅動架構實踐


Zocdoc的事件驅動架構實踐 1

本文最初發佈於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定理強行規定了你要在可用性和一致性之間進行選擇。在我們的用例中,可用性是更好的選擇,因為我們無法承受由於次要操作(如預約歸因)而導致的預訂流中斷。

Zocdoc的事件驅動架構實踐 2

2. 查詢

第二個挑戰出現在實現從多個服務檢索數據的查詢時。如果這些服務中的任何一個需要來自多個API的數據來進行操作,則可能導致複雜的應用程序端連接,通常涉及許多同步請求/響應調用。

當微服務像這樣根據需要自由地調用其他微服務時,它會創建一個緊耦合複雜的依賴關係圖,很難進行推理和擴展(這也是我們過去一直在解決的問題)。此外,如果任何依賴的服務API發生故障,在處理時都需要仔細考慮,理清回退邏輯。

Zocdoc的事件驅動架構實踐 3

進化為事件驅動

對於許多像我們這樣的應用程序,解決方案是使用事件驅動架構。這種架構中,在實體或域模型變化時,微服務會異步發布事件,而不是同步調用API。其他微服務訂閱這些事件並更新它們自己的實體,從而發布後續事件。這通常被稱為基於編排的Saga模式

它支持跨多個服務的數據一致性,而不使用分佈式事務,但提供的保證更弱,如最終一致性(通常稱為BASE模型)。由於事件驅動的系統本質上是異步的,因此,它們通常比傳統的REST(或API)架構響應更快,並且可以通過傳入事件觸發器來激活它們。

這促成了服務之間的松耦合(和易伸縮),因為事件生產者不需要了解使用者以及它如何處理事件。它還消除了典型的請求-響應風格的API的阻塞/等待,釋放了服務資源。

事件使用者還可以維護自己的“物化視圖”或數據副本,以供臨時查詢或與自己的數據進行連接。例如,我們的預約歸因服務接收到一個PracticeWebsiteAdded事件,通知它新添加了url,它會更新自己的PracticeUrl數據存儲副本,以便和Booking數據進行連接。

雖然通常有許多模式屬於事件驅動的範疇,但是這個特定的模式與Martin Fowler所說的事件傳遞狀態轉移是一致的。這種模式會生成大量重複的數據,但是由於目前存儲成本較低,通常不需要考慮這個問題。另外,減少耦合和更好的彈性所帶來的好處超過了對數據冗餘的關注。

Zocdoc的事件驅動架構實踐 4

事件流和原子性

然而,要使這些事件驅動的系統可靠,必須滿足一個核心條件——服務必須原子地寫入其數據庫並發布事件。這通常會導致以下基於事務的模式。

1. 事務性發件箱模式

在這種方法中,你將在服務的數據庫中創建一個額外的“發件箱”表。在接收到修改/創建業務實體的請求時,你必須更新你的實體表,並且,作為同一數據庫事務的一部分,還必須在表示待發布​​事件的發件箱表中插入一條記錄。然後,一個異步進程會監視該表中的新條目,並將這些事件發佈到數據流或消息代理。

Zocdoc的事件驅動架構實踐 5

2. 事務日誌跟踪或更改數據捕獲(CDC)

這裡的思想是跟踪數據庫的事務日誌,並將實體表中的每個更改作為事件發布。與基於輪詢的方法相反,這種基於日誌的更改數據捕獲(CDC)幾乎是實時的,而且開銷非常小。

Debezium是一個流行的分佈式平台,它為MySQL、Postgres和SQL Server等多個數據庫提供了CDC連接器。如果你使用DynamoDB數據庫的話,AWS以DynamoDB流的形式為CDC提供了一種更簡單的機制。在任何DynamoDb表中,這些流都提供按時間順序排列的條目級修改序列,並將這些信息存儲在日誌中長達24小時。

Zocdoc的事件驅動架構實踐 6

在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基礎設施

Zocdoc的事件驅動架構實踐 7

在AWS的生態下

結合DynamoDB靈活的基於鍵值的數據模型和條目級活動的流式推送,我們能夠將事件源+ CQRS應用到我們的practice-website-service。最後,我們得到兩個不同的DynamoDB表,它們支持不同的查詢模式,並有一個DynamoDB流將這兩個表連接起來。對於狀態表,我們的模式相當簡單。我們使用practiceId作為散列(分區)鍵,使用Url作為範圍(排序)鍵,因為我們必須保證(practiceId,Url)的值唯一。

Zocdoc的事件驅動架構實踐 8

對於事件,我們不能使用相同的主鍵,因為對於給定的組合(practiceIdurl)可能有多個事件,而(practiceIdEventType)也不太合適,因為單個條目可能有多個類型為UPDATED的事件。很自然地,我們就選擇使用(practiceIdEventId)作為主鍵。我們選擇用GUID(保證惟一性)和時間戳(允許按EventId排序)的組合作為Event ID。

Zocdoc的事件驅動架構實踐 9

然後,我們使用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":"[email protected]"
               }
            },
            "OldImage":{
            },
            "SequenceNumber":"499848100000000006586459546",
            "SizeBytes":307,
            "StreamViewType":{
               "Value":"NEW_IMAGE"
            }
         },
         "EventID":"f00000ffe0000e448db06a1abcdefghij",
         "EventName":{
            "Value":"INSERT"
         },
         "EventSource":"aws:dynamodb",
         "EventVersion":"1.1",
         "UserIdentity":null
      }
   ]
}

Zocdoc的事件驅動架構實踐 10

優缺點

事件源有幾個好處。首先,它提供了可靠的數據審計日誌,是審計、遵從性、數據治理和數據分析系統的最佳選擇。它讓實現臨時查詢成為可能,從而可以確定業務實體在任何時間點的狀態。如前所述,它還解決了實現事件驅動的架構原子性和數據一致性中的一些關鍵問題。

另外,因為它持久化事件而不是域對象,所以它基本上避免了對象-關係阻抗不匹配的問題。當與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支持多個事務時)。

Zocdoc的事件驅動架構實踐 11

還有其他解決最終一致性的方法,它們也可能適合你的用例,這取決於你的需要,比如維護數據的內存緩存和在讀取期間檢查事件存儲。

小結

事件驅動架構是分離微服務並獲得更好的可伸縮性的最佳選擇。在設計事件驅動的系統時,我們需要對導致域實體更改的事件建模,並自動將它們發佈到事件流。在AWS中,DynamoDB流為我們提供了一種很好的方式來實現這一點。

當我們希望永遠保留這些事件並將它們用作事實來源時,我們可以採用事件源模式。在此模式中,事件被持久化到充當記錄系統的事件存儲中。它通常與CQRS模式結合使用,從存儲的事件創建物化視圖。雖然這種方法有幾個好處,比如完全可重放性、可審計性和原子性,但它不是銀彈。你最終會創建更複雜的系統,並被迫處理最終一致性。

關於作者

Anwesha Das是Zocdoc提供商團隊的一名工程師。此前,她曾在金融行業開發實時交易應用程序。她有物理學背景,對雲計算、系統架構和數據工程有著濃厚的興趣。

英文原文:

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