最近我們團隊的同學在開發中發生了一件 「有意思」 的事情,那就是透過一行程式碼,讓網站卡死了,真的是離了大譜。團隊同學寫了一篇記錄,希望能長個記性吧~
背景
今天下午,老魚簡歷告警群裏突然提示了幾個下載簡歷失敗的提示。
我看到後,心裏一緊,趕緊開啟頁面看看我能不能下載,結果我這裏下載是正常的。於是我就感覺事情不簡單,趕緊本地啟動計畫偵錯,我本以為本地啟動後控制台會有報錯,但是實際上並沒有,沒辦法,只能使用絕招:
二分法
先定位到出問題程式碼。漫長的縮短問題程式碼後,最後問題程式碼找到了
/**
* 判斷是否為連結
* @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);
}
怎麽避免?
這次出現這個問題的原因有兩個
經驗不足 :如果我知道,不好的正則出導致 災難性回溯 的話,我在拿到 AI 給我寫的正則,我就會問它給我的正則是否會導致 災難性回溯 。
沒有充足的測試 :如果我的計畫有對這種工具方法的充足的測試,應該也不會產生這個 bug 了。
總結
遇到 bug 不要慌,從簡單到難的使用排查問題的方法。先定位到問題。例如:我遇到bug,先定位前端問題還是後端問題,再定位問題的大的位置,逐漸縮小範圍,最終找到問題的位置。然後解決問題。有沒有覺得這其實就是使用二分法的思想來定位問題。找到問題的程式碼了,那其實就勝利一大半了,剩下的就是寫出正確的程式碼,做充足的測試,最後復盤這次 bug,以後不要再犯同樣的錯就好了!
👇🏻 點選下方閱讀原文,獲取魚皮往期編程幹貨。
往期推薦