震驚,一行MD5居然讓小夥伴都回不了家!!!

2023-03-15 12:00:39

作者:京東零售 付偉

1. 前言

大家好,當你點開這篇文章的時候也許心想是哪個 XX 小編混到這裡,先不要著急扔臭雞蛋,本文是一篇標準(正經)的問題覆盤文章。好了,一行MD5居然讓小夥伴下不了班,到底是什麼問題呢,讓我們一起來看看吧。

2. 正文

2.1 需求是什麼

這裡不再介紹具體的業務。簡而言之,有兩個介面(查詢、確認)對前端頁面提供服務。

查詢介面返回的資料依賴於本地資料與外部介面計算後的結果,也就是頁面展示的是資料快照。確認介面是按照頁面的展示結果請求外部介面。

考慮到使用者開啟展示頁面時的資料與提交操作可能間隔很久,實際請求時結果已發生變化,而這種操作會影響業務結果。因此在提交時會進行一次 check,如果發現資料發生變化需要提示頁面進行重新整理。

為了方便大家理解,我簡單的畫了個圖,畢竟上面太囉嗦了。

  • 查詢介面

  • 確認介面

雖然這個圖有點草率,但是相信看到這裡的小夥伴(預設都是聰明的)都對需求瞭然於胸了。

2.2 我怎麼搞得

掰扯了半天,我們的主角MD5還沒有出場,彆著急風雨總在彩虹後。

可以看出,這裡需要前端將查詢介面的返回值重新組裝作為確認介面的入參。而後端需要再次走資料聚合的邏輯與前端傳過來的業務值進行比較,如果不匹配則提示頁面需要重新整理。

一切看起來都順理成章,那麼小編遇到了什麼問題呢?

簡單來說有兩點:

  1. 前端同學表示值不好傳,因為這個頁面比較複雜,具體原因小編也沒深究,可能是被糊弄了。
  2. 後端同學(也就是小編)發現,這樣查詢介面和確認介面耦合很嚴重,如果確認介面需要新的入參,那麼就需要改動查詢介面。隨著查詢介面邏輯越來越複雜,確認介面的一個入參就需要一層一層的傳過來。很不友好。

呵呵,機智的小編靈機一動,便想到了了MD5,看看百度百科怎麼說

MD5 資訊摘要演演算法(英語:MD5 Message-Digest Algorithm),一種被廣泛使用的密碼雜湊函數,可以產生出一個 128 位(16 位元組)的雜湊值(hash value),用於確保資訊傳輸完整一致。

一圖勝千言

在工程,它差不多就是這麼用。

String md5= Md5Utils.get(String source);

可能有聰明的小夥伴會說了,這是雜湊函數存在雜湊碰撞,不同的字串也有可能生成相同的雜湊值。

是的沒錯,但是在小編的業務場景中,這種出現的概率微乎其微,忽略不計,解釋權歸小編所有。

那麼具體怎麼做的呢,還是看圖說話:

  • 改造後的查詢介面

  • 改造後的確認介面

我們需要對查詢介面返回的業務集關鍵屬性進行組合雜湊,這樣可以生成資料快照值。確認介面無需再傳入業務集合,只需要傳入資料快照值,後端進行對比即可知道是否發生變更。

一切都是那麼的美好,接下來就到了動人心魄的編碼環節。話不多說,小編的專案中引入了hutool包,什麼你不知道糊塗包?

Hutool 是一個小而全的 Java 工具類庫,通過靜態方法封裝,降低相關 API 的學習成本,提高工作效率,使 Java 擁有函數式語言般的優雅,讓 Java 語言也可以「甜甜的」。Hutool 中的工具方法來自每個使用者的精雕細琢,它涵蓋了 Java 開發底層程式碼中的方方面面,它既是大型專案開發中解決小問題的利器,也是小型專案中的效率擔當;

真不錯,果然是效率擔當,一行程式碼就搞定了。

	/**
	 * 生成資料雜湊
	 */
	private String generateSnapShotHash(AcceptListQueryWrapResultDTO wrapResultDTO) {
		StringBuilder builder = new StringBuilder();
		for (AcceptListQueryResultDTO item : wrapResultDTO.getAllList()) {
			builder.append(item.getQuotationId()).append(item.getOperateType()).append(item.getPriceTypeCN());
		}
		return MD5.create().digestHex16(builder.toString());
	}

請各位看官記住這行程式碼

MD5.create().digestHex16(builder.toString());

畢竟它就是糊弄你點進來的罪魁禍首。

2.3 出了什麼事

當小編開發完以後,開心的部署在了測試環境。和前端聯調的時候,發現第一次請求總是超時 ???

一想可能是mock平臺的問題,畢竟三方的查詢介面還沒開發完成,就不以為然。請注意,只是第一次超時。同樣的請求引數第二次光速返回。呵呵,你說不是環境的問題,小編自己都不大信呢。

友方的介面開發完了,小編期待的換上了對方的介面。結果現實給了小編一記左勾拳,還是第一次超時。這不科學?於是小編對自身產生了懷疑?難道不是環境的問題?

於是連忙在本地測試了一下,居然是光速返回。作為自信的人一定不是程式碼的問題,那麼這個鍋往哪裡甩呢?又臭又硬的小編狠狠的思考了一分鐘,又將鍋甩給了業務閘道器(統一接收HTTP請求)肯定是它的毛病,畢竟測試環境的閘道器出問題很常見。

於是開開心心的準備上預發了。上了預發絕對沒問題!!!小編信誓旦旦的對QA說道。

上帝為你關上一扇門的同時也會為你關上一扇窗,預發環境第一次還是超時!!!小編覺得很慚愧對不起一起上線的小夥伴,畢竟大家都準備十點下機了。

小編陷入了沉思中。。。

2.4 怎麼修好的

排查了預發環境的介面,友方的傑夫介面TP99只有幾毫秒,閘道器也沒有問題,也許是資料庫的原因,排查發現也沒有問題。頓時,小編又迷茫了。

山重水複疑無路柳暗花明又一村,機智的小編想到了國內知名廠商開源的一款java診斷工具Arthas,利用它可以檢視方法詳細耗時。點我檢視 主動開啟另一扇窗。

當你遇到以下類似問題而束手無策時,Arthas可以幫助你解決:

這個類從哪個 jar 包載入的?為什麼會報各種類相關的 Exception?

我改的程式碼為什麼沒有執行到?難道是我沒 commit?分支搞錯了?

遇到問題無法線上上 debug,難道只能通過加紀錄檔再重新發布嗎?

線上遇到某個使用者的資料處理有問題,但線上同樣無法 debug,線下無法重現!

是否有一個全域性視角來檢視系統的執行狀況?

有什麼辦法可以監控到 JVM 的實時執行狀態?

怎麼快速定位應用的熱點,生成火焰圖?

怎樣直接從 JVM 內查詢某個類的範例?

由於預發環境還是比較麻煩,於是小編在測試環境準備好了arthas環境。

下面簡單介紹下使用步驟:

  1. 下載全量包 arthas-bin.zip
  2. 解壓
  3. chmod -777 arthas-boot.jar
  4. 啟動 sudo -u admin -EH java -jar /home/export/App/arthas-boot.jar

當看到圖示出現時,即啟動成功。具體使用方法可以檢視官網,此處不再贅述。

我們使用trace命令檢視方法耗時,同時在頁面請求該查詢介面。

trace  --skipJDKMethod false com.jd.universal.inquiry.service.protocol.jsf.AcceptListWebErpServiceImpl queryList

可以看到這行生成資料快照的方法,耗時佔整個介面的99.57%,緊接著我們繼續監控generateSnapShotHash方法:

trace  --skipJDKMethod false com.jd.universal.inquiry.service.protocol.jsf.AcceptListWebErpServiceImpl generateSnapShotHash

可以看到方法的耗時都集中在

[99.99% 36562.318173ms ] cn.hutool.crypto.digest.MD5:create() #103

接著再次頁面點選請求操作,出現以下情況:

可以看到後面多次請求
cn.hutool.crypto.digest.MD5:create()方法耗時僅不到一毫秒。和我們之前遇到的狀況一致。此時已確定是這行MD5導致的第一次載入很慢。

雖然原因找到了,但是還是得看下為什麼這行程式碼只有在第一次時這麼慢,於是我們進入該方法看看它到底搞什麼么蛾子。

可以看到初始化方法如下:

由於現象是程式第一次執行很慢,後續很快,根據小編多年的寫/修BUG經驗懷疑是這段初始化中存在靜態載入。

MessageDigest是JDK自帶的類,為應用程式提供摘要演演算法的,這裡我們關注點就落在了上面的一行。我們點進去看一下:

果然我們看到了他在嘗試載入BouncyCastle庫,我們來看一下這個庫的介紹:

BouncyCastle(輕量級密碼術包)是一種用於 Java 平臺的開放原始碼的輕量級密碼術包;Bouncycstle 包含了大量的密碼演演算法,其支援橢圓曲線密碼演演算法,並提供 JCE 1.2.1 的實現。

所以問題的答案就呼之欲出了,隨著原始碼的深入,我們看到:

  private void setup()
   {
        loadAlgorithms(DIGEST_PACKAGE, DIGESTS);

        loadAlgorithms(SYMMETRIC_PACKAGE, SYMMETRIC_GENERIC);

        loadAlgorithms(SYMMETRIC_PACKAGE, SYMMETRIC_MACS);

        loadAlgorithms(SYMMETRIC_PACKAGE, SYMMETRIC_CIPHERS);

        loadAlgorithms(ASYMMETRIC_PACKAGE, ASYMMETRIC_GENERIC);

        loadAlgorithms(ASYMMETRIC_PACKAGE, ASYMMETRIC_CIPHERS);

        loadAlgorithms(KEYSTORE_PACKAGE, KEYSTORES);

        loadAlgorithms(SECURE_RANDOM_PACKAGE, SECURE_RANDOMS);

        loadPQCKeys();  // so we can handle certificates containing them.
     //省略。。。
    }

正是由於這些演演算法實現的載入,導致MD5.create()第一次呼叫時耗時超過數十秒。

好了,既然找到了問題。那麼改動起來就很簡單了,小編嘗試尋找了糊塗包中提供的方法,發現並沒有入參可以關閉該三方加密包的初始化。於是換用了Google提供的MD5的實現。重新打包,部署,一次成功,完美。

3. 後語

QA同學在測試環境測出了這個問題,而自信的本人不屑一顧,堅持自己愚昧的觀點,先認為是Mock的問題,接著又說是閘道器的問題。由於小編的盲目自信,導致上線到很晚,表示非常的慚愧。總結失敗的原因:

  1. 合理評估使用第三方包
  2. 測試環境遇到的問題盡力去追,不要盲目下結論
  3. 要聽QA的話

4. 參考

Bouncy Castle 加密演演算法包

arthas 官方檔案

使用 Arthas 進行生產程式碼熱修復