DoorDash如何使用 Apache Kafka 和 Elasticsearch 構建更快的索引?

banq發表於2021-07-21

保持愉快的線上訂購體驗包括確保大型搜尋索引在規模上保持有效。對於 DoorDash 來說,這是一個特別的挑戰,因為商店、商品和其他資料的數量每天都在增加。在這種負載下,重新索引所有更改並更新我們的搜尋資料庫可能需要長達一週的時間。 
我們需要一種快速的方法來索引我們平臺的所有可搜尋資料,以改進產品發現,確保我們為消費者提供所有可用的訂購選項。此外,該專案還將提高我們平臺上的實驗速度,因此我們可以更快地提高搜尋效能。 
我們的解決方案涉及構建一個新的搜尋索引平臺,該平臺在我們的資料來源上使用增量索引。我們基於三個開源專案構建了這個平臺,Apache KafkaApache FlinkElasticsearch
 

DoorDash 的搜尋索引問題 
我們的舊索引系統既不可靠也不可擴充套件,而且速度很慢。可靠的索引系統將確保商店和商品的變化實時反映在搜尋索引中。增量索引有助於更快地重新整理資料,構建新索引以在更短的時間內引入新的分析器和附加欄位,最終有助於改進檢索。
DoorDash 中來自新業務垂直領域的團隊希望建立自己的搜尋體驗,但在索引搜尋資料時不想重新發明輪子。因此,我們需要一個即插即用的解決方案來改善新的搜尋體驗,同時又不減慢這些垂直業務團隊的開發速度。 
 

為索引文件構建事件驅動的管道 
我們透過構建一個新的搜尋索引平臺來解決這些問題,該平臺提供快速可靠的索引以支援不同的垂直行業,同時還提高了搜尋效能和搜尋團隊的生產力。它使用 Kafka 作為訊息佇列和資料儲存,使用 Flink 進行資料轉換並將資料傳送到 Elasticsearch。

DoorDash如何使用 Apache Kafka 和 Elasticsearch 構建更快的索引?
上面的圖 1 顯示了我們搜尋索引管道中的各種元件。這些元件分為四個部分:

  • 資料來源:這些是對資料進行CRUD 操作的系統。我們稱它們為資料的真實來源。在我們的堆疊中,我們使用Postgres作為資料庫,使用Snowflake作為資料倉儲。 
  • 資料目的地:這是針對搜尋進行了最佳化的資料儲存。在我們的例子中,我們選擇了 Elasticsearch。
  • Flink 應用程式:我們在索引管道中新增了兩個自定義 Flink 應用程式,用於轉換資料的彙編器和用於將資料傳送到目標儲存的接收器。組裝人員負責組裝 Elasticsearch 文件中所需的所有資料。接收器負責根據模式塑造文件並將資料寫入目標 Elasticsearch 叢集。
  • 訊息佇列:我們使用 Kafka 作為我們的訊息佇列技術。上面圖 1 中的 Kafka 2 元件使用壓縮並無限期保留的日誌主題。

這些元件結合在一起構成了端到端的資料管道。使用 Kafka 將資料來源中的資料更改傳播到 Flink 應用程式。Flink 應用程式實現業務邏輯來管理搜尋文件並將其寫入目的地。現在我們瞭解了高階元件,讓我們來看看不同的索引用例。

 

索引更改資料捕獲 (CDC) 事件 
DoorDash 的商家資料不斷建立和更新,需要透過我們的索引管道解決方案來解決。例如,這些更新可以是從商家運營商向商店新增標籤到更新選單的任何內容。我們需要儘快將這些變化反映在消費者體驗上,否則消費者將在應用程式中看到陳舊的資料。這些對平臺的更新儲存在資料儲存中,例如PostgresApache Cassandra。迭代工作流還以每天的節奏處理資料倉儲中的資料,為商業智慧應用程式等提供動力。
為了從服務的資料庫中可靠地捕獲這些更新事件,我們探索了使用Debezium 聯結器Aurora /Postgres啟用變更資料捕獲(#CDC),Debezium 聯結器是一個紅帽開發的用於捕獲行級更改的開源專案。儲存團隊進行的初始效能測試表明,這種策略開銷太大,效能不佳,尤其是當服務使用相同的資料庫來服務線上流量時。因此,我們在應用程式中實現了儲存鉤子,它負責處理資料更新請求,每當底層資料儲存發生更改時,透過 Kafka 傳播更改事件。我們稱這種方法為應用級 CDC。
使用應用程式級 CDC,我們可能會遇到一致性問題。一個分散式應用程式有多個例項。可以透過兩個不同的例項提供兩個單獨的更新呼叫。如果我們在 Kafka 訊息中包含更新的值,則無法保證一致性並解決問題,因為在某些情況下,應用程式的多個例項會推送更新相同值的事件。 
例如,如果應用程式例項 #1 傳送一個事件 ,{store_id: 10, is_active=true}而應用程式例項 #2 傳送一個事件{store_id: 10, is_active=false},則在消費者端會發生衝突。
為確保一致性,我們僅在 Kafka 事件中傳送更改的實體 ID。收到 Kafka 事件後,我們的 Assembler 應用程式呼叫應用程式上的REST API 以收集有關 Kafka 事件中存在的實體的其他資訊。REST API 呼叫可確保有關實體的資料一致性。彙編器合併資訊以建立一個事件,該事件將其推送到 Kafka 以供 Sink 應用程式使用。彙編器實現了視窗化重複資料刪除,可防止在指定時間內多次呼叫同一實體的 REST API。彙編器還進行事件聚合,以便批次呼叫 REST 端點。例如,在 10 秒內,它會彙總商店的商品更新。它為該儲存呼叫 REST API,包括所有重複資料刪除和聚合的專案。
總而言之,我們使用應用程式級 CDC 來捕獲資料更改事件。我們透過簡化的事件和 REST API 解決一致性問題。我們使用重複資料刪除和視窗函式來最佳化事件處理。 
 

索引 ETL 資料
商店和商品文件的許多屬性對我們的檢索過程至關重要,例如機器學習模型生成的分數和標籤,每天批次更新一次。這些資料要麼是模型生成的,就像機器學習模型執行最新的資料一樣,要麼是人工策劃的,比如我們的操作員手動為特定商店用“雞”標記商品。在每晚執行相應的 ETL 作業後,這些資料會填充到我們資料倉儲中的表中。 
在使用新的搜尋索引平臺之前,我們沒有將資料上傳到索引的可靠方法,而是使用緩慢且不精確的解決方法。我們希望透過為我們的新搜尋索引平臺提供在 24 小時內將 ETL 資料可靠地攝取到我們的索引中的機制來改進我們現有的管道。 
ETL 用例的 CDC 模式與上一節中描述的增量更新案例非常不同。在增量更新的情況下,商家資料儲存不斷更新,導致一天中不斷更新。另一方面,對於 ETL 用例,當 ETL 執行時,更新會同時發生,在下一次執行之前沒有其他更新。
我們決定不對 ETL 源使用應用程式級 CDC 的變體,因為每次 ETL 執行時我們都會看到更新的大高峰,而這個高峰可能會給我們的系統帶來過度壓力並降低效能。相反,我們想要一種機制來在一段時間內分散 ETL 攝取,這樣系統就不會不堪重負。
作為前進的方向,我們開發了一個自定義 Flink 源函式,該函式定期將 ETL 表中的所有行分批流式傳輸到 Kafka,其中選擇批次大小以確保下游系統不會不堪重負。 
 

將文件傳送到 Elasticsearch
一旦 Assembler 應用程式將資料釋出到目標主題,我們就會有一個消費者讀取hydrated水合訊息,根據特定的索引模式轉換訊息,並將它們傳送到相應的索引。此過程需要管理架構、索引和叢集。我們為每個 ElasticSearch 索引維護一個唯一的 Kafka 消費者組,以便消費者可以維護每個索引的偏移量。為了轉換訊息,我們使用了一個 DocumentProcessor(s),它從目標主題接收一個水合事件並輸出準備好被索引的格式化文件。 
Sink 程式利用Flink Elasticsearch Connector將 JSON 文件寫入 Elasticsearch。開箱即用,它具有速率限制和節流功能,這對於在系統處於高寫入負載下時保護 Elasticsearch 叢集至關重要。該流程還支援批次索引,我們在一個時間視窗內收集所有文件和相關操作並執行批次請求。索引文件的任何失敗都會導致文件被記錄並儲存在死信佇列中,該佇列可以在以後處理。
 

快速回填新索引
通常,我們可能希望向索引新增新屬性,例如將與商店或商品關聯的市場 ID 新增到文件中,因為它有助於我們進行分片。同樣,我們可能需要快速重新建立一個新索引,例如當我們想嘗試不同的索引結構來執行效率基準時。 
在遺留系統中,我們依賴於一個緩慢且不可靠的工作,通常需要一個月的時間來重新索引所有商店和專案文件。鑑於索引持續時間長,很難正確估計與重新索引過程相關的錯誤率。因此,我們從未確定索引質量。我們經常收到關於索引和真實來源之間的商店詳細資訊不匹配的投訴,必須手動修復。
使用我們的新搜尋索引平臺,我們希望有一個流程能夠在 24 小時內快速重新建立新索引或回填現有索引中的屬性。對於引導過程,我們需要一種機制來快速重新建立所有需要在 Elasticsearch 中建立索引的文件。這個過程包括兩個步驟: 

  1. 流式傳輸與需要在 ElasticSearch 中索引的文件對應的所有實體 ID 
  2. 在將實體 ID 傳送到下游進行索引之前,透過進行外部呼叫將實體 ID 對映到其最終形式。 

將實體 ID 對映到實體的最終形式的管道已經建立,作為我們在上面提到的線上組裝器工作的一部分。因此,所需要的只是流式傳輸所有需要在 Elasticsearch 中建立索引的文件 ID。因此,我們維護需要在資料倉儲的引導表中索引的所有實體 ID 的最新副本。當我們需要引導時,我們使用 ETL 部分中描述的源函式將這些引導表中的所有行流式傳輸到 Kafka。我們封裝了在單個作業中執行上述兩個步驟的邏輯。
如果我們在引導管道的同時執行增量索引管道,我們就有在 Elasticsearch 中獲取過時資料的風險。為了避免這些問題,我們在每次載入程式執行時縮減增量索引器,並在載入程式完成後將其重新擴充套件。
綜上所述,我們回填和重新建立索引的步驟如下:
  • 建立索引並根據需要更新其屬性,並更新組裝器和接收器中的業務邏輯和配置以填充新屬性。
  • 縮小線上彙編程式。 
  • 擴大引導作業。
  • 載入程式完成後,縮減載入程式作業並擴充套件線上彙編程式。一旦偏移變為最近,引導過程就完成了。

 

啟用強制重新索引功能 
有時,我們在 Elasticsearch 中的一些文件可能有過時的資料,這可能是因為上游的某些事件沒有交付,或者我們的一項下游服務響應時間過長。在這種情況下,我們可以強制重新索引任何有問題的文件。 
為完成此任務,我們傳送一條訊息,其中包含要索引到線上組裝器從中獲取資料的主題中的實體的 ID。一旦訊息被消費,我們上面描述的索引管道就會啟動,每個文件都會在 Elasticsearch 中重新索引。
我們用唯一的標籤對一次性索引任務中傳送的訊息進行註釋,這為我們提供了文件在透過索引流的各個階段時的詳細跟蹤。除了向我們提供文件確實被索引的保證之外,它還為我們提供了豐富的除錯資訊,幫助我們驗證並幫助發現任何可能阻止它首先被索引的錯誤。
 

結果
我們新的搜尋索引平臺更可靠。增量索引速度有助於更快地重新整理資料,並在我們的消費者應用程式中更迅速地出現。更快的重新索引使我們能夠在短時間內構建新索引以改進我們的檢索: 

  • 將回填我們整個商店目錄的時間從一週縮短到 6.5 小時
  • 將回填我們整個專案目錄的時間從兩週減少到 6.5 小時
  • 將平臺上現有商店和商品重新編制索引的時間從 1 周減少到 2 小時

相關文章