編按:本文綜合整理自 Tailscale 官方部落格、SQLite 版本更新日誌、SQLite WAL 文件,並加入 Siami 編輯部觀點與分析。
一場長達半年的捉迷藏:Tailscale 為什麼盯上 SQLite
2025 年底開始,Tailscale 的 control plane 出現一連串離奇當機。狀態頁事件一個接一個冒出來,工程團隊最初以為只是「hypergrowth 帶來的擴充陣痛」,但時間一拉長,他們發現:每一次當機的根因,竟然都指向同一個東西——SQLite 的 WAL-Reset bug。
這個 bug 藏在 SQLite 原始碼裡至少 16 年。Tailscale 並不是第一個受害者,但他們的部署方式剛好把這個極度罕見的競態條件(race condition)「踩」出來:19 次資料庫損毀、影響數百萬裝置、耗時六個月才水落石出。
Tailscale 工程師 Brad Fitzpatrick 在 Hacker News 上回憶:「我們找到之後,馬上跟 SQLite 買了企業支援合約讓他們幫我們 debug,這錢花得值得。我們應該寫一篇部落格講這件事。」——這就是 2026 年 8 月 12 日這篇重磅文章的前因後果。
Tailscale 的 SQLite 架構:為什麼他們會踩到這顆雷
Tailscale 的 control plane 對外看起來是一個單一端點 controlplane.tailscale.com,但內部切成多個 coordination shards,每個 tailnet 落在某個 shard 上。每個 shard 各自跑一份 SQLite,每份 SQLite 由「一個」Go process 獨佔讀寫——這正是 SQLite 官方推薦的 single-writer 用法。
備份流程很直觀:每幾分鐘做一次快照、把整個 SQLite 檔丟上 S3。從 2023 年初到 2025 年中,跑了兩年半完全沒事。
問題出在他們「手動接管了 checkpoint」。
一般情況下,SQLite 會自己決定什麼時候把 WAL(Write-Ahead Log)合併回主資料庫檔,這個過程對開發者透明。但 Tailscale 為了做「快速且一致」的備份,手動觸發 checkpoint,而且頻率比預設高很多。
這個非標準的操作模式,跟後來的 bug 出現條件剛好對上。
半年內連環爆:19 次資料庫損毀
時間軸大概是這樣:
- 2025 年 8 月:備份 pipeline 回報某個 shard 的 SQLite 有 corruption,跑
PRAGMA integrity_check確認中招。修補 + 調查,找不到原因。 - 接下來六個月:又壞、又壞、又壞,累計 19 次。有時候相隔幾小時、有時候相隔數週。
- 2025 年 10-12 月:竟然整整六週沒事——但 12 月底又炸了,像一份不請自來的聖誕禮物。
每一次損毀發生時,shard 上的 control plane 必須停機修復或還原。對那個 tailnet 來說,整個控制平面就消失:
- 新上線的裝置沒辦法拿到 peer 列表,連不上別人。
- 已上線的裝置彼此仍能通訊,但學不到新變更。
- Web admin console 和 API 暫時不能用。
最慘的時候,停機時間超過一小時。雖然只影響少數 shard(多數 tailnet 從未遇到),但每次都會在 Tailscale 公開狀態頁上發全站事件——這對品牌信任的累積傷害,比直接受影響的範圍大得多。
Tailscale 在文中坦言:「重複發生的 downtime 會侵蝕信任,無論你實際上是否被影響。」
從「為什麼壞」到「怎麼壞」:八個月的除錯日記
Tailscale 工程團隊做了所有「教科書上建議的初步檢查」:
- 看最近的程式碼變更——沒人改過底層 SQLite 互動程式碼,那段幾年沒動過。
- 對所有低階 SQLite 互動程式碼做細到不能再細的 code review——沒找到會造成損毀的 bug。
- 找損毀事件的共同特徵——不是單一 shard、不是單一客戶、不是特定 tailnet 功能、不是特定時段、不是特定負載等級。完全找不到 pattern。
這種「沒有可重現的觸發條件」是最難處理的:你沒辦法做合成測試,只能在 production 環境部署被動的鑑識遙測,等下一次損毀自己發生,把現場證據撈起來。
更煩的是:損毀「間隔完全沒規律」。有時候幾小時內連發,有時候安靜好幾週。這讓 Tailscale 無法預估除錯進度、也沒辦法規劃下一步——因為永遠不知道下一次「現場證據」什麼時候會掉下來。
聘請外援:跟 SQLite 核心團隊結盟
意識到這不是快速能解的問題,Tailscale 跟 SQLite 簽了專業支援合約。這個決策很關鍵:
- 直接取得 SQLite 核心開發者的深度專業知識。
- 多次密集的技術對談,一起梳理 Tailscale 的架構與每一次事件。
兩邊團隊列出幾個候選理論:
close()時 POSIX advisory lock 被其他 thread 取消。- 誤用 SQLite 管理的記憶體。
- 多執行緒同時呼叫 SQLite,但關閉了 thread safety。
每一次損毀後,他們都補抓更多資料、加更多診斷,逐一排除這些理論。「我們慢慢收斂到真正的 bug。」
柳暗花明:自製 VFS shim 抓到兇手
在挖到根因之前,Tailscale 也沒閒著——他們繼續跑 production,但做了幾件「止血」的事:
- Control plane shards 一偵測到 corruption 立刻硬停,避免損毀擴大。
- 部署自動備份監控。
- 改進還原流程,把最早期一小時的停機時間逐步壓短。
真正的突破來自「自製觀察工具」。
SQLite 內部切成幾層:parser/code generator → pager → virtual filesystem(VFS,目前主流有 Unix 跟 Windows 兩種實作)。每一層都可以被替換或包一層 shim。SQLite 團隊為這次 debug 專門寫了一個叫 tmstmpvfs 的 VFS shim,會把對 VFS 的所有呼叫加上時間戳記和詳細 log,原始碼公開在 SQLite 官方 repo。
Tailscale 把這個 shim 部署到 production,等下一次損毀。
沒等太久。
兇手現形:WAL-Reset bug 的運作原理
新 shim 的 log 讓 SQLite 開發者終於看到那顆潛伏 16 年的競態條件:在 checkpoint 與 write transaction 之間,有一個極度罕見的競態。
具體發生的事情是這樣的:
- 一個 write transaction 剛好在 checkpoint 的某個特定時點發生。
- Checkpoint 機制「以為」某些 WAL 頁面已經被複製到主資料庫檔,但其實沒有。
- 那些頁面永遠不會被寫進主資料庫,那部分資料永久遺失。
- 主資料庫檔變成損毀狀態——因為其他參考到這些頁面的內容(例如 index)已經被寫進去了,導致引用關係錯亂。
SQLite 開發者把這個 bug 命名為「WAL-Reset bug」,並估計它在 SQLite 程式碼裡潛伏了至少 16 年。它能藏這麼久是因為「太罕見」——罕見到 SQLite 開發者必須故意加程式碼去觸發它,才能在測試環境驗證修復。
修補方式是在 checkpoint 函式加一個「檢查 WAL 是否被另一個 thread reset 過」的步驟(commit 7168988acbec2d8d)。
為什麼 Tailscale 會比其他人更容易踩到?兩個原因:
- 他們手動控制 checkpoint 流程。
- 他們 checkpoint 的頻率比預設高很多。
「即使是一個罕見的觸發條件,只要 checkpoint 跑得夠頻繁,就遲早會被打中。」Tailscale 在文中這樣總結。
虛驚一場:3.52.0 撤回事件
SQLite 開發者把修復包進 3.52.0,Tailscale 興奮地準備升級:
- 先部署到幾個 canary shard。
- 觀察穩定後,部署到整個 control plane。
備份監控立刻亮紅燈:13 個資料庫被回報 corruption。
Tailscale 工程團隊照標準流程修復,結果發現這些「損毀」其實不是真的損毀——而是 3.52.0 同時引入的另一個 bug 導致的誤報。
具體原因是:Tailscale 把高精度 timestamp 存成 text,再用 VIRTUAL generated column 轉成浮點數。3.52.0 的最佳化改變了 text-to-float 的捨入行為,導致 stale expression index 問題——PRAGMA integrity_check 因此誤判為損毀。
canary shard 剛好沒有會觸發新捨入行為的 timestamp,所以分階段 rollout 完全沒抓到。
SQLite 團隊的反應很快:撤回 3.52.0,改發只含 WAL-Reset bug 修復的 3.51.3。Tailscale 也順手把自己端的 timestamp 精度降到整數秒(text-to-integer 沒有捨入歧義)。後續的 3.53.0 則加上了 self-healing index 功能,從根本上避免 stale expression index 問題。
🎉 Party Time:兩個月後的「確認警報」
修補正式部署完,理論上問題應該解決了。但 Tailscale 沒有立刻慶功——他們知道「沒看到損毀」不代表「修好了」(中間已經有過一次六週的假平靜)。
他們做了一件聰明的事:在自家 SQLite driver 加一個 patch,當「write transaction 跟 WAL-reset 在時間上重疊」時只記 warning、不中斷。如果 warning 觸發、但資料庫沒事,那就等於證明「修補擋下了真正的損毀」。
部署完,等待。
再等待。
再等待。
然後兩個月後,那個 alert 終於響了。
「這條 alert 證明 WAL-Reset bug 的精確觸發條件在我們 production 真的會發生——也就是說,它就是那半年 downtime 的兇手。」
從那個 weirdly joyous alert 之後,Tailscale 又跑了四個月沒出過任何資料庫事件。
🚨 為什麼這件事重要
這是一個教科書級別的「開源生態鏈」故事,三個層面值得所有在做基礎設施的人思考:
1. 「無聊的技術」配上「非標準用法」就會變刺激
Tailscale 用 SQLite 是教科書式的 single-writer 架構,跟 SQLite 官方建議完全一致。問題出在他們「手動控制 checkpoint」這件事——雖然仍然是公開、文件化、被支援的配置,但已經偏離了 SQLite 主流使用方式。
Tailscale 自己的結論:「用無聊的技術、但用非標準的方式去用,永遠是一個風險。」
對其他公司的啟示:如果你打算對任何基礎元件做「客製化操作」,請準備好「踩到別人沒踩過的 bug」的後果。
2. 開源商業支援是真的有用
Tailscale 跟 SQLite 簽企業支援合約的決定,是整個除錯的轉捩點。在沒有 SQLite 核心開發者直接參與之前,他們繞了五個月沒有進展;簽約後幾個月內 bug 就定位並修復。
這個模式值得所有重度依賴某個開源元件的團隊參考:你付的支援費,最終會幫整個社群找到一顆藏了 16 年的 bug。Brad Fitzpatrick 在 HN 的那句「Worth every penny」背後是真實的六個月 downtime 成本。
3. 除錯文化的價值
Tailscale 在 bug 完全無法重現、無規律、無明確 pattern 的情況下,仍然堅持「被動部署鑑識遙測、等現場證據」。這個紀律讓他們在 8 個月後(從第一次損毀算起)成功定位。
很多團隊會在「追了三個月沒結果」之後就放棄、容忍 downtime、或改架構繞過。Tailscale 選擇跟 SQLite 開發者結盟、把 production 當鑑識現場——這是耐心與紀律的成果。
🚨 數據解讀:WAL-Reset bug 為什麼能藏 16 年
幾個值得放大的數字:
| 指標 | 數值 | 解讀 |
|---|---|---|
| 損毀間隔 | 數小時到數週不等 | 完全沒有規律,無法用統計模型預測下次 |
| 累積損毀次數 | 19 次 / 6 個月 | 在 Tailscale 的 checkpoint 頻率下,罕見條件也會被打中 |
| Bug 在 SQLite 存在時間 | ≥ 16 年 | SQLite 開發者必須刻意寫程式才能觸發它來測試 |
| 除錯總時間 | 約 8 個月 | 從 2025/8 第一次損毀到 2026 修復並部署 |
| WAL-Reset alert 部署後首次觸發 | 2 個月後 | 證明 production 確實存在該條件;觸發 = bug 被擋下 |
| 修復後平靜運行 | 4 個月(截至發文) | Tailscale 確認根除 |
最值得品味的是「bug 必須刻意觸發才能測試」這件事——意思是 SQLite 團隊主動把它寫進 regression test 套件,未來如果有人改壞 checkout 邏輯,CI 會立刻抓到。這是開源專案在處理「極度罕見但會毀損資料」的 bug 時,唯一可靠的解法。
結語:這類故事為什麼值得寫進部落格
Tailscale 之所以願意把這段痛苦的經歷攤開來講(連「早期限於一小時的停機」這種丟臉的細節都沒藏),有三個理由:
- 感謝客戶的耐心——六個月的不穩定對依賴 Tailscale 跑業務的團隊是真實的困擾。
- 回饋開源社群——他們付費給 SQLite 團隊支援,並資助開發
tmstmpvfs這個 VFS shim,這個工具現在公開在 SQLite repo 裡,能幫未來的除錯。 - 建立同業除錯參考——當下一家公司遇到「罕見、隨機、無規律」的資料庫損毀,這篇文章提供了一條被驗證過的思路。
文末一句話很 Tailscale:「希望不會再發生這種事——但如果再發生,我們準備好了。」
參考連結:
網友熱門留言 (4)