淩晨兩點,林夕的電腦螢幕上是密密麻麻的測試日誌。
他重新搭建了一個完整的本地測試環境——資料庫、快取、訊息佇列,甚至模擬了生產環境的網路延遲。彈窗功能本身已經沒問題,但趙磊那條微信像根刺,紮在腦子裏。
“三年前陳啟明離職前,最後一次提交就是改那裏。”
林夕開啟SVN的提交曆史,時間軸拉到三年前。會員積分係統的前端模組,最後一次大改動確實在2019年11月12日,也就是“雙十一”後的第二天。
提交人是Qiming.Chen。
他點開那個提交的差異檔案。改動很大,幾乎重構了整個兌換流程的前端程式碼。在兌換按鈕的點選處理函式裏,陳啟明加了一段注釋,用英文寫的:
`java
// WARNING: Async request chain may drop data under extreme concurrency.
// Need distributed lock or idempotent design. Temporary fix applied.
// TODO: Re-architect before next major promotion.
`
(警告:非同步請求鏈在極端並發下可能丟失資料。需要分散式鎖或冪等設計。已應用臨時修複方案。TODO:在下一次大型促銷前重構。)
林夕的目光落在“Temporary fix applied”上。這個臨時修複是什麽?
他繼續往下看程式碼。陳啟明在發起兌換請求前,增加了一個本地儲存的timestamp(時間戳),並把這個timestamp作為請求引數傳送到後端。後端會在處理請求時校驗這個timestamp,如果與上一次請求的時間戳過於接近(小於500毫秒),就拒絕重複請求。
這是一種簡單的防重複提交方案。但問題在於——這個時間戳儲存在瀏覽器的localStorage裏,而兌換成功後,程式碼裏並沒有清除它。
林夕在本地模擬了這個場景:使用者點選兌換,彈出廣告,確認後請求傳送。但如果使用者手快,在請求還沒返回時又點了一次兌換按鈕——這時候彈窗會再次彈出,但時間戳還是同一個,第二個請求會被後端拒絕。
這看起來沒問題,甚至是個安全的設計。
但接著,林夕發現了一個細節:陳啟明在程式碼裏設定的時間戳有效期是5秒。5秒後自動失效。這意味著,如果使用者第一次請求因為網路問題卡住了,5秒後他再次點選,會被視為新請求,而不是重複請求。
“那麽真正的bug在哪裏?”林夕自言自語。
他繼續往下翻,看到了陳啟明提交後的第二天,另一個提交記錄。提交人是Zhou.Kai。
這個提交隻有一行改動:把時間戳的有效期從5秒改成了60秒。
備注寫的是:“延長防重複提交視窗,避免促銷期間使用者體驗問題。”
林夕盯著這行改動,手指在桌麵上輕輕敲擊。
5秒改60秒。在普通場景下沒什麽區別,但在“極端並發”的場景下——比如“雙十一”零點,成千上萬人同時點選兌換——這意味著什麽?
他開啟計算器,開始推算:
假設兌換介麵的處理能力是每秒1000次請求。如果每個使用者因為彈窗、網路延遲等原因,在60秒內平均發起1.5次有效請求,那麽……
數字跳出來的瞬間,林夕明白了。
這不是bug。這是一個容差設計缺陷被一個臨時修複方案掩蓋,而這個臨時修複又被一個看似合理的引數調整削弱了效果。
陳啟明看到了根本問題,但來不及重構,隻能加個臨時補丁。周凱延長了補丁的有效期,讓它在非極端情況下更“友好”,但也讓它在高並發場景下更脆弱。
而這個“脆弱”的點,現在正要加上一個新的彈窗——一個會增加使用者等待時間、可能讓更多人重複點選的因素。
林夕看了眼時間,淩晨三點半。
他做了三件事:
第一,在本地模擬高並發場景。寫了一個指令碼,模擬1000個使用者同時點選兌換按鈕,其中30%的使用者會因為彈窗而誤操作二次點選。
第二,監控在這種情況下,後端的響應時間、錯誤率和資料一致性。
第三,檢查現有生產環境的監控資料,看看曆史上這個介麵在促銷期間的真實表現。
前兩個測試結果印證了他的推測:當並發量超過800時,錯誤率開始上升,出現了少數“兌換成功但積分未扣除”的資料不一致情況。
第三個任務遇到了障礙——他沒有生產環境監控係統的完整許可權。隻能看到最近7天的基本指標,曆史資料需要申請。
他寫了一份簡短的申請郵件,說明需要分析會員積分兌換介麵的曆史效能資料,用於評估新功能的影響。傳送給運維組的李工,抄送周凱。
郵件發出去時,窗外天色已經矇矇亮。
林夕趴在桌上睡了兩個小時。七點半,被手機震動吵醒——是李工的回複:
“小林,資料已發你郵箱。另外,提醒一下,那個資料庫單點故障的問題,我昨晚檢查了,確實如你所說。已經安排今晚淩晨三點做主從複製重建,屆時兌換功能會有幾分鍾隻讀。你的新功能上線最好避開這個時間。”
郵件附件裏有三年的監控資料圖表。
林夕用冷水洗了把臉,開始分析。
圖表很清晰地顯示了規律:每年“雙十一”“618”等大促期間,兌換介麵的錯誤率都會有一個小峰值。但最異常的是——三年前的“雙十一”,錯誤率並沒有特別高,反而是平穩的。從兩年前開始,錯誤率峰值纔出現。
那個時間點,恰好是陳啟明離職半年後。
他繼續往下翻,看到一張更詳細的圖表:介麵響應時間分佈。正常情況下,95%的請求在200毫秒內完成。但在高並發時,會出現一些“長尾請求”——超過5秒甚至10秒才返回。
而這些長尾請求,很多最終是失敗的。
林夕的腦海裏開始拚接碎片:
1. 陳啟明的臨時方案(5秒有效期)能cover住大多數長尾請求——如果請求卡住超過5秒,使用者重試會被視為新請求。
2. 周凱改成60秒後,長尾請求如果卡住,使用者重試會被拒絕,隻能幹等。
3. 但使用者不會幹等——他們會重新整理頁麵、重新進入,這會產生全新的會話和全新的時間戳,繞過防重複機製。
4. 於是,同一個兌換請求,可能在後端被處理兩次。
而資料庫的單點故障,會讓這種重複處理的問題雪上加霜——在高負載下,事務可能無法正常回滾。
九點,公司晨會。
林夕帶著黑眼圈走進會議室。周凱已經在了,正在和產品經理討論什麽,看到他時點了點頭。
會上,周凱特意提到了彈窗需求:“小林做得不錯,方案很細致,測試也通過了。今天上午走發布流程,趕在午間流量高峰前上線。”
產品經理是個短發幹練的女生,叫蘇晴,她問:“這個改動會影響兌換成功率嗎?下週我們有個小促銷。”
“理論上不會。”周凱說,“小林做了完整測試,對吧?”
所有人的目光看向林夕。
“功能本身沒問題。”林夕頓了頓,“但我昨晚分析曆史監控資料時,發現兌換介麵在高並發下存在一些長尾請求問題。彈窗會增加使用者等待時間,可能會放大這個效應。我建議,如果要在促銷期間上線,最好配合後端做一些優化。”
會議室安靜了幾秒。
“什麽優化?”蘇晴追問。
“比如在防重複提交機製裏,加入會話級別的鎖,或者把時間戳有效期調回原來的5秒。”林夕說得很謹慎,“這個需要後端配合,改動不大,但能降低風險。”
周凱笑了:“小林,你很嚴謹,這很好。但我們不能因為理論上的風險,就過度設計。現有的機製執行兩年了,沒出過大問題。這次隻是加個前端彈窗,不會影響後端邏輯。”
他說得有理有據,周圍幾個程式設計師點頭附和。
“可是監控資料顯示——”
“資料顯示的是過去的情況。”周凱溫和地打斷他,“這樣吧,你的擔憂也有道理。咱們折中:今天先正常上線彈窗功能,同時你寫個技術方案,提出完整的優化建議,我們下週評審。這樣既不影響業務,也能係統性地解決問題。”
話說到這個份上,林夕隻能點頭:“好。”
散會後,趙磊湊過來,小聲說:“你提陳啟明的方案了?”
“沒提名字,隻說了調引數的建議。”
“聰明。”趙磊拍拍他的肩,“那事兒是個敏感話題。不過……你真覺得會出問題?”
“不知道。”林夕實話實說,“但資料不會說謊。”
上午十點半,彈窗功能順利上線。發布過程很平穩,監控曲線一切正常。周凱在團隊群裏發了大拇指表情:“小林第一週就獨立完成需求上線,很棒。”
林夕盯著監控大盤,看著兌換介麵的實時資料。請求量、成功率、響應時間……所有指標都在綠色區間。
也許真的是自己多慮了。
下午,他按照周凱的要求,開始寫技術優化方案。檔案剛開了個頭,蘇晴忽然從產品區走過來,臉色不太好。
“林夕,有使用者反饋,兌換積分時彈窗出現兩次。你們前端是不是有什麽快取問題?”
林夕心裏一緊:“複現路徑是什麽?”
“使用者說,第一次點選兌換,彈窗出來,他點了確認。頁麵卡了一會兒,沒跳轉,他就又點了一次兌換按鈕,結果又彈出一個一模一樣的彈窗。”蘇晴把手機遞過來,上麵是使用者的反饋截圖,“第二次他點了取消,然後頁麵顯示兌換成功。但他查記錄,積分被扣了兩次。”
林夕接過手機,那股熟悉的寒意又從脊背爬上來。
“使用者操作時間間隔大概多久?”
“使用者說就幾秒鍾,他以為頁麵卡死了。”
林夕開啟自己的測試環境,開始複現。當他模擬網路延遲,讓第一次請求卡住5秒以上時——使用者第二次點選,確實會彈出第二個視窗。
而如果使用者第二次點了取消,前端邏輯會終止流程,但那個已經發出去的第一次請求呢?
它還在路上,正在前往那個單點故障的資料庫。
“我需要查一下這個使用者的資料。”林夕站起來,“可能需要後端配合。”
蘇晴點頭:“我已經通知後端負責人了。但……”她壓低聲音,“周凱說先別聲張,可能是使用者誤操作,讓我們私下查清楚再說。”
林夕看著她:“如果這不是個例呢?”
蘇晴沒說話。她的眼神告訴林夕,她也想到了同樣的可能性。
傍晚六點,林夕還在和後端的同事一起查日誌。那個使用者確實被扣了兩次積分,但隻有一條兌換記錄——另一條積分扣減,沒有對應的業務記錄,像是直接操作了資料庫。
更蹊蹺的是,兌換介麵的監控顯示,從下午兩點開始,錯誤率有微小的上升,從0.01%升到了0.05%。幅度很小,還在正常波動範圍內。
但林夕拉出了曆史同期對比——平時這個時間的錯誤率,通常在0.005%以下。
“會不會是發布後的正常波動?”後端同事問。
“可能。”林夕說,“但我想看一下更詳細的資料,比如錯誤型別分佈。”
“那個需要更高許可權,得找李工或者周凱批。”
林夕看了眼周凱辦公室的方向。燈亮著,門關著。
他拿起手機,又放下。
《孫子兵法·軍形篇》:“故善戰者,立於不敗之地,而不失敵之敗也。”
善於作戰的人,先確保自己立於不敗之地,同時不放過任何擊敗敵人的機會。
現在,他還不知道敵人在哪,甚至不知道敵人是不是真的存在。
但他知道,有些東西,已經開始轉動了。
而第一塊倒下的骨牌,可能已經被人輕輕推了一下。
隻是此刻,所有人都還沒聽見它墜落的聲音。