案例分析 | 由Decimal操作计算引发的Spark数据丢失问题
轉載自??案例分析 | 由Decimal操作計算引發的Spark數據丟失問題
供稿 | Hadoop Team
編輯 | 顧欣怡
本文3058字,預計閱讀時間10分鐘
導讀
eBay的Hadoop集群上面每天運行著大量Spark計算任務。對于數據計算任務,其計算性能十分重要,數據質量也不可忽視,特別是對于金融數據,數據發生損壞將會產生嚴重后果。本文分享一次數據質量相關的問題以及我們排查該問題的過程和解決方案。
?
一、癥狀
一天,金融分析團隊的同事報告了一個問題,他們發現在兩個生產環境中(為了區分,命名為環境A和B), Spark大版本均為2.3。但是,當運行同樣的SQL語句,對結果進行對比后,卻發現兩個環境中有一列數據并不一致。
此處對數據進行脫敏,僅顯示發生數據丟失那一列的數據,如下:
由此可見,在環境A中可以查詢到該列數據,但是在環境B中卻出現了部分數據缺失。
?
二、排查
上述兩個查詢中用的Spark大版本是一致的,團隊的同事通過對比兩個環境中的配置,發現有一個參數在最近進行了變更。該參數為:spark.sql.decimalOperations.allowPrecisionLoss, 默認為true。
在環境A中未設置此參數,所以為true,而在環境B下Spark client的spark-defaults.conf中,該參數設置為false。
該參數為PR SPARK-22036 引入,是為了控制在兩個Decimal類型操作數做計算的時候,是否允許丟失精度。在本文中,我們就針對乘法這種計算類型做具體分析。
?
關于Decimal類型
在詳細介紹該參數之前,先介紹一下Decimal。
Decimal是數據庫中的一種數據類型,不屬于浮點數類型,可以在定義時劃定整數部分以及小數部分的位數。對于一個Decimal類型,scale表示其小數部分的位數,precision表示整數部分位數和小數部分位數之和。
一個Decimal類型表示為Decimal(precision, scale),在Spark中,precision和scale的上限都是38。
一個double類型可以精確地表示小數點后15位,有效位數為16位。
可見,Decimal類型則可以更加精確地表示,保證數據計算的精度。
例如一個Decimal(38, 24)類型可以精確表示小數點后23位,小數點后有效位數為24位。而其整數部分還剩下14位可以用來表示數據,所以整數部分可以表示的范圍是-10^14+1~10^14-1。
?
關于精度和Overflow
關于精度的問題其實我們小學時候就涉及到了,比如求兩個小數加減乘除的結果,然后保留小數點后若干有效位,這就是保留精度。
乘法操作我們都很清楚,如果一個n位小數乘以一個m位小數,那么結果一定是一個(n+m)位小數。
舉個例子, 1.11 * 1.11精確的結果是 1.2321,如果我們只能保留小數點后兩位有效位,那么結果就是1.23。
上面我們提到過,對于Decimal類型,由于其整數部分位數是(precision-scale),因此該類型能表示的范圍是有限的,一旦超出這個范圍,就會發生Overflow。而在Spark中,如果Decimal計算發生了Overflow,就會默認返回Null值。
舉個例子,一個Decimal(3,2)類型代表小數點后用兩位表示,整數部分用一位表示,因此該類型可表示的整數部分范圍為-9~9。如果我們CAST(12.32 as Decimal(3,2)),那么將會發生Overflow。
下面介紹spark.sql.decimalOperations. allowPrecisionLoss參數。
當該參數為true(默認)時,表示允許Decimal計算丟失精度,并根據Hive行為和SQL ANSI 2011規范來決定結果的類型,即如果無法精確地表示,則舍入結果的小數部分。
當該參數為false時,代表不允許丟失精度,這樣數據就會表示得更加精確。eBay的ETL部門在進行數據校驗的時候,對數據精度有較高要求,因此我們引入了這個參數,并將其設置為false以滿足ETL部門的生產需求。
設置這個參數的初衷是美好的,但是為什么會引發數據損壞呢?
用戶的SQL數據非常長,通過查看相關SQL的執行計劃,然后進行簡化,得到一個可以復現的SQL語句,如下:
?
上面的select語句將會返回一個NULL。
我們將上述語句的執行計劃打印出來。
?
執行計劃很簡單,里面有一個二元操作(乘法),左邊的case when 是一個Decimal(34, 24)類型,右邊是一個Literal(1)。
程序員都知道,在編程中,如果兩個不同類型的操作數做計算,就會將低級別的類型向高級別的類型進行類型轉換,Spark中也是如此。
一條SQL語句進入Spark-sql引擎之后,要經歷Analysis->optimization->生成可執行物理計劃的過程。而這個過程就是不同的Rule不斷作用在Plan上面,然后Plan隨之轉化的過程。
在Spark-sql中有一系列關于類型轉換的Rule,這些Rule作用在Analysis階段的Resolution子階段。
其中就有一個Rule叫做ImplicitTypeCasts,會對二元操作(加減乘除)的數據類型進行轉換,如下圖所示:
用文字解釋一下,針對一個二元操作(加減乘除), 如果左邊的數據類型和右邊不一致,那么會尋找一個左右操作數的通用類型(common type), 然后將左右操作數都轉換為通用類型。針對我們此案例中的 Decimal(34, 24) 和Literal(1), 它們的通用類型就是Decimal(34, 24),所以這里的Literal(1)將被轉換為Decimal(34, 24)。
這樣該二元操作的兩邊就都是Decimal類型。接下來這個二元操作會被Rule DecimalPrecision中的decimalAndDecimal方法處理。
在不允許精度丟失時,Spark會為該二元操作計算一個用來表達計算結果的Decimal類型,其precision和scale的計算公式如下表所示,這是參考了SQLServer的實現。
?
此處我們的操作數都已經是Decimal(34, 24)類型了,所以p1=p2=34, s1=s2=24。
如果不允許精度丟失,那么其結果類型就是 Decimal(p1+p2+1, s1+s2)。由于precision和scale都不能超過上限38,所以這里的結果類型是Decimal(38, 38), 也就是小數部分為38位。于是整數部分就只剩下0位來表示,也就是說如果整數部分非0,那么這個結果就會Overflow。在當前版本中,如果Decimal Operation 計算發生了Overflow,就會返回一個Null的結果。
這也解釋了在前面的場景中,為什么使用環境B中Spark客戶端跑的結果,非Null的結果中整數部分都是0,而小數部分精度更高(因為不允許精度丟失)。
好了,問題定位到這里結束,下面講解決方案。
?
三、解決方案
01 合理處理操作數類型
通過觀察Spark-sql中Decimal 相關的Rule,發現了Rule DecimalPrecision中的nondecimalAndDecimal方法,這個方法是用來處理非Decimal類型和Decimal類型操作數的二元操作。
此方法代碼不多,作用就是前面提到的左右操作數類型轉換,將兩個操作數轉換為一樣的類型,如下圖所示:
?
文字描述如下:
如果其中非Decimal類型的操作數是Literal類型, 那么使用DecimalType.fromLiteral方法將該Literal轉換為Decimal。例如,如果是Literal(1),則轉化為Decimal(1, 0);如果是Literal(100),則轉化為Decimal(3, 0)。
如果其中非Decimal類型操作數是Integer類型,那么使用DecimalType.forType方法將Integer轉換為Decimal類型。由于Integer.MAX_VALUE 為2147483647,小于3*10^9,所以將Integer轉換為Decimal(10, 0)。當然此處省略了其他整數類型,例如,如果是Byte類型,則轉換為Decimal(3,0);Short類型轉換為Decimal(5,0);Long類型轉換為Decimal(20,0)等等。
如果其中非Decimal類型的操作是float/double類型,則將Decimal類型轉換為double類型(此為DB通用做法)。
因此,這里用DecimalPrecision Rule的nonDecimalAndDecimal方法處理一個Decimal類型和另一個非Decimal類型操作數的二元操作的做法要比前面提到的ImplicitTypeCasts規則處理更加合適。ImplicitTypeCasts 會將Literal(1) 轉換為Decimal(34, 24), 而DecimalPrecision將Literal(1)轉換為Decimal(1, 0) 。
經過DecimalPrecision Rule的nonDecimalAndDecimal處理之后的兩個Decimal類型操作數會被DecimalPrecision中的decimalAndDecimal方法(上文提及過)繼續處理。
上述提到的案例是一個乘法操作,其中,p1=34, s1=24, p2 =1, s2=0。
其結果類型為Decimal(36,24),也就是說24位表示小數部分, 12位表示整數部分,不容易發生Overflow。
前面提到過,Spark-sql中關于類型轉換的Rule作用在Analysis階段的Resolution子階段。而Resolution子階段會有一批Rule一直作用在一個Plan上,直到這個Plan到達一個不動點(Fixpoint),即Plan不再隨Rule作用而改變。
因此,我們可以在ImplicitTypeCasts規則中對操作數類型進行判斷。如果在一個二元操作中有Decimal類型的操作數,則此處跳過處理,這個二元操作后續會被DecimalPrecision規則中的nonDecimalAndDecimal方法和decimalAndDecimal方法繼續處理,最終到達不動點。
我們向Spark社區提了一個PR SPARK-29000, 目前已經合入master分支。
?
02 用戶可感知的Overflow
除此之外,默認的DecimalOperation如果發生了Overflow,那么其結果將返回為NULL值,這樣的計算結果異常并不容易被用戶感知到(此處非常感謝金融分析團隊的同事幫我們檢查到了這個問題)。
在SQL ANSI 2011標準中,當算術操作發生Overflow時,會拋出一個異常。這也是大多數數據庫的做法(例如SQLService, DB2, TeraData)。
PR SPARK-23179 引入了參數spark.sql. decimalOperations.nullOnOverflow 用來控制在Decimal Operation 發生Overflow時候的處理方式。
默認是true,代表在Decimal Operation發生Overflow時返回NULL的結果。
如果設置為false,則會在Decimal Operation發生Overflow時候拋出一個異常。
因此,我們在上面的基礎上合入該PR,引入spark.sql.decimalOperations.nullOnOverflow參數,設置為false, 以保證線上計算任務的數據質量。
?
四、總結
本文分析了一個Decimal操作計算時發生的數據質量問題。我們不僅修復了其不合適的類型轉換問題,減小了其結果Overflow的幾率,還引入了一個參數,以便在計算發生Overflow時拋出異常,讓用戶感知到計算中存在的問題,保證線上計算的數據質量。
在大數據計算場景中,我們不僅關心數據計算得快不快,更關心結果數據的質量高不高。這需要各個團隊的密切配合,平臺開發人員需要提供可靠穩定的計算平臺,業務團隊需要寫出高質量的SQL,數據服務團隊則要提供良好的調度和校驗服務。相信在各個團隊的共同努力下,eBay在大數據這條路上能走得更遠、更寬闊。
總結
以上是生活随笔為你收集整理的案例分析 | 由Decimal操作计算引发的Spark数据丢失问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 行酒令怎么玩 行酒令玩法介绍
- 下一篇: Mybatis生成器插件扩展,生成fin