當前位置: 妍妍網 > 碼農

一行程式碼,把計畫搞崩潰了!

2024-05-15碼農

最近我們團隊的同學在開發中發生了一件 「有意思」 的事情,那就是透過一行程式碼,讓網站卡死了,真的是離了大譜。團隊同學寫了一篇記錄,希望能長個記性吧~

背景

今天下午,老魚簡歷告警群裏突然提示了幾個下載簡歷失敗的提示。


我看到後,心裏一緊,趕緊開啟頁面看看我能不能下載,結果我這裏下載是正常的。於是我就感覺事情不簡單,趕緊本地啟動計畫偵錯,我本以為本地啟動後控制台會有報錯,但是實際上並沒有,沒辦法,只能使用絕招: 二分法 先定位到出問題程式碼。漫長的縮短問題程式碼後,最後問題程式碼找到了

/**
 * 判斷是否為連結
 * @param urlString 接受一個字串
 * @returns 一個 boolean 值,true 表示是連結,false 表示不是連結
 */

exportconst isLink = (urlString: string) => {
const regex = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
return regex.test(urlString);
};


說實話看到是因為這段程式碼的我是這樣的:

我本來是不會正則的,這個是 AI 幫我寫的判斷連結的方法。我當時就光測試它是否能判斷出一個字串是否是連結,而忽略了特殊字串是否會有效能問題。最後,我給大家復現一下這個bug 最小程式碼範例:

/^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/.test('https://www.suse.com/nbswebapp/createSummary.jsp?')


只要你在遊覽器控制台輸入這個,這個頁面立馬卡死。

問題原因分析

那這是為什麽呢?聽聽萬能的 AI 這麽說的:在JavaScript中使用正規表式時,如果正規表式的設計不當,特別是在處理復雜的字串或者特定的模式時,可能會導致效能問題,甚至使瀏覽器卡死。這種現象通常稱為「 災難性回溯 」(Catastrophic Backtracking)。在你提供的正規表式中:

/^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/


這個運算式用於匹配URL,其中包含了幾個可選的元件和量詞(如 * ? ),這些都可能導致正規表式引擎進行大量的回溯嘗試,尤其是當輸入字串在某些位置上與模式不匹配時。具體來說,問題可能出現在 ([\/\w \.-]*)* 這部份。這裏使用了兩個巢狀的量詞( * 外面還有一個 * ),這種巢狀的量詞是造成災難性回溯的常見原因。當輸入的URL在最後一部份(即 [\/\w \.-]* 應該匹配的部份)稍有不符時,正規表式引擎可能會嘗試所有可能的分割方式來匹配整個模式,這會導致處理時間急劇增加。為了避免這種問題,可以嘗試最佳化正規表式的結構,避免使用巢狀的量詞,或者使用更具體的字元類匹配來減少潛在的回溯。例如,可以修改為:

/^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]+)?\/?$/


這裏,我將內部的 * 修改為 + ,使得這部份至少匹配一次,減少了巢狀量詞的使用,這應該可以幫助減輕瀏覽器的負擔。總之,設計正規表式時需要註意避免復雜的巢狀和過度的通配符使用,特別是在處理可能非常長或復雜的字串時。這樣不僅可以避免瀏覽器卡死的問題,還可以提高套用的響應速度和使用者體驗。實測,。這個正則是有問題的:

我現在是用新的檢測字串是否是連結的方法,充足測試下來沒有問題:

/**
 * 判斷字串是否為連結
 */

exportfunctionisLink(urlString: string): boolean{
const pattern = newRegExp(
'^(https?:\\/\\/)?' + // 協定
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // 網域名稱
'((\\d{1,3}\\.){3}\\d{1,3}))' + // 或IP(v4)地址
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // 埠和路徑
'(\\?[;&a-z\\d%_.~+=-]*)?' + // 查詢參數
'(\\#[-a-z\\d_]*)?$',
'i',
); // 錨點
return !!pattern.test(urlString);
}

怎麽避免?

這次出現這個問題的原因有兩個

  1. 經驗不足 :如果我知道,不好的正則出導致 災難性回溯 的話,我在拿到 AI 給我寫的正則,我就會問它給我的正則是否會導致 災難性回溯

  2. 沒有充足的測試 :如果我的計畫有對這種工具方法的充足的測試,應該也不會產生這個 bug 了。

總結

遇到 bug 不要慌,從簡單到難的使用排查問題的方法。先定位到問題。例如:我遇到bug,先定位前端問題還是後端問題,再定位問題的大的位置,逐漸縮小範圍,最終找到問題的位置。然後解決問題。有沒有覺得這其實就是使用二分法的思想來定位問題。找到問題的程式碼了,那其實就勝利一大半了,剩下的就是寫出正確的程式碼,做充足的測試,最後復盤這次 bug,以後不要再犯同樣的錯就好了!


👇🏻 點選下方閱讀原文,獲取魚皮往期編程幹貨。

往期推薦