轉自:https://yq.aliyun.com/articles/461770?spm=a2c4e.11163080.searchblog.129.49792ec1bgg2MF
目錄
4.4.2 將reduce join 轉化為map join
Apache Spark廣泛用于大規模數據處理方面,憑借支持交互查詢、流式計算的性能快速嶄露頭角。Spark需要快速的處理海量數據,由此針對Spark進行性能調優顯得十分必要。Spark調優包含眾多方面,本文中僅針對于:資源、程序開發、數據傾斜三方面的調優進行闡述。資源優化方面,針對特定生產環境設置集群配置參數;程序開發優化方面,對于RDD的使用、算子的使用、數據及數據結構等方面進行了闡述;數據傾斜優化方面,在簡單了解數據傾斜之后,提出了解決數據傾斜的三類方法:優化數據源、優化并行度、優化數據結構。
關鍵字:?Spark調優、數據傾斜、開發調優
Spark調優綜述
Apache Spark 是專為大規模數據處理而設計的快速通用的計算引擎,在非常短的時間內嶄露頭角,它的API更豐富,且支持交互式查詢、流式計算、機器學習等。與曾經引爆大數據產業革命的Hadoop Mapreduce相比它具有更快的速度。然而,想要讓Spark作業擁有更好的性能需要一定的技巧。如果沒有對于Spark作業進行合理的優化,那么Spark作業的執行速度可能大大降低,這樣Spark的速度優勢就不能完全體現。由此可見,對于Spark作業進行優化十分有必要。
想要對Spark作業進行優化,了解Spark作業的執行原理十分有必要。通過對于Spark作業的執行流程的分析,有助于做出適合的優化操作。同時,Spark的表現實際上是由很多方面決定的,對于Spark性能調優也是由很多部分組成,不是調節幾個參數就可以大幅度提升作業性能的。我們需要結合實際應用場景對Spark作業進行綜合分析,調節多個方面,才能獲得更好的性能。在本文中我們將對資源、開發、數據傾斜三個方面進行原理介紹及優化。
2.1?優化資源參數
了解了Spark作業的基本原理之后,對于資源相關的參數進行調優就比較容易理解了。對資源參數進行調優就是對Spark作業運行過程中的需要使用到資源的地方,通過調節各種參數,來優化資源的使用率,從而提升Spark作業的執行性能。
各個參數對應于作業運行原理中的某個部分。在Spark中我們有三種方式來設置資源參數,按照優先級排序依是:(1)用戶代碼中顯示調用set()方法設置;(2)通過Spark-submit傳遞參數;(3)配置文件。當以上三種方法均沒有設置參數的值時,Spark將使用系統默認值,下面對主要參數的配置進行產闡述。
(1)num-executors。該參數用于設計Spark作業總的Executor的個數。YARN集群管理器盡可能根據num-executor設置在工作節點上啟動Executor。這個參數十分重要,Spark默認只會啟動很少的進程,這時并行度不夠,任務執行速度十分緩慢。一般為每個Spark作業設置50~100個Executor,設置Executor太多大部分隊列無法給予充分的資源;設置Executor太少無法充分利用集群性能。
(2)executor-memory。這個參數設置每個Executor進程的內存,Executor內存的大小,很多程度上直接決定了Spark作業的性能,而且跟很常見的java虛擬機內存溢出異常也有關系。這里executor-memory最大可以設置為式(3.1)所示,其中rMemory表示資源隊列的最大內存限制。
executor-memory=rMemory/num-executor?? ???????公式(3.1)
(3)executor-core,該參數用于設置每個Executor進程的CPU core數量。因為每個CPU core同一時間只能執行一個task線程,所以executor-core的個數決定了Executor進程的并發線程能力。該參數設置為2-4比較合適。
(4)driver-memory,該參數用于設置Driver進程的內存。這個參數通常不設置,但是要注意的一點是,使用collect算子時,一定要保證Driver內存足夠大,否則會出現內存溢出的錯誤。
(5)Spark.default.parallelism,該參數用于設置每個Stage默認的task數量。該參數使用默認值時,Spark會根據底層HDFS的block數量設置task數量,通常一個block對應一個task,這樣task的數量通常是偏少的。由于task是真正執行Spark作業的線程,如果task數量太少,那么Executor中將面臨有足夠資源卻沒有偶task執行的窘境,針對Executor所做的優化也將前功盡棄。這里Spark.default.parallelism的大小可以用式(3.2)計算。
Spark.default.parallelism=[2,3]*num-executors * executor-cores ????公式(3.2)
(6)Spark.storage.memoryFraction,該參數用于設置RDD持久化數據在Executor內存中能占的比例,默認是0.6。也就是說,默認Executor 60%的內存,可以用來保存持久化的RDD數據。當Spark作業中有較多RDD需要進行持久化操作時,可以將該參數值調高;當Spark作業中有較少RDD需要進行持久化操作時,可以將該參數值調低。
(7)Spark.Shuffle.memoryFraction,該參數用于設置Shuffle過程中一個task拉取到上個Stage的task的輸出后,進行聚合操作時能夠使用的Executor內存的比例,默認是0.2。如果Spark作業中RDD持久化操作較少,Shuffle操作較多時,可以將該參數調高;如果Spark作業中RDD持久化操作較多,Shuffle操作較少時,可以將該參數調低。
在編寫Spark程序之初我們就要注意性能優化。實現同一個目的的Spark程序往往因為:使用的算子不同、RDD的復用程度不同、序列化方式不同等表現出性能方面的差異。開發調優就是優化RDD、優化算子、優化數據的過程,通過遵循開發調優的原則并將這些原則根據具體的業務應用到實際中,可能很好的實現Spark作業的性能提升。
3.1?優化RDD
3.1.1避免創建重復的RDD
通常來說,一個Spark作業就是對某個數據源創建RDD,然后對這個RDD進行轉化和行為操作。通過轉化操作,得到下一個RDD;通過行為操作,得到處理結果。在開發過程中需要注意,對于同一份數據,只應該創建一個RDD,不能創建多個RDD代表同一份數據。使用多個RDD代表同一份數據源時常常會增加作業的性能開銷,這些開銷包括:(1)創建新RDD的開銷;(2)從外部系統中加載數據到RDD中的開銷;(3)重復計算的開銷。
3.1.2?盡可能復用一個RDD
在對不同的數據執行算子操作時應該盡量復用一個RDD。例如,當RDD A的數據格式是key-value類型的,RDD B的數據格式是value類型的,但是這兩個RDD的value數據完全相同;那么,RDD A包含了RDD B中的所有信息,理論上來說RDD B可以被替代,而實際開發中也應該盡量減少多個RDD數據有重復或者包含的情況,這樣可以盡可能減少RDD的數量從而減少算子執行的次數。
3.1.3?對多次使用的RDD進行持久化
Spark使用懶惰計算,執行轉化操作時并不馬上執行命令而是維護一張記錄了執行RDD轉化關系的譜系圖,如圖3.1所示。每次同一個RDD執行多個算子運算時都會重新從源頭處計算一次,得到該RDD后,在對這個RDD執行算子操作,這種方式極大的損耗了Spark集群的資源。對于這種情況,應該對于多次使用的RDD進行持久化操作,持久化操作會將RDD數據保存到內存或者磁盤中,以后每次使用這個RDD時都不必重新計算,而是從內存或磁盤中直接取出該持久化RDD。
圖3.1 RDD轉化譜系圖
RDD的持有化有幾種不同的級別,分別是:MEMORY_ONLY、MEMORY_AND_DISK、MEMORY_ONLY_SER、MEMORY_AND_DISK_SER、DISK_ONLY、MEMORY_ONLY_2等,表3.1中對各種級別的含義進行了簡單的介紹。這幾種持久化級別使用的優先級排序如下:
(1)MEMORY_ONLY性能最高,直接將RDD存儲在內存中,省區了序列化及反序列化、從磁盤讀取的時間,從但是對于內存的容量有較高的要求;
(2)MEMORY_ONLY_SER會將數據序列化后保存在內存中,通過序列化壓縮了RDD的大小,但是相較于MEMORY_ONLY多出了序列化及反序列化的時間;
(3)MEMORY_AND_DISK_SER優先將RDD緩存在內存中,內存緩存不下時才會存在磁盤中;
(4)DISK_ONLY和后綴為_2的級別通常不建議使用,完全基于磁盤文件的讀寫會導致性能的極具降低;后綴為2的級別會將所有數據都復制一份副本到其他節點上,數據復制及網絡傳輸會導致較大的性能開銷。
表3.1 RDD持久化級別
持久化級別含義
MEMORY_ONLY使用未序列化的Java對象格式,將數據保存在內存中。如果內存不夠存放所有的數據,則數據可能就不會進行持久化。那么下次對這個RDD執行算子操作時,那些沒有被持久化的數據,需要從源頭處重新計算一遍。這是默認的持久化策略,使用cache()方法時,實際就是使用的這種持久化策略。
MEMORY_AND_DISK使用未序列化的Java對象格式,優先嘗試將數據保存在內存中。如果內存不夠存放所有的數據,會將數據寫入磁盤文件中,下次對這個RDD執行算子時,持久化在磁盤文件中的數據會被讀取出來使用。
MEMORY_ONLY_SER基本含義同MEMORY_ONLY。區別是會將RDD中的數據進行序列化,RDD的每個partition會被序列化成一個字節數組。這種方式更加節省內存,從而可以避免持久化的數據占用過多內存導致頻繁GC。
MEMORY_AND_DISK_SER基本含義同MEMORY_AND_DISK。區別是會將RDD中的數據進行序列化,RDD的每個partition會被序列化成一個字節數組。這種方式更加節省內存,從而可以避免持久化的數據占用過多內存導致頻繁GC。
DISK_ONLY使用未序列化的Java對象格式,將數據全部寫入磁盤文件中。
MEMORY_ONLY_2,
MEMORY_AND_DISK_2
對于上述任意一種持久化策略,如果加上后綴_2,代表的是將每個持久化的數據,都復制一份副本,并將副本保存到其他節點上。
3.2?優化算子
3.2.1?盡量避免使用Shuffle算子
Spark作業最消耗性能的部分就是Shuffle過程,應盡量避免使用Shuffle算子。Shuffle過程就是將分布在集群中多個節點上的同一個key,拉取到同一個節點上,進行聚合或者join操作,在操作過程中可能會因為一個節點上處理的key過多導致數據溢出到磁盤。由此可見,Shuffle過程可能會發生大量的磁盤文件讀寫的IO操作,以及數據的網絡傳輸操作,Shuffle過程如圖3.2所示。Shuffle類算子有:distinct、groupByKey、reduceByKey、aggregateByKey、join、cogroup、repartition等,編寫Spark作業程序時,應該盡量使用map類算子替代Shuffle算子。
圖3.2 Shuffle過程
3.2.1?使用高性能算子
Spark提供了幾十種算子,使用這些算子可以讓Spark作業不同的性能,在編寫Spark程序時應盡量使用性能高的算子替換性能低的算子。這里給出幾種算子的替換方案:
(1)使用mapPartitions替代普通map。mapPartition類的算子每次會處理一個分區的數據,而普通map每次只會處理一條數據。一次處理一個分區的數據節省了多次建立連接、多次打開數據流的時間;但是,mapPartitions也有可能會出現內存溢出問題,需要謹慎使用。
(2)foreachPartitions替代foreach。原理類似于“使用mapPartitions替代普通map”,foreachPartitions函數也是每次處理一個分區,而foreach函數每次只處理一條數據。比如在foreach函數中,將RDD中所有數據寫MySQL,那么如果是普通的foreach算子,就會一條數據一條數據地寫,每次函數調用可能就會創建一個數據庫連接,此時就勢必會頻繁地創建和銷毀數據庫連接,性能是非常低下;但是如果用foreachPartitions算子一次性處理一個partition的數據,那么對于每個partition,只要創建一個數據庫連接即可,然后執行批量插入操作,此時性能是比較高的。實踐中發現,對于1萬條左右的數據量寫MySQL,性能可以提升30%以上。
(3)使用filter之后進行coalesce操作。對于一個RDD使用filter進行過濾后,分區中的數據量可能會大為縮減,每個task任務處理的數據量大為減少,有些浪費資源,這時考慮將RDD的分區縮減。coalesce函數可以重新劃分分區,但是要注意使用該函數會引起Shuffle。
(4)使用repartitionAndSortWithinPartitions替代repartition與sort類操作。repartitionAndSortWithinPartitions函數是Spark官方網站推薦的一個函數,如果需要現對RDD進行分區操作然后排序,那么不必使用repartition與sort的組合,直接使用repartitionAndSortWithinPartitions函數性能會更好。因為該算子可以在分區的同時進行排序操作,Shuffle操作與sort操作同時進行。
3.3?優化數據
3.3.1?使用Kryo優化序列化性能
在一個Spark作業中,有三處涉及到了序列化:(1)在算子函數中使用到外部變量時,該變量將會被序列化后進行網絡傳輸;(2)將自定的類型作為RDD的泛型類型是,所有自定義類型對象都會進行序列化。此時,自定義的類必須要實現Serializable接口;(3)使用可序列化的持久化策略時,RDD的每個分區都會被序列化成為一個大的字節數組。
對于這三種涉及到序列化的地方,可以使用java提供的序列化機制,這也是Spark作業默認的序列化機制,但是這種序列化機制要保存全類名,效率較低。這里提供一種性能更好的序列化方法:Kryo序列化類庫,這種方法較java序列化方法速度快了10倍作業。但是使用Kryo時需要自行注冊需要序列化的自定義類,比較有難度。
3.3.2?優化數據結構
在java中有三種類型比較耗費內存:(1)對象;(2)字符串;(3)集合類型。因此Spark編碼時應盡量不要使用以上三種數據結構。盡量使用字符串代替對象;使用原始類型代替字符串;使用數組代替集合,這樣可以減少內存的占用,降低垃圾回收的頻率提高性能。
4.1走近數據傾斜
4.1.1?數據傾斜的原因
數據傾斜是在進行數據計算時,數據分散度不夠大量數據集中到少數機器上計算,這些數據的計算速度遠遠低于平均計算速度,導致整個計算過程過慢。發生數據清晰時常有以下現象:(1)Executor lost,OOM,Shuffle過程出錯;(2)Driver OOM;(3)單個Executor執行時間特別久,整體任務卡在某個階段不能結束;(4)正常運行的任務突然失敗。
數據傾斜的產生的原理十分簡單:在Spark作業進行Shuffle時會將個個節點上相同的key拉取到某個節點的一個task上進行操作,此時,如果某一個key對應的數據量特別大時,該key對應的task就要處理非常龐大的數據量,這就發生了數據傾斜。通過圖4.1可以很好的理解這一過程:在三個節點上,hello對應7條數據,world對應1條數據,you對應1條數據,執行Shuffle操作時,第一個task需要處理7條數,其運行時間遠大于其他兩個task。由于木桶效應,整個Stage的運行速度是由運行最慢的task決定的。
圖4.1 Shuffle過程中數據傾斜的產生
4.1.2?定位數據傾斜的位置
數據傾斜現象只會發生在Shuffle過程中,當出現數據傾斜時應檢查代碼中可能會觸發Shuffle操作的算子。對于不同的數據傾斜現象,有不同的定位方法:
(1)某個task執行特別慢的情況
對于這種情況,首先要確定數據傾斜發生在第幾個Stage中。如果是用yarn-client模式提交,那么本地是直接可以看到log的,可以在log中找到當前運行到了第幾個Stage;如果是用yarn-cluster模式提交,則可以通過Spark Web UI來查看當前運行到了第幾個Stage。此外,無論是使用yarn-client模式還是yarn-cluster模式,我們都可以在Spark Web UI上深入看一下當前這個Stage各個task分配的數據量,從而進一步確定是不是task分配的數據不均勻導致了數據傾斜。
知道數據傾斜發生在哪一個Stage之后,接著根據Stage劃分原理,推算出來發生傾斜的那個Stage對應代碼中的哪一部分。精準推算Stage與代碼的對應關系,需要對Spark的源碼有深入的理解,這里有一個相對簡單實用的推算方法:只要看到Spark代碼中出現了一個Shuffle類算子或者是Spark SQL的SQL語句中出現了會導致Shuffle的語句,那么就可以判定,以此為界限劃分出了前后兩個Stage。
(2)程序異常報錯
這種情況比較容易定位有問題的代碼,可以直接查看yarn-client模式下本地log的異常信息,或通過yarn-cluster模式下的log中的異常信息。一般來說,通過異常棧信息就可以定位到你的代碼中哪一行發生了內存溢出。然后在那行代碼附近找找,一般也會有Shuffle類算子,此時很可能就是這個算子導致了數據傾斜。
要注意的是,不能單純靠偶然的內存溢出就判定發生了數據傾斜。因為代碼的bug、偶然出現的數據異常等,也可能會導致內存溢出。通過Spark Web UI查看報錯的那個Stage的各個task的運行時間以及分配的數據量,才能確定是否是由于數據傾斜才導致了這次內存溢出。
4.2?優化數據源
4.2.1?使用Hive ETL預處理數據
當Spark作業的數據來自Hive,且Hive表中數據不均勻,即少量的key對應了大多數的數據時,對于Hive中的數據進行處理是解決數據傾斜的一種辦法。
該方法的思路是在Spark作業之前先對Hive中的數據進行聚合、join等處理,然后Spark作業處理的數據就是解決了數據傾斜問題的數據。該方法從根本上解決了數據傾斜,但是這也是一種危機轉移方案,雖然Spark作業避免了數據傾斜,但是在Hive中的預操作中依舊存在數據傾斜問題。當對于Spark作業的實時性要求很高時可以采用這種方案,將數據傾斜提前在上游的Hive ETL中完成,周期內僅執行一次,周期內其他時間的操作都會提速。
4.2.2?過濾少數導致傾斜的key
當導致數據傾斜的key很少,且少量的key對作業結果影響并不大,那么過濾掉少數導致傾斜的key是一種不錯的處理方法。
如果我們判斷那少數幾個數據量特別多的key,對作業的執行和計算結果不是特別重要的話,那么干脆就直接過濾掉那少數幾個key。比如,在Spark SQL中可以使用where子句過濾掉這些key或者在Spark Core中對RDD執行filter算子過濾掉這些key。如果需要每次作業執行時,動態判定哪些key的數據量最多然后再進行過濾,那么可以使用sample算子對RDD進行采樣,然后計算出每個key的數量,取數據量最多的key過濾掉即可。這種方法實現簡單,效果也很好,可以完全規避掉數據傾斜。但是在實際應用場景中,導致數據傾斜的key往往較多,所以該方法適用范圍較小。
4.3?優化集群并行度
4.3.1?提高Shuffle操作的并行度
當必須要正面解決數據傾斜問題時,該方案較為適合,這也是處理數據傾斜最簡單的方法之一。
通過增加Shuffle read task的數量,可以讓原本分配給一個task的key分配給多個task,從而讓每個task處理比原來更少的數據,原理如圖4.2所示。這種方案實現起來比較簡單,可以有效的緩解數據傾斜的影響。但是這種方法通常無法從根本解決問題,因為如果有一些極端情況出現,如:某個key對應的數據量占整個數據集的90%,那么該key所對應的90%的數據還是會分配到一個task中,這是數據傾斜現象還是是產生。
圖4.2?提高Shuffle操作的并行度
4.4?優化算法
4.4.1?兩階段聚合
對RDD執行reduceByKey等聚合類Shuffle算子或者在Spark SQL中使用group by語句進行分組聚合時,比較適用這種方案。將原本相同的key通過附加隨機前綴的方式,變成多個不同的key,就可以讓原本被一個task處理的數據分散到多個task上去做局部聚合,進而解決單個task處理數據量過多的問題。接著去除掉隨機前綴,再次進行全局聚合,就可以得到最終的結果。
這種方案的核心思想是將會產生數據傾斜的一次聚合作業,分為兩個階段進行聚合。第一次聚合,先為每個key標記一個隨機數,隨機數范圍為[0,n],此時一個key被分為n份,對標記后的key進行局部聚合;第二個階段,將局部聚合后的key所標記的隨機數去除,然后在對key進行聚合。最終,Spark聚合作業就完成了,具體原理如圖4.3所示。這種方案對于聚合類的Shuffle操作導致的數據傾斜效果很不錯,但是僅僅適用于聚合類的操作。
圖4.3?兩階段聚合原理圖
4.4.2?將reduce join 轉化為map join
本方案適合于以下情況使用:RDD中使用join類型的操做或Spark SQL中使用join語句,且join操作中的一個RDD表數據量較小。普通的join是會走Shuffle過程的,而一旦Shuffle,就相當于會將相同key的數據拉取到一個Shuffle read task中再進行join,此時就是reduce join。但是如果一個RDD是比較小的,則可以采用廣播小RDD全量數據+map算子來實現與join同樣的效果,也就是map join,此時就不會發生Shuffle操作,也就不會發生數據傾斜。
本方案使用廣播變量與map類算子代替了join操作,從而完全規避掉Shuffle操作,徹底避免了數據傾斜的發生和出現。首先,將較小的RDD中的數據通過collect算子拉去到Driver端內存中,然后將該RDD的數據通過Broadcast變量廣播出去;然后,對另一個RDD執行map操作,在算子函數內,從Broadcast變量中獲取較小RDD的全量數據,與當前RDD的每一條數據按照連接key進行比對,如果連接key相同的話,那么就將兩個RDD的數據用你需要的方式連接起來,具體原理如圖4.4所示。本方案對于join操作導致的數據傾斜十分有效,但是本方案僅僅局限于執行join操作的兩個RDD中有一個數據量較小時。
圖4.4?將reduce join?轉化為map join
4.4.3?采樣傾斜key并分拆join操作
本方案適合于以下情況使用:RDD中使用join類型的操做或Spark SQL中使用join語句,且兩個RDD數據集的數據量都比較大,且出現數據傾斜的原因是一個RDD中少數幾個key的數據量過大,另一個RDD的key分布均勻。對于join導致的數據傾斜,如果只是某幾個key導致了傾斜,可以將少數幾個key分拆成獨立RDD,并附加隨機前綴打散成n份去進行join,此時這幾個key對應的數據就不會集中在少數幾個task上,而是分散到多個task進行join。
本方案的操作過程如下:
(1)對包含少數幾個數據量過大的key的那個RDD,通過sample算子采樣出一份樣本來,然后統計一下每個key的數量,計算出來數據量最大的是哪幾個key;
(2)將這幾個key對應的數據從原來的RDD中拆分出來,形成一個單獨的RDD A,并給每個key都打上n以內的隨機數作為前綴,而不會導致傾斜的大部分key形成另外一個RDD B;
(3)將需要join的另一個RDD也過濾出傾斜key對應的數據并形成一個單獨的RDD C,將每條數據膨脹成n條數據,這n條數據按順序附加一個0~n的前綴,不會導致傾斜的大部分key形成另外一個RDD D;
(4)將RDD A與RDD C進行join,此時導致數據傾斜的key分成n份,分散到多個task中去進行join,得到RDD E;
(5)將RDD B和RDD D進行join操作,得到RDD F;
(6)對RDD E與RDD F執行union操作,得到 RDD G,RDD G即為最終結果。
以上操作步驟可以用圖4.5來表示,該方案針對于某幾個key導致的數據傾斜十分有效,只需要針對少數導致數據傾斜的key進行擴容n倍,不需要對全量數據進行擴容,避免了占用過多內存;但對于導致數據傾斜的key的數量特別多時,這種方案是無能為力的。
圖4.5?采樣傾斜key并
分拆join操作
4.4.4?使用隨機前綴和擴容RDD進行join
本方案適合于以下情況使用:RDD中使用join類型的操做或Spark SQL中使用join語句,且兩個RDD數據集的數據量都比較大,且出現數據傾斜的原因是一個RDD中有大量的key導致數據傾斜。該方案將導致數據傾斜的RDD中的所有key均附加隨機前綴,然后將處理后的key分散到不同的key中進行處理,而不是讓一個task處理大量的數據。該方案與4.4.3小節方案類似,但是本方案需要更多的內存資源。
本方案的操作過程如下:
(1)查看RDD/Hive表中的數據分布情況,找到那個造成數據傾斜的RDD/Hive表;
(2)將該RDD的每條數據都打上一個n以內的隨機前綴;
(3)對另外一個正常的RDD進行擴容,將每條數據都擴容成n條數據,擴容出來的每條數據都依次打上一個0~n的前綴;
(4)將兩個處理后的RDD進行join。
該方案對于所有join類型的數據傾斜都可以處理,效果較好,同時本方案更注重與緩解數據傾斜,而不是徹底避免數據傾斜,對于內存資源的要求很高。
Spark性能優化是一項繁復的任務,需要結合實際生產情況對于多個方面級進行優化,僅僅對于某一方面的調整很難取得集群性能上巨大的提升。本文對于Spark調優主要的三個方面:資源優化、開發程序優化、數據傾斜優化進行了闡述,并在每個部分給出了常見的調優方法。在撰寫本文的過程中,我對于Spark整體有了更深入的理解,希望能夠砥礪前行。