基於日誌的回放對比系統設計

一、背景

上半年公司的網關係統進行了重構,需要把零售業務已有的網關接口遷移到新網關上。這些接口每天都有成千上萬次請求,爲商家提供各種服務,稍有不慎就容易出現較大故障,所以如何遷移是個比較慎重的問題。

這個遷移項目主要的驗證重點是:確保新網關對於接口的請求和返回的處理和老網關一致;而主要的驗證難點在於,僅從功能層面進行手工驗證很難覆蓋各種場景,尤其是如何構造各種請求參數以及檢查各種返回的內容。若要人肉進行細緻的接口級別的驗證,那麼花費的時間就會很長、效率很低。

經過統計和梳理,涉及的接口超過了 1000 個,在這個數量級上,總花費的時間成本很高,研發團隊難以承受。這不得不讓我們停下來思考一下,是否有另一種高效的方式來解決這個問題。這時就想到了採用錄製回放對比的方式。

隨着業務的快速發展,迴歸時需要考慮的場景越來越多,測試的耗時越來越長,越來越多的公司提出並採用錄製回放的方式來提高效率,比如阿里開源的 jvm-sandbox-repeater,基於已有的流量進行錄製,無需人肉準備腳本和數據,通過將錄製的流量進行回放,還原真實的場景,最後提供回放結果判斷是否符合預期,提高業務迴歸效率。

從場景上來說,網關遷移驗證做的也是迴歸測試,這個思路也是通用的,所以設計了一個類似的系統進行回放對比驗證。

二、系統設計

如果需要採用錄製回放對比的方式,首先第一步就是錄製,在錄製前需要確認下數據和流量的來源。數據越豐富、多樣性越強,回放能夠發現的問題越多,一些實時的流量採集方式通過攔截轉發的方式獲取數據,所以需要一定的配置和改造成本。幸運的是網關的日誌對於接口的請求參數會進行記錄,實際上已經完成了錄製這個過程,可以採用成本最小的方式:通過拉取測試環境老網關的日誌來獲取請求數據,然後進行解析再進行回放和對比。

基本流程如下,下面會對各步驟的邏輯進行說明

2.1 日誌拉取和清洗

作爲整個流程的第一步:通過拉取日誌獲得需要回放的流量。

回放對實時性要求不高,但是當初請求的時刻和回放的時刻之間的時間差也不能太長:比如早上創建一個商品並進行了查詢操作,下午可能進行了刪除,如果選擇晚上再進行回放的話,查詢結果就會爲空,數據的質量就會下降,造成數據損耗。需要考慮設置一個較短的時間間隔進行回放,目前通過定時任務每隔一小時啓動一次進行拉取和回放,根據當前啓動時間自動生成對應的時間範圍,去日誌平臺獲取日誌:比如任務啓動時間是 11:10 分,那麼自動轉換成 10:00-11:00 的時間範圍去拉取日誌進行回放。

獲得日誌後,第二步就是日誌清洗。

日誌清洗的作用有兩個:

  1. 過濾。有一些接口是不需要遷移到新網關的,進行過濾,減少對最後統計結果的干擾。另外,錄製回放運行時也會請求網關,需要過濾掉這部分人工流量。
  2. 採樣。雖然是測試環境,一天的數據量也有 10W+ ,全部處理時耗較長;同時,初期會有大量重複問題爆出,增大排查成本。根據二八定律以及實際的觀察,小部分接口貢獻了大部分流量,考慮每個接口雨露均沾和數據的多樣性,選擇接口+場景作爲採樣緯度,返回結果中的不同 code 認爲是不同的場景,這樣除了正常請求,也考慮到異常請求的回放。

初期可以設置一個較小的採樣閾值 Limit 來收集問題,減少人工排查問題的成本。等問題修復穩定運行一段時間後,再考慮逐步增大閾值。

2.2 進行回放

考慮到數據是不斷變化的,同時對於登陸態生命週期是否正常也需要進行觀察,所以回放並不是拿新網關請求的返回和日誌中記錄的老網關進行對比,而是同時請求兩個網關,再對各自的返回進行比較。在請求過程中,可能遇到接口超時等情況,遵循儘可能成功的原則,做對應的處理,比如超時進行重試等。

另一個影響比對效率的因素就是性能,每一個請求都可以獨立處理,就意味着可以併發執行,如 python 的 multiprocessing 多進程的方法:

    p = multiprocessing.Pool(8)
    while line:
        p.apply_async(process_line,
                      args=(...))
        line = file.readline()
    p.close()
    p.join()

在 6C 的機器上對 2000 條日誌進行回放,通過不同的併發數觀察結果如下:

pool(1) 耗時:1292s
pool(6) 耗時:212s
pool(8) 耗時:155s
pool(12) :耗時:102s

發現性能提升還是很明顯的,根據實際運行機器的CPU性能配置適合的併發數即可。

2.3 結果比較

整個系統中最核心的邏輯就是結果對比邏輯了。從新網關獲取到的返回 response1 和老網關獲取到的返回 response2 ,理論上是要一致的。初步的比較邏輯就是採用遞歸比較,將返回對象中的每個元素進行比較,判斷內容是否一致。

實際的運行過程中,往往會出現各種正常情況下返回結果不一致的情況:

  1. 接口請求返回含有時間字段,而時間字段是根據當前處理時間生成的。
  2. 一些寫接口返回的是新生成的對象 id ,每次請求返回都不同。比如創建一個商品,下了一筆訂單,返回的都是新生成的商品 id 或者訂單號。
  3. 返回數據列表時,有些場景返回的數據是不保證順序的,導致偶爾的比對失敗。

針對上述情況,設計了兼容模式進行特殊處理。首先定義一個 dict 存儲接口名以及需要忽略的字段

ignore_dict = {
    'youzan.retail.stock.receiving.order.export.1.0.0': ['response'],
    'youzan.retail.trademanager.refundorder.export.1.0.0': ['response'],
    'youzan.retail.trade.api.service.pay.qrcode.1.0.1': ['url'],
    'youzan.retail.product.spu.queryone.1.0.0': ['list']
}

其中定義兩個特殊字段 response 和 list:

  1. 對於 response 字段,只檢查字段長度和類型,不檢查內容是否一致,主要針對返回序列號、訂單號的場景
  2. 對於 list 字段,對於其中的list類型做無序比較處理

核心對比方法如下,對於需要忽略的字段和類型進行特殊處理,減小誤報:

def compare_data(data_1, data_2, COMP_SWITCH, ignore_list):

    if isinstance(data_1, dict) and isinstance(data_2, dict):
        diff_data = {}
        only_data_1_has = {}
        only_data_2_has = {}
        d2_keys = list(data_2.keys())
        for d1k in data_1.keys():
            # 忽略某些字段
            if COMP_SWITCH and __doignore(d1k, ignore_list):
                continue
            if d1k in d2_keys: 
                d2_keys.remove(d1k)

                temp_only_data_1_has, temp_only_data_2_has, temp_diff_data = compare_data(data_1.get(d1k),
                                                                                          data_2.get(d1k), COMP_SWITCH,
                                                                                          ignore_list)
                if temp_only_data_1_has:
                    only_data_1_has[d1k] = temp_only_data_1_has
                if temp_only_data_2_has:
                    only_data_2_has[d1k] = temp_only_data_2_has
                if temp_diff_data:
                    diff_data[d1k] = temp_diff_data
            else:
                only_data_1_has[d1k] = data_1.get(d1k)  
        for d2k in d2_keys:  
            if COMP_SWITCH and __doignore(d2k, ignore_list):
                continue
            only_data_2_has[d2k] = data_2.get(d2k)
        return only_data_1_has, only_data_2_has, diff_data
    else:
        if data_1 == data_2:
            return None, None, None
        else:
            if COMP_SWITCH and isinstance(data_1, list) and isinstance(data_2, list):
                if __process_list(data_1, data_2, COMP_SWITCH, ignore_list):
                    return None, None, None
                else:
                    return None, None, [data_1, data_2]
            else:
                return None, None, [data_1, data_2]

比對邏輯中加入了 COMP_SWITCH 開關,先按常規方式進行比較,當常規方式比較失敗,再開啓兼容模式進行比較,這樣的做法是爲了統計和觀察哪些接口會使用兼容模式,使用兼容模式是否合理(還有兼容模式本身有沒有bug~~)。

2.4 處理失敗

每次執行任務會生成一個批次號,每個接口請求回放對比完成後,保存結果時也會帶上批次號寫入數據庫。當任務執行完成後,會根據批次號將失敗的結果再拉取出來進行重試。

失敗重試時,回放的邏輯和正常運行時略有不同,兩次回放間隔了 30s 依次進行,這是避免當多次請求操作同一個對象時,因爲第一次請求執行比較慢導致第二次請求命中唯一性校驗而失敗。

當重試結果仍然失敗,那麼需要人工介入進行排查,判斷是否是新網關邏輯的問題還是有字段需要特殊兼容的問題,進行處理。

2.5 結果判斷和遷移策略

對於回放成功的接口,會記錄當時回放成功返回的code等信息到結果表,同時記錄對應code的次數加1,多次回放後會形成如下的統計結果:

{'200': 94, '234000001': 16, '234000002': 1}

當統計結果中存在正常的code和異常的code,同時沒有回放失敗記錄的情況,就可以認爲新網關對該接口的返回和老網關是一致的。回放通過的次數越多,可信性就越強。在這種情況下再將接口的流量切換到新網關,觀察日常業務調用的情況,通過拉取新網關日誌,記錄返回的code和數目,當新網關返回的結果中也包含正常和異常的code時,認爲在新網關也能正常請求,可以準備線上遷移了。

通過QA環境的對比回放,同時在預發環境全量遷移後觀察業務功能是否正常,最後線上逐步灰度流量和監控,最後確保接口遷移到新網關是正常的。

三、面向普通測試場景的解決方案

因爲屬於迴歸類的工具,這套系統除了可以解決驗證新老網關返回對比是否一致的問題外,還能夠幫助驗證分支環境和基礎環境之間請求對比是否一致的問題(尤其是系統重構的情況),對業務場景進行迴歸。

爲支持普通迴歸場景,只要在原來的系統上進行少量的改動:

主要的改動就是配置需要回放的接口和需要回放到的環境。在回放階段,之前是回放到新網關和老網關,現在都回放到同一網關,通過不同的 X-Service-Chain 請求頭,控制請求到達不同的後端環境,然後獲得返回值進行對比。

四、最後

本文提供了一種通過採集日誌進行回放對比來解決接口對比一致性的思路,運用到了新老網關重構驗證等迴歸場景。

在新老網關遷移驗證過程中,該系統起到了非常大的作用。通過該系統校驗了接口 1200+,產生回放記錄數目 30w 條,發現問題 30+ ,最終保障了線上切換工作的平穩進行。

由於能夠獲取了接口和參數,可以進一步做一些權限、後端參數校驗這樣的檢查,減少測試人員在這方面投入的測試時間,這套系統已經投入實際使用,後面有機會可以再介紹其實現原理和使用效果。

期待未來能夠挖掘出更多的使用場景,解決更多的驗證問題,提高測試的效率和準確性。
作者介紹

本文轉載自公衆號有贊coder(ID:youzan_coder)。

原文鏈接

基於日誌的回放對比系統設計

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章