一、症狀
一天,金融分析團隊的同事報告了一個問題,他們發現在兩個生產環境中(爲了區分,命名爲環境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在大數據這條路上能走得更遠、更寬闊。
本文轉載自公衆號eBay技術薈(ID:eBayTechRecruiting)。
原文鏈接: