← 倒帶五天

第 3 章:第3章:漏洞

第3章:漏洞

星期二早上,林哲翰六點就醒了。

他躺在床上,看著天花板那道裂紋,想著一件事。在原始時間線,今天早上他睡過頭了,八點半才到公司,錯過了方佑廷發現JWT問題的時刻。等他到的時候,方佑廷已經自己寫了一個patch,沒有經過code review就commit了。

那個patch就是一切問題的起點。

他六點四十五分出門。七點十分到公司。辦公室只有警衛老張。

他坐下來,打開電腦,等。

七點四十分,方佑廷來了。他看起來比昨天更累,眼睛下面的黑眼圈更深了。

「你昨天幾點走的?」林哲翰問。

「兩點多吧。」方佑廷把背包放下,打了個哈欠。「Race condition修好了,但跑測試的時候又發現另一個問題。」

「什麼問題?」

「Lock的粒度太大。加了lock之後,整個API的throughput掉了一半。」

林哲翰記得這個。在原始時間線,方佑廷為了解決這個問題,把lock的範圍縮小了,但縮小的方式有問題——他只鎖了讀取的部分,沒有鎖寫入的部分。結果race condition還是存在,只是機率變低了。

「你怎麼修的?」林哲翰問。

「把lock拆成兩段。讀取的時候鎖一次,寫入的時候鎖一次。」

「這樣中間會有空窗期。」

方佑廷停下來看他。「……對。你說的對。」

「用一個lock包住整個讀取加寫入的過程。」

「但這樣效能——」

「效能之後再優化。先讓它正確。」

方佑廷推推眼鏡,想了一下。「好。」

他轉回螢幕,開始改。

林哲翰看著他。在原始時間線,沒有人跟他說過這句話。他自己試了三天,最後選擇了一個折衷方案——降低race condition的機率,但不完全消除。因為完全消除的代價是效能下降,而效能是demo的重要指標。

但林哲翰現在知道,那個折衷方案在demo那天會失敗。

不是因為race condition被觸發——而是因為方佑廷在修race condition的過程中,改了另一個模組的介面。那個改動導致前端在特定條件下會發送錯誤格式的請求,而錯誤格式的請求會觸發一個完全不同的bug——一個在正常測試中不會被發現的bug。

那個bug才是demo當天真正出問題的原因。

Race condition不是問題。方佑廷為了修race condition而改動的介面才是問題。

林哲翰需要阻止這件事發生。但他不能直接告訴方佑廷「你接下來會改壞另一個模組」。他只能引導。

「佑廷,」他說,「你改JWT模組的時候,有沒有動到其他模組的介面?」

「有。JWT驗證通過之後,會呼叫user service拿user info。我把回傳格式從JSON改成protobuf,想說這樣效能會好一點。」

「不要改。」

「為什麼?」

「前端那邊預期的是JSON格式。你改了,前端不會動。」

方佑廷皺眉。「但湘芸說前端的parser很彈性,應該可以處理不同格式。」

「許湘芸不懂技術細節。你不要聽她的。」

方佑廷看了他一眼,表情有點微妙。「你今天真的很奇怪。」

「哪裡奇怪?」

「你平常不會管這麼多。你讓我自己的code自己負責。」

林哲翰沒有回答。

方佑廷等了几秒,然後聳聳肩。「好,不改了。用JSON。」

他轉回去改code。

林哲翰鬆了一口氣。但他知道這只是開始。在原始時間線,問題不只是介面格式——而是方佑廷在壓力下做的每一個決定都會有連鎖反應。他需要確保方佑廷不會再做出那些決定。

但他不能二十四小時盯著他。


上午十點,自動化測試系統發出了alert。

林哲翰的Slack跳出一則通知:JWT驗證模組的整合測試失敗。

他點開看。測試案例是「多租戶環境下的token隔離」——兩個不同租戶的user同時發送請求,系統應該確保A租戶拿不到B租戶的資料。

測試結果:失敗。A租戶拿到了B租戶的資料。

在原始時間線,這個alert是在星期三下午才出現的。現在是星期二早上。

時間線已經變了。

因為他昨天讓方佑廷提早開始修JWT的問題,所以整合測試提早跑了,問題提早被發現。

這代表什麼?代表他的改變確實影響了時間線。但也代表問題比他記憶中更早浮現。

他站起來,走到方佑廷的位子。「看到alert了嗎?」

方佑廷的臉色很凝重。「看到了。我還在看。」

「是race condition嗎?」

「不是。Lock已經加了。但……」他指著螢幕上的code,「你看這裡。Token驗證通過之後,會去cache拿user info。但cache的key只有user ID,沒有tenant ID。」

林哲翰看了一眼。方佑廷說得對。Cache的key設計有問題——不同租戶可能會有相同的user ID(雖然機率很低),但如果發生了,A租戶的請求會拿到B租戶的user info。

這個問題在單租戶測試中不會出現。只有在多租戶、高並發的環境下才會被觸發。

而demo那天,投資方會用多個租戶帳號同時測試。

「這個cache key要改,」林哲翰說,「加上tenant ID。」

「但這樣cache命中率會下降——」

「改。」

方佑廷看著他,嘴巴張了一下,然後閉上。「好。」

林哲翰回到位子上。他打開Slack,看到趙品睿在工程群組裡問:「JWT測試失敗是怎麼回事?」

方佑廷回覆:「正在修。Cache key設計有問題,今天會修好。」

趙品睿回了個「好」。

但林哲翰知道這個「好」是什麼意思。在原始時間線,趙品睿在星期三晚上找他談過一次,問他「JWT的問題嚴不嚴重」,他說「不嚴重,方佑廷在修了」。然後趙品睿說「好,但不要影響demo」。

這次他沒有說「不嚴重」。


中午,許湘芸走到他位子旁邊。

「你今天有空嗎?」她問。

「什麼事?」

「吃飯的時候聊一下。」

他們去了公司樓下的便當店。許湘芸點了一個雞腿飯,林哲翰點了一個排骨飯。他們坐在角落。

「我跟你說一件事,」許湘芸打開便當,但沒有動筷子,「昨天品睿跟投資方通電話的時候,我在旁邊聽到了幾句。」

「什麼?」

「投資方問品睿,我們的技術主管能不能信任。品睿說可以。」

林哲翰的筷子停住了。

「然後投資方問,如果demo出了問題,誰要負責。品睿說——」她停了一下,「他說『工程團隊會負責』。」

林哲翰放下筷子。

「工程團隊,」他重複了一次,「不是我。」

「對。他說的是工程團隊。不是你的名字。」

林哲翰懂了。

在原始時間線,趙品睿從來沒有在投資方面前說出「林哲翰」這個名字。他只會說「我們的技術團隊」、「我們的工程主管」。模糊的稱謂。這樣出了問題,他可以說是「團隊」的問題,不是「他」的問題。

但如果demo成功了,他會說「這是我的技術主管的功勞」。

趙品睿從來不會把責任跟名字綁在一起。

「你為什麼跟我說這個?」林哲翰問。

許湘芸看著他。「因為我覺得你應該知道。你昨天在會議上公開反對他,他今天就把責任推給你了。」

「他沒有推給我。他說縮小範圍是我的技術決策。」

「對。但如果demo成功,他會說這是他的決定。如果失敗,他會說是你的。」

林哲翰沉默了。

「你覺得demo會失敗嗎?」許湘芸問。

「我不知道。」

「你昨天說風險很高。」

「風險還是很高。但比昨天好一點。」

許湘芸吃了一口飯。「你今天很不一樣。」

「每個人都這樣說。」

「因為是真的。」她看著他,「你平常不會這樣。你平常會讓品睿決定所有事情,然後你執行。但今天你開始自己做決定了。」

「這樣不好嗎?」

「我不知道。但品睿注意到了。」

他們沉默地吃完了飯。


下午三點,方佑廷說cache key改好了。

林哲翰走過去看。Code看起來正確——cache key現在是「tenant_id:user_id」的格式,不會再有跨租戶的問題。

「跑一下整合測試。」林哲翰說。

方佑廷跑了。等了十分鐘。測試通過。

「好了,」方佑廷靠在椅背上,看起來鬆了一口氣,「應該沒問題了。」

「還有什麼沒測的?」

「效能測試。加了lock之後,throughput可能會掉。但這個要跑一整天的壓力測試才準。」

「今天跑。」

「今天?」

「今天跑。明天看結果。如果效能有問題,我們還有時間調。」

方佑廷想了一下。「好。我來設。」

他開始設定壓力測試的參數。林哲翰站在旁邊看,確認他設定的並發數夠高——至少要到demo預期的十倍,才能確保不會出問題。

「並發數設多少?」林哲翰問。

「一百?」

「設五百。」

方佑廷轉頭看他。「五百?demo最多同時三十個人。」

「設五百。」

方佑廷看了他一眼,沒有問為什麼,把數字改成五百。

林哲翰回到位子上。他打開終端機,開始看其他模組的code。在原始時間線,JWT的問題只是冰山一角。demo那天會出問題,不只是因為JWT,而是因為整個系統在高並發下的行為跟預期不一樣。

他需要確保每個模組都能承受壓力。

但他只有一個人。而他不能告訴任何人他記得未來。


晚上八點,辦公室只剩下三個人。林哲翰、方佑廷、陳立偉。

方佑廷在等壓力測試跑完。陳立偉在改前端的code。林哲翰在看系統架構文件。

「哲翰。」陳立偉突然開口。

林哲翰轉頭。陳立偉很少主動說話。

「你覺得這樣對嗎?」陳立偉問。

「什麼?」

「全部的人都在加班。為了demo。但demo只是給投資方看的,不是真的上線。」

「我知道。」

「那你覺得值得嗎?」

林哲翰看著他。陳立偉的表情很平靜,但眼神裡有一種林哲翰在原始時間線沒有注意到的東西——不是不滿,是疲倦。

「你覺得呢?」林哲翰反問。

陳立偉沉默了一下。「我覺得不值得。但我不會說出來。」

「為什麼?」

「因為我需要這份工作。」

林哲翰沒有回答。

陳立偉轉回去繼續寫code。

方佑廷的壓力測試跑完了。他看著結果,臉色不太好。

「Throughput掉了百分之四十,」他說,「五百並發的時候,平均回應時間超過兩秒。」

「正常範圍是多少?」

「五百並發,平均回應時間應該在五百毫秒以內。」

「差了四倍。」

「對。」

林哲翰走過去看報告。數據很明確——加了lock之後,JWT驗證變成效能瓶頸。五百並發的時候,所有請求都在排隊等lock。

在原始時間線,這個問題在demo前才被發現。那時候已經來不及修了,只能降並發數。但投資方在demo的時候不會只開三十個連線——他們會開更多,因為他們要測試系統的極限。

「有辦法優化嗎?」林哲翰問。

「有。用read-write lock。讀的時候不用鎖,只有寫的時候才鎖。」

「這樣race condition又會回來。」

「不會。因為token驗證只有讀取,沒有寫入。Token是client端帶過來的,server端只驗證,不改。」

林哲翰想了一下。方佑廷說得對。JWT驗證是唯讀操作——server只驗證token有沒有過期、簽名對不對,不會修改token的狀態。所以不需要互斥鎖,只需要確保讀取的時候狀態不被其他執行緒改掉。

但問題是,cache的更新是寫入操作。當cache miss的時候,server會去database拿user info然後寫入cache。這個寫入操作跟讀取操作之間會有race condition。

「用lock-free的cache,」林哲翰說,「或者用immutable data structure。」

方佑廷的眼睛亮了。「對。如果cache的值是不可變的,就不需要lock。每次更新都是寫一個新的值,不是改舊的值。」

「這樣記憶體用量會增加。」

「但效能會好很多。」

「做吧。」

方佑廷開始改code。他的手指在鍵盤上飛快地敲,眼睛盯著螢幕,完全沉浸進去。

林哲翰站在旁邊看了一會兒,然後回到位子上。

他看了一眼時間。晚上八點四十分。

在原始時間線,這個時間他已經回家了。他不知道方佑廷一個人在辦公室裡做了什麼。現在他知道了——方佑廷一個人在改code,沒有人在旁邊看,沒有人在旁邊提醒他。

然後他改壞了。

林哲翰打開Slack,傳了一則訊息給方佑廷:「改完之後叫我。我要review。」

方佑廷回了一個「好」。

林哲翰靠在椅背上。窗外的台北市燈火通明。十四樓看出去,可以看到內湖科技園區的夜景——一棟一棟的辦公大樓,每一棟裡面都有人在加班。

他想起母親。這個時間她應該已經下班了。她做清潔工,早上五點出門,下午四點回家。她不知道他的工作是什麼,只知道他在「電腦公司」上班。

她從來沒有問過他賺多少錢。但他每個月匯一萬五回去,她從來沒有花過。她把錢存起來,說要幫他娶老婆。

他想起方佑廷。這個24歲的年輕人,正在一個人改code,為了五天後的demo。他不知道五天後會發生什麼。他不知道他寫的code會被用來當作開除他的理由。

林哲翰閉上眼睛。

他記得所有的事情。但他不知道這樣夠不夠。

5310 字 •