網易視頻雲技術分享:一個SparkSQL的作業的一生

來源:互聯網
上載者:User

標籤:


網易視頻雲是網易傾力打造的一款雲端式計算的分布式多媒體處理叢集和專業音視頻技術,提供穩定流暢、低時延、高並發的ApsaraVideo for Live、錄製、儲存、轉碼及點播等音視頻的PAAS服務,線上教育、遠程醫學、娛樂秀場、線上金融等各行業及企業使用者只需經過簡單的開發即可打造線上音視頻平台。現在,網易視頻雲的技術專家給大家分享一則技術文:一個SparkSQL的作業的一生。

Spark是時下很火的計算架構,由UC Berkeley AMP Lab研發,並由原班人馬建立的Databricks負責商業化相關事務。而SparkSQL則是Spark之上搭建的SQL解決方案,主打互動查詢情境。

人人都說Spark/SparkSQL快,各種Benchmark滿天飛,但是到底Spark/SparkSQL快麼,或者快在哪裡,似乎很少有人說得清。因為Spark是基於記憶體的計算架構?因為SparkSQL有強大的最佳化器?本文將帶你看一看一個SparkSQL作業到底是如何執行的,順便探討一下SparkSQL和Hive On MapReduce比起來到底有何其別。

SQL On Hadoop的解決方案已經玲琅滿目了,不管是元祖級的Hive,Cloudera的Impala,MapR的Drill,Presto,SparkSQL甚至Apache Tajo,IBM BigSQL等等,各家公司都試圖解決SQL互動情境的效能問題,因為原本的Hive On MapReduce實在太慢了。

那麼Hive On MapReduce和SparkSQL或者其他互動引擎相比,慢在何處呢?讓我們先看看一個SQL On Hadoop引擎到底如何工作的。

現在的SQL On Hadoop作業,前半段的工作原理都差不多,類似一個Compiler,分來分去都是這基層。

小紅是資料分析,她某天寫了個SQL來統計一個分院系的加權均值分數匯總。

SELECT dept, avg(math_score * 1.2) + avg(eng_score * 0.8) FROM studentsGROUP BY dept;

其中STUDENTS表是學生分數表(請不要在意這個表似乎不符合範式,很多Hadoop上的資料都不符合範式,因為Join成本高,而且我寫表介紹也會很麻煩)。

她通過網易大資料的猛獁系統提交了這個查詢到某個SQL On Hadoop平台執行,然後她放下工作,切到視頻網頁看一會《琅琊榜》。

在她看視頻的時候,我們的SQL平台可是有很努力的工作滴。

首先是查詢解析。

這裡和很多Compiler類似,你需要一個Parser(就是著名的程式員約架專用項目),Parser(確切說是Lexer加Parser)的作用是把一個字串流變成一個一個Token,再根據文法定義產生一棵抽象文法樹AST。這裡不詳細展開,童鞋們可以參考編譯原理。比較多的項目會選ANTLR(Hive啦,Presto啦等等),你可以用類似BNF的範式來寫Parser規則,當然也有手寫的比如SparkSQL。AST會進一步封裝成一個簡單的基本查詢資訊對象,這個對象包含了一個查詢基本的資訊,比如基本語句的類型是SELECT還是INSERT,WHERE是什麼,GROUP BY是什麼,如果有子查詢,還需要遞迴進去,這個東西大致來說就是所謂的邏輯計劃。

TableScan(students)

-> Project(dept, avg(math_score * 1.2) + avg(eng_score * 0.8))

->TableSink

上面是無責任示意,具體到某個SQL引擎會略有不同,但是基本上都會這麼幹。如果你想找一個代碼乾淨易懂的SQL引擎,可以參考Presto(可以算我讀過的開原始碼寫的最漂亮的了)。

到上面為止,你已經把字串轉換成一個所謂的LogicalPlan,這個Plan距離可以求值來說還比較殘疾。最基本來說,我還不知道dept是個啥吧,math_score是神馬類型,AVG是個什麼函數,這些都不明了。這樣的LogicalPlan可以稱為Unresolved(殘疾的)Logical Plan。

缺少的是所謂的中繼資料資訊,這裡主要包含兩部分:表的Schema和函數資訊。表的Schema資訊主要包含表的列定義(名字,類型),表的物理位置,格式,如何讀取;函數資訊是函數簽名,類的位置等。

有了這些,SQL引擎需要再一次遍曆剛才的殘廢計劃,進行一次深入的解析。最重要的處理是列引用綁定和函數綁定。列引用綁定決定了一個運算式的類型。而有了類型你可以做函數綁定。函數綁定幾乎是這裡最關鍵的步驟,因為普通函數比如CAST,和彙總函式比如這裡的AVG,分析函數比如Rank以及Table Function比如explode都會用完全不同的方式求值,他們會被改寫成獨立的計劃節點,而不再是普通的Expression節點。除此之外,還需要進行深入的語義檢測。比如GROUP BY是否囊括了所有的非彙總列,彙總函式是否內嵌了彙總函式,以及最基本的類型相容檢查,對於強型別的系統,類型不一致比如date = ‘2015-01-01’需要報錯,對於弱類型的系統,你可以添加CAST來做Type(類型) Coerce(苟合)。

然後我們得到了一個尚未最佳化的邏輯計劃:

TableScan(students=>dept:String, eng_score:double, math_score:double)

->Project(dept, math_score * 1.2:expr1, eng_score * 0.8:expr2)

->Aggregate(avg(expr1):expr3, avg(expr2):expr4, GROUP:dept)

->Project(dept, expr3+expr4:avg_result)

->TableSink(dept, avg_result->Client)

所以我們可以開始上肉戲了?還早呢。

剛才的計劃,還差得很遠,作為一個SQL引擎,沒有最佳化怎麼好見人?不管是SparkSQL還是Hive,都有一套最佳化器。大多數SQL on Hadoop引擎都有基於規則的最佳化,少數複雜的引擎比如Hive,擁有基於代價的最佳化。規則最佳化很容易實現,比如經典的謂詞下推,可以把Join查詢的過濾條件推送到子查詢預先計算,這樣JOIN時需要計算的資料就會減少(JOIN是最重的幾個操作之一,能用越少的資料做JOIN就會越快),又比如一些求值最佳化,像去掉求值結果為常量的運算式等等。基於代價的最佳化就複雜多了,比如根據JOIN代價來調整JOIN順序(最經典的情境),對SparkSQL來說,代價最佳化是最簡單的根據表大小來選擇JOIN策略(小表可以用廣播分發),而沒有JOIN順序交換這些,而JOIN策略選擇則是在隨後要解釋的物理執行計畫產生階段。

到這裡,如果還沒報錯,那你就幸運滴得到了一個Resolved(不殘廢的)Logical Plan了。這個Plan,再配上運算式求值器,你也可以折騰折騰在單機對錶查詢求值了。但是,我們不是做分布式系統的嗎?資料分析妹子已經看完《琅琊榜》的標題了,你還在悠閑什麼呢?

為了讓妹子在看完電視劇之前算完幾百G的資料,我們必須藉助分布式的威力,畢竟單節點算的話夠妹子看完整個琅琊榜集了。剛才產生的邏輯計劃,之所以稱為邏輯計劃,是因為它只是邏輯上看起來似乎能執行了(誤),實際上我們並不知道具體這個東西怎麼對應Spark或者MapReduce任務。

邏輯執行計畫接下來需要轉換成具體可以在分布式情況下執行的物理計劃,你還缺少:怎麼和引擎對接,怎麼做運算式求值兩個部分。

運算式求值有兩種基本策略,一個是解釋執行,直接把之前帶來的運算式進行解釋執行,這個是Hive現在的模式;另一個是代碼產生,包括SparkSQL,Impala,Drill等等號稱新一代的引擎都是代碼產生模式的(並且配合高速編譯器)。不管是什麼模式,你最終把運算式求值部分封裝成了類。代碼可能長得類似如下:

// math_score * 1.2val leftOp = row.get(1/* math_score column index */);

val result = if (leftOp == null) then null else leftOp * 1.2;

每個獨立的SELECT項目都會產生這樣一段運算式求值代碼或者封裝過的求值器。但是AVG怎麼辦?當初寫wordcount的時候,我記得彙總計算需要指派在Map和Reduce兩個階段呀?這裡就涉及到物理執行轉換,涉及到分布式引擎的對接。

AVG這樣的彙總計算,加上GROUP BY的指示,告訴了底層的分布式引擎你需要怎麼做彙總。本質上來說AVG彙總需要拆分成Map階段來計算累加,還有條目個數,以及Reduce階段二次累加最後每個組做除法。

因此我們要算的AVG其實會進一步拆分成兩個計劃節點:Aggregates(Partial)和Aggregates(Final)。Partial部分是我們計算局部累加的部分,每個Mapper節點都將執行,然後底層引擎會做一個Shuffle,將相同Key(在這裡是Dept)的行分發到相同的Reduce節點。這樣經過最終彙總你才能拿到最後結果。

拆完彙總函式,如果只是上面案例給的一步SQL,那事情比較簡單,如果還有多個子查詢,那麼你可能面臨多次Shuffle,對於MapReduce來說,每次Shuffle你需要一個MapReduce Job來支撐,因為MapReduce模型中,只有通過Reduce階段才能做Shuffle操作,而對於Spark來說,Shuffle可以隨意擺放,不過你要根據Shuffle來拆分Stage。這樣拆過之後,你得到一個多個MR Job串起來的DAG或者一個Spark多個Stage的DAG(有向非循環圖)。

還記得剛才的執行計畫嗎?它最後變成了這樣的物理執行計畫:

TableScan->Project(dept, math_score * 1.2: expr1, eng_score * 0.8: expr2)

-> AggretatePartial(avg(expr1):avg1, avg(expr2):avg2, GROUP: dept)

-> ShuffleExchange(Row, KEY:dept)

-> AggregateFinal(avg1, avg2, GROUP:dept)

-> Project(dept, avg1 + avg2)

-> TableSink

這東西到底怎麼在MR或者Spark中執行啊?對應Shuffle之前和之後,物理上它們將在不同批次的計算節點上執行。不管對應MapReduce引擎還是Spark,它們分別是Mapper和Reducer,中間隔了Shuffle。上面的計劃,會由ShuffleExchange中間斷開,分別發送到Mapper和Reducer中執行,當然除了上面的部分還有之前提到的求值類,也都會一起序列化發送。

實際在MapReduce模型中,你最終執行的是一個特殊的Mapper和特殊的Reducer,它們分別在初始化階段載入被序列化的Plan和求值器資訊,然後在map和reduce函數中依次對每個輸入求值;而在Spark中,你產生的是一個一個RDD變換操作。

比如一個Project操作,對於MapReduce來說,虛擬碼大概是這樣的:

void configuration() {

context = loadContext()

}void map(inputRow) {

outputRow = context.projectEvaluator (inputRow);

write(outputRow);

}

對於Spark,大概就是這樣:

currentPlan.mapPartitions { iter => projection = loadContext()

iter.map { row => projection(row) } }

至此為止,引擎幫你愉快滴提交了Job,你的叢集開始不緊不慢地計算了。

到這裡為止,似乎看起來SparkSQL和Hive On MapReduce沒有什麼區別?其實SparkSQL快,並不快在引擎。

SparkSQL的引擎最佳化,並沒有Hive複雜,畢竟人Hive多年積累,十多年下來也不是吃素的。但是Spark本身快呀。

Spark標榜自己比MapReduce快幾倍幾十倍,很多人以為這是因為Spark是“基於記憶體的計算引擎”,其實這不是真的。Spark還是要落磁碟的,Shuffle的過程需要也會將中間資料吐到本地磁碟上。所以說Spark是基於記憶體計算的說法,不考慮手動Cache的情景,是不正確的。

SparkSQL的快,根本不是剛才說的那一坨東西哪兒比Hive On MR快了,而是Spark引擎本身快了。

事實上,不管是SparkSQL,Impala還是Presto等等,這些標榜第二代的SQL On Hadoop引擎,都至少做了三個改進,消除了冗餘的HDFS讀寫,冗餘的MapReduce階段,節省了JVM啟動時間。

在MapReduce模型下,需要Shuffle的操作,就必須接入一個完整的MapReduce操作,而接入一個MR操作,就必須將前階段的MR結果寫入HDFS,並且在Map階段重新讀出來,這才是萬惡之源。

事實上,如果只是上面的SQL查詢,不管用MapReduce還是Spark,都不一定會有顯著的差異,因為它只經過了一個shuffle階段。

真正體現差異的,是這樣的查詢:

SELECT g1.name, g1.avg, g2.cntFROM (SELECT name, avg(id) AS avg FROM students GROUP BY name) g1JOIN (SELECT name, count(id) AS cnt FROM students GROUP BY name) g2ON (g1.name = g2.name)ORDER BY avg;

而他們所對應的MR任務和Spark任務分別是這樣的:

一次HDFS中間資料寫入,其實會因為Replication的常數擴張為三倍寫入,而磁碟讀寫是非常耗時的。這才是Spark速度的主要來源。

另一個加速,來自於JVM重用。考慮一個上萬Task的Hive任務,如果用MapReduce執行,每個Task都會啟動一次JVM,而每次JVM啟動時間可能就是幾秒到十幾秒,而一個短Task的計算本身可能也就是幾秒到十幾秒,當MR的Hive任務啟動完成,Spark的任務已經計算結束了。對於短Task多的情形下,這是很大的節省。


網易視頻雲技術分享:一個SparkSQL的作業的一生

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.