Spark SQL: Relational Data Processing in Spark
Spark SQL : Spark中關係型處理模塊
說明: 類似這樣的說明並非是原作者的內容翻譯,而是本篇翻譯作者的理解(可以理解爲批准),所以難免有誤,特注!
當然翻譯也可能有誤!
Date | Contents |
---|---|
2019.03.12 | First Edition |
2019.04.07 | Second Edition, 修改部分錯誤及添加批註 |
文章目錄
- Spark SQL: Relational Data Processing in Spark
- Spark SQL : Spark中關係型處理模塊
- Abstract(摘要)
- Keywords(關鍵詞)
- 1 Introduction (簡介)
- Background and Goals(背景和目標)
- 2.1 Spark Overview(Spark概述)
- 2.2 Previous Relational Systems on Spark(早期Spark上關係型系統)
- 2.3 Goals for Spark SQL(Spark SQL 要完成的目標)
- 3 Programming Interface(編程接口)
- 3.1 DataFrame API(DataFrame應用編程接口)
- 3.2 Data Model(數據模型)
- 3.3 DataFrame Operations(DataFrame常用操作)
- 3.4 DataFrames versus Relational Query Languages(DataFrame和關係型查詢語言)
- 3.5 Querying Native Datasets(查詢原生數據集)
- 3.6 In-Memory Caching(緩存)
- 3.7 User-Defined Functions(用戶自定義函數)
- 4. Catalyst Optimizer(Catalyst 優化器)
- 4.1 Trees(樹結構)
- 4.2 Rules(規則)
- 4.3 Using Catalyst in Spark SQL(在Spark SQL中使用Catalyst優化器)
- 4.3.1 Analysis (分析階段)
- 4.3.2 Logical Optimization(邏輯優化階段)
- 4.3.3 Physical Planning(物理計劃)
- 4.3.4 Code Generation(代碼生成)
- 4.4 Extension Points(擴展點)
- 5 Advanced Analytics Features(高級分析特性)
- 5.1 Schema Inference for Semistructured Data(半結構化數據的元數據獲取)
- 5.2 Integration with Spark's Machine Learning Library(和Spark機器學習算法庫的整合)
- 5.3 Query Federation to External Databases(外部數據庫數據聯邦)
- 6 Evaluation(評估)
- 6.3 Pipeline Performance(管道性能)
- 7 Research Applications(研究型應用)
- 8 Related Work(相關研究)
- 9 Conclusion(總結)
- 10 Acknowldegments(致謝)
- 11 References(參考)
Abstract(摘要)
Spark SQL是Apache Spark中的一個新模塊,其在Spark的函數式編程API中引入了關係型處理能力。
函數式編程 : 是種編程典範,它將電腦運算視爲函數的計算。函數編程語言最重要的基礎是λ演算(lambda calculus)。而且λ演算的函數可以接受函數當作輸入(引數)和輸出(傳出值)。
簡單說, 函數式編程就是函數也和值一樣可以當做返回值,以及輸入參數值。
關係型處理: 比如使用SQL來解決某個問題,如數據查詢、統計分析等
基於在Shark上的經驗,開發出了Spark SQL,而這個工具就可以使得使用Spark的開發者在可以進行關係型處理的同時(如聲明式的查詢,存儲優化),也能調用Spark中原有的複雜的分析庫(如機器學習相關算法等)。
Shark : 請查詢 Hive ,Hive On Spark ,Shark的愛恨糾纏。
聲明式查詢:
存儲優化:
和之前的系統對比,Spark SQL添加了兩個主要功能:第一,通過聲明式的DataFrame API使得在之前的過程型處理代碼中(也就是Spark RDD API)可以很容易的加入關係型處理過程;
Spark RDD 可以理解爲過程型處理的API
第二,新增了一個高度可擴展的優化器,Catalyst(此優化器通過使用Scala編程實現,並利用scala特有的特性),使用此優化器,可以很容易的添加可組合的(可以理解爲可定製)規則、控制代碼生成過程、定義擴展點(extension points?拓展點?)。利用Catalyst,針對現代的複雜數據分析,定製開發了很多特性(如 JSON元數據自動獲取、機器學習函數調用、外部數據庫聯邦查詢(federation?))。
machine learning types:
query federation to external databases:
我們把Spark SQL看做是一個SQL-on-Spark 以及 Spark世界的一個變革,因爲Spark SQL在保持Spark編程模型優勢的同時,還提供了更豐富的API以及優化機制。
Keywords(關鍵詞)
Databases;Data Warehouse; Machine Learning; Spark;Hadoop
1 Introduction (簡介)
大數據系統是一個集處理技術、數據源及存儲格式技術的混合應用系統。早期類似的系統,如MapReduce(提供了一個強大但是低階的過程化處理接口)。直接使用這種系統進行編程,一般比較複雜,而且爲了得到更高的系統性能,需要用戶手動進行優化。所以,很多新的系統更加傾向於提供一個更高效、用戶易用的編程接口,而這種接口通常提供針對大數據處理的關係型的接口。諸如Pig,Hive,Dremel和Shark就是這樣的系統,通過聲明式的查詢來提供更多自動化的優化。
雖然關係型系統的活躍度展示了用戶更加傾向於編寫聲明式的查詢,但是這種關係型的系統一般針對大數據應用提供的可用的操作是比較少的,同時還存在下面的問題。
- 第一,用戶需要執行ETL作業,而這些作業需要多種數據源的支持(支持讀取以及寫入),同時這些數據源的數據可能是半結構化或非結構化的數據。這時,這些系統(指的就是上面的如Pig,Hive,Shark等系統)就需要用戶自己編寫額外代碼。
- 第二,用戶需要進行更多高級的分析。例如機器學習或圖處理,但是這中操作在關係型系統中是一個非常大的挑戰。事實上,我們觀察到很多數據操作都會用到關係型查詢及過程化處理算法。不幸的是,對於關係型和過程型這兩種系統直到現在還沒有一個系統可以同時具有這兩種優勢,這就使得用戶只能選擇其中的一個或另一個。
這篇論文主要介紹了我們在Spark中引入的一個新模型,Spark SQL。Spark SQL基於早前的Shark設計。而Spark SQL就可以讓用戶直接使用關係型和過程型處理的API,而不用二選一。
Spark SQL通過提供兩個特性來縮小關係型和過程型系統之間的差距。
- 第一,Spark SQL提供一個可以處理外部數據源及Spark內置的分佈式數據結構(也就是RDD)的API,叫做DataFrame。這個API和R中的data frame概念比較類似,但是當進行action操作的時候是一種懶操作(部分代碼不是立即執行,而是在執行某些操作的時候才執行,可以和Pig對比),基於這種“懶操作”,Spark 引擎就可以進行關係型優化。
- 第二,爲了支持大數據系統中的多種數據源及算法庫的調用,Spark SQL引入了一個設計精妙的、可擴展的優化器,叫做Catalyst。Catalyst使得添加數據源、優化規則、域數據類型(如機器學習中的數據類型)更加容易。
optimization rules: 規則優化,即可以自定義優化規則;
data types for domains : 機器學習中的數據類型
DataFrame API同時提供豐富的關係型或過程型操作。它是一個結構化記錄的集合,可以使用Spark的過程化API或關係型API(此種API支持多種優化)來對其進行操作。
DataFrame可以從Spark RDD轉換得到,所以可以整合到現有的Spark代碼中。其他的Spark組件,如機器學習庫,也可以使用DataFrame作爲輸入或作爲輸出。在很多常見的情況下,DataFrame API 會比Spark 過程型API(也就是RDD API)能取得更高的性能,同時也更加易於操作。舉例來說,在DataFrame中,可以使用一個SQL來完成多個聚合操作,但是如果使用函數式API(RDD API),那麼就會很複雜。同時,DataFrame存儲數據時,會直接使用Columnar format(列式格式),這種格式比直接使用Java/Python 類存儲數據更加緊湊(也即是佔用空間更小)。最後,和R或Python中的 data frame API不同的是,在Spark SQL中 DataFrame會使用一個關係型優化器來作爲其處理引擎進行處理,即Catalyst(而Catalyst也是本文的重點)。
Columnar format: 列式存儲;
爲了在Spark SQL中支持多種數據源和分析型的作業流程,我們設計了一個拓展的查詢優化器,Catalyst。Catalyst充分利用Scala語言中的特性,比如使用模式識別來實現可組合的規則(Turing-complete language:圖靈完整性語言?)。
Turing-complete language: What is Turing Complete?
It(指代“模式識別”?)提供了一個樹轉換框架,這個框架可以執行分析、計劃、運行時代碼生成工作。通過這個框架,Catalyst就可以獲得如下的增強:
- 添加新的數據源,包括半結構化數據(如:JSON),和“smart”數據存儲。(例如HBASE)
什麼叫“smart” data store ? 聰明的數據存儲,從後文來看,指的應該是數據在存儲之前會應用過濾,這樣其數據就會比較小。
- 用戶自定義函數;
- 以及用戶自定義的域類型,如機器學習。
函數式編程語言非常適合用來構建編譯器,所以使用Scala來編寫一個可拓展的優化器也就沒有什麼好奇怪的了。我們確實發現Catalyst能使我們更加高效、快速的在Spark SQL中增加功能,在Spark SQL 發佈後,外部的開發者也可以很容易的添加一些功能,所以說Catalyst是更易用的。
在2014年3月發行了Spark SQL,現在它是Spark中最活躍的模塊之一。在寫這篇paper的時候,Spark是大數據中最活躍的開源項目,超過400個開發者。Spark SQL已經被應用在很多大數量級的場景中。舉例來說,一個大型的互聯網公司使用Spark SQL建立了一個數據處理流,可以在一個8000節點的機器上處理、分析100PB的數據。每個單獨的查詢,一般都會操作數十TB的數據。另外,很多用戶已經開始接受Spark SQL不單單是一個SQL查詢引擎,而一個整合了過程型處理的編程模型的觀念。例如,Databricks Cloud的2/3的用戶,其託管的服務中運行Spark任務的都是使用Spark SQL。從效率方面看,我們發現Spark SQL在Hadoop的關係型產品中是很有競爭力的。對比傳統RDD 代碼,它可以取得10倍性能及更好的內存效率優勢。
更一般的,Spark SQL可以看做是Spark 核心(Core) API的一次重要變革。Spark 原始的函數式編程API確實太一般(general)了,
quite general : 這裏說的應該是其沒有提供很多標準化的操作,進而導致其能進行自動優化的方面較少。
在自動優化方面只提供了很有限的機會。Spark SQL不僅使得Spark對用戶來說更易用,而且對已有的代碼也可以進行優化。目前,Spark社區針對Spark SQL添加了更多API操作,如把DataFrame作爲新的“ML pipeline”機器學習的標準數據表示格式,我們希望可以把這種表示擴展到Spark的其他組件,如Spark GraphX、Spark Streaming中。
此篇文章,按以下順序進行:
- 開篇我們介紹了Spark背景以及Spark SQL要實現的目標(第二節$2)。
- 接着,介紹了DataFrame API(第三節,$3),Catalyst 優化器(第四節,$4)以及在Catalyst之上構建的高級特性(第五節,$5)。
- 在第六節對Spark SQL進行評估。
- 在第7節針對在Catalyst的一些外部研究。最後,在第八節,cover 相關工作。
Background and Goals(背景和目標)
2.1 Spark Overview(Spark概述)
Apache Spark 是一個通用的集羣計算引擎,可以使用Scala,Python,Java API進行操作(當然,現在也有R的API了),其包含流處理、圖處理、機器學習等模塊。在2010年發佈後,很快就被廣泛使用(這種多種語言集成的特性和DryadLINQ類似),
DryadLINQ:Dryad和DryadLINQ是微軟研究院的兩個項目,用於輔助 C# 開發人員在在計算機集羣或數據中心裏處理大規模的數據。 百度百科
並且是最活躍的大數據開源項目。在2014年,Spark已經擁有400位開發者,並且很多發行商在其上發佈了很多版本。
Spark提供了一個函數式編程API,可以操作分佈式數據集(RDDs)。每個RDD是一個在集羣中被分區的Java或Python的object的集合。可以使用map、filter、reduce來操作RDD,這些函數的參數也是函數,通過這些函數(map。。。)可以把數據進行轉換後發往集羣的各個節點。比如,下面的Scala代碼主要統計text文件中包含“ERROR”的個數。
lines = spark.textFile("hdfs://...")
errors = lines.filter(s => s.contains("ERROR"))
println("errors.count()")
上面這段代碼通過讀取一個HDFS文件,生成一個string類型的RDD,變量名爲liens。接着,使用filter進行轉換,得到一個新的RDD,叫做errors。最後,執行一個count操作,進行計數統計。
RDDs是具有容錯性的,在系統中丟失的數據可以根據RDD的血緣圖來進行恢復(在上面的代碼中,如果有數據丟失,可以利用血緣圖重新其丟失分區的父節點(如運行filter),來重建丟失的分區數據)。當然RDD也可以顯式的緩存到內存或硬盤上,以此來支持循環操作,提升效率。
最後一個關於API的點是RDD執行時是lazy的(根據代碼先構建一個有向無環圖,然後當有執行代碼時,才執行代碼,類比Pig操作)。
Spark的API主要分爲兩大類:Transformation & Action, Transformation主要是對一個RDD進行轉換操作,得到的仍然是RDD(分佈式數據集),而Action會對一個RDD進行操作,但是得到的是一般數據結構(如Python 數據類型或Scala數據類型)
每個RDD代表一個計算數據集的“logical plan(邏輯計劃)”,直到一個明確的輸出時,Spark才執行代碼,比如count操作。
count操作執行後,返回的就是一個Scala的Int類型的數據。
使用邏輯計劃這樣的設計方案使得Spark引擎可以做一些執行優化,例如pipeline(管道式操作、流水線式操作)操作的時候。例如,在上面的例子中,Spark會在讀取HDFS文件的每行記錄時,直接應用filter函數,然後進行計數,這樣操作的話,避免了存儲中間的結果,如lines或errors。雖然類似的優化很有用,但是這種優化也是有限的,因爲Spark引擎並不知道RDD數據中的結構。
Spark不知道數據結構:指的是任意的Java或Python類,類的結構,如字段,Spark引擎是不知道的。
以及用戶函數的語義。
如,用戶自定義函數,會引入任意的代碼,而這些代碼,Spark是不知道其代表什麼意義,所以也就無從優化。如果能讓Spark知道這些代碼執行的邏輯,比如說執行了一個filter/where操作,那麼它就可以優化,所以Spark SQL裏面就是把這些代碼直接用一個filter的操作進行封裝,用戶調用的時候直接調用filter,那麼Spark引擎就會知道用戶執行的是一個filter操作,而不是用戶自定義一個函數。
2.2 Previous Relational Systems on Spark(早期Spark上關係型系統)
我們第一次嘗試在Spark上建立的關係型接口是Shark,
Shark: Shark官網,目前已經是這個樣子了:
Shark has been subsumed by Spark SQL, a new module in Apache Spark. Please see the following blog post for more information: Shark, Spark SQL, Hive on Spark, and the future of SQL on Spark.
上面總結一句話,就是:Shark不再維護了,你們都去使用Spark SQL吧。
這個系統可以使得Apache Hive能夠運行在Spark之上(也就是在Spark上的關係型處理接口),同時實現了一些傳統RDBMS的優化,比如列式處理
列式存儲:(Columnar Database By definition, a columnar database stores data by columns rather than by rows, which makes it suitable for analytical query processing, and thus for data warehouses.)
儘管Shark使得Spark在關係型處理上擁有更高的性能,但是卻有三個不可迴避的問題。
- 第一,Shark只能處理存儲在Hive Catalog裏面的數據,對於Spark程序中已經存在的數據並沒有幫助(比如,在上面代碼中的 errors RDD上建立關係型查詢等)。
- 第二,如果使用Spark來調用Shark,那麼只能通過一個SQL字符串,這對於模塊化的代碼是非常不方便(模塊化),而且容易出錯的。
- 第三,Hive優化器是爲MapReduce量身定做的,很難去拓展以及添加新的特性,比如機器學習中需要用到的特殊數據類型或支持新的數據源。
2.3 Goals for Spark SQL(Spark SQL 要完成的目標)
基於Shark的經驗,我們想拓展關係型處理過程,使其可以既可以處理原生RDD,同時可以支持更多的數據源。所以,針對Spark SQL,設置瞭如下目標;
- 提供一個更易於編程的API,同時支持原生RDD、關係型處理、及外部數據源;
- 使用現有的DBMS的技術,來提供更高的效率支持;
- 可以容易可以添加新的數據源支持,包括半結構化數據或外部數據庫,適合用來做查詢聯邦;
query federation:查詢聯邦,指的是查詢統一接口,如底層有多個查詢,然後前端只有一個輸入接口,可以查看這個解釋
- 添加高級分析算法的支持,如圖處理或機器學習算;
3 Programming Interface(編程接口)
在圖1中可以看到,Spark SQL基於Spark,是Spark之上的一個模塊。Spark SQL暴露了一個SQL接口,所以可以使用JDBC/ODBC、命令行終端或 DataFrame API (整合了 Spark支持的所有編程語言的編程接口) 來進行操作。接下來將會首先介紹DataFrame API(可以使用戶同時使用過程和關係型編程代碼)。同時,高級的函數也可以通過UDFs在SQL中實現,關於UDF的部分將在3.7節展開。
3.1 DataFrame API(DataFrame應用編程接口)
在 Spark SQL中使用最主要的抽象封裝是: DataFrame:一個分佈式的擁有元數據的數據結構。
可以理解爲列名和列類型,就像數據庫中的元數據信息一樣的行數據集合。
一個 DataFrame和傳統關係型數據庫中的表等價,而且還可以像原生的分佈式數據集(指RDD)一樣被操作。
即 RDD API和 DataFrame是比較相似的
但是和RDD不一樣的是,DataFrame會跟蹤模式(Schema)的處理過程,同時支持很多能被優化的關係型操作。
DataFrame可以通過外部表(或外部數據源)創建,或從已存在的RDD(一個Java或Python的類的RDD)中創建(參見3.5節)。一旦被創建好,就可以執行很多關係型操作,如where,groupBy等,這些操作接受表達式(expressions)作爲參數,這種操作和R以及Python的data frame類似。
表達式(expressions):這種表達式是一種DSL(Domain-specific language),領域特定語言:針對某一領域,具有受限表達性的一種計算機程序設計語言。DSL編程也叫聲明式編程。
DataFrame也可以被看做由Row類型的RDD組成,這樣就可以在其上執行類似過程型處理API了,如map操作。
最後,和傳統data frame的API不同的是,Spark的DataFrame是“懶”的(lazy),這裏的“懶”指的是每個DataFrame 代表一個計算某個數據集的邏輯計劃,只有明確調用“輸出操作”纔會發生實際的計算,如save(保存)操作。這個特性使得Spark可以對DataFrame上的操作進行更多的優化。
怎麼理解更多的優化,例如現在要執行一個映射,把dataframe中的某一數值列全部映射爲其2倍,接着,過濾這些值,當其值對3求餘爲1的數字才保留,最後統計符合條件的數據個數。
針對上面的問題,一般的處理的過程就是先遍歷第一遍,求得映射值,然後再針對映射值,遍歷第二遍,再求得符合條件的數據,最後,遍歷第三遍得到個數。
而如果使用DataFrame,那麼由於在執行count的時候纔會觸發執行,然後根據最後的count的DataFrame的邏輯計劃,可以知道map和filter,以及count都可以進行整合。所以在執行的時候,就會遍歷一次數據集,同時進行映射、過濾及統計。而這就是所謂的更多的優化!
爲了說明這種問題(指的就是上面更多的優化),下面通過代碼實例來分析。下面的Scala代碼中定義了一個從Hive中讀取得到的DataFrame:users,並且在這個users DataFrame基礎上應用where計算得到另一個young DataFrame,最後打印結果。
ctx = new HiveContext()
users = ctx.table("users")
young = users.where(users("age") < 21)
println(young.count())
在上面代碼中,變量users、young都是一個DataFrame,而代碼片段
users(“age”) < 21
就是一個 expression(表達式,DSL),這種表達式被被用一個抽象語法樹來實現,而不是像傳統Spark RDD API中使用Scala函數來實現。
這麼做的好處是啥
(根據抽象語法樹(AST)Spark就知道代碼實際執行的是啥,而如果是一個Scala函數,Spark是沒辦法知道的)。
簡單理解:如果使用if else來表達一個規則,並且規則中的處理邏輯寫死,那麼當知道if的條件時,總是可以確定執行的邏輯(抽象語法樹其實簡單理解就是if else);而如果現在要執行一個用戶傳過來的函數,那麼你就不知道用戶函數裏面寫的是啥了。
總的來說,每個DataFrame就是一個邏輯計劃。當用戶調用count函數時(一個輸出操作),這時Spark就會根據邏輯計劃來構建一個物理計劃,進而計算最終結果。在這個過程中,可能會包含一些優化。例如,當數據源是一個列式存儲系統時,那麼在進行filter過濾時,就只需要讀取age列(而不需要讀取其他列,效率高),同時,在進行count時,可能只用到了索引來進行計數,而完全沒有讀取實際的數據。
下面,將對DataFrame進行詳細介紹。
3.2 Data Model(數據模型)
Spark SQL爲DataFrame選用一個基於Hive的嵌套的數據模型。其支持所有主流的SQL數據類型,如boolean,interger,double,decimal,string,data,timestamp以及複雜數據類型(非原子類型):structs(結構類型),arrays(數組),maps(鍵值對),unions(聯合類型)。複雜類型也可以進行嵌套以實現更加有用的類型。
原子類型: 例如上面的boolean、integer等,非原子類型其實就是原子類型的組合。
和很多傳統DBMSes(數據庫管理系統)不同,Spark SQL爲查詢語言和API中的複雜數據類型提供一流的支持。同時,Spark SQL也支持用戶自定義類型(在4.4.2節將會介紹)。
使用這種框架,我們可以對很多數據源或不同格式數據進行非常精確的數據定義.
(model data,model這裏應該是動詞)
這些數據源或格式包括Hive,傳統數據庫,JSON,原生Java、Scala、Python類。
3.3 DataFrame Operations(DataFrame常用操作)
用戶可以在DataFrame上使用DSL(領域特定語言,參考上面)進行一系列關係型操作,就像R中的data frames以及Python中的Pandas一樣。DataFrame支持常見的關係型操作,包括projection
Projection:(是一種操作,直譯爲投影,可以理解爲一種數據展現,比如這種操作的一個select,其實就是查詢,查詢就會有結果,而這個結果就是原始數據的“投影”,可以這樣理解)
filter(過濾操作,如where操作),join和aggregations(聚合操作,如groupBy)。這些操作都使用表達式(expression),由於這些表達式都是由有限的DSL組成的,所以Spark可以知道每個表達式的結構。例如,下面的代碼計算每個department中female employee(女僱員)的個數。
employees
.join(dept, emplyees("deptId") === dept("id"))
.where(employees("gender") === "female")
.groupBy(dept("id"), dept("name"))
.agg(count("name"))
在這段代碼中,employees是一個DataFrame,employees(“deptId”)是一個代表deptId列的表達式。基於表達式(Expression)可以進行很多操作,然後返回的仍然是表達式。例如,包含常見的比較操作(如 === 代表相等測試,> 代表大於)和算數操作(如+,-)。表達式也支持聚合操作,如count(“name”)。所有這些操作建立了一個表達式的抽象語法樹(AST,Abastrct Syntax Tree),而AST接下來就會被Catalyst進行優化。這就和傳統Spark API使用任意的Java,Scala,Python代碼的函數進行傳遞不一樣,因爲函數傳遞會導致這些函數裏面的具體操作對於Spark執行引擎來說是不透明的,所以也就說不上什麼優化了。如果想查看上面代碼中具體API,可以查看Spark官網。
除了關係型的DSL之外,DataFrame也可以被註冊成爲一個臨時表,進而,就可以使用SQL來進行查詢。下面的代碼就是一個示例:
users.where(users("age") < 21)
.registerTempTable("young")
ctx.sql("select count(*) , avg("age") from young")
這種SQL註冊表的方式,在某些場合(如聚合操作中)可以很方便的進行操作,且表意清晰,同時可以使得程序通過JDBC/ODBC來訪問數據。通過在catalog中註冊臨時表的DataFrame,仍然是非固化的視圖,所以在後續的SQL以及原始DataFrame 表達式中仍然有優化的空間。
unmaterialized views : 暫譯爲非固化視圖。簡單理解,雖然DataFrame可以註冊成一個臨時表,但是這個表就是一個簡單的視圖,比如要進行計算的時候,還是會從一開始進行計算。例如,要查看這個臨時表的前兩行,那麼會先計算DataFrame,然後在取出前兩行。而如果是固化的視圖(固化的應該叫表),那麼直接取出前兩行即可,不需要進行計算。
但是,DataFrame也可以被固化,將在3.6節討論。
3.4 DataFrames versus Relational Query Languages(DataFrame和關係型查詢語言)
雖然,在表面上來看,DataFrame和如SQL或Pig一樣提供關係型查詢語言的操作,但是由於Spark SQL可以整合入多種編程語言中,所以對於用戶來說,Spark SQL會非常易於使用。例如,用戶可以把代碼分解成Scala,Java,或Python的函數,並把DataFrame傳到這些函數中,以此來建立一個邏輯計劃,同時仍然可以在整個邏輯計劃中享有Spark的優化(當執行輸出操作時就會進行優化)。類似的,開發者可以使用控制結構,像 if 語句 或 循環語句 來構建任務。 一個用戶提到DataFrame的API是非常簡明的,它的聲明式特性就和SQL一樣,但是DataFrame可以對中間結果進行命名,體現出構建計算以及進行調試的便捷性。
爲了簡化在DataFrame中的編程,DataFrame會在API中提前分析邏輯計劃(比如識別expression(表達式)中的列名是否在給定的表中,或給定的列數據類型是否是正確的(可以理解爲是否和數據庫中是匹配的)),但是其執行仍然是lasy的。所以,Spark SQL 會在用戶輸入一行非法的代碼的時候就報錯,而不是等到執行的時候。這種處理對於用戶來說,同樣是一個減負的操作(好過處理一下子處理一個大的SQL,此處的做法就是把大SQL進行分解)。
3.5 Querying Native Datasets(查詢原生數據集)
真實業務流程經常從很多異構的數據源中抽取數據,接着使用很多不同的分析工具或算法來對數據進行分析。爲了能夠和過程型Spark代碼互通(就是Spark RDD API),Spark SQL允許用戶可以直接從RDD來構造DataFrame。Spark SQL 可以通過反射自動得到元數據(Schema)信息
Schema : 元數據信息,如、列信息、列名,列類型等)。
在Scala或Java中,數據類型信息通過JavaBeans或Scala的Case class獲取。在Python中,Spark SQL 對數據集進行抽樣,然後動態的去匹配,進而獲取元數據信息
動態獲取,就是先看能否轉換爲double,然後看能否轉換爲int,最後纔是string,基本就是這種思想。
舉例來說,在下面的Scala代碼中定義了一個DataFrame(從RDD[User]轉換而來)。Spark SQL 自動的識別了列名(如“name”和“age”)以及其對應的數據類型(string,int)。
case class User(name:String, age:Int)
// create an RDD of User objects
usersRDD = spark.parallelize(List(User("Alice",22),User("Bob",19)))
// view the RDD as a DataFrame
usersDF = usersRDD.toDF
在底層實現上,Spark SQL會創建一個指向RDD的邏輯數據掃描操作。這個操作會被編譯成一個可以接觸原始對象的字段(原始對象就是指的User類)的物理操作。需要注意的是,這種操作和傳統的ORM(類關係映射)是非常不一樣的。ORM系統一般在把整個類轉換成不同的格式的時候會引起很大的轉換消耗。但是,Spark SQL卻可以直接就地操作字段,所以可以根據每個查詢需要的字段來進行提取。
查詢原生數據集的特性(直接訪問類的字段)使得用戶可以在現有的Spark代碼中執行關係型操作的優化。
說白了,就是在原RDD的代碼中引入Spark SQL的優化機制。
同時,如果用戶想把RDD和一個外部的結構化數據源進行合併,那也會非常簡單。例如,可以把users RDD(上一個代碼)和Hive中的一個表合併:
views = cxt.table("pageviews")
usersDF.join(views,usersDF("name") === views("user"))
3.6 In-Memory Caching(緩存)
就像之前的Shark一樣,Spark SQL也可以使用列式存儲在內存中緩存熱數據.
hot data,經常使用的數據一般稱爲熱數據)。
和Spark原生的緩存機制
原生緩存指的是使用RDD API進行緩存,直接把數據作爲JVM類存儲。
不同的是,使用列式存儲系統進行緩存可以減少一個量級的內存佔用空間,因爲Spark SQL應用柱狀壓縮方案(columnar compression schemes),比如字典編碼及行程編碼(run-length encoding:行程編碼(Run Length Encoding,RLE), 又稱遊程編碼、行程長度編碼、變動長度編碼 等,是一種統計編碼。主要技術是檢測重複的比特或字符序列,並用它們的出現次數取而代之。)緩存技術對於迭代查詢,特別是對於機器學習中的迭代算法非常有用。在DataFrame中,可以直接調用cache()函數來進行緩存。
關於緩存的實現方案,有興趣的可以深入瞭解下,這裏只是簡單翻譯,並沒有拓展。
3.7 User-Defined Functions(用戶自定義函數)
用戶自定義函數(UDFs)是對數據庫系統的一個很重要的拓展。比如,MySQL中使用UDFs來提供對JSON數據的支持。一個更高階的例子是MADlib的UDFs的使用,它可以在Postgres 或其他數據庫中實現學習算法。但是,數據庫系統一般需要使用不同的編程環境(比如Postgres裏面使用Java來開發UDF,而本身使用的是Postgres的環境,也就是不能直接使用Postgres SQL的環境來實現UDFs)來實現這些UDFs。Spark SQL中的DataFrame API卻可以不需要額外的編程環境,就可以直接實現UDFs,同時還不用複雜的打包、註冊操作過程。這也是該API的一個重要的特性。
在Spark SQL中,UDFs可以通過Scala,Java或Python函數來註冊生成,稍後,這些函數會在Spark底層轉換爲對應的Spark API來實現。例如,給定一個機器學習模型中的model的變量,可以把其預測函數重新註冊成一個UDF:
val model: LogisticRegressionModel = ...
ctx.udf.register("predict",(x:Float,y:Float) => model.predict(Vector(x,y)))
ctx.sql("SELECT predict(age,weight) FROM users")
UDFs被註冊後,就可以通過JDBC/ODBC來給其他商業智能工具調用。UDFs除了可以處理標量數據外,也可以處理整個表(通過提供表名即可,就像在MADLib中的一樣),同時,也可以使用分佈式的Spark API,其實就是Spark Core API,所以也就可以爲SQL用戶提供更多高級的分析函數。最後,UDF函數定義和查詢引擎都是使用通用的語言(如Scala或Python)來編寫的,所以用戶可以使用標準工具來進行debug(調試,如使用IntelliJ IDEA或Eclipse、PyCharm工具來調試等)。
上面的例子說明了一個在流程化處理中的通用例子。例如,如果需要用到關係運算或高級分析函數處理的場景,那麼在SQL中來實現是很複雜的。但是,DataFrame API可以無縫的整合這些函數。
4. Catalyst Optimizer(Catalyst 優化器)
爲了實現Spark SQL,我們基於函數式編程語言Scala設計了一個新的增強優化器,Catalyst。Catalyst的增強設計有兩個目的。第一個,我們希望能在Spark SQL中很容易的添加新的優化技術及特性,特別是解決多種大數據問題(比如,半結構化數據和高級分析主題)。第二,我們想讓外部開發者幫我們擴展優化器
一時沒有想到好的翻譯
(
for example, by adding data source specific rules that can push filtering or aggregation into external storage systems, or support for new data types.)
Catalyst 不僅支持基於規則的優化,也支持基於成本(運行耗時等)的優化。
雖然之前已經引入過可拓展的優化器,但是需要複雜的特定領域語言(domain specific language)來表達規則。同時,需要一個“優化器編譯器”來把規則轉換爲可執行代碼。這造成了很大的學習曲線和維護負擔。
(也就是別人修改或維護比較難)。
相對的,Catalyst使用Scala標準的語言特性來開發,如pattern-matching(模式匹配)。這樣,開發者不管是構建規則或者編寫Spark代碼,都可以只使用Scala語言來完成。
即不需要引入額外的語言,如 DSL。
函數式編程語言天生就適合用來構建編譯器,所以Scala也就很適合用來構建Catalyst。儘管如此,在我們看來,Catalyst仍然是第一個質量很高並且使用Scala這種函數式編程語言實現的查詢優化器。
Catalyst的核心包含一個用來表示抽象語法樹以及應用規則來操作AST的通用庫。在這樣的框架基礎上,我們創建了很多特定的庫用來處理:
-
關係型查詢操作(如,表達式,邏輯查詢計劃等);
-
一些可以處理查詢執行的不同階段的規則,查詢執行的階段有:分析,邏輯優化,物理計劃;
-
代碼生成(會編譯部分查詢,並生成Java二進制代碼)。
關於代碼生成,我們使用了Scala另外的一個特性,quasiquotes。
quasiquotes: quasiquotes官網解釋
TODO : 舉個例子
它可以使得程序在運行時生成代碼很簡單(通過組合表達式,實際指的是可以直接用字符串來代替代碼)。最後,Catalyst提供多個公共的拓展接口,包括外部數據源和用戶自定義類型。
4.1 Trees(樹結構)
在Catalyst中最重要的數據類型就是由一系列節點構成的樹結構。每個節點包含一個節點類型,同時包含零個或多個子節點。如果要定義新的節點類型,在Scala中,可以通過繼承TreeNode class來實現。這些類是不可變的,可以使用函數式轉換(transformations)來操作這些類。
簡單來說,假設我們有下面三個類,每個類代表一個節點類型。接着,就可以使用這三個節點類來構建一個非常簡單的表達式。
- Literal(value:Int): 常量類;
- Attribute(name:String): 輸入row的一個屬性,如“x”;
- Add(left:TreeNode,right: TreeNode) : 兩個表達式的和;
row 可以參考DataFrame轉換爲RDD時的Row
使用這些類就可以構建表達式樹:比如,構建表達式x+(1+2)的樹,可以使用下面的Scala代碼來構建(參考圖2):
Add(Attribute(x), Add(Literal(1)), Literal(2))
4.2 Rules(規則)
規則可以理解爲一個函數,可以把一個樹轉換生成另外一個樹,所以可以使用規則來操作樹。
TODO 下段有待加強。
While一個規則可以在其輸入的樹上運行任意的代碼(這裏的樹指的是一個Scala的類),最常使用的方式是使用一系列的模式匹配來找到以及替換具有特定結構的子樹。
模式識別(pattern matching)是很多函數式編程語言都具有的一個特性,可以從可能的嵌套的代數數據類型(algebraic data type)結構中找到匹配的值。在Catalyst中,trees(樹結構)提供transform方法,可以應用模式識別函數來遞歸的遍歷樹中的所有節點,這樣就可以針對每個節點來匹配與之對應的結果或模式。例如,可以使用如下的方式實現常量之間的加法:
tree.transform{
case Add(Literal(c1), Literal(c2) => Literal(c1+c2))
}
把上面的函數應用到 表達式x+(1+2)樹上就會生成一個新的樹x+3.這裏的‘case’關鍵字是Scala標準的模式識別語法格式,‘case’可以匹配object的類型或者使用給定的名稱進行匹配來提取值(比如這裏的c1,c2)。
傳給transform的模式識別表達式是一個partial function
partial function:偏函數。Scala 偏應用函數是一種表達式,你不需要提供函數需要的所有參數,只需要提供部分,或不提供所需參數。參考 Scala 偏應用函數
也就是表達式只需要匹配所有可能的輸入樹的一部分(也可以理解爲一個節點)即可。Catalyst會測試一個給定的規則以確定可以應用到樹中的哪個部分,同時在不匹配的時候會自動的跳過或者遍歷其子樹。這個特性意味着規則只會針對匹配樹應用優化,而對不匹配的則不應用。
所以,這些規則不需要作爲新的操作符添加到系統中。(說的意思就是,這些規則是類似一個plugin,可以隨插隨用,而不需要改動系統的代碼)。
原文爲:Thus, rules do not need to be modified as new types of operators are added to the system. TODO:上面翻譯的準確性待驗證。
在同一個transform調用中規則可以同時匹配多個模式,所以實現多個轉換匹配將會非常簡單,如下:
tree.transform{
case Add(Literal(c1), Literal(c2)) => Literal(c1 + c2)
case Add(left, Literal(0)) => left
case Add(Literal(0), right) => right
}
事實上,可能需要多次應用規則才能完整的轉換一個樹。Catalyst 把規則進行分組,稱爲批操作(batches),同時針對每個批處理遞歸執行,直到到達一個固定的點,這個點就是當再次應用規則的時候,樹不會再次改變。
Runing rules to fixed point means that each rule can be simple and self-contained, and yet still eventually have larger global effects on a tree.
->
應用規則到一個固定點指的是每個規則是簡單的及自包容的,同時最終仍然可以對樹產生一個更大的全局的效果。
更大的全局的效果? TODO
在上面的例子中,重複應用就可以得到一個更大的樹,如(x+0) +(3 + 3).
舉另外一個例子,第一個批處理可能會分析表達式,同時爲每個屬性匹配並分配類型,而第二個批處理就可以使用這些分配好的類型進行常量整合(就是先合併常量項)。執行每個批處理後,開發者也可以對新生成的樹執行完整性檢查(例如,檢查是否所有的屬性都匹配到了類型,這些完整性檢查也可以通過遞歸的來實現)。
最後,規則的條件和規則的實體可以包含任意的Scala代碼。這個特性使得Catalyst比領域特定語言(domain specific language)不僅在優化器上更具優勢,同時也保持了針對簡單規則簡潔性。
根據我們的經驗,對不變的樹應用函數式轉換操作可以使得整個優化器很容易進行推導及調試。函數式轉換也可以使得在優化器中很容易實現並行處理,儘管我們還沒有利用這個特性進行優化。
4.3 Using Catalyst in Spark SQL(在Spark SQL中使用Catalyst優化器)
在Catalyst中,一般的樹轉換框架可以使用四個步驟來實現(如圖3所示):
- 通過分析邏輯計劃來替換引用;
- 優化邏輯計劃;
- 物理計劃;
- 代碼生成(編譯部分查詢代碼到Java二進制);
在物理計劃階段,Catalyst可能生成多個計劃,同時會根據成本來比較這些計劃並選擇某個計劃。
成本: 通過某些手段可以評估計劃執行的耗時,以此來擇優選取計劃。
其他三個階段都是完全基於規則的。每個階段使用不同的樹節點類型,Catalyst內含表達式、數據類型、邏輯、物理操作符相關的庫。接下來詳細描述着四個階段。
4.3.1 Analysis (分析階段)
Spark SQL最開始是一個待計算的關係表達式,這個表達式要麼是從一個SQL解析器中得到的抽象語法樹(AST,abstract syntax tree),要麼是通過DataFrame API得到的。不管是怎樣得到的,待計算的關係表達式都可能包含仍未解析的屬性引用或其他關係表達式。
例如,在如下的SQL中 :
SELECT col FROM sales
如果我們不查看錶sales的話,那麼對於col列的類型我們是不知道,甚至我們都不知道col列名是否是一個合法的列名。一個未被解析的屬性指的是還不知道該屬性的數據類型或者該屬性不能匹配輸入表的字段(或者別名)。
Spark SQL中使用Catalyst規則以及Catalog引用(可以理解爲元數據信息,有所有表的相關信息)來跟蹤所有數據源中的表,以此來解析其出現的屬性信息。所以,最開始,會構建一個“未解析的邏輯計劃(unresolved logical plan)”樹,這個樹包含未綁定的屬性及數據類型,接着會應用規則來解析,具體如下:
- 在catalog中根據名字查找關係(此處的關係可以理解爲表);
- 映射列,如列名 col,to the input provided given operator’s children;
- 確定哪些屬性是一樣的,同時給他們一個唯一的ID(後面會針對表達式進行優化,如col=col,可以防止同樣的屬性被解析多次,降低效率)
- 通過表達式推斷類型(propagating and coercing types through expressions),例如:針對表達式1 + col,如果想知道這個表達式的數據類型,那麼就需要先知道col的類型,然後儘可能的把該表達式的數據類型轉換成“正確”的類型。
最後,該分析器(analyzer)的實現代碼大約有1000行。
4.3.2 Logical Optimization(邏輯優化階段)
在邏輯優化階段,會對邏輯計劃應用標準的基於規則的優化,包括
- 常量合併(constant folding);
- predicate pushdown(謂詞下推?簡而言之,就是在不影響結果的情況下,儘量將過濾條件提前執行。);
- projection pruning(映射修剪?);
- null propagating(空傳播?它將空的源值直接轉換爲空的目標值);
- 布爾表達式簡化;
- 其他規則。
一般來說,在很多的情況下添加規則都非常簡單。比如,當已經在Spark SQL中添加了一個固定精度的DECIMAL類型,那麼如果要對DECIMAL進行小精度的聚合操作,如SUM或AVG操作,那麼僅僅使用12行代碼就可以寫一個規則來實現這樣的需求。其過程如下:先把其轉換爲一個unscaled 64-bit LONGs,接着對其進行聚合操作,得到結果後,再次轉換即可。一個簡單的版本實現如下(只實現了SUM操作):
object DecimalAggregates extends Rule[LogicalPlan] {
/** Maximum number of decimal digits in a Long */
val MAX_LONG_DIGITS = 18
def apply(plan: LogicalPlan): LogicalPlan = {
plan transformAllExpressions {
case Sum(e @ DecimalType.Expression(prec, scale))
if prec + 10 <= MAX_LONG_DIGITS =>
MakeDecimal(Sum(LongValue(e)), prec + 10, scale) }
}
另外一個例子,LIKE表達式可以通過12行類似的規則來進行優化,可以簡單的使用String.startsWith或String.contains來實現正則中的簡單判斷。在規則中可以使用任意Scala代碼使得這種優化在簡潔性上遠遠超過了使用模式識別來匹配子樹結構的方式。
而邏輯優化規則的實現代碼有近800行左右。
4.3.3 Physical Planning(物理計劃)
在物理計劃階段,Spark SQL根據一個邏輯計劃,使用與Spark執行引擎匹配的物理操作符來生成一個或多個物理計劃。接着,使用成本代價模型來選擇一個計劃。目前,根據代價模型的優化器只用在了Join操作上:針對比較小的DataFrame,Spark SQL使用broadcast join。
broadcast join : 使用Spark中的一個端到端的廣播工具類來實現。
這個框架支持廣泛的基於代價模型的優化,但是,一般情況下代價都需要對整個樹遞歸地應用規則來進行評估,所以將來會實現更多的基於代價模型的優化算法。
物理執行器也可以進行基於規則的物理優化,比如在一個map函數中直接應用pipelining projections(管道投影?)或者過濾。另外,它可以把邏輯計劃中的操作放入到支持predicate或者projection pushdown的數據源中執行(效率更高,等於是直接使用數據源的引擎,少了一層轉換)。
predicate : 斷言
projection pushdown: 謂詞下推
在 4.4.1中會介紹這些數據源相關的API。實現物理計劃規則的代碼有將近500行。
4.3.4 Code Generation(代碼生成)
最後一個查詢優化階段包含需要在每個機器上運行的生成的Java二進制代碼。因爲Spark SQL經常操作內存中的數據集(這個操作是CPU受限的),所以,我們想支持代碼生成,以此來加速執行。
這裏對比的地方是:1. 使用轉換來調用要執行的任務;2. 通過代碼生成要執行的任務; 所以如果有速度提升,那麼就是直接生成代碼,然後執行任務,其耗時更少。
儘管如此,一般的代碼生成引擎構建都比較複雜,基本上相當於一個編譯器了。Catalyst基於Scala語言的一個特殊的特性,“quasiquotes”(把字符串替換爲代碼的特性),使得代碼生成更加簡單。Quasiquotes允許在Scala中使用代碼構建抽象語法樹(AST),構建的抽象語法樹會被傳給Scala編譯器,進而在運行時生成二進制代碼。我們使用Catalyst來轉換一個SQL表達式代表的樹到AST,以此來使用Scala代碼對該表達式進行評估(可以理解爲執行SQL表達式),進而編譯和執行生成的代碼。
舉個簡單的例子,在4.2節中引入的Add操作、Attribute、Literal樹節點,可以使用這些簡單的樹節點來構造這樣的一個表達式:
(x+y)+1.
如果沒有代碼生成,這樣的表達式就會針對數據的每行進行操作,也就是從Add,屬性,常量構成的樹的根節點開始,往下遍歷,這就會造成大量的分支和virtual function(虛擬函數?)的調用,從而降低執行的效率。如果使用代碼生成,那麼就可以編寫一個函數來把某個固定的表達式樹轉換成一個Scala的AST,如下:
def compile(node: Node): AST = node match {
case Literal(value) => q"$value"
case Attribute(name) => q"row.get($name)"
case Add(left, right) =>
q"${compile(left)} + ${compile(right)}"
}
以q開頭的字符串就是quasiquotes,意味着儘管這些看起來像字符串,但是他們會被Scala編譯器在編譯的時候轉換成表示AST樹的代碼。Quasiquotes可以使用“$”符號連接變量或其他AST樹。例如,Literal(1)在Scala AST樹中直接轉換爲1,而Attribute(“x”)可以轉換爲row.get(“x”)。所以,類似Add(Literal(1), Attribute(“x”))的AST樹就會生成1+row.get(“x”)的Scala表達式。
Quasiquotes在編譯的時候會進行類型檢查,以確保AST或字符串能被正確的替換(這個功能比字符串的拼接更加的實用,同時,它可以直接生成Scala的AST而不是在執行時還需要Scala轉換器進行轉換)。此外,它們是高度可組合的,因爲每個節點代碼生成規則不需要知道其子節點生成的樹是怎麼樣子的。最後,如果Catalyst沒有對其進行優化的化,生成的代碼仍可以被Scala編譯器進行表達式級別的優化。圖4對比了使用Quasiquotes生成的代碼效率和手動優化的代碼效率。
我們發現使用quasiquotes來進行代碼生成整個邏輯很清晰,所以就算是新的參與者也可以很容易的針對新的表達式類型添加規則。Quasiquotes也可以在原生Java類型上工作的很好,當訪問Java類中的字段時,可以生成一個直接字段的訪問,而不是拷貝類到一個Spark SQL中的Row,然後使用Row的方法來訪問某個字段(所以其效率高)。最後,因爲我們編譯的Scala代碼可以直接調用我們的表達式解釋器,所以雖然整合表達式的代碼生成式評估(code-generated evaluation)和直譯式評估(interpreted evaluation)不復雜,但是我們還沒有對這塊進行代碼生成。
最後,Catalyst的代碼生成器實現一共有700行左右代碼。
4.4 Extension Points(擴展點)
Catalyst針對可組合的規則設計使得用戶或第三方可以很容易的進行拓展。開發者可以針對執行階段的查詢優化器的多個階段添加多批次規則,只要他們遵守一定的規則(如保證在分析階段,所有的變量都得到解析等)。但是,如果想在不理解Catalyst規則的情況下,仍能很容易地添加一些類型的拓展的話,那麼就需要一些其他的模塊,爲此,我們也添加了兩個輕量級的公共拓展模塊:數據源(data sources)和用戶自定義類型(user-defined types)。這兩個模塊同樣需要依賴核心引擎來與優化器的其他部分進行交互。
4.4.1 Data Sources(數據源)
開發者可以使用Spark SQL的多種API來定義新的數據源,通過這些API定義的數據源可能會觸發不同級別的優化。所有數據源都需要實現createRelation函數,此函數接受一個鍵值對的set參數,返回一個代表此關係的BaseRelation的類。
if one can be successfully loaded,指的應該是自定義的數據源的類能被成功加載。
每個BaseRelation包含一個schema(元數據)和一個可選的使用bytes呈現的估計大小。
批註:非結構化數據源可以使用一個用戶期望的schema作爲參數,例如,一個CSV文件數據源可以讓用戶設置列名和列類型)。例如,一個代表MySQL的數據源可能需要一個表名作爲參數,同時會向MySQL請求該表的估計大小(table size)。
爲了使得Spark SQL可以讀取數據,BaseRelation可以實現多個接口中的任意一個,這就可以使得接口實現更加靈活。
let them expose varying degrees of sophistication,沒有直譯,意譯)
舉例如下:
- TableScan :其中最簡單的TableScan,需要the relation返回一個RDD[Row],這個RDD包含數據表中的所有記錄。
- PrunedScan :更高級的PrunedScan接收一個列名數組,並且返回只包含給定列的Row數組。
- PrunedFilteredScan: 第三個接口,PrunedFilteredScan接收設置的列名和一個Filter類型的數組參數(Catalyst表達式的語法),可以進行謂詞下推(predicate pushdown,當前過濾器支持相等判斷、和常量對比、IN語法,each on one attribute)。
過濾器需要是具有advisory(可以理解爲強健性),例如,數據源需要返回能通過每個過濾器的數據,同時需要針對不能夠評估的數據也需要返回false(也就是說針對不能評估的數據,不是報錯,而是直接過濾掉這些數據)。
a CatalystScan interface is given a complete sequence of Catalyst expression trees to use in predicate pushdown, though they are again advisory.
最後,CatalystScan接口is given a complete sequence of Catalyst 表達式樹in predicate pushdown,儘管他們也是advisory的。
這些接口使得數據源可以實現不同級別的優化,同時仍可以使得開發者可以很簡單的添加實質上任何簡單數據源的類型。目前已經使用這些接口實現了以下數據源:
- CSV文件數據源,簡單的掃描整個文件,但是也允許用戶設置一個元數據(schema);
- Avro數據源,一個自描述二進制格式的嵌套式數據源;
- Parquet數據源,一個列式文件格式(像filters一樣支持列裁剪(column pruning,在讀數據的時候,只關心感興趣的列,而忽略其他列));
- JDBC數據源,可以並行的掃描RDBMS的表區間數據,同時可以添加過濾器來最小化數據傳輸。
如果想要使用這些數據源,程序員需要在SQL表達式中指定他們的包名,同時可以把鍵值對傳入從而對配置項進行修改。例如,Avro數據源可以接受一個路徑參數,如下:
CREATE TEMPORARY TABLE messages
USING com.databricks.spark.avro
OPTIONS (path "messages.avro")
所有的數據源可以附加網絡本地化信息。
network locality information: 網絡本地化信息,指的是數據的本地特性。
例如,可以附加如下信息:數據的每個分區從哪個機器讀取更加高效。這個信息的附加,是通過返回的RDD類添加的,因爲RDD中有內建的數據本地化API。
最後,把數據寫入已存在或新表的接口也是存在的。這些接口更加簡單,因爲Spark SQL提供了一個RDD的Row類用來作爲寫入數據的類型。
4.4.2 User-Defined Types(UDTs)(用戶自定義類型)
在Spark SQL中,我們希望添加的一個能允許高級分析的特性就是用戶自定義類型(user-defined types)。例如,機器學習相關應用可能需要一個向量類型,圖算法可能需要一個類型來表示一個圖(這在關係型表中是可能的)。雖然,添加新的類型非常具有挑戰性,但是,數據類型應用存在於執行引擎的各個方面,所以我們還是添加了這個特性。例如,在Spark SQL中,內置的數據類型使用列式壓縮格式存儲,方便在內存中進行緩存(見Section 3.6節)。同時,在前一節中提到的數據源API,我們需要把所有可能的數據類型暴露給新數據源的創建者。
在Catalyst中,我們通過映射用戶自定義類型到由Catalyst內置的類型的組合及結構化處理的類型來解決這個問題(在3.2節已作說明。爲了註冊一個Scala的類型作爲UDT,用戶需要提供一個類到Catalyst Row的內置類型的映射關係以及相反的映射關係。在用戶代碼中就可以使用Scala類型,在Spark SQL的查詢語句中,這些Scala類型就會在底層被轉換爲內置的類型。同樣的,他們也可以直接註冊UDFs(見Section3.7),來直接操作他們的類型。
舉一個小例子,假設我們想註冊一個二維的點(x,y)作爲一個UDT。我們可以使用兩個DOUBLE類型來代表這樣的向量。可以使用下面的代碼來註冊一個UDT:
class PointUDT extends UserDefinedType[Point] {
def dataType = StructType(Seq( // Our native structure
StructField("x", DoubleType),
StructField("y", DoubleType)
))
def serialize(p: Point) = Row(p.x, p.y)
def deserialize(r: Row) =
Point(r.getDouble(0), r.getDouble(1))
}
使用上述代碼,就可以把Points類型的數據轉換爲本地類型,也就是在Spark SQL中可以把這樣的數據轉換爲DataFrame類型,同時,可以使用在Points類上定義的UDFs來進行操作。另外,Spark SQL會對Ponits類型的數據在進行緩存時使用列式存儲(把x,y壓縮並存儲到不同的列中),同時Points類型數據可以寫入到Spark SQL中支持的數據源中(在數據源中使用兩個DOUBLE來存儲)。我們將會在5.2節中說明如何在Spark機器學習庫中使用這一特性。
5 Advanced Analytics Features(高級分析特性)
在本節中,主要描述了三個新添加到Spark SQL中的特性,這三個特性是針對“大數據”的環境中挑戰所提出的。
- 第一,在常見的大數據環境中,數據經常是非結構化或半結構化的。雖然解析這樣的數據從程序上來說是可行的,但是常常其處理代碼會包含很多冗長的模板式代碼(想象下連接數據庫的時候的常規代碼,這也就是爲什麼hibernate、mybatis出現的原因之一)。爲了使用戶能立馬查詢數據(就是方便查詢數據,如果直接查看JSON,那麼顯示肯定沒有解析JSON後,結構化查看清晰),Spark SQL 內置了一個專門爲JSON和其他半結構化數據處理的元數據推算算法。
- 第二,大批量處理經常會進行聚合、連接操作,特別是機器學習處理。我們把Spark SQL嵌入到一個更高級別的API中,作爲Spark機器學習庫處理的手段。
- 最後,數據管道處理可以把不同的存儲系統的數據進行整合。
基於4.4.1節提出的數據源API,Spark SQL可以實現查詢聯邦,
query federation: 查詢聯邦:可以簡單理解所有查詢都從這一個接口。
允許單個程序高效的查詢不同的數據源。這些特性都基於Catalyst框架的。
5.1 Schema Inference for Semistructured Data(半結構化數據的元數據獲取)
在大規模環境中半結構化數據是非常常見的數據源,因爲這樣的數據很容易生成,同時,隨着時間的推移,也很容易添加列(如果是固定列的表,那麼添加列就需要修改元數據,比較麻煩)。很大部分使用Spark的用戶中,其都使用JSON作爲輸入數據。不幸的是,在類似Spark和MapReduce這樣的框架中,JSON是一個非常難以操作的數據類型。比如很多用戶會使用類似ORM的映射庫(如使用Jackson)來把JSON結構映射成Java類,而另一些用戶會使用一些底層API直接對每行數據進行轉換。
在Spark SQL中,針對JSON數據源,可以直接從部分數據記錄中獲得元數據。例如,給定一個如圖5所示的JSON數據,通過推導可以得到如圖6所示的元數據。用戶可以直接把JSON文件註冊成一個表,從而可以直接使用SQL語法來訪問其中的字段,如:
SELECT loc.lat, loc.long FROM tweets
WHERE text LIKE ’%Spark%’ AND tags IS NOT NULL
元數據推理算法需要讀取數據一次,當然也可以通過設置參數,通過只讀取部分抽樣數據來應用推理算法。這個算法基於之前的XML和數據庫之間的轉換的工作基礎上,但是更加簡單,因爲只需要得到一個靜態的樹結構,其他的可能需要在任意一個元素上進行遞歸嵌套,所以會有任意的深度,這樣會更加複雜。
特別的,算法嘗試去獲得一個STRUCT(結構化)類型的樹結構,每個STRUCT類型可能包含原子類型、數組或其他STRUCTs類型。針對一個唯一路徑中的JSON類根節點的每個字段(如tweet.loc.latitude),算法會爲其找到其最匹配的Spark SQL中的類型。
最匹配也就是找到字段對應的值,然後一個個匹配,看其是否是數值、字符串等。
例如,如果某個字段的值都是整數,並且能夠剛好放入32bits,那麼字段類型就是INT;如果不能放入32bits,那麼就是LONG(64-bit)或DECIMAL(任意精度)類型;如果有小數值,那麼就是FLOAT類型。針對會出現多個類型值的字段,如同時存在字符串,數值類型的值,那麼Spark SQL使用STRING來作爲其類型,並且保持原始JSON中出現的字符串。針對包含數組的類型,那麼會使用上述類似的思路來找到數組中每個元素的類型。內部通過reduce函數來實現這個算法,這個算法從每個記錄的schemata開始推斷每個記錄的字段類型,
schemata: 例如 trees of types ?
然後使用“most specific supertype”函數整合這些字段類型
(most specific supertype函數指的是最多匹配的類型)。
這樣使得算法只需要一次讀取以及高效傳輸,因爲在每個節點可以進行本地聚合操作。
舉個小例子,在表5和表6中,算法針對loc.lat和loc.long字段都進行了泛化。在其中的一個記錄中,每個字段都是interger類型,但是在另一個記錄中,是一個floating的類型,所以最終返回FLOAT類型。注意到tags字段,算法推斷出其類型爲字符串的數組類型,同時不能爲null。
實際上,我們發現這個算法在現實生活中的JSON數據集中應用很好。例如,它能正確的識別tweets的JSON(Twitter’s firehose數據集)並得到一個可用的元數據,其包含大約100個不重複字段和多級嵌套的字段。同時,多個Databricks的用戶都已經成功應用該算法到其內部JSON數據集中。
在Spark SQL中,我們同樣使用該算法來得到Python類RDD的元數據(見Section 3),同時因爲Python的數據類型是非靜態類型,所以一個RDD可以包含不同的數據類型。我們計劃在後面會添加CSV和XML文件的元數據推導。開發者能很容易把數據集轉換爲表,轉換爲表後可以直接進行查詢或與其他數據進行連接。他們(開發者)認爲這樣的操作對其生產環境具有很大價值。
5.2 Integration with Spark’s Machine Learning Library(和Spark機器學習算法庫的整合)
作爲Spark SQL在其他Spark模塊中的應用的例子,如Spark MLlib(機器學習算法庫),在其中引入了一個使用DataFrame的高階API[26]。這個新API是基於機器學習的pipelines(管道、流水線)的理念,這個理念是其他一些高階ML庫(例如SciKit-Learn[33])的一種抽象。流水線作業指的是在數據上的一系列轉換操作,例如特徵提取(feature extraction)、歸一化(normalization)、降維(dimensionality reduction)和建模(model training),這些過程前一個的輸出對應後一個的輸入,從而構成一個流水線作業。一般來說,流水線作業是一個非常有用的抽象,因爲ML的工作流一般由很多個步驟組成。把這些步驟表示成可組合的項使得改變流水線中的某個環節或對整個流水線作業進行參數尋優都會變得非常容易。
爲了進行pipeline stages之間的數據交換,MLlib的開發者需要一種比較緊湊(是因爲數據可能很大,所以需要比較緊湊)並且仍保持靈活的格式,同時允許每行記錄可以存儲多種類型的字段數據的格式。例如,針對一個數據集,其包含文本列和數值列。用戶可能會針對文本列執行一個特徵化算法(例如TF-IDF),從而得到一個向量列,然後對其他的數值字段執行歸一化操作,最後對整個數據集執行降維操作等等,這一系列構成一個pipeline(管道)。新API使用DataFrame來表示這個數據集,在DataFrame中,每個列代表數據中的一個特徵。所有可以在Pipeline中調用的算法都可以接收輸入列名和輸出列名的參數,以及任意輸入列名的子集,從而產生新的數據集。這使得開發者可以很容易的在保留原始數據的情況下,構建複雜的pipeline。
這裏說的問題是,pipeline不改變原始數據,而只添加處理後的新列。
爲了說明這個API,在圖7中簡單展示了一個簡單的pipeline,以及各個DataFrame生成過程中的各個列信息。
爲了在MLlib中使用Spark SQL,只需要創建一個用戶友好的vector數據類型。這個向量UDT可以存儲稀疏向量或密集向量,使用四個基本數據類型字段即可表示:
- 一個boolean類型,用來表示類型(是密集或稀疏);
- 一個整型,表示vector的大小;
- 一個下標數組,作爲稀疏向量的索引;
- 一個代表值的double數組(只針對spark的非零值纔有顯示,針對dense數據,則全部顯示)。
使用DataFrame除了可以追蹤和操作列外,還有另外一個原因:通用性。
也就是使用DataFrame後,這些API可以在多種Spark支持的語言中通用,例如支持Scala、python 、R等。
而在這之前,在MLlib中的每個算法都有自己的一套數據結構,例如分類中使用labeled point類,而使用推薦算法則需要使用rating類(內含一個用戶,物品參數)。同時,這些類需要在不同的語言中都實現一遍。
例如,需要把相同的代碼在Scala、Python、R中都實現一遍。
Spark說白了就是一個數據的轉換,而使用DataFrame可以在所有語言、所有算法中達到很高的通用性。
這種通用性指的就是數據轉換的核心邏輯,而不需要關注底層如何實現。
而這種通用性在Spark添加新的語言支持的時候就顯得尤爲重要。
最後,在MLlib中使用DataFrame來表示數據後,在SQL中使用這些算法也會非常簡單。我們可以簡單的定義類MADlib的UDF(用戶自定義函數,user defined function),就像在3.7節中描述的一樣,最終在內部會在對應數據表上調用算法。我們當前也在研究在SQL中使用pipeline的方法。
5.3 Query Federation to External Databases(外部數據庫數據聯邦)
數據管道(data pipeline)經常需要整合異構的數據源。 例如,一個推薦的流水線任務可能需要整合一個用戶信息庫中的訪問日誌數據和用戶的社交流數據。鑑於這些數據源經常在不同的機器或物理隔離的位置上,直接來查詢這些數據將會導致非常低效。鑑於此,Spark SQL中使用Catalyst來對數據源進行 謂詞下推優化。
predicate down :謂詞下推,一種優化機制。
例如,下面的代碼,分別從一個JDBC數據源和一個JSON數據源中讀取數據,得到兩個表,並把兩個表進行join操作,以此來從訪問日誌中得到最近註冊的用戶。這兩個數據源都不用用戶定義就可以自動匹配schema(元數據,即列信息),非常便利。JDBC數據源會自動執行filter優化,也就是直接在MySQL端進行過濾,從而減少數據傳輸。
CREATE TEMPORARY TABLE users USING jdbc
OPTIONS(driver "mysql" url "jdbc:mysql://userDB/users")
CREATE TEMPORARY TABLE logs
USING json OPTIONS (path "logs.json")
SELECT users.id, users.name, logs.message
FROM users JOIN logs WHERE users.id = logs.userId AND users.registrationDate > "2015-01-01"
在底層,JDBC數據源使用在4.4.1節中描述的PrunedFiltered-Scan接口,這個接口可以得到請求的列名以及在這些列上的斷言。
predicates:斷言,其實就是各種縮小範圍的判斷條件。(例如equality、comparison或者IN clause)。
在本例中,JDBC數據源會在MySQL中運行這樣的代碼:
SELECT users.id, users.name FROM users WHERE users.registrationDate > "2015-01-01"
在未來的Spark SQL版本中,我們也會在針對鍵值對的數據源(如HBase或Cassandra)添加“謂詞下推”。
6 Evaluation(評估)
我們從兩個方面來評估Spark SQL的性能:
- SQL查詢處理性能
- Spark程序性能。
特別的,我們證明了Spark SQL中的增強框架不僅增加了更豐富的函數,而且對比之前的Spark-based SQL引擎有更大的性能提升。另外,對於Spark應用程序開發者來說,使用DataFrame API來進行開發效率遠遠大於原生的Spark API,同時使得Spark程序編碼更加簡化以及易於理解。最後,整合關係型和過程型的應用程序會比單獨運行SQL或執行過程型代碼運行的更快。
6.1 SQL Performance(SQL性能)
我們使用Shark、Impala來和Spark SQL進行性能對比,使用AMPLab提供的 big data benchmark測試。
big data benchmark :一個針對不同技術進行測試的網頁,一個簡單的截圖如下:
benchmark包含四種不同類型及參數的查詢,具體爲:
- 掃描(scan);
- 聚合(aggregation);
- 連接(join);
- 用戶自定義MapReduce任務。
本實驗使用6個 EC2構成的集羣(1主節點,5從節點),每個節點有4核,30G內存以及一個800G的SSD硬盤,部署HDFS2.4,Spark1.3,Shark0.9.1 和Impala 2.1.1.數據使用Parquet格式的壓縮數據,共110G。
圖8顯示了按查詢類型分組,不同查詢的結果對比。查詢1-3對比了不同參數下的性能對比。其中 1a, 2a, etc 使用更少的數據 而 1c, 2c, etc 使用更多的數據。查詢4使用一個Python-based的Hive UDF來做實驗,屬於一個計算密集型任務(UDF沒有在Impala中支持,所以就沒有列出)。
從所有的查詢中來看,Spark SQL基本上會比Shark要快,而和Impala旗鼓相當。而Spark SQL和Shark的主要不同點是在Catalyst中的代碼生成(code generation,見4.3.4節)模塊,而使用diam生成可以減少CPU開銷,提升性能。而這個特性也是Spark SQL能在很多查詢中,能和基於C++和LLVM的Impala比肩的原因。而和Impala差距最大的就是3a查詢,在這個查詢中Impala使用了一個更好的join計劃,
because the selectivity of the queries makes one of the tables very small。
因爲對於查詢的選擇性,使得其中的一個表很小,所以就可以做優化。
6.2 DataFrames vs. Native Spark Code (DataFrame和RDD對比)
Spark SQL不單單可以運行SQL查詢,對於非SQL開發者來說,也可以通過DataFrame API來編寫簡單且高效的Spark代碼。Catalyst可以針對DataFrame的程序進行優化(而對於使用RDD編程的代碼卻不能提供優化,而這裏的RDD編程其實就是對應hand written code),例如predicate pushdown(斷言優化?)、管道操作(pipelining)、自動連接操作
所謂自動,指的是不管你是先過濾表,再連接,或者是先連接再過濾,Catalyst會自動幫你優化成先過濾再連接操作。
即使沒有這些優化,使用DataFrame API也可以獲得更高效的性能,因爲DataFrame 代碼在底層會進行代碼生成(code generation)。特別是針對Python編寫的應用,因爲Python原生就會比JVM要慢。
針對DataFrame和RDD編程性能的評估,我們針對分佈式的聚合操作分別給出了兩種Spark的實現,一種是Spark RDD,一種是Spark DataFrame。使用的數據包含10億個(a,b)這樣的鍵值對,其中a是由10萬個唯一值中的隨機一個,使用的集羣仍然是之前使用的5個節點的集羣。我們通過計算每個a值對應的b值的平均值來評估時間消耗。
首先,先看下使用Spark中Python API實現的版本:
sum_and_count = \
data.map(lambda x: (x.a, (x.b, 1))) \
.reduceByKey(lambda x, y: (x[0]+y[0], x[1]+y[1])) \
.collect()
[(x[0], x[1][0] / x[1][1]) for x in sum_and_count]
作爲對比,實現相同功能的代碼在DataFrame API中的實現只需要簡單的一行,如下:
df.groupBy("a").avg("b")
在圖9中,可以看出DataFrame版本的實現比Python RDD版本的性能要好12倍左右,同時也更加簡潔。這是因爲DataFrame的API只有邏輯計劃是由Python構建的,而後的物理執行則是由原生Spark代碼生成的JVM二進制代碼,所以會獲得更好的執行性能。
實際上,DataFrame版本的代碼會比Scala版本的執行效率高2倍,這主要歸功於代碼生成:如果使用RDD編程,那麼對於鍵值對的內存分配會非常低效,而在DataFrame版本中的卻可以避免這種情況。
6.3 Pipeline Performance(管道性能)
針對同時使用關係型和過程型代碼的應用,DataFrame API也可以取得性能提升,用戶可以在一個程序中編寫完整的操作,接着,把這些操作整合成管道進而進行計算。
例如,考慮一個包含兩個階段的管道操作:從一個文本消息語料中提取一個子集,並計算出現最頻繁的單詞。儘管這個例子很簡單,但是很多真實的管道操作也是和此相似的,如針對特定的人羣來進行統計最受歡迎的tweet(推特)。
在此次試驗中,我們在HDFS上生成了一個100億條人工生成的數據集。每個記錄平均包含10個從英語詞典中抽取的單詞。管道操作的第一個階段(stage)使用一個關係型的filter也選擇了大概90%的數據。第二個stage計算每個單詞的出現次數。
針對兩種不同的思路,分別說明如下:
- 先應用SQL 查詢來獲取子集,然後使用RDD代碼來實現單詞出現的次數。這會使用不同的執行引擎,例如SQL查詢會使用Hive引擎,而RDD代碼則使用Spark引擎。
- 通過DataFrame來實現這個管道操作。使用DataFrame的關係型操作符來實現filter,使用RDD API來執行單詞計數。
其中,第二個實現思路避免了SQL查詢結果保存到HDFS文件上的操作(寫文件降低效率),第二個思路是直接把filter和map的單詞計數形成一個pipeline。
圖10對比了兩種思路的性能和效率,從圖中可以看出,DataFrame的執行方案的性能是另一種的2倍。
7 Research Applications(研究型應用)
Spark SQL除了應用於真實的生產環境外,一些研究者同樣對把Spark SQL應用在實驗項目上有很大興趣。在此,爲了說明Catalyst的擴展性,主要說明兩個研究型項目:一個是近似查詢處理(approximate query processing),另一個是genomics(基因學)。
7.1 Generalized Online Aggregation(廣義在線聚合)
Zeng et al (應該是個人)在他們的項目中使用Catalyst來提升在線聚合的泛化能力(generality of online aggregation)。他們的工作使得在線聚合的執行能支持任意嵌套的聚合查詢。
它允許用戶通過查看在總數據的一小部分上計算的結果來查看執行查詢的進度。
這些部分的結果也包含正確率,這就使得用戶可以在正確率達到一定程度後,結束查詢。
爲了在Spark SQL內部實現這個系統,該項目研發者添加了一個新的算子,這個算子對原始數據進行抽樣,並返回多組抽樣的結果。在調用transform函數的時候,其查詢計劃就會把原始所有數據的查詢替換爲逐個的抽樣的子集的查詢。
但是,在在線的環境中,只是簡單的用抽樣的數據來替換整個數據集並不能夠計算出正確的結果。類似標準聚合操作需要使用有狀態的副本來替換,
有狀態的副本: 副本這裏可以理解爲數據集,也就是有狀態的數據集。有狀態的數據集在生成的時候,會先進行計算,然後和之前計算好的批次的數據集結果進行整合。
也就是說需要同時考慮當前抽樣子集數據以及之前批處理子集的結果。再者,一些可能根據一個近似結果來進行元組過濾的操作一定需要被替換成能夠考慮當前估算誤差的版本。
所有這些transformations都可以通過Catalyst 規則來表示,只需要不斷修改算子樹(operator tree)直到輸出正確的在線結果。不是基於樣本數據的樹片段會被這些規則忽略,同時這些樹片段可以使用標準的代碼路徑執行。以Spark SQL作爲基礎,該作者可以通過約2k行代碼實現一個相當完整的原型應用。
7.2 Computational Genomics(計算基因組學)
在計算基因組學中常見的操作涉及基於數值偏移檢查重疊區域。這個問題可以表示爲一個包含不相等表達式的join操作。
例如,兩個數據集a和b,其結構爲(start LONG, end LONG)。那麼區間join操作可以使用下面的SQL表示:
SELECT * FROM a JOIN b
WHERE a.start < a.end
AND b.start < b.end
AND a.start < b.start
AND b.start < a.end
除了使用特殊的優化外,在很多系統中,上面的查詢都會被以一種非常低效的算法來執行,例如使用嵌套的循環join來執行。作爲對比,一些專有的系統可以使用一個區間樹(interval tree)來執行上面的查詢SQL。在ADAM項目中的研究人員在Spark SQL中的一個版本中構建了一個特殊的planning rule(計劃規則),使得他們可以在標準的數據處理能力及特殊的處理代碼直接得到平衡。
這些實現代碼大約有100行。
8 Related Work(相關研究)
Programming Model(編程模型)
最初應用在大集羣的一些系統中,他們的設計之初就是尋求一種可以整合關係型處理和過程型處理引擎的模型。在這些系統中,Shark[38]最像Spark SQL,都是在Spark引擎中執行,同時同樣提供關係型查詢和高級的分析過程的整合。Spark SQL比Shark更強的地方在於提供更多操作算子及更友好的API(也就是DataFrame)。在DataFrame中一個查詢可以被分割成多個模塊(見Section3.4)。同時,DataFrame也支持在原生RDD上執行關係型查詢,它還支持除了Hive外很多的數據源。
Spark SQL的設計深受 DryadLINQ[20]的影響。DryadLINQ 可以把用C#編寫的查詢進行編譯,併發送到一個分佈式的DAG執行引擎中,而這正是激發設計Spark SQL的初衷。LINO查詢通常是關係型的,但是也可以直接在C#的類上操作。Spark SQL 超越DryadLINO的地方在於其提供了一個和常見的數據科學庫[32,30]中提供的接口,叫做DataFrame 。DataFrame支持數據源、數據類型以及循環算法執行等。
其他的系統只是在其內部使用一個關係型數據模型然後把過程型處理代碼轉換成UDFs。例如,Hive或Pig[36,29]提供關係型查詢語言,同時也使用了很多UDF接口。ASTERIX在內部使用一個半結構化的數據模型。Stratosphere[8]也使用一個半結構化的模型,提供Scala、Java API,可以方便用戶調用UDF。PIQL[7]同樣提供一個Scala DSL(領域特定語言)。和這些系統對比,Spark SQL在整合原生Spark應用方面顯得更加契合,因爲用戶可以直接在用戶自定義的類上(如原生的Java或Python object)執行查詢,而且開發者可以在一個語言中使用關係型和過程型API來進行編程。除此之外,通過Catalyst 優化器,Spark SQL不僅實現了優化(例如代碼生成),而且還實現了其他功能(例如JSON的元數據識別以及機器學習中的數據類型),這在很多大數據計算框架中都是沒有的。我們相信這些特性對於提供一個整合的、易用的大數據環境是非常必要的。
最後,DataFrame API既可以用於單機程序[32,30],也可以用於集羣[13,10]。和之前的API不同,Spark SQL通過一個關係型優化器來優化DataFrame的計算。
Extensible Optimizers(可擴展的優化器)
Catalyst優化器和其他優化器框架有一樣的目標,如EXODUS[17]、Casscades[16]等優化器。之前,人們一直認爲優化器框架需要有一個領域特定語言來編寫規則,同時要有一個“優化器編譯器(Optimizer compiler)”來把這些規則翻譯爲能執行的代碼。而在Spark SQL中最主要的改良就是使用函數式編程語言的標準特性來構建優化器,而使用這種方式同樣可以提供和之前一樣的功能(甚至更強),並且降低了維護和學習成本。編程語言中的這些高級特性使得Catalyst的設計受益很大,例如代碼生成的實現就是使用quasiquotes(見Section 4.3.4),而據我們所知quasiquotes是實現這個任務的最簡單的方法之一,同時使得其具有較強的組合能力。雖然可擴展性很難定量的評估,但是,Spark SQL在發佈最開始的8個月中,已經有超過50個外部貢獻者參與進來,這就很能說明問題了。
對於代碼生成,LegoBase[22]最近發表了一種使用Scala中生成式編程(generative programming)的方式來實現的方式,而這種方式就很可能用來替代使用quasiquotes,從而有更高的性能提升。
Advanced Analytics(高級分析)
Spark SQL基於最近的一些成果,纔可以在大規模集羣上進行一些高級分析算法,諸如專注於迭代算法[39]和圖分析[15,24]的平臺。就像MADlib一樣,Spark SQL也很希望對用戶提供更多可用的分析函數,但是MADlib[12]和Spark SQL的實現是不一樣的。在MADlib中只能使用Postgres中有限的UDF接口,而Spark SQL中的UDFs已經發展成爲一個成熟的Spark程序。最後,一些技術,如Sinew和Invisible Loading[35,1]都在需求在半結構化數據(例如JSON)查詢上的優化。我們希望可以在Spark SQL中應用這些先進的技術。
9 Conclusion(總結)
我們研究併發布了一個在Apache Spark中的一個新模塊,Spark SQL,提供了多種關係型處理的操作。Spark SQL使用聲明式DataFrame API,進而提供關係型操作,以及提供諸如自動優化的特性,同時可以使得用戶能把關係統操作和複雜的分析操作相混合成管道操作。它支持廣泛的定製的大規模數據處理,包含半結構化數據,查詢聯邦(query federation)以及機器學習中的數據類型。
爲了使用這些特性,Spark SQL在內部實現了一個可擴展的優化器,Catalyst。Catalyst利用嵌入Scala編程語言的優勢可以很方便的加入優化規則、數據源、數據類型。用戶的反饋以及一些測試程序說明Spark SQL使得編寫能整合關係型和過程型處理的數據管道操作更加簡單和高效,同時提供比之前的SQL -on -Spark 引擎更高的性能提升。
Spark SQL 現已開源,其官網http://spark.apache.org 。
10 Acknowldegments(致謝)
…
11 References(參考)
[1] A. Abouzied, D. J. Abadi, and A. Silberschatz. Invisible loading: Access-driven data transfer from raw files into database systems. In EDBT, 2013.
[2] A. Alexandrov et al. The Stratosphere platform for big data analytics. The VLDB Journal, 23(6):939–964, Dec. 2014.
[3] AMPLab big data benchmark. https://amplab.cs.berkeley.edu/benchmark.
[4] Apache Avro project. http://avro.apache.org.
[5] Apache Parquet project. http://parquet.incubator.apache.org.
[6] Apache Spark project. http://spark.apache.org.
[7] M. Armbrust, N. Lanham, S. Tu, A. Fox, M. J. Franklin, and
D. A. Patterson. The case for PIQL: a performance insightful
query language. In SOCC, 2010.
[8] A. Behm et al. Asterix: towards a scalable, semistructured
data platform for evolving-world models. Distributed and
Parallel Databases, 29(3):185–216, 2011.
[9] G. J. Bex, F. Neven, and S. Vansummeren. Inferring XML
schema definitions from XML data. In VLDB, 2007.
[10] BigDF project. https://github.com/AyasdiOpenSource/bigdf.
[11] C. Chambers, A. Raniwala, F. Perry, S. Adams, R. R. Henry,
R. Bradshaw, and N. Weizenbaum. FlumeJava: Easy, efficient
data-parallel pipelines. In PLDI, 2010.
[12] J. Cohen, B. Dolan, M. Dunlap, J. Hellerstein, and C. Welton.
MAD skills: new analysis practices for big data. VLDB, 2009.
[13] DDF project. http://ddf.io.
[14] B. Emir, M. Odersky, and J. Williams. Matching objects with
patterns. In ECOOP 2007 – Object-Oriented Programming,
volume 4609 of LNCS, pages 273–298. Springer, 2007.
[15] J. E. Gonzalez, R. S. Xin, A. Dave, D. Crankshaw, M. J.
Franklin, and I. Stoica. GraphX: Graph processing in a
distributed dataflow framework. In OSDI, 2014.
[16] G. Graefe. The Cascades framework for query optimization.
IEEE Data Engineering Bulletin, 18(3), 1995.
[17] G. Graefe and D. DeWitt. The EXODUS optimizer generator.
In SIGMOD, 1987.
[18] J. Hegewald, F. Naumann, and M. Weis. XStruct: efficient schema extraction from multiple and large XML documents. In ICDE Workshops, 2006.
[19] Hive data definition language. https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL.
[20] M. Isard and Y. Yu. Distributed data-parallel computing using a high-level programming language. In SIGMOD, 2009.
[21] Jackson JSON processor. http://jackson.codehaus.org.
[22] Y. Klonatos, C. Koch, T. Rompf, and H. Chafi. Building
efficient query engines in a high-level language. PVLDB,
7(10):853–864, 2014.
[23] M. Kornacker et al. Impala: A modern, open-source SQL
engine for Hadoop. In CIDR, 2015.
[24] Y. Low et al. Distributed GraphLab: a framework for machine
learning and data mining in the cloud. VLDB, 2012.
[25] S. Melnik et al. Dremel: interactive analysis of web-scale
datasets. Proc. VLDB Endow., 3:330–339, Sept 2010.
[26] X. Meng, J. Bradley, E. Sparks, and S. Venkataraman. ML
pipelines: a new high-level API for MLlib. https://databricks.com/blog/2015/01/07/ml-pipelines-a-new- high-level-api-for-mllib.html.
[27] S. Nestorov, S. Abiteboul, and R. Motwani. Extracting schema from semistructured data. In ICDM, 1998.
[28] F. A. Nothaft, M. Massie, T. Danford, Z. Zhang, U. Laserson, C. Yeksigian, J. Kottalam, A. Ahuja, J. Hammerbacher,
M. Linderman, M. J. Franklin, A. D. Joseph, and D. A. Patterson. Rethinking data-intensive science using scalable analytics systems. In SIGMOD, 2015.
[29] C. Olston, B. Reed, U. Srivastava, R. Kumar, and A. Tomkins. Pig Latin: a not-so-foreign language for data processing. In SIGMOD, 2008.
[30] pandas Python data analysis library. http://pandas.pydata.org.
[31] A. Pavlo et al. A comparison of approaches to large-scale data
analysis. In SIGMOD, 2009.
[32] R project for statistical computing. http://www.r-project.org. [33] scikit-learn: machine learning in Python.
http://scikit-learn.org.
[34] D. Shabalin, E. Burmako, and M. Odersky. Quasiquotes for
Scala, a technical report. Technical Report 185242, École
Polytechnique Fédérale de Lausanne, 2013.
[35] D. Tahara, T. Diamond, and D. J. Abadi. Sinew: A SQL
system for multi-structured data. In SIGMOD, 2014.
[36] A. Thusoo et al. Hive–a petabyte scale data warehouse using
Hadoop. In ICDE, 2010.
[37] P. Wadler. Monads for functional programming. In Advanced
Functional Programming, pages 24–52. Springer, 1995.
[38] R. S. Xin, J. Rosen, M. Zaharia, M. J. Franklin, S. Shenker,
and I. Stoica. Shark: SQL and rich analytics at scale. In
SIGMOD, 2013.
[39] M. Zaharia et al. Resilient distributed datasets: a fault-tolerant
abstraction for in-memory cluster computing. In NSDI, 2012.
[40] K. Zeng et al. G-OLA: Generalized online aggregation for
interactive analysis on big data. In SIGMOD, 2015.