第 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會被用來當作開除他的理由。
林哲翰閉上眼睛。
他記得所有的事情。但他不知道這樣夠不夠。