在數據領域可用的框架中,只有少數框架在採用和交付方面達到了 Spark 的水平。顯然,該框架已經成為一個贏家,特別是在數據工程方面。本文是對 Spark 組件的一個非常簡單的介紹,其主要目的是提供對 Spark 架構的一般理解。
本文最初發佈於 Towards Data Science 博客,由 InfoQ 中文站翻譯並分享。
為什麼要了解 Spark?
在數據領域可用的框架中,只有少數框架在採用和交付方面達到了 Spark 的水平。顯然,該框架已經成為一個贏家,特別是在數據工程方面。
如果你正在閱讀這篇文章,這意味著你已經理解了我這樣說的原因,所以我們直接進入主題。
為什麼要了解 Spark 的內部構造?
有人可能會說,開車並不需要了解發動機的工作原理,是這樣。不過,有人可能會說,瞭解發動機會讓你成為更好的駕駛員,因為你將能夠了解整個車輛的性能、侷限性和根本問題。
同理,你不需要了解 Spark 的內部構造就可以使用它提供的 API。但是,如果你瞭解的話,就可以減輕糟糕的性能和隱藏的 Bug 所帶來的許多痛苦。此外,你還將掌握在整個分佈式系統領域隨處可見的概念。
方法
在我看來,學習有兩個方面:
知識 和 技術 。前者涉及到通過書本、結構化課程等形式獲取知識。它更關注 是什麼 。後者與技能有關,即“邊做邊學”,更側重於 如何做 。這是我們這裡要探討的。我們將從每個初學者都能解決的一個簡單問題開始,然後逐步演進以說明 Spark 的架構設計。在這個過程中,我們還將瞭解 HDFS (部分人稱為 Hadoop),因為它是一個非常適合 Spark 的平臺。
為了做到語言無關,本文使用的所有代碼都是偽代碼。
問題
你剛入職並分得了一項簡單的任務: 數一數數組中有多少個偶數 。
你將從存儲在本地文件系統中的 CSV 文件中讀取該數組。不用多想,你可能會寫出下面這段代碼:
新需求一
客戶對上述解決方案的巨大成功感到滿意,現在,他們認為可以把所有問題都交給你,所以他們要求你 計算這些偶數的平均值 。
你肯定知道 SOLID 原則,特別是 單一責任原則 ,即類或方法應該只因為一個原因更改。然而,你決定打破規則,像下面這樣實現:
新需求二
由於你做得快,人們又提出了另一個需求: 返回所有偶數的和 。
這時,你不僅開始考慮 SOLID 原則,而且開始考慮事情進行的方式。你知道,通常情況下,一件事發生了一次並不意味著它會發生兩次,但如果它發生了兩次,第三次發生就在眼前。因此,你開始考慮實現更容易擴展的東西,並記起了 面向對象編程 中封裝的概念。
另外,如果你實現了適當的抽象,那麼當另一個需求出現時,你甚至可能不必更改你的實現。
一套可以處理所有這些需求的抽象
你開始考慮,如果他們讓你數偶數,那麼他們很可能會進一步問你奇數,或者是低於或高於某一個值的數,或在一個範圍內的數,等等。因此,即使你是 YAGNI (你並不需要它)應用程序方面的專家,也會決定實現一些能夠支持所有這些情況的東西。
最後,你可以得出結論,所有這些操作都與從數組中過濾值有關,因此,你決定提供一個過濾器函數,它可以接收過濾器條件,而不是編寫每種可能用到的過濾器。
此外,為了簡化設計,你決定在每次調用對象操作時更改其狀態。
接受新挑戰
你做到了。現在,你不僅實現了所有需求,而且還可以處理從數組中過濾值的新需求。如果客戶現在想要奇數而不是偶數,那麼它們唯一要做的就是向
filter 方法傳遞一個新條件,這樣就行了。真神奇!但是,你一直在等待的新需求來了: 他們現在需要你處理一個 3TB 的數組 。你考慮放棄。你自己的硬盤只有 500GB,所以你需要 6 臺你這樣的機器專門用於存儲文件才能開始這項工作。但你的客戶喜歡你,也很有說服力,他們給你加薪,還承諾提供 30 臺新機器用於解決問題,而不是 6 臺。
劃分
有了 30 臺新機器的使用權,你開始考慮如何解決這個問題。一臺機器無法包含整個文件,因此你必須將其切成更小的塊,然後分塊放到新硬盤中。另外,由於你有足夠的資源,作為備份,你還可以將相同的切片存儲在多臺機器上。也許每個切片兩個拷貝,這意味著你可以在三個不同的地方找到這個切片。
你格式化硬盤,開始複製文件。在這個過程中,你認為,將所有切片保存在每臺機器上遵循同一規範的父文件夾下是個不錯的主意,並且要為每個切片加上一個前綴標識符,這和它屬於更大文件的哪一部分有關。你還認為,在至少兩臺機器中另有一個目錄,其中包含一些元數據,描述哪些目錄包含切片以及對於每個切片 id 哪些機器中包含其備份,也是一個好主意。
由於有些機器將只包含數據,而有些機器只包含提供方向和名稱的元數據,因此,你將前者稱為數據機器,後者稱為名字機器。但是,由於你實際上是在創建一個網絡,所以將機器稱為節點更合適,因此,你將數據機器命名為 數據節點,將元數據機器命名為 名字節點 。
在給事物命名的過程中,你會意識到,切片與蛋糕和奶酪的聯繫比與數據塊的聯繫更緊密。你感到非常有啟發性和創造性,因此決定給這些切片起一個更好的名字: 分區 。因此,無論你的程序最終變成什麼,它都會將整個文件劃分為多個分區來處理。
在所有這些命名和決策之後,你會得到這樣的結果:
克服挑戰
現在,你已經將文件劃分為跨一組節點(從現在開始,我們將創造性地稱其為 集群 )的分區,並且有備份和元數據,可以幫助你的程序找到每個分區及其備份。既然移動分區沒有任何意義,那麼問題就變成了: 在每臺機器上執行同一段代碼都會得到同一個結果嗎?
你應該在每次需要運行程序時將整個程序發送到每臺機器上嗎?或者,你應該準備好程序的一些部分,只需要把客戶編寫的部分發給它們?後者聽起來更好,所以你選了它。在這個過程中,第一個要求是讓你的 ArrayOperator 類在每臺機器上都可用,且只發送特定於 main 方法的部分。
你還希望要運行的代碼儘可能接近數據,因此,數據節點也必須運行程序。從這個角度來看,節點不僅要存儲數據,還要執行實際工作,因此,你決定將它們稱為工作者(
worker )。代碼的某些部分也可以並行運行。例如,對於上面的程序, average() 、 sum() 和 size() 可以並行執行,因為它們是相互獨立的。要實現這一特性,工作者需要支持獨立的執行線,所以你決定將每個工作者轉換成某種守護進程,由它生成獨立執行任務的進程(與此同時,你意識到, 任務 這個名稱足以指代每個可以獨立執行的單元)。你仍然很受啟發,於是決定創造性地將那些執行任務的進程稱為執行器( executor )。
現在,你所要做的是設計你的主方法(它會訪問客戶代碼),由它推動將客戶代碼分割成組成作業的任務,然後詢問名字節點哪個數據節點包含該文件的哪個分區,然後並行將任務發送給工作者機器,它會準備好啟動執行任務並返回結果的執行器。因為這段代碼將推動整個過程,同樣,你決定創造性地稱它為驅動器( driver )。
驅動器還需要找個方法將所有結果彙總在一起。在本例中,它需要將從每個工作者接收到的所有總數相加。但考慮到目前為止取得的進展,這只是小菜一碟。
總而言之,驅動器將協調任務完成作業。這又是你的想象。要描述一組任務,還有哪個名稱比作業( job )更合適?
作出突破
經過幾個晚上,你終於把所有的部件組裝在一起了。多麼了不起的壯舉!經過測試,一切都符合預期。你急切地想要做一個演示,在投入了大量的資金之後,客戶也同樣渴望看到這樣的演示。
演示開始時,你先是表揚了自己(這是應該的),然後繼續解釋架構。你的客戶會更加興奮。你運行程序,可一切都分崩離析,因為你的 5 臺機器離線了,2 臺是因為內核崩潰,2 臺是因為硬盤故障,還有 1 臺是因為一個未測試的特性導致了錯誤。除了你,每個人都哭了。客戶失去了信心,但你毫不動搖。實際上,你又表揚了自己一次,因為你已經把一切都弄清楚了。這些問題並不是偶然的。
你承諾一週內會重新做一次演示。客戶離開的時候相當的暴躁,並且有點憂傷,但你堅持了下去。
由於每個分區包含兩個其他的副本,並且你有 28 臺機器(記住,你為名字節點留了 2 臺),如果 5 臺機器的故障就導致整個集群宕機,那麼你很不走運。
但是,如何利用冗餘呢?可以肯定的一點是,它應該在驅動器端啟動,因為它負責與所有節點通信。如果你有一個節點失敗,驅動器就會首先注意到。為了在作業啟動時找出分區的位置,驅動器已經與名字節點建立了聯繫,因此,它可能還可以從名字節點那裡獲取失敗的工作者 / 數據節點中的所有副本的位置。有了這些信息,它就可以重新發送要在副本上執行的任務,這樣就完成了!
使用前面的方法,你用一種 富有彈性 的方式對 分佈式數據 進行了 分佈式處理 。
就這樣去做吧。
嶄新的開始
你打電話給客戶,要求重新做一次演示。他們還是假裝很沮喪,但幾乎無法掩飾他們的興奮。他們來看你,他們進來,說著關於上次的笑話。而你只聽到了“藍屏”,卻不太在意其中的笑點。
在開始之前,你做了一件令人震驚的事情:你要求他們隨機關閉兩個工作者 / 數據節點。他們看起來很驚訝,但情緒高昂(看他們帶著狡黠的微笑著隨意地選擇機器,試圖打敗你,這很有趣)。
去掉兩個節點後,你就開始演示了,效果非常好。他們哭了,但這次是不同的眼淚。他們向你祝賀,為不相信你而道歉,再次為你提供加薪,當然,還有一個新需求: 數組現在將包含具有多個屬性的對象,而不是數字 。更具體地說,這些記錄將包含姓名、年齡和工資,他們想知道叫 Felipe 的人的平均年齡和最高工資是多少。他們還希望保存結果,以便以後可以訪問,而不需要再處理。
你一點也不驚訝。
錦上添花
在這個時候,你不必想太多。你一直都在做抽象,所以現在的問題是更上一層樓。
你放棄了以前的設計,改成了下面這個樣子:
在新的設計下,你現在可以處理任何類型的記錄(這就是為什麼你將其名稱改為 GeneralOperator )。
這真是太神奇了!想想看。你有一個系統,可以讀、寫和處理任何類型的數據集,並且是用一個分佈式的富有彈性的方式。往大了說,你可以聲稱自己擁有一個支持任何類型數據集的 分佈式彈性 框架。
你感覺到了手中的力量,但你認為,魔法的核心 GeneralOperator 名字不夠吸引人,或者至少不是那麼一目瞭然。你沒有更好的想法,所以你決定稱它為彈性分佈式數據集閱讀器、寫入器和處理器( Resilient and Distributed Datasets Reader, Writer and Processor )。但這太長了。也許可以用縮略詞,比如 RDDRWP?哎,這更糟。那麼只用 RDD 呢?易於發音,聽起來也不錯,就這樣了。
小結
目前為止,你已經完成了如下工作:
- 你已經設計了一種基礎設施,它以分佈式方式存儲複製的數據分區,它由保存數據的 數據節點 和包含有關元數據的 名字節點 組成(難道它們不應該有自己的名稱嗎? HDFS 怎麼樣?)
- 你創建了一個稱為 彈性分佈式數據集 (簡稱 RDD )的結構,它可以讀寫和處理存儲在 Hadoop 集群中的數據。
- 你已經設計了一個基礎設施,通過 工作者 (控制執行任務的節點)和 執行器 (實際執行任務)在分佈式分區上並行執行 任務 。
- 你設計了一個 驅動器 應用程序,它可以將客戶提供的 作業 分解為多個 任務 ,通過與名字節點通信找出分區的位置,並將任務發送給遠程工作者。
夥計,你真棒!但你創造的一切不值得有一個好名字嗎?你想法很多,火花一個接一個。是的,
Spark !聽起來像個名字!擴展
你創造的這些東西當然有很大的價值,但它可能有一個陡峭的學習曲線。另一方面,很長一段時間以來(可能太長了), 結構化查詢語言 ( SQL )一直是用來處理數據的語言。把這種能力加入 Spark 怎麼樣?
讓我們和客戶聊聊。
注意
上面是對 Spark 組件的一個非常簡單的介紹,其主要目的是提供對 Spark 架構的一般理解。簡便起見,我有意省略了與 Catalyst、調度、類型轉換、Shuffling、計劃、資源分配、專用 API 方法等相關的內容。我將在以後的文章中討論。
查看英文原文:
https://towardsdatascience.com/understand-spark-as-if-you-had-designed-it-c9c13db6ac4b
關注我並轉發此篇文章,私信我“領取資料”,即可免費獲得InfoQ價值4999元迷你書!