一次幾乎不可能的數據庫遷移

作者 | Brad Fitzpatrick、David Crawshaw

譯者 | 冬雨

策劃 | 蔡芳芳

最初,我們把一個文件當作數據庫,將數據轉化爲 JSON 大對象寫入進去,後來,它的速度越來越慢,我們決定進行數據庫的遷移,這個過程中我們遇到了一些問題和障礙,但最終我們成功完成了這一次不太可能的數據庫遷移。

Brad 加入一家初創公司

大約一年前,當我剛加入 Tailscale(https://tailscale.com/)時,我問 Crawshaw(https://github.com/crawshaw)的第一件事是:“嗯…… 你們使用的是什麼數據庫呢?MySQL、PostgreSQL、SQLite?“我知道他喜歡 SQLite。

“一個文本文件,” 他回答道。

“嗯?”

“是的,我們將一個大型 JSON 對象寫入一個文本文件。”

“怎麼寫?什麼時候寫?寫什麼?”

“嗯,無論什麼時候,只要有什麼東西一變,我們都會在我們的單進程中獲取一個鎖,然後重寫這個文件!” 他高興地笑着說。

聽起來很瘋狂。其實就是很瘋狂。的確,它很容易測試,但是,不能擴展。這些,我們都知道。但是,當時它還行得通。

後來,它不行了。

即使使用高速 NVMe 驅動器,並且將數據庫分成兩部分 (重要數據和 tmpfs 上的可能丟失的臨時數據),有些事情也會變得越來越慢。我們知道這一天終將到來。文件達到了 150MB 的峯值,我們正在以磁盤 I/O 允許的最快速度寫入它。這已經到了極限。

那麼,遷移到 MySQL 或 PostgreSQL,如何?或者是 SQLite?

不,Crawshaw 另有主意。

聊聊大衛的背景故事

Tailscale 的協調服務器(我們的 “控制面板”)以控制 CONTROL(https://getsmart.fandom.com/wiki/CONTROL)聞名遐邇。它目前是單個 VM 上的單個 Go 進程。它最早的原型使用的是 SQLite。我們最初的設計與最終的設計有非常大的差異,包括同步到客戶端機器上的配置數據庫,以及所有我們最終不再需要的其他概念。在這個過程中,我們每週都要對 SQL 數據模型進行非常大規模的重組,這需要大量的鍵盤輸入工作。SQL 已經得到了廣泛使用,它持久、有效,但將其引入到任何編程語言中幾乎都需要做大量的粘合。(通常大家都試圖用 ORM 來避免這種情況,用令人生厭的大量魔法字和效率損失來取代那些同樣令人生厭的鍵盤輸入工作。)

一天,我厭倦了重構,就把它徹底丟在一邊,建了一個內存數據模型進行實驗。這樣,迭代速度更快了。幾周後,一位客戶想要試用一下。我還沒有做好提交數據模型和用 SQL 來完成它的準備,所以我選擇了一條捷徑:將持有所有數據的對象包裝在一個 sync.Mutex 中。所有訪問都要經過它,在編輯時,將整個結構傳遞給 json.Marshal,然後寫入磁盤。這就是我們用大約 20 行 Go 實現的數據模型持久層。

我們本來計劃要遷移爲別的語言的,但忙着忙着就給忘記了。

JSONMutexDB 的後面是什麼?

下一步顯然是遷移到 SQL。我最喜歡的仍然是 SQLite,但是我不能說服自己把一個快速增長的服務遷移到它上面。它當然是可行,尤其是我們的控制面板的設計並不需要典型的 web 服務高可用性:短時間停機的無非就是使新節點無法登錄而已,正在工作的網絡可以保持正常工作。

其後是 MySQL(或 PostgreSQL)。我對 1998 年以後的 MySQL 不是特別熟悉,但我確信它是可以的。不過,開源數據庫的 HA 情況有些令人驚訝:您可以使用傳統的滯後副本,也可以提交到具有令人非常驚訝的事務語義的無主副本集羣。我對試圖在這些語義之上設計一個穩定的 API 或良好的網絡圖計算並不感興趣。CockroachDB(https://github.com/cockroachdb/cockroach#what-is-cockroachdb) 曾經看起來很有前途,而實際上現在仍然很有前途!但對於一個數據庫來說,它還是相對比較新的,我不太放心把一些特性附着在一個新的 DBMS 上,因爲如果我們需要將這些特性中遷移出來時,可能很難做得到。

讓我們的控制服務器依賴於 MySQL 或 PostgreSQL 還意味着我們對控制服務器的測試將變得緩慢和醜陋。Brad 與 Perkeep(https://perkeep.org/)曾就此有過爭論,他之前寫過 perkeep.org/pkg/test/dockertest,它的確可行,但我們不想要求未來的員工都這麼做。它需要在你的機器上部署 Docker 環境,速度不是特別快。

後來有一天我們看到一份 Jepsen 寫的 etcd 報告(https://jepsen.io/analyses/etcd-3.4.3)。這篇報告不似 Jepsen 之前那種滿篇吐槽的風格,裏面還指出了一些 etcd(https://etcd.io/)的優點。結合 Dave Anderson(https://github.com/danderson)的一些正面體驗,我們開始考慮是否可以直接使用 etcd。由於是它用 Go 編寫的,我們可以直接將它連接到我們的測試中,並直接使用它。無需 Docker,無需 mock,就可以測試我們在生產環境中實際使用的東西。

事實上,我們寫入到磁盤的核心數據模型嚴格遵循了以下模式:

1type AllTheData struct { BigLock sync.Mutex Somethings map[string]Something Widgets map[string]Widget Gadgets map[string]Gadget}
2

這很好地映射到了 KV-store 上。因此,我們將 etcd 作爲一個 “最小可行的數據庫”。它做了我們當前所需要的最關鍵的事情,那就是 1) 將 BigLock 拆解成更類似於 sync.RWMutex 的東西。2) 減少 I/O,只寫改變的數據,而不是整體都寫。

(我們會謹慎避免使用任何難以映射到 CockroachDB 的 etcd 特性。)

這樣做的缺點是,etcd 雖然在 Kubernetes 中很流行,但是數據庫系統的用戶相對較少。作爲一家公司,Tailscale 正致力於在其上打造一款創新代幣(https://mcfunley.com/choose-boring-technology)。但這款數據庫從概念上講非常小,以致於我們不必把它當作一個黑盒。當我們在 etcd 3.4 中遇到一個異常緩慢的主鍵分頁的極端情況時,我能夠閱讀它的源代碼並在一個小時內編寫出一個修復程序。(後來,我發現 etcd 的下一個版本也已經做了一樣的修復

(https://github.com/etcd-io/etcd/commit/26c930f27d46776da5fedae69267ba0b69c31185),所以我們將其反向移植了過來。)

我們的 etcd 客戶端包裝器

我們用於 etcd 的客戶端是開放源碼的,網址是

github.com/tailscale/tailetc(https://github.com/tailscale/tailetc)。它圍繞了兩個概念:1)DB 中的總數據量足夠小,可以放入服務器的內存中;2) 讀比寫更常見。鑑於這一點,我們希望降低讀取成本。我們的方法是對 etcd 註冊一個監控。每次更改都被髮送到這個客戶端,這個客戶端在一個 sync.RWMutex 後面維護一個龐大的緩存 map[string] interface{}。當你創建一個 Tx 並且做一次 Get 時,這個值從這個緩存中讀出 (這個緩存可能在 etcd 之後,但是通過跟蹤 modrev 來保持事務一致性:即一個全局遞增的 ID, etcd 使用它來界定鍵 - 值對的修訂)。爲了避免緩存中的混疊錯誤,我們將對象複製出來,但是通過對緩存中的對象實現更有效的克隆調用,避免了每次 Get 時的 JSON 解碼。

最終結果是,從 etcd 獲取一個值不需要任何網絡流量。

當我在設計一個包時,我感受到了編寫 Go 時它的類型系統的侷限性,這樣的感受並不多,它是其中之一。如果我使用的是一種具有各種花哨功能的語言,那麼我可以在離開緩存的對象上放置某種 const 限定符,從而避免對內存進行克隆。即便如此,在我們的服務器上執行的性能分析卻表明,複製並不是一個性能問題,所以該例可能說明,我實際上並不需要那些心心念唸的更復雜的類型系統。通常情況下,假設很可能並不正確,性能分析才更具啓發意義。

一個障礙:索引

選擇最小可行的 “nosql” 的最大問題是缺乏每個標準 SQL DBMS 所提供的出色的索引系統。我們要麼在 etcd 中存儲索引,要麼在客戶端的內存中管理索引。我們使用 JSONMutexDB 在內存中生成它們,因爲更改數據模型要容易得多。使用 etcd 的一個簡單做法是將它們寫入數據庫,但這將產生非常複雜的數據模型。不幸的是,如果我們想要同時運行多個控制進程以實現高可用性和更好的發佈管理,就意味着我們不再只有一個管理數據的進程,因此我們的索引需要支持事務 (以及回滾)。因此,我們投入了大約兩到三週的工程時間來設計事務一致的內存索引。這一點描述起來有些複雜,所以筆者將在後續的博客文章中專題解釋,敬請期待。

遷   移

而遷徙本身卻沒什麼特別值得注意的,這其實件好事。我們這兩個系統並行運行了一段時間,並在某個時間點停止了舊系統的使用。最令人興奮的是,當我們關閉 JSON 寫入時,提交延遲降低了很多。在管理面板中編輯網絡時這一點尤爲明顯。我們有漂亮的 Grafana 圖表,在切換之前我們就調整了 Prometheus 配置以保持更多的歷史紀錄。不論在哪種情況下,寫操作都能從幾乎一秒 (有時更糟!) 的時間縮短到毫秒級。剛開始的時候,寫入並不是我們的第二目標。永遠不要低估 “臨時” 起意會產生多麼長久的影響!

未    來

在這項工作中,除了確保 Tailscale 控制面板可以在可預見的未來擴展外,最令人興奮的事情是我們發佈過程的改進。我們可以輕鬆地將多個控制面板實例附加到一個一致的數據庫中,這意味着我們可以切換爲藍綠部署(https://en.wikipedia.org/wiki/Blue-green_deployment)。這將讓 Tailscale 的工程師們有信心去嘗試部署特性,因爲變更所能造成的最差結果是有限的。我們的目標是將開發速度保持在接近 JSONMutexDB 早期的水平,當時我們可以在不到一秒的時間內重新編譯並在本地運行,每天部署上 10 幾次。

原文鏈接:

An unlikely database migration

https://tailscale.com/blog/an-unlikely-database-migration/

譯者簡介:

冬雨,小小技術宅一枚,從事研發過程改進及質量改進方面的工作,關注編程、軟件工程、敏捷、DevOps、雲計算等領域,非常樂意將國外新鮮的 IT 資訊和深度技術文章翻譯分享給大家,已翻譯出版《深入敏捷測試》、《持續交付實戰》。


本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/KvCyG18XLfjbLTQAQDJEeg