Spark SQL 1.3.0概覽

摘要:DataFrame API的引入一改RDD API高冷的FP姿態,令Spark變得更加平易近人。外部數據源API體現出的則是兼容幷蓄,Spark SQL多元一體的結構化數據處理能力正在逐漸釋放。

關於作者:連城,Databricks工程師,Spark committer,Spark SQL主要開發者之一。在4月18日召開的 2015 Spark技術峯會 上,連城將做名爲“四兩撥千斤??Spark SQL結構化數據分析”的主題演講。

自2013年3月面世以來,Spark SQL已經成爲除Spark Core以外最大的Spark組件。除了接過Shark的接力棒,繼續爲Spark用戶提供高性能的SQL on Hadoop解決方案之外,它還爲Spark帶來了通用、高效、多元一體的結構化數據處理能力。在剛剛發佈的1.3.0版中,Spark SQL的兩大升級被詮釋得淋漓盡致。

DataFrame

就易用性而言,對比傳統的MapReduce API,說Spark的RDD API有了數量級的飛躍並不爲過。然而,對於沒有MapReduce和函數式編程經驗的新手來說,RDD API仍然存在着一定的門檻。另一方面,數據科學家們所熟悉的R、Pandas等傳統數據框架雖然提供了直觀的API,卻侷限於單機處理,無法勝任大數據場景。爲了解決這一矛盾,Spark SQL 1.3.0在原有SchemaRDD的基礎上提供了與R和Pandas風格類似的DataFrame API。新的DataFrame AP不僅可以大幅度降低普通開發者的學習門檻,同時還支持Scala、Java與Python三種語言。更重要的是,由於脫胎自SchemaRDD,DataFrame天然適用於分佈式大數據場景。

DataFrame是什麼?

在Spark中,DataFrame是一種以RDD爲基礎的分佈式數據集,類似於傳統數據庫中的二維表格。DataFrame與RDD的主要區別在於,前者帶有schema元信息,即DataFrame所表示的二維表數據集的每一列都帶有名稱和類型。這使得Spark SQL得以洞察更多的結構信息,從而對藏於DataFrame背後的數據源以及作用於DataFrame之上的變換進行了針對性的優化,最終達到大幅提升運行時效率的目標。反觀RDD,由於無從得知所存數據元素的具體內部結構,Spark Core只能在stage層面進行簡單、通用的流水線優化。

平易近人、兼容幷蓄--Spark SQL 1.3.0概覽0 

創建DataFrame

在Spark SQL中,開發者可以非常便捷地將各種內、外部的單機、分佈式數據轉換爲DataFrame。以下Python示例代碼充分體現了Spark SQL 1.3.0中DataFrame數據源的豐富多樣和簡單易用:

[py]  view plain copy
  1. # 從Hive中的users表構造DataFrame  
  2. users = sqlContext.table("users")  
  3.   
  4. # 加載S3上的JSON文件  
  5. logs = sqlContext.load("s3n://path/to/data.json", "json")  
  6.   
  7. # 加載HDFS上的Parquet文件  
  8. clicks = sqlContext.load("hdfs://path/to/data.parquet", "parquet")  
  9.   
  10. # 通過JDBC訪問MySQL  
  11. comments = sqlContext.jdbc("jdbc:mysql://localhost/comments", "user")  
  12.   
  13. # 將普通RDD轉變爲DataFrame  
  14. rdd = sparkContext.textFile("article.txt") \  
  15.                   .flatMap(lambda line: line.split()) \  
  16.                   .map(lambda word: (word, 1)) \  
  17.                   .reduceByKey(lambda a, b: a + b) \  
  18. wordCounts = sqlContext.createDataFrame(rdd, ["word", "count"])  
  19.   
  20. # 將本地數據容器轉變爲DataFrame  
  21. data = [("Alice", 21), ("Bob", 24)]  
  22. people = sqlContext.createDataFrame(data, ["name", "age"])  
  23.   
  24. # 將Pandas DataFrame轉變爲Spark DataFrame(Python API特有功能)  
  25. sparkDF = sqlContext.createDataFrame(pandasDF)  

可見,從Hive表,到外部數據源API支持的各種數據源(JSON、Parquet、JDBC),再到RDD乃至各種本地數據集,都可以被方便快捷地加載、轉換爲DataFrame。這些功能也同樣存在於Spark SQL的Scala API和Java API中。

使用DataFrame

和R、Pandas類似,Spark DataFrame也提供了一整套用於操縱數據的DSL。這些DSL在語義上與SQL關係查詢非常相近(這也是Spark SQL能夠爲DataFrame提供無縫支持的重要原因之一)。以下是一組用戶數據分析示例:

[py]  view plain copy
  1. # 創建一個只包含"年輕"用戶的DataFrame  
  2. young = users.filter(users.age < 21)  
  3.   
  4. # 也可以使用Pandas風格的語法  
  5. young = users[users.age < 21]  
  6.   
  7. # 將所有人的年齡加1  
  8. young.select(young.name, young.age + 1)  
  9.   
  10. # 統計年輕用戶中各性別人數  
  11. young.groupBy("gender").count()  
  12.   
  13. # 將所有年輕用戶與另一個名爲logs的DataFrame聯接起來  
  14. young.join(logs, logs.userId == users.userId, "left_outer")  

除DSL以外,我們當然也可以像以往一樣,用SQL來處理DataFrame: 

[py]  view plain copy
  1. young.registerTempTable("young")  
  2. sqlContext.sql("SELECT count(*) FROM young")  

最後,當數據分析邏輯編寫完畢後,我們便可以將最終結果保存下來或展現出來: 

[py]  view plain copy
  1. # 追加至HDFS上的Parquet文件  
  2. young.save(path="hdfs://path/to/data.parquet",  
  3.            source="parquet",  
  4.            mode="append")  
  5.   
  6. # 覆寫S3上的JSON文件  
  7. young.save(path="s3n://path/to/data.json",  
  8.            source="json",  
  9.            mode="append")  
  10.   
  11. # 保存爲SQL表  
  12. young.saveAsTable(tableName="young", source="parquet" mode="overwrite")  
  13.   
  14. # 轉換爲Pandas DataFrame(Python API特有功能)  
  15. pandasDF = young.toPandas()  
  16.   
  17. # 以表格形式打印輸出  
  18. young.show()  

幕後英雄:Spark SQL查詢優化器與代碼生成

正如RDD的各種變換實際上只是在構造RDD DAG,DataFrame的各種變換同樣也是lazy的。它們並不直接求出計算結果,而是將各種變換組裝成與RDD DAG類似的邏輯查詢計劃。如前所述,由於DataFrame帶有schema元信息,Spark SQL的查詢優化器得以洞察數據和計算的精細結構,從而施行具有很強針對性的優化。隨後,經過優化的邏輯執行計劃被翻譯爲物理執行計劃,並最終落實爲RDD DAG。

平易近人、兼容幷蓄--Spark SQL 1.3.0概覽1  

這樣做的好處體現在幾個方面:

1. 用戶可以用更少的申明式代碼闡明計算邏輯,物理執行路徑則交由Spark SQL自行挑選。一方面降低了開發成本,一方面也降低了使用門檻??很多情況下,即便新手寫出了較爲低效的查詢,Spark SQL也可以通過過濾條件下推、列剪枝等策略予以有效優化。這是RDD API所不具備的。

2. Spark SQL可以動態地爲物理執行計劃中的表達式生成JVM字節碼,進一步實現歸避虛函數調用開銷、削減對象分配次數等底層優化,使得最終的查詢執行性能可以與手寫代碼的性能相媲美。

3. 對於PySpark而言,採用DataFrame編程時只需要構造體積小巧的邏輯執行計劃,物理執行全部由JVM端負責,Python解釋器和JVM間大量不必要的跨進程通訊得以免除。如上圖所示,一組簡單的對一千萬整數對做聚合的測試中,PySpark中DataFrame API的性能輕鬆勝出RDD API近五倍。此外,今後Spark SQL在Scala端對查詢優化器的所有性能改進,PySpark都可以免費獲益。

外部數據源API增強

平易近人、兼容幷蓄--Spark SQL 1.3.0概覽2 

從前文中我們已經看到,Spark 1.3.0爲DataFrame提供了豐富多樣的數據源支持。其中的重頭戲,便是自Spark 1.2.0引入的外部數據源API。在1.3.0中,我們對這套API做了進一步的增強。

數據寫入支持

在Spark 1.2.0中,外部數據源API只能將外部數據源中的數據讀入Spark,而無法將計算結果寫回數據源;同時,通過數據源引入並註冊的表只能是臨時表,相關元信息無法持久化。在1.3.0中,我們提供了完整的數據寫入支持,從而補全了多數據源互操作的最後一塊重要拼圖。前文示例中Hive、Parquet、JSON、Pandas等多種數據源間的任意轉換,正是這一增強的直接成果。

站在Spark SQL外部數據源開發者的角度,數據寫入支持的API主要包括:

1. 數據源表元數據持久化

1.3.0引入了新的外部數據源DDL語法(SQL代碼片段)

CREATE [TEMPORARY] TABLE [IF NOT EXISTS]  
  <table-name> [(col-name data-type [, ...)]  
USING <source> [OPTIONS ...]  
[AS <select-query>]

由此,註冊自外部數據的SQL表既可以是臨時表,也可以被持久化至Hive metastore。需要持久化支持的外部數據源,除了需要繼承原有的RelationProvider以外,還需繼承CreatableRelationProvider。

2. InsertableRelation

支持數據寫入的外部數據源的relation類,還需繼承trait InsertableRelation,並在insert方法中實現數據插入邏輯。

Spark 1.3.0中內置的JSON和Parquet數據源都已實現上述API,可以作爲開發外部數據源的參考示例。

統一的load/save API

在Spark 1.2.0中,要想將SchemaRDD中的結果保存下來,便捷的選擇並不多。常用的一些包括:

rdd.saveAsParquetFile(...)rdd.saveAsTextFile(...)rdd.toJSON.saveAsTextFile(...)rdd.saveAsTable(...)....

可見,不同的數據輸出方式,採用的API也不盡相同。更令人頭疼的是,我們缺乏一個靈活擴展新的數據寫入格式的方式。

針對這一問題,1.3.0統一了load/save API,讓用戶按需自由選擇外部數據源。這套API包括:

1.SQLContext.table

從SQL表中加載DataFrame。

2.SQLContext.load

從指定的外部數據源加載DataFrame。

3.SQLContext.createExternalTable

將指定位置的數據保存爲外部SQL表,元信息存入Hive metastore,並返回包含相應數據的DataFrame。

4.DataFrame.save

將DataFrame寫入指定的外部數據源。

5.DataFrame.saveAsTable

將DataFrame保存爲SQL表,元信息存入Hive metastore,同時將數據寫入指定位置。

Parquet數據源增強

Spark SQL從一開始便內置支持Parquet這一高效的列式存儲格式。在開放外部數據源API之後,原有的Parquet支持也正在逐漸轉向外部數據源。1.3.0中,Parquet外部數據源的能力得到了顯著增強。主要包括schema合併和自動分區處理。

1.Schema合併

與ProtocolBuffer和Thrift類似,Parquet也允許用戶在定義好schema之後隨時間推移逐漸添加新的列,只要不修改原有列的元信息,新舊schema仍然可以兼容。這一特性使得用戶可以隨時按需添加新的數據列,而無需操心數據遷移。

2.分區信息發現

按目錄對同一張表中的數據分區存儲,是Hive等系統採用的一種常見的數據存儲方式。新的Parquet數據源可以自動根據目錄結構發現和推演分區信息。

3.分區剪枝

分區實際上提供了一種粗粒度的索引。當查詢條件中僅涉及部分分區時,通過分區剪枝跳過不必要掃描的分區目錄,可以大幅提升查詢性能。

以下Scala代碼示例統一展示了1.3.0中Parquet數據源的這幾個能力(Scala代碼片段):

// 創建兩個簡單的DataFrame,將之存入兩個獨立的分區目錄 
val df1 = (1 to 5).map(i => (i, i * 2)).toDF("single", "double") 
df1.save("data/test_table/key=1", "parquet", SaveMode.Append) 
val df2 = (6 to 10).map(i => (i, i * 2)).toDF("single", "double") 
df2.save("data/test_table/key=2", "parquet", SaveMode.Append) 
// 在另一個DataFrame中引入一個新的列,並存入另一個分區目錄 
val df3 = (11 to 15).map(i => (i, i * 3)).toDF("single", "triple") 
df3.save("data/test_table/key=3", "parquet", SaveMode.Append) 
// 一次性讀入整個分區表的數據 
val df4 = sqlContext.load("data/test_table", "parquet") 
// 按分區進行查詢,並展示結果 
val df5 = df4.filter($"key" >= 2) df5.show()

這段代碼的執行結果爲: 

6 12 null 2  
7 14 null 2  
8 16 null 2  
9 18 null 2  
10 20 null 2  
11 null 33 3  
12 null 36 3  
13 null 39 3  
14 null 42 3  
15 null 45 3

可見,Parquet數據源自動從文件路徑中發現了key這個分區列,並且正確合併了兩個不相同但相容的schema。值得注意的是,在最後的查詢中查詢條件跳過了key=1這個分區。Spark SQL的查詢優化器會根據這個查詢條件將該分區目錄剪掉,完全不掃描該目錄中的數據,從而提升查詢性能。

小結

DataFrame API的引入一改RDD API高冷的FP姿態,令Spark變得更加平易近人,使大數據分析的開發體驗與傳統單機數據分析的開發體驗越來越接近。外部數據源API體現出的則是兼容幷蓄。目前,除了內置的JSON、Parquet、JDBC以外,社區中已經涌現出了CSV、Avro、HBase等多種數據源,Spark SQL多元一體的結構化數據處理能力正在逐漸釋放。

爲開發者提供更多的擴展點,是Spark貫穿整個2015年的主題之一。我們希望通過這些擴展API,切實地引爆社區的能量,令Spark的生態更加豐滿和多樣。

“2015 OpenStack技術大會”、“2015 Spark技術峯會”、“2015 Container技術峯會” 4月17-18日在北京召開。日程全部公開!  OpenCloud 2015,懂行的人都在這裏!更多講師和日程信息請關注OpenCloud 2015介紹和官網。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章