
當雲端帳單一夜暴漲二十倍:一次資料庫流量失血的完整偵查紀錄
前言 (Introduction)
某個月初,我打開雲端帳單,看到一個平常只要幾百元台幣的專案,變成了四萬多。
第一反應當然是「被盜刷了吧」。但很快我就發現一個更難堪的事實:沒有人入侵,每一分錢都是我自己寫的程式碼花掉的。這個 side project 是一個處理財經 Podcast 的內容平台——把語音串流轉成逐字稿、摘要,再和市場標的數據(Market Ticker Data)連結起來。流量不大,使用者是個位數到雙位數的規模,照理說怎麼樣都燒不出這種帳單。
這篇想記錄的不是「我多慘」,而是那兩天的偵查流程:怎麼從一個總金額,一路拆到「是哪兩個函式、以什麼頻率、燒掉了哪個計費項目」。事後回頭看,這套流程其實比結論更有價值,因為它可以套用在任何一次「帳單看起來不對勁」的場合。
架構設計 (Architectural Overview)
出事當下的系統長這樣:
使用者 → Edge 前端(CDN 託管)
→ FastAPI Gateway(歐洲 VPS 上的 Docker,dev / staging / prod 三套)
→ Redis 快取(VPS 本地)
→ 文件資料庫(雲端託管,資料中心在美國中部)
→ 非同步內容 Pipeline(uv workspace,負責轉錄、摘要、實體抽取)
關鍵的地理事實:運算在歐洲,資料在美國。這個配置是歷史演化的結果——文件資料庫是專案早期為了開發速度選的,VPS 是後來為了成本搬的,兩者從來沒有被放在同一張地圖上檢視過。每一次後端向資料庫要資料,payload 都要跨一次大西洋。
還有一個容易被忽略的乘數:dev、staging、prod 三套環境跑在同一台 VPS 上,共用同一個雲端文件資料庫。任何一段「每 N 分鐘做一次」的背景邏輯,實際執行頻率都要乘以三。
方法論拆解 (Methodology Breakdown)
從總金額到函式名的五層下鑽。
第一層:服務級 → SKU 級帳單分解
帳單頁面預設按「服務」分組,只能告訴你「文件資料庫佔了 99%」。真正有資訊量的是切到 SKU 級(計費細項)。我的分解結果大致是:
| 計費項目 | 佔比 | 直覺預期 |
|---|---|---|
| 網路輸出流量(Internet Data Transfer Out) | 約 78% | 完全沒想到 |
| 文件讀取次數(Read Ops) | 約 17% | 以為是主因 |
| 其他(儲存、寫入、秘密管理) | 不到 1% | 符合預期 |
這一步直接推翻了我的第一個假設。我原本以為「讀太多次」是問題,實際上「每次讀取搬運的位元組 × 跨洲距離」才是帳單的本體。讀取單價大約是每十萬次幾塊錢台幣,流量單價卻是每 GiB 三塊多——當你的文件平均十幾 KB、一天讀兩千多萬次,流量費用會是讀取費用的四到五倍。
第二層:監控指標的時間序列
帳單只有金額,沒有行為。下一步是拉雲端監控的每日操作數時間序列:
- 文件讀取:每天穩定約兩千多萬次
- 文件寫入:每天約兩三萬次
- 讀寫比:接近一千比一
然後把粒度切細到每小時:讀取量全天平坦,凌晨三點和晚上八點幾乎一樣高。
這是整個偵查裡最有診斷力的一個訊號。使用者流量有日夜節律,定時器沒有。一條 24 小時等高的讀取曲線,幾乎可以直接宣判:兇手是排程,不是人。
第三層:讀寫比與資料規模的矛盾
資料庫本體多大?不到 1 GB。一天兩千多萬次讀取,等於把整個資料庫每天重讀上萬遍。這種比例不可能來自任何正常的產品行為,只可能來自某種「每隔幾分鐘就全量重算」的迴圈。
第四層:程式碼掃描——每一條讀取路徑列冊
接下來回到程式碼,把後端與 Pipeline 兩個 tier 的每一個資料庫讀取呼叫點列冊:誰觸發(請求?定時器?)、掃全集合還是抓單筆、前面有沒有快取、估計每次執行讀幾筆。
結果指向兩個「快取預熱迴圈」(refresh-ahead loop):
- 熱門標籤看板:每 10 分鐘,強制略過快取,全量掃描標籤集合(裡面積了數千個歷史遺留的雜訊標籤),再對每個有效標籤做多達數百筆的 N+1 文件抓取——而且同一份文件會被不同標籤重複抓取,沒有去重。
- 主題板塊看板:每 5 分鐘全量掃描數千筆內容文件。
兩個迴圈 × 三套環境 × 全天候,精確對上了監控曲線的平坦形狀,估算讀取量也對上了每天兩千多萬次。破案。
第五層:建立成本模型,決定修法優先序
最後把單價寫成一個粗模型:
月費 ≈ 讀取次數 × 讀取單價 + 讀取次數 × 平均文件大小 × 流量單價
代入數字後有兩個推論。第一,降低「每次讀取的 payload」和降低「讀取次數」一樣重要,因為流量項的係數更大。第二,只要資料和運算不在同一個地方,這個模型的第二項就永遠掛在那裡——這個推論直接埋下了後來整個資料層搬遷的種子(下一篇的主題)。
生產環境踩坑與優化 (Production Optimization)
Refresh-ahead 的反噬。 這兩個迴圈的本意是好的:把昂貴的重算移出請求路徑,讓使用者永遠打到熱快取。問題出在三個實作細節:迴圈用「強制刷新」旗標繞過快取讀(於是快取只剩寫入、沒有讀取,形同擺設);刷新頻率(5–10 分鐘)遠高於資料實際變動頻率(內容一天才進來幾批);以及它在所有環境都跑,包括根本沒人看的 dev 和 staging。修法就是這三件事的反面:非正式環境直接關掉、頻率降到每小時、讓快取 TTL 蓋過刷新間隔,讓「預熱」重新成立。
「資料多久變一次」是刷新頻率唯一的錨。 事後我給自己的規則是:任何定時重算,先回答「上游資料的變動頻率是多少」。我的內容一天進來幾批,看板卻每 5 分鐘重算一次——中間那 288 分之 287 次都是純粹的燒錢。
空結果不能快取太久。 這是修復過程中一位擔任「唱反調審查」的第二雙眼睛抓到的:上游短暫故障時重算會得到空集合,如果把空集合寫進長 TTL 快取,一次抖動會讓看板空白一兩個小時。修法是空結果不寫快取,讓下一次請求自然重試。
帳單警報應該在第一天就設。 這次失血持續了整整一個月才被月結帳單揭發。事後我補了預算警報(50%、90%、100% 三段通知)。以 burn rate 來算,警報能把「一個月後發現」變成「兩天內發現」,價差是二十倍的帳單。
修復效果的量化驗證。 熱修上線後,監控上每 6 小時的讀取量從約六百萬次掉到十一萬次,降幅 98%。這裡的教訓是驗證要看監控指標而不是帳單——帳單有一到兩天的延遲,監控是小時級的。
總結 (Conclusion)
這次事件對我而言的三個帶得走的教訓:
- 帳單要拆到 SKU 級才有資訊量。 服務級分組只會告訴你「資料庫很貴」,SKU 級才會告訴你「貴的是流量不是讀取」——而這兩者的修法完全不同。
- 平坦的使用曲線 = 定時器嫌疑。 任何與人類作息無關的資源消耗曲線,先查排程,再查流量。
- 成本模型是架構決策的輸入。 「payload × 距離」這一項,只要資料和運算分居兩洲就不會消失。止血只能砍係數,搬家才能砍掉整一項——這就是下一篇的故事。
Reference
- Firestore Pricing:理解文件讀取、寫入與網路輸出各自的計費模型與單價量級。
- Google Cloud VPC Network Pricing:理解跨洲輸出流量的單價階梯,以及它為何常常大於操作次數費用。
- AWS Caching Best Practices:理解 refresh-ahead 與 cache-aside 的取捨與各自的失效模式。
- Google SRE Workbook — Alerting on SLOs:理解如何把「異常燒錢」當作 SLO 違反來設計警報門檻。