數據測試實踐:從一個bug開始的大數據引擎兼容性探索

語言: CN / TW / HK

作者:京東零售 李曉潔

我們常常忘記,天才也取決於其所能掌握的數據,即使阿基米德也無法設計出愛迪生的發明。——Ernest Dimnet

在大數據時代,精準而有效的數據對於每個致力於長期發展的組織來説都是重要資產之一,而數據測試更是不可或缺的一部分。數據測試不僅關注數據加工的代碼邏輯,還要考慮大數據執行引擎帶來的影響,因為各種引擎框架將對同一份數據產生不同的計算或檢索結果。本文將從一個年度賬單bug引入,講解在數據測試實踐中對大數據執行引擎兼容性差異的探索。

一、需求內容

京東-我的京東-年度賬單是一年一次,以用户視角對在平台一年的消費情況進行總結。賬單從購物,權益,服務等方面切入,幫助用户挖掘在自我難以認知的數據角度,通過這種方式讓用户從賬單中發掘打動內心的立意,並主動進行分享和傳播。本次,我京年度賬單以“2022購物印象”為主題,通過不同的數據維度組成村落故事線,用户以虛擬人物形象貫穿始終,用户瀏覽完故事線後,可生成購物印象。

年度賬單其中一個報表為用户年度購買的小家電品類。該報表使用年度賬單彙總表中的小家電品類集合字段,計算了2022年度某用户全年最後購買的兩款小家電所在的品類。本文bug分享將圍繞這個字段展開。

二、 缺陷描述

缺陷描述:在APP層用户年度賬單彙總模型app_my_jd_user_bill_year_sum中,對於小家電品類集合字段,APP表結果與手動計算結果不一致。

以用户'Mercury'、'樂樂1024'、'活力少年'的購買數據為例,上游ADM層以array<string>類型存儲用户每月購買的小家電相關品類,如下圖所示:

• 根據小家電品類集合字段定義,APP層應取這三個用户全年最後購買的2個品類,即'Mercury'在2022年11月購買的VR頭戴顯示器、電炒鍋,'樂樂1024'在2022年10月購買的衝牙器、空氣淨化器,'活力少年'在2022年10月購買的VR頭戴顯示器、電炒鍋。因此,經手動計算,APP層正確計算結果應為:

• 而APP層年度賬單彙總表中的小家電集合品類如下,結果錯誤,不符合預期結果。

三、 缺陷排查過程

1. 執行引擎兼容差異

測試排查中,首先發現了Hive和Spark引擎之間的語法兼容差異。

• 當使用APP層腳本中小家電品類集合口徑構建SQL,手動對上游表執行查詢時發現,Hive引擎得到的集合有序,執行結果正確:

• 使用Spark引擎執行查詢時,集合亂序,執行結果錯誤:

2. 腳本梳理

缺陷原因為集合亂序導致的取數錯誤。每個用户在上游ADM存在12個數組對應12個月購買小家電品類的集合,需要集合函數(collect)將12個月分組數據倒序排序,匯合成1個列表,然後取列表前兩個元素。

HQL提供兩種分組聚合函數:collect_list()collect_set(),區別在於collect_set()會對列表元素去重。由於用户不同月購買的品類集合可能重複,因此腳本使用了collect_set()

然而collect_set()將導致集合亂序,集合中元素不再按月份倒序排列,取出List[0]和List[1]不是用户全年最後購買的兩個小家電品類。

SELECT
    user_pin,
    small_electrical_appliance_list,
    concat_ws('|', small_electrical_appliance_list[0], small_electrical_appliance_list[1]) AS small_electrical_appliance
FROM(
    SELECT
        user_pin,
        collect_set(concat_ws(',', small_electrical_appliance_list_split)) AS small_electrical_appliance_list
    FROM(
    SELECT
        dt,
        user_pin,
        small_electrical_appliance_list,
        concat_ws(',', small_electrical_appliance_list) AS small_electrical_appliance
    FROM adm_my_jd_user_bill_month
    WHERE
        dt >= '2022-01'
        AND dt <= '2022-12'
    ORDER BY dt DESC) tmp 
    lateral VIEW explode(SPLIT(small_electrical_appliance, ',')) tmp AS small_electrical_appliance_list_split
    GROUP BY user_log_acct ) 


3. 結論

• 計算腳本邏輯錯誤,不應使用collect_set()聚合分組。

• 在原生Hive/Spark中,collect_set()函數均無法保證集合有序,而大數據平台Hive對集合計算有序。因此,該腳本在Hive引擎下可以達到生成全年最後購買兩個小家電品類的預期目標,但spark引擎則無法得到正確結果。

• Hive執行效率較低,研發通常通過Spark引擎執行,最終導致結果錯誤。

四、大數據計算引擎兼容差異

1. collect_list()/collect_set() 在hive/spark和presto之間的區別

collect_set()collect_list()在Presto中無法兼容。

• 替代函數:array_agg() (https://prestodb.io/docs/current/functions/aggregate.html?highlight=array_agg#array_agg)

Hive/Spark Presto
collect_list() array_agg()
collect_set() array_distinct(array_agg())

2. 行轉列函數在hive和presto之間的區別

• Hive使用lateral VIEW explode()執行行轉列的操作,而Presto不支持該函數。這種單列的值轉換成和student列一對多的行的值映射.

◦ Hive/Spark query:

lateral VIEW explode(SPLIT(small_electrical_appliance, ',')) tmp AS small_electrical_appliance_list_split

• Presto支持UNNEST來擴展array和map。文檔:(https://prestodb.io/docs/current/migration/from-hive.html)

◦ Presto query:

CROSS JOIN UNNEST(SPLIT(small_electrical_appliance, ',')) AS small_electrical_appliance_list_split;

3. 隱式轉換在引擎之間的區別

• Hive/Spark支持包括字符串類型到數字類型在內的多種隱式轉換,如將字符串'07'轉化為數字7,然後進行比較操作。

◦ Hive隱式轉換規則:詳見鏈接 Allowed Implicit Conversions

• 雖然Presto也有自己的一套隱式類型轉換規則包含在public Optional<Type> coerceTypeBase(Type sourceType, String resultTypeBase)方法中,但對數據類型的要求更為嚴格。一些在Hive中常見的數字與字符串進行比較的查詢語句,Presto會直接拋類型不一致的錯誤。

◦ 下圖為Hive和Presto的隱式轉換規則,藍色區域是Presto和Hive都支持的類型轉換,綠色區域是Presto不支持但是Hive支持的類型轉換,紅色區域是兩者都不支持的類型轉換。可以看到,hive的隱式轉換更為廣泛,而presto尤其在字符類型的隱式轉換中更為嚴格。

• 隱式轉換示例:

--Hive/Spark隱式轉換
'07' >= 6 -- true (CAST('07' AS DOUBLE) >= CAST(6 AS DOUBLE))
'test' <> 1 -- NULL
'1' = 1.0 -- true

--Presto隱式轉換
'07' >= 6 -- false (CAST('07' AS Varchar) >= CAST(6 AS Varchar))
'test' <> 1 -- true
'1' = 1.0 -- ERROR:io.prestosql.spi.PrestoException: Unexpected parameters (varchar(1), decimal(2,1)) for function $operator$equal. Expected: $operator$equal(T, T) T:comparable