DoorDash使用 Kafka 和 Flink 構建可擴充套件的實時事件處理
在 DoorDash,實時事件是深入瞭解我們業務的重要資料來源,但構建能夠處理數十億實時事件的系統具有挑戰性。事件由我們的服務和使用者裝置生成,需要處理並傳輸到不同的目的地,以幫助我們在平臺上做出資料驅動的決策。舉幾個用例:
- 幾乎所有的事件都需要傳輸到我們的OLAP資料倉儲進行業務分析。例如,Dasher 分配團隊(Dasher 是我們對送貨司機的稱呼)依靠資料倉儲中的分配資料來檢測其分配演算法中的任何錯誤。
- 一些事件將由下游系統進一步處理。例如,我們的 ML 平臺處理交付事件以生成實時特徵,例如最近餐廳的平均等待時間。
- 一些移動事件將與我們的時間序列指標後端整合以進行監控和警報,以便團隊可以快速識別最新移動應用程式版本中的問題。例如,我們的 DoorDash 消費者應用程式中的任何結帳頁面載入錯誤都需要傳送到Chronosphere進行監控。
我們的遺留系統是如何工作的 從歷史上看,DoorDash 有一些資料管道,它們從我們遺留的單體 Web 應用程式中獲取資料,並將資料攝取到我們的主要資料倉儲Snowflake中。每個管道的構建方式不同,只能處理一種事件,並且在資料最終進入資料倉儲之前涉及多個躍點。示例如圖 1 所示:
圖 1:DoorDash 的舊資料管道
這種方法有幾個問題:
- 構建多個試圖實現類似目的的管道是成本低效的。
- 混合不同型別的資料傳輸並透過多個訊息傳遞/排隊系統而沒有精心設計可觀察性會導致操作困難。
這些導致高資料延遲、高成本和運營開銷。 介紹 Iguazu 我們的事件處理系統 兩年前,我們開始建立一個名為 Iguazu 的實時事件處理系統,以取代傳統的資料管道,並隨著資料量隨著業務的增長而滿足我們預期的以下事件處理需求:
- 異構資料來源和目的地:從各種資料來源中提取資料,包括傳統的單體 Web 應用程式、微服務和移動/Web 裝置,並交付到不同的目的地,包括第三方資料服務。將可靠且低延遲的資料攝取到資料倉儲中是重中之重。
- 易於訪問:使不同團隊和服務能夠輕鬆利用資料流並構建自己的資料處理邏輯的平臺。
- 端到端模式實施和模式演變:模式不僅提高了資料質量,而且還促進了與資料倉儲和 SQL 處理的輕鬆整合。
- 可擴充套件、容錯且易於操作的小型團隊:我們希望構建一個可以輕鬆擴充套件以滿足業務需求且運營開銷最小的系統。
為了實現這些目標,我們決定將戰略從嚴重依賴 AWS 和第三方資料服務轉變為利用可以定製並更好地與 DoorDash 基礎設施整合的開源框架。Apache Kafka和Apache Flink等流處理平臺在過去幾年中已經成熟並變得易於採用。這些是我們可以用來自己構建東西的優秀構建塊。
在過去的兩年裡,我們構建了實時事件處理系統,並將其擴充套件為每天處理數千億個事件,交付率達到 99.99%。系統的整體架構如下圖 2 所示。在以下部分中,我們將詳細討論系統的設計以及我們如何解決一些主要的技術挑戰:
- 使用統一的 API 和事件格式簡化事件釋出,以避免採用瓶頸
- 為不同型別的資料消費者消費事件提供多種抽象
- 在基礎架構即程式碼環境中使用 Github 自動化和工作流程自動化入職流程
圖2:Iguazu 整體系統架構圖
簡化和最佳化事件製作 構建處理系統的第一步是選擇處理事件的最佳技術和方法。我們選擇Apache Kafka作為我們的流資料釋出/訂閱系統,因為 Kafka 已被證明是統一異構資料來源同時提供高吞吐量和高效能的出色解決方案。 利用和增強 Kafka Rest Proxy 我們希望每個 DoorDash 服務都能夠輕鬆地為 Kafka 生成事件。一個明顯的選擇是建立一個 Kafka 客戶端並將其與所有服務整合。但是,這種方法有一些缺點:
- 每個服務都需要配置 Kafka 連線,這對於不熟悉 Kafka 的團隊來說可能會出現問題並減慢採用速度
- 跨不同服務的統一和最佳化的 Kafka 生產者配置將很困難
- 對於移動和 Web 應用程式,直接連線到 Kafka 是不可行的
因此,我們決定利用Confluent 的開源 Kafka Rest Proxy進行事件生成。代理為我們提供了一箇中心位置,我們可以在其中增強和最佳化事件生成功能,而無需與客戶端應用程式協調。它透過 HTTP 介面提供對 Kafka 的抽象,消除了配置 Kafka 連線的需要,並使事件釋出更加容易。由於跨不同客戶端例項和應用程式的批處理能力,記錄批處理對於降低代理的 CPU 利用率至關重要,它也得到了顯著改進。 Kafka REST 代理提供了我們需要的所有開箱即用的基本功能,包括:
- 支援不同型別的有效負載格式,包括 JSON 和二進位制檔案
- 支援批處理,這對於減少客戶端應用程式和 Kafka 代理的處理開銷至關重要
- 與Confluent 的模式登錄檔整合,可以使用模式驗證和轉換 JSON 有效負載
最重要的是,我們根據需要定製了其餘代理,並新增了以下功能:
- 生產多個 Kafka 叢集的能力。生成多個叢集的能力對我們來說至關重要,因為我們的事件迅速擴充套件到多個 Kafka 叢集,並且事件到叢集的對映是我們系統中的一個重要抽象。
- 非同步 Kafka 產生請求。使用非同步生產無需在響應客戶端請求之前先獲得代理的確認。相反,響應會在有效負載經過驗證並新增到 Kafka 生產者的緩衝區後立即發回。此功能大大減少了響應時間,並提高了批處理和系統的整體可用性。雖然當 Kafka 不可用時,此選項可能會導致少量資料丟失,但透過代理端生產者重試和密切監視從代理收到的確認來抵消風險。
- 預取 Kafka 主題後設資料並生成測試 Kafka 記錄,作為 Kubernetes pod 就緒探測的一部分。此增強將確保代理 pod 將預熱所有快取和 Kafka 連線,以避免冷啟動問題。
- 支援 Kafka 標頭作為代理生成請求負載的一部分。我們的事件有效負載格式依賴於事件信封,它是生產記錄中 Kafka 標頭的一部分。我們將在後面的部分中詳細介紹我們的事件有效負載格式和序列化。
最佳化生產者配置 雖然 Kafka 的預設配置適用於需要高一致性的系統,但對於吞吐量和可用性很重要的非事務性事件釋出和處理來說,它並不是最有效的。因此,我們微調了 Kafka 主題和代理生產者的配置,以實現高效率和吞吐量:
- 我們使用 2 的複製因子和一個最小的同步副本。與三個副本的典型配置相比,這節省了磁碟空間並降低了 broker 在複製時的 CPU 使用率,同時仍然提供了足夠的資料冗餘。
- 生產者配置為在分割槽的領導者(而不是追隨者)持久化資料後立即接收來自代理的確認。這種配置減少了生產請求的響應時間。
- 我們透過在 50 毫秒到 100 毫秒之間設定一個合理的逗留時間來利用 Kafka 的粘性分割槽器,這顯著提高了批處理並降低了代理的 CPU 利用率。
總而言之,此調整將 Kafka 代理 CPU 利用率降低了 30% 到 40%。 在 Kubernetes 中執行 Rest 代理 事實證明,Kafka REST 代理很容易在我們自己的 Kubernetes 基礎架構中構建和部署。它作為內部服務構建和部署,並利用了 DoorDash 基礎設施提供的所有 CI/CD 流程。基於 CPU 利用率在服務上啟用Kubernetes 水平 pod 自動縮放。這顯著降低了我們的運營開銷並節省了成本。
圖 3:顯示 Kubernetes 水平 pod 自動縮放對 Kafka Rest Proxy 生效
既然我們描述了簡化和高效的事件生成,讓我們在下一節中關注我們為促進事件消費所做的工作。 不同抽象的事件處理 如開頭所述,伊瓜蘇的一個重要目標是建立一個便於資料處理的平臺。Flink 的分層 API架構完全符合這個目標。
Data Stream API 和 Flink SQL 是我們支援的兩個主要抽象。 我們選擇 Apache Flink 還因為它的低延遲處理、基於事件時間的處理的原生支援、容錯以及與各種源和接收器的開箱即用整合,包括 Kafka、Reddis(透過三分之一-party OSS)、ElasticSearch 和 S3。
在 Kubernetes 中使用 Helm 進行部署 我們的平臺提供了一個基本的 Flink docker 映象,其中包含與 DoorDash Kubernetes 基礎設施的其餘部分良好整合的所有必要配置。
Flink 的高可用性設定和 Flink 指標是開箱即用的。為了更好的故障隔離和獨立擴充套件的能力,每個 Flink 作業都以獨立模式部署,作為一個單獨的 Kubernetes 服務。 在使用資料流 API 開發 Flink 應用程式時,工程師首先會克隆一個 Flink 應用程式模板,然後新增自己的程式碼。
應用程式和 Flink 作業的配置(如並行度和工作管理員計數)將在terraform模板中定義。
在構建過程中,將使用組合的應用程式 jar 檔案和我們內部的 Flink 基礎 docker 映象建立一個 docker 映象。部署過程將同時採用 terraform 模板和應用程式 docker 映像,並根據生成的Helm Chart將應用程式部署在我們的 K8s 叢集中的獨立 Flink 作業叢集中。該過程如下圖所示:
圖 4:使用 terraform 和 Helm 構建和部署 Flink 應用程式的過程
提供 SQL 抽象 雖然 Flink 的資料流 API 對於後端工程師來說並不難理解,但對於資料分析師和其他臨時資料使用者來說,它仍然是一個主要障礙。
對於這些使用者,我們提供了一個平臺來使用 SQL 以宣告方式建立 Flink 應用程式,而無需擔心基礎設施級別的細節。該工作的詳細資訊可在此部落格中找到。
在開發基於 SQL 的應用程式時,所有必要的處理邏輯和接線都被捕獲在一個YAML檔案中,該檔案非常簡單,每個人都可以閱讀或創作。YAML 檔案捕獲了許多高階抽象,例如,連線到 Kafka 源並輸出到不同的接收器。以下是此類 YAML 檔案的示例: 要建立 Flink 作業,使用者只需要使用 YAML 檔案建立 PR。PR 合併後,將啟動 CD 管道並將 YAML 檔案編譯為 Flink 應用程式並進行部署。 在以上兩節中,我們介紹了在伊瓜蘇發生和消費的事件。但是,如果沒有統一的事件格式,生產者和消費者仍然很難相互理解。在下一節中,我們將討論作為生產者和消費者之間協議的事件格式。
事件格式、序列化和模式驗證 從一開始,我們就為在Iguazu 伊瓜蘇製作和處理的事件定義了統一的格式。
統一的事件格式大大降低了消費事件的門檻,減少了事件生產者和消費者之間的摩擦。 所有事件都有一個標準的信封和有效負載。信封包含事件的上下文(例如建立時間和來源)、後設資料(包括編碼方法)和對模式的引用。有效負載包含事件的實際內容。
信封還包括作為 JSON blob 的非模式化事件屬性。信封中的這個 JSON 部分有助於使部分事件模式變得靈活,其中的更改不涉及正式的模式演變過程。 從內部微服務產生的事件有效負載將經過模式驗證和編碼。無效的有效載荷將直接在生產者端丟棄。對於從移動/Web 裝置產生的事件,它們採用原始 JSON 格式,我們使用單獨的流處理應用程式進行模式驗證和轉換為模式化格式以供下游流程使用。
我們為事件生產者和消費者建立了序列化/反序列化庫,以便與這種標準事件格式進行互動。在 Kafka 中,事件信封儲存為 Kafka 記錄頭,有效負載儲存為記錄值。對於事件消費者,我們的庫將解碼 Kafka 標頭和值並重新建立事件以供消費者處理。
圖 5:顯示從 Kafka 記錄到 Event 的反序列化以及 Event 的概念如何在儲存 (Kafka) 與應用程式執行時中表示
我們幾乎所有的微服務都是基於GRPC和Protobuf 的。所有事件都由 Protobuf 在集中共享的 Protobuf Git 儲存庫中定義。在 API 級別,我們對 Event 的定義是對 Protobuf 訊息的包裝,以使我們的微服務易於使用。
但是,對於大多數事件的最終目的地,Avro格式仍然比 Protobuf 得到更好的支援。對於這些用例,我們的序列化庫負責將 Protobuf 訊息無縫轉換為 Avro 格式,這要歸功於Avro 的 protobuf 庫,並在需要時轉換回 Protobuf 訊息。
我們大量利用 Confluent 的模式登錄檔進行通用資料處理。所有事件都在模式登錄檔中註冊。隨著最近在 Confluent 模式登錄檔中引入的 Protobuf 模式支援,我們實現了使用 Protobuf 和 Avro 模式進行通用資料處理的能力。
我們一開始面臨的一個挑戰是我們如何強制和自動化模式註冊。我們不想在生成事件時在執行時註冊模式,因為:
- 它會在某個時間顯著增加模式更新請求,從而導致模式登錄檔的可伸縮性問題。
- 任何不相容的模式更改都會導致模式更新失敗和客戶端應用程式的執行時錯誤。
相反,最好在構建時註冊和更新模式,以減少更新 API 呼叫量,並有機會在週期的早期捕獲不相容的模式更改。
我們建立的解決方案是將模式登錄檔更新整合為我們集中式 Protobuf Git 儲存庫的 CI/CD 流程的一部分。在拉取請求中更新 Protobuf 定義時,CI 流程將使用模式登錄檔驗證更改。如果是不相容的更改,CI 過程將失敗。在 CI 透過併合並拉取請求後,CD 程式實際上將使用模式登錄檔註冊/更新模式。CI/CD 自動化不僅消除了手動模式註冊的開銷,而且還保證:
- 在構建時檢測不相容的架構更改,以及
- 已釋出的 Protobuf 類二進位制檔案與模式登錄檔中的模式之間的一致性。
在上面的部分中,我們透過 Iguazu 中的統一事件格式討論了事件生成、消費和它們的繫結。在下一節中,我們將以低延遲和容錯的方式描述 Iguazu 與其最重要的資料目的地 - 資料倉儲的整合。
資料倉儲整合 正如文章開頭提到的,資料倉儲整合是Iguazu 伊瓜蘇的主要目標之一。
Snowflake 仍然是我們主要的資料倉儲解決方案。我們希望事件以強一致性和低延遲交付給 Snowflake。 資料倉儲整合是作為一個兩步過程實現的。
在第一階段,來自 Kafka 的 Flink 應用程式使用資料並以Parquet檔案格式上傳到 S3。此步驟有助於將攝取過程與 Snowflake 本身分離,因此任何與 Snowflake 相關的故障都不會影響流處理,並且鑑於 Kafka 的保留有限,資料可以從 S3 回填。
此外,在 S3 上擁有 Parquet 檔案可以啟用資料湖解決方案,我們稍後透過內部Trino安裝對此進行了探索。 上傳資料到 S3 的實現是透過 Flink 的StreamingFileSink完成的。
在作為 Flink 檢查點的一部分完成 Parquet 檔案的上傳時,StreamingFileSink 保證了強一致性和一次性交付。StreamingFileSink 還允許在 S3 上進行自定義分桶,我們利用它在 S3 目錄級別對資料進行分割槽。這種最佳化大大減少了下游批處理器的資料載入時間。
在第二階段,資料透過Snowpipe從 S3 複製到 Snowflake 。由 SQS 訊息觸發,Snowpipe 可以在 S3 檔案可用時立即載入資料。Snowpipe 還允許在複製過程中進行簡單的資料轉換。鑑於其宣告性,與流處理相比,它是一個很好的選擇。 一個重要的注意事項是,每個事件都有自己的用於 S3 上傳的流處理應用程式和自己的 Snowpipe。因此,我們可以為每個事件單獨擴充套件管道並隔離故障。 到目前為止,我們介紹了資料是如何從客戶端到資料倉儲的端到端流動的。在下一節中,我們將討論伊瓜蘇的運營方面,並瞭解我們如何使其自助服務以減輕運營負擔。
努力打造自助平臺 如上所述,為了實現故障隔離,Iguazu 中的每個事件都有自己的從 Flink 作業到 Snowpipe 的管道。然而,這需要更多的基礎設施設定,並使操作成為挑戰。 一開始,將新賽事加入Iguazu 伊瓜蘇是一項需要大量支援的任務。DoorDash 嚴重依賴於基礎設施即程式碼原則,大部分資源建立,從 Kafka 主題到服務配置,都涉及到不同terraform儲存庫的拉取請求。這使得自動化和建立高階抽象成為一項具有挑戰性的任務。請參閱下圖,瞭解加入新活動所涉及的步驟。
圖 6:編排複雜步驟以建立從服務到 Snowflake 的管道的自動化到位
為了解決這個問題,我們與我們的基礎架構團隊合作,建立了正確的拉取審批流程,並使用 Git 自動化來自動化拉取請求。本質上,建立了一個 Github 應用程式,我們可以在其中以程式設計方式建立和合並來自我們作為控制器的服務之一的拉取請求。我們還利用了Cadence 工作流引擎,並將該流程實現為可靠的工作流。整個自動化將事件啟動時間從幾天縮短到幾分鐘。 為了使其更接近自助服務,我們使用Retool框架建立了 UI,供使用者探索模式並從那裡載入事件。使用者可以使用正規表示式搜尋架構,為事件選擇正確的架構,啟動工作流以建立必要的資源和服務,並有機會自定義 Snowflake 表和 Snowpipe。
我們 Iguazu 的最終目標是使其成為一個自助服務平臺,使用者可以在其中透過正確的抽象和最少的支援或人工干預自行參與活動。
圖 7:顯示模式探索 UI,使用者可以在其中深入瞭解主題和版本。
圖 8:顯示用於檢視/自定義 Snowflake 表架構和 Snowpipe 的 UI
學習和未來的工作 我們發現建立具有平臺思維方式的事件處理系統很重要。不同技術相互疊加的臨時解決方案不僅效率低下,而且難以擴充套件和運營。
選擇正確的框架並建立正確的構建塊對於確保成功至關重要。Apache Kafka、Apache Flink、Confluent Rest Proxy 和 Schema 登錄檔被證明具有可擴充套件性和可靠性。研究和利用這些框架的最佳點顯著減少了開發和操作這個大規模事件處理系統所需的時間。
為了使系統使用者友好且易於採用,需要正確的抽象。從宣告式 SQL 方法、事件的無縫序列化到高階使用者引導 UI,我們努力使其成為一個簡單的過程,以便我們的使用者可以專注於他們自己的業務邏輯,而不是我們的實現細節。
我們還想在Iguazu 伊瓜蘇實現很多目標。我們已經開始了在Iguazu 伊瓜蘇之上構建客戶資料平臺的旅程,我們可以輕鬆地以自助方式轉換和分析使用者資料。會話化是我們想要解決的一個重要用例。
為了能夠會話化大量資料,我們開始透過利用 Kubernetes StatefulSet、持久化卷和探索使用Spinnaker的新部署方式來增強對 Flink 狀態處理的支援。與資料湖更好地整合是我們前進的另一個方向。結合 SQL 抽象和正確的表格式,從流處理應用程式直接訪問資料湖可以使用同一應用程式進行回填和重放,並提供另一種替代方案Lambda 架構。
相關文章
- 使用 .NET Core 構建可擴充套件的實時資料處理系統套件
- 使用Kafka和Flink構建實時資料處理系統Kafka
- 實時資料處理:Kafka 和 FlinkKafka
- DoorDash如何使用 Apache Kafka 和 Elasticsearch 構建更快的索引?ApacheKafkaElasticsearch索引
- 使用 Postgres 的全文搜尋構建可擴充套件的事件驅動搜尋架構套件事件架構
- 讀構建可擴充套件分散式系統:方法與實踐14流處理系統套件分散式
- 優步是如何使用Apache Flink和Kafka實現實時Exactly-Once廣告事件處理?ApacheKafka事件
- Flink SQL 在快手的擴充套件和實踐SQL套件
- 讀構建可擴充套件分散式系統:方法與實踐15可擴充套件系統的基本要素套件分散式
- 讀構建可擴充套件分散式系統:方法與實踐09可擴充套件資料庫基礎套件分散式資料庫
- 使用 Python 構建可擴充套件的社交媒體情感分析服務Python套件
- 如何構建可控,可靠,可擴充套件的 PWA 應用套件
- 讀構建可擴充套件分散式系統:方法與實踐07無伺服器處理系統套件分散式伺服器
- 使用 Zephir 輕鬆構建 PHP 擴充套件PHP套件
- 圖片處理擴充套件 Grafika 的簡單使用套件
- 一個簡單的 PHP 時間處理擴充套件PHP套件
- 構建高可用性、高效能和可擴充套件的Zabbix Server架構套件Server架構
- 使用Kafka分割槽擴充套件Spring Batch大資料排程批處理 – ArnoldKafka套件SpringBAT大資料
- 使用KEDA和Kafka在 Kubernetes 上自動擴充套件 - PiotrKafka套件
- Django與微服務架構:構建可擴充套件的Web應用Django微服務架構套件Web
- 圖片處理擴充套件 Intervention/image 的簡單使用套件
- 用擴充套件的方式在 PHP 中使用 Kafka套件PHPKafka
- MemQ:可替代Kafka的高效、可擴充套件的雲原生PubSub系統MQKafka套件
- 03 Windows批處理的作用域和延遲擴充套件Windows套件
- 如何使用Zebee構建高度可擴充套件的分散式工作流中介軟體?套件分散式
- Django內建許可權擴充套件案例Django套件
- 可擴充套件性套件
- 使用Storm、Kafka和ElasticSearch處理實時資料 -javacodegeeksORMKafkaElasticsearchJava
- 讀構建可擴充套件分散式系統:方法與實踐08微服務套件分散式微服務
- 實用的可選項(Optional)擴充套件套件
- dubbo是如何實現可擴充套件的?套件
- kotlin 擴充套件(擴充套件函式和擴充套件屬性)Kotlin套件函式
- [譯] 如何使用原生 JavaScript 構建簡單的 Chrome 擴充套件程式JavaScriptChrome套件
- Swift---協議和擴充套件、 錯誤處理、泛型Swift協議套件泛型
- 如何構建一個優雅擴充套件套件
- 可擴充套件的使用者表設計套件
- 簡要剖析:可擴充套件的微服務架構套件微服務架構
- 谷歌的三大可擴充套件核心架構谷歌套件架構