介紹 Corral:一個無伺服器的 MapReduce 框架
這篇文章給出了一個我們最新專案的技術概述和架構設計理由,corral —— 一個無服務的 MapReduce 框架。
我最近在用 Hadoop 和 Spark 為一個我幫助教學的班級工作。PySpark 的確很棒,但是 Hadoop MapReduce 我從來沒有真正關注,直到我發現 ofollow,noindex" target="_blank">mrjob 。MapReduce 的觀念是極為強大的,但是大量的樣板檔案需要用 Java 編寫,甚至是一個簡單的 Hadoop 作業,在我看來那是不必要的。
Hadoop 和 Spark 也需要了解一些基礎設施知識。一些服務像 EMR 和 Dataproc 使其更方便,但是需要很大的成本。
曾經有些關於使用 Lambda 做為 MapReduce 平臺的謠言。AWS 釋出了一個(有限的)參考框架結構,並且有些企業解決方案好像也採用了這種方法。但是,我沒有找到一個完全使用這個方法的開發的開源專案。
與此同時,AWS 宣佈本地 Go 對 Lambda 的支援。Go 的啟動時間短,易於部署(即單二進位制包),和通用的速度使其成為該專案的理想選擇。
我的想法是:使用 Lambda 作為一個執行環境,類似 Hadoop MapReduce 使用 YARN。本地驅動程式協調函式呼叫,S3 用於資料儲存。
這是 corral 的結果,一個用於編寫可在 AWS Lamb da 中執行的任意 MapReduce 應用程式框架。
MapReduce 的 Golang 介面
眾所周知,Go 沒有泛型,所以我不得不為 mappers 和 reducers 構建一個令人信服的介面而動些腦筋。Hadoop MapReduce 在指定輸入/輸出格式,分割記錄的方式等方面有很大的靈活性。
我之前考慮用 interface{} 型別做為健和值,但用 Rob Pike 的話 說,“interface{} 什麼也沒說”。所以我決定使用極簡主義介面:keys 和 values 都用字串,輸入檔案按換行符分割。這些簡化假設使整個系統的實現更簡單和清晰。Hadoop MapReduce 贏得可定製性,因此我決定採用易用性。
我很滿意 Map 和 Reduce 的最終介面(其中一些是受 Damian Gryski 的 dmrgo 啟發):
type Mapper interface { Map(key, value string, emitter Emitter) } type Reducer interface { Reduce(key string, values ValueIterator, emitter Emitter) } type Emitter interface { Emit(key, value string) error }
ValueIterator
只有一個方法: Iter()
,迭代一系列字串。
Emitter
和 ValueIterator
隱藏了需要內部框架實現(改組,分割槽,檔案系統互動等)。我也很高興決定對值用迭代器來代替普通的切片(這可能更慣用),因為迭代器允許框架方面更加的靈活(例如:延遲流值而不是全部放入記憶體)。
無服務 MapReduce
從框架方面,我花了些時間來決定用一個高效的方式將 MapReduce 實現為一個完全無狀態的系統。
Hadoop MapReduce 架構為其帶來以下好處……
- 持久,長時間執行的工作節點
- 資料區域性性在工作節點
- 通過 YARN/Mesos 等作為抽象的,容錯的主節點和工作節點容器。
使用 AWS 堆疊可以很容易地複製最後兩方面。S3 和 Lambda 之間的頻寬相對不錯(至少對我而言),而 Lambda 的構建使得開發人員“不必考慮伺服器”。
在 Lambda 上覆制最棘手的事情是持久工作節點。Lambda 有最大5分鐘的超時時限。因此,Hadoop 使用 MapReduce 的很多方式都不再適用。例如,在 mapper worker 和 reducer worker 之間直接傳輸資料是不可行的,因為 mapper 需要“儘快”完成。否則,在 reducer 仍在工作時,您可能會冒 mapper 超時的風險。
這種限制在 shuffle/partition 階段最明顯。理想情況下,mappers 將“生存”足夠長的時間以按需將資料傳輸到 reducers(即使在 map 階段),並且 reducers 將“活”足夠長時間去做一個完整的二級排序,使用它們的磁碟當對一個較大的合併排序溢位時。5分鐘的上限使得這些方法難以實現。
最後,我決定使用 S3 作為無狀態 partition/shuffle 的後端。
對 mapper 輸出使用友好的字首名稱,可以方便 reducers 輕鬆選擇它們需要讀取的檔案。
處理輸入資料顯然更為直接。與 Hadoop MapReduce 一樣,輸入檔案被拆分為塊。Corral 將這些檔案塊分組為“輸入箱”,並且每個 mapper 讀取/處理一個輸入箱。輸入拆分和容器大小是可以根據需要進行配置的。
自發布應用
Corroal 讓我最興奮的一點是,它能夠自我部署到 AWS Lambda。我希望能夠快速將 corral 作業部署到 Lambda 上——不得不通過 web 介面手動將釋出包重新上傳到 Lambda 上是一種拖累,而像 Serverless 這樣的框架依賴於非 Go 工具,這些工具包含起來很繁瑣。
我最初的想法是,構建 corral 二進位制檔案作為釋出包上傳到 Lambda 上。這個想法確實有效……直到您處理跨平臺構建目標時。Lambda 期望使用 GOOS=linux
編譯二進位制檔案,因此任何二進位制檔案在 macOS 或 Windows 上不能執行。
我幾乎放棄了這個想法,但後來我偶爾發現了 Kelsey Hightower 在2017年的GopherCon上釋出的 Self Deploying Kubernetes Applications 。Kelsey 描述了一個類似的方法,儘管他的程式碼是在 Kubernetes 而不是 Lambda 上執行的。但是,他描述了我需要的“缺失連結”:讓特定平臺的二進位制檔案重新編譯為目標 GOOO=linux。
因此,總而言之,corral 用於部署到 Lambda 的過程如下:
- 使用者編譯針對其所選平臺的 corral 應用程式。
- 在執行時,corral app 為 GOOS=linux 重新編譯自己,並將生成的二進位制檔案壓縮為 zip 檔案。
- 然後 Corral 上傳 zip 檔案到 Lambda,建立一個 Lambda 函式。
- Corral 呼叫此 Lambda 函式作為 map/reduce 任務的執行程式。
通過在執行時對環境進行一些巧妙的檢查,Corral 能夠使用與驅動程式和遠端執行程式完全相同的原始碼。如果二進位制檔案檢測到它在 Lambda 環境中,則它會監聽呼叫請求;否則它表現正常。
順便說一句,自我上傳或自我重新編譯應用程式的想法對我來說是相當興奮的。我記得當我上計算機理論課時,“自嵌入”程式的概念(通常在不可判斷性證明的上下文中引用)是很有趣的。但我想不到你確實想要一個程式使用內部反射的案例。
在某種程度上,自我釋出應用程式是這個想法的實際例子。它是一個自我重編,上傳到雲上的程式,並遠端呼叫自己(儘管通過不同的程式碼路徑)。夠整潔!
一旦部署後,這個 corral 上傳到 Lambda 的二進位制檔案有條件地表現為 Mapper 或 Reducer,具體取決於它的呼叫輸入。您在本地執行的二進位制檔案在 Map/Reduce 階段保持執行並呼叫 Lambda 函式。
系統中的每個元件都執行相同的源,但有很多並行副本執行在 Lambda 上(由驅動協調)。這導致 MapReduce 快速的並行。
像檔案系統一樣對待S3
像 mrjob 一樣,Corral 試圖與它執行到檔案系統無關。這允許它在本地和 Lambda 執行之間透明地切換(並允許擴充套件空間,例如,如果 GCP 在雲函式上開始支援 Go)。
但是,S3 不是一個真正的檔案系統;他使一個物件儲存。像檔案一樣使用 S3 需要一點聰明。例如,當讀取輸入分割時,corral 需要尋找檔案的某個部分並開始閱讀。預設情況下,對 S3 的 GET 請求將返回整個資料。當您的物件可能幾十千兆位元組時,這並不是很好。
幸運的是,您可以在 S3 的 GET 請求中設定一個 API/RESTObjectGET.html#RESTObjectGET-requests-request-headers" rel="nofollow,noindex" target="_blank">Range 請求頭 以接收物件塊。Corral 的 S3FileSystem 利用這一點,根據需要下載一個物件塊。
寫入 S3 也需要一些思考。對於較小的上傳,一個標準的“PUT Object”請求就可以。對於較大的上傳, 分段上傳 變得更具吸引力。分段上傳允許功能等同於寫入本地檔案;您將資料流傳輸到檔案,而不是將其保留在記憶體中一次寫入所有內容。
令我驚訝的是,沒有一個出色的 S3 客戶端提供 io.Reader 和 io.Writer 介面。 s3gof3r 是我能找到最接近的了;它非常出色,但(以我的經驗)洩露了太多記憶體,我無法在記憶體有限的 Lambda 環境中使用它。
在 Lambda 上管理記憶體
雖然 AWS Lambda 在過去幾年中一直在增長,但感覺缺少分析 Lambda 函式的 工具。預設,Lambda 把日誌記在 Cloudwatch。如果您的函式報錯,則錯誤堆疊會被記錄下來。因此,“崩潰”錯誤除錯相對簡單。
但是,如果您的函式耗盡記憶體或時間,您看到的是這樣:
REPORT RequestId: 16e55aa5-4a87-11e8-9c63-3f70efb9da7eDuration: 1059.94 msBilled Duration: 1100 ms Memory Size: 1500 MB Max Memory Used: 1500 MB
在本地,像 pprof 這樣的工具非常適合瞭解記憶體洩露的來源。但在 Lambda 你就沒那麼好運了。
在 corral 早期的版本中,我花了幾個小時追蹤由 s3gof3r 引起的記憶體洩露。由於 Lambda 容器 可以重複使用,即使很小的記憶體洩露也會導致最終的故障。換句話說,記憶體使用在呼叫中持續存在——漏洞抽象(沒有雙關語)。
能看到為 AWS Lambda 的分析工具真是太棒了,特別是因為 Golang 是一個對分析非常容易的執行環境。
當 Lambda 變的昂貴
顯然,corral 的目標是對 Hadoop MapReduce 提供一個便宜,快速的選擇。AWS Lambda 是便宜的,所以這是一個大扣籃,對吧?
是,不是。Lambda 的免費等級每月為您提供 400,000 GB/秒。這聽起來很多,但是長時間執行的應用程式很快就會用完。
最終,corral 仍然能非常便宜。但是,您需要調整應該程式以儘可能少使用記憶體。在 corral 設定最大記憶體上限盡可能降低成本。
在 AWS Lambda 時間是一個 as-you-use-it 資源——您需要支付的使用時間為毫秒。記憶體是通過 use-it-or-lose-it 計費的。如果您設定最大記憶體為 3GB 但僅用了 500MB ,您仍然需要為全部 3GB 記憶體付費。
表現
雖然不是主要的設計考慮因素,但 corral 的表現相對可觀。其中很大一部分原因歸功於 Lambda 提供的幾乎無限的並行性。我使用 Amplab 的"大資料基準" 來了解一下 corral 的表現。這個基準測試基本的過濾,聚合和連線。
正如我所料,corral 在過濾和聚合方面做得相當好。然而,它在連線方面表現平平。沒有 二級排序 ,連線變得昂貴。
Amplab 基準測試可測高達大約 125GB 的輸入資料。我很好奇用大約 1TB 的資料做更多的基準測試,看看效能是否會線性地或多或少的變化。
在 corral 示例資料夾 中能找到更多資訊和基準測試統計。
結論
就是這樣:corral 讓您編寫一個簡單的 MR 作業,無摩擦地將其釋出到 Lambda ,並在 S3 的資料集上執行該作業。
值得注意的是,我沒有與 AWS 生態系統結合。Corral 與 Lambda 和 S3 毫無關係,因為將來可以新增 GCP 的雲函式和資料儲存的聯結器(if/when GCP 新增 CF 支援 Go)。
就個人而言,我發現這個專案對工作相當有益。構建 MapReduce 系統的線上資源要少於使用它的,因此有一些實現細節需要解決。
隨意在 corral 庫 中提出問題。我很想知道這個專案是否有足夠的市場來證明有必要持續發展。:smile: