Categories
程式開發

由Decimal操作計算引發的Spark數據丟失問題


一、症狀

一天,金融分析團隊的同事報告了一個問題,他們發現在兩個生產環境中(為了區分,命名為環境A和B), Spark大版本均為2.3。但是,當運行同樣的SQL語句,對結果進行對比後,卻發現兩個環境中有一列數據並不一致。此處對數據進行脫敏,僅顯示發生數據丟失那一列的數據,如下:

由Decimal操作計算引發的Spark數據丟失問題 1

由此可見,在環境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語句,如下:

由Decimal操作計算引發的Spark數據丟失問題 2

上面的select語句將會返回一個NULL。

我們將上述語句的執行計劃打印出來。

由Decimal操作計算引發的Spark數據丟失問題 3

執行計劃很簡單,裡面有一個二元操作(乘法),左邊的case when 是一個Decimal(34, 24)類型,右邊是一個Literal(1)。

程序員都知道,在編程中,如果兩個不同類型的操作數做計算,就會將低級別的類型向高級別的類型進行類型轉換,Spark中也是如此。

一條SQL語句進入Spark-sql引擎之後,要經歷Analysis->optimization->生成可執行物理計劃的過程。而這個過程就是不同的Rule不斷作用在Plan上面,然後Plan隨之轉化的過程。

在Spark-sql中有一系列關於類型轉換的Rule,這些Rule作用在Analysis階段的Resolution子階段

其中就有一個Rule叫做ImplicitTypeCasts,會對二元操作(加減乘除)的數據類型進行轉換,如下圖所示:

由Decimal操作計算引發的Spark數據丟失問題 4

用文字解釋一下,針對一個二元操作(加減乘除), 如果左邊的數據類型和右邊不一致,那麼會尋找一個左右操作數的通用類型(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操作計算引發的Spark數據丟失問題 5

此處我們的操作數都已經是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操作計算引發的Spark數據丟失問題 6

文字描述如下:

如果其中非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)。

原文鏈接

https://mp.weixin.qq.com/s/yKFzO41l-2n617xICN2ObQ