最初,我們把一個(gè)文件當(dāng)作數(shù)據(jù)庫(kù),將數(shù)據(jù)轉(zhuǎn)化為 JSON 大對(duì)象寫(xiě)入進(jìn)去,后來(lái),它的速度越來(lái)越慢,我們決定進(jìn)行數(shù)據(jù)庫(kù)的遷移,這個(gè)過(guò)程中我們遇到了一些問(wèn)題和障礙,但最終我們成功完成了這一次不太可能的數(shù)據(jù)庫(kù)遷移。
Brad 加入一家初創(chuàng)公司
大約一年前,當(dāng)我剛加入 Tailscale(https://tailscale.com/)時(shí),我問(wèn) Crawshaw(
https://github.com/crawshaw)的第一件事是:“嗯……你們使用的是什么數(shù)據(jù)庫(kù)呢?MySQL、PostgreSQL、SQLite?“我知道他喜歡 SQLite。
“一個(gè)文本文件,”他回答道。
“嗯?”
“是的,我們將一個(gè)大型 JSON 對(duì)象寫(xiě)入一個(gè)文本文件。”
“怎么寫(xiě)?什么時(shí)候?qū)懀繉?xiě)什么?”
“嗯,無(wú)論什么時(shí)候,只要有什么東西一變,我們都會(huì)在我們的單進(jìn)程中獲取一個(gè)鎖,然后重寫(xiě)這個(gè)文件!”他高興地笑著說(shuō)。
聽(tīng)起來(lái)很瘋狂。其實(shí)就是很瘋狂。的確,它很容易測(cè)試,但是,不能擴(kuò)展。這些,我們都知道。但是,當(dāng)時(shí)它還行得通。
后來(lái),它不行了。
即使使用高速 NVMe 驅(qū)動(dòng)器,并且將數(shù)據(jù)庫(kù)分成兩部分(重要數(shù)據(jù)和 tmpfs 上的可能丟失的臨時(shí)數(shù)據(jù)),有些事情也會(huì)變得越來(lái)越慢。我們知道這一天終將到來(lái)。文件達(dá)到了 150MB 的峰值,我們正在以磁盤(pán) I/O 允許的最快速度寫(xiě)入它。這已經(jīng)到了極限。
那么,遷移到 MySQL 或 PostgreSQL,如何?或者是 SQLite?
不,Crawshaw 另有主意。
聊聊大衛(wèi)的背景故事
Tailscale 的協(xié)調(diào)服務(wù)器(我們的“控制面板”)以控制 CONTROL(
https://getsmart.fandom.com/wiki/CONTROL)聞名遐邇。它目前是單個(gè) VM 上的單個(gè) Go 進(jìn)程。它最早的原型使用的是 SQLite。我們最初的設(shè)計(jì)與最終的設(shè)計(jì)有非常大的差異,包括同步到客戶端機(jī)器上的配置數(shù)據(jù)庫(kù),以及所有我們最終不再需要的其他概念。在這個(gè)過(guò)程中,我們每周都要對(duì) SQL 數(shù)據(jù)模型進(jìn)行非常大規(guī)模的重組,這需要大量的鍵盤(pán)輸入工作。SQL 已經(jīng)得到了廣泛使用,它持久、有效,但將其引入到任何編程語(yǔ)言中幾乎都需要做大量的粘合。(通常大家都試圖用 ORM 來(lái)避免這種情況,用令人生厭的大量魔法字和效率損失來(lái)取代那些同樣令人生厭的鍵盤(pán)輸入工作。)
一天,我厭倦了重構(gòu),就把它徹底丟在一邊,建了一個(gè)內(nèi)存數(shù)據(jù)模型進(jìn)行實(shí)驗(yàn)。這樣,迭代速度更快了。幾周后,一位客戶想要試用一下。我還沒(méi)有做好提交數(shù)據(jù)模型和用 SQL 來(lái)完成它的準(zhǔn)備,所以我選擇了一條捷徑:將持有所有數(shù)據(jù)的對(duì)象包裝在一個(gè) sync.Mutex 中。所有訪問(wèn)都要經(jīng)過(guò)它,在編輯時(shí),將整個(gè)結(jié)構(gòu)傳遞給 json.Marshal,然后寫(xiě)入磁盤(pán)。這就是我們用大約 20 行 Go 實(shí)現(xiàn)的數(shù)據(jù)模型持久層。
我們本來(lái)計(jì)劃要遷移為別的語(yǔ)言的,但忙著忙著就給忘記了。
JSONMutexDB 的后面是什么?
下一步顯然是遷移到 SQL。我最喜歡的仍然是 SQLite,但是我不能說(shuō)服自己把一個(gè)快速增長(zhǎng)的服務(wù)遷移到它上面。它當(dāng)然是可行,尤其是我們的控制面板的設(shè)計(jì)并不需要典型的 web 服務(wù)高可用性:短時(shí)間停機(jī)的無(wú)非就是使新節(jié)點(diǎn)無(wú)法登錄而已,正在工作的網(wǎng)絡(luò)可以保持正常工作。
其后是 MySQL(或 PostgreSQL)。我對(duì) 1998 年以后的 MySQL 不是特別熟悉,但我確信它是可以的。不過(guò),開(kāi)源數(shù)據(jù)庫(kù)的 HA 情況有些令人驚訝:您可以使用傳統(tǒng)的滯后副本,也可以提交到具有令人非常驚訝的事務(wù)語(yǔ)義的無(wú)主副本集群。我對(duì)試圖在這些語(yǔ)義之上設(shè)計(jì)一個(gè)穩(wěn)定的 API 或良好的網(wǎng)絡(luò)圖計(jì)算并不感興趣。CockroachDB(
https://github.com/cockroachdb/cockroach#what-is-cockroachdb) 曾經(jīng)看起來(lái)很有前途,而實(shí)際上現(xiàn)在仍然很有前途!但對(duì)于一個(gè)數(shù)據(jù)庫(kù)來(lái)說(shuō),它還是相對(duì)比較新的,我不太放心把一些特性附著在一個(gè)新的 DBMS 上,因?yàn)槿绻覀冃枰獙⑦@些特性中遷移出來(lái)時(shí),可能很難做得到。
讓我們的控制服務(wù)器依賴于 MySQL 或 PostgreSQL 還意味著我們對(duì)控制服務(wù)器的測(cè)試將變得緩慢和丑陋。Brad 與 Perkeep(https://perkeep.org/)曾就此有過(guò)爭(zhēng)論,他之前寫(xiě)過(guò)
perkeep.org/pkg/test/Dockertest,它的確可行,但我們不想要求未來(lái)的員工都這么做。它需要在你的機(jī)器上部署 Docker 環(huán)境,速度不是特別快。
后來(lái)有一天我們看到一份 Jepsen 寫(xiě)的 etcd 報(bào)告(
https://jepsen.io/analyses/etcd-3.4.3)。這篇報(bào)告不似 Jepsen 之前那種滿篇吐槽的風(fēng)格,里面還指出了一些 etcd(https://etcd.io/)的優(yōu)點(diǎn)。結(jié)合 Dave Anderson(
https://github.com/danderson)的一些正面體驗(yàn),我們開(kāi)始考慮是否可以直接使用 etcd。由于是它用 Go 編寫(xiě)的,我們可以直接將它連接到我們的測(cè)試中,并直接使用它。無(wú)需 Docker,無(wú)需 mock,就可以測(cè)試我們?cè)谏a(chǎn)環(huán)境中實(shí)際使用的東西。
事實(shí)上,我們寫(xiě)入到磁盤(pán)的核心數(shù)據(jù)模型嚴(yán)格遵循了以下模式:
type AllTheData struct {
BigLock sync.Mutex
Somethings map[string]Something
Widgets map[string]Widget
Gadgets map[string]Gadget
}
復(fù)制代碼
這很好地映射到了 KV-store 上。因此,我們將 etcd 作為一個(gè)“最小可行的數(shù)據(jù)庫(kù)”。它做了我們當(dāng)前所需要的最關(guān)鍵的事情,那就是 1)將 BigLock 拆解成更類似于 sync.RWMutex 的東西。2)減少 I/O,只寫(xiě)改變的數(shù)據(jù),而不是整體都寫(xiě)。
(我們會(huì)謹(jǐn)慎避免使用任何難以映射到 CockroachDB 的 etcd 特性。)
這樣做的缺點(diǎn)是,etcd 雖然在 Kubernetes 中很流行,但是數(shù)據(jù)庫(kù)系統(tǒng)的用戶相對(duì)較少。作為一家公司,Tailscale 正致力于在其上打造一款創(chuàng)新代幣(
https://mcfunley.com/choose-boring-technology)。但這款數(shù)據(jù)庫(kù)從概念上講非常小,以致于我們不必把它當(dāng)作一個(gè)黑盒。當(dāng)我們?cè)?etcd 3.4 中遇到一個(gè)異常緩慢的主鍵分頁(yè)的極端情況時(shí),我能夠閱讀它的源代碼并在一個(gè)小時(shí)內(nèi)編寫(xiě)出一個(gè)修復(fù)程序。(后來(lái),我發(fā)現(xiàn) etcd 的下一個(gè)版本也已經(jīng)做了一樣的修復(fù)(
https://github.com/etcd-io/etcd/commit/26c930f27d46776da5fedae69267ba0b69c31185),所以我們將其反向移植了過(guò)來(lái)。)
我們的 etcd 客戶端包裝器
我們用于 etcd 的客戶端是開(kāi)放源碼的,網(wǎng)址是
github.com/tailscale/tailetc(
https://github.com/tailscale/tailetc)。它圍繞了兩個(gè)概念:1)DB 中的總數(shù)據(jù)量足夠小,可以放入服務(wù)器的內(nèi)存中;2)讀比寫(xiě)更常見(jiàn)。鑒于這一點(diǎn),我們希望降低讀取成本。
我們的方法是對(duì) etcd 注冊(cè)一個(gè)監(jiān)控。每次更改都被發(fā)送到這個(gè)客戶端,這個(gè)客戶端在一個(gè) sync.RWMutex 后面維護(hù)一個(gè)龐大的緩存 map[string] interface{}。當(dāng)你創(chuàng)建一個(gè) Tx 并且做一次 Get 時(shí),這個(gè)值從這個(gè)緩存中讀出(這個(gè)緩存可能在 etcd 之后,但是通過(guò)跟蹤 modrev 來(lái)保持事務(wù)一致性:即一個(gè)全局遞增的 ID, etcd 使用它來(lái)界定鍵-值對(duì)的修訂)。為了避免緩存中的混疊錯(cuò)誤,我們將對(duì)象復(fù)制出來(lái),但是通過(guò)對(duì)緩存中的對(duì)象實(shí)現(xiàn)更有效的克隆調(diào)用,避免了每次 Get 時(shí)的 JSON 解碼。
最終結(jié)果是,從 etcd 獲取一個(gè)值不需要任何網(wǎng)絡(luò)流量。
當(dāng)我在設(shè)計(jì)一個(gè)包時(shí),我感受到了編寫(xiě) Go 時(shí)它的類型系統(tǒng)的局限性,這樣的感受并不多,它是其中之一。如果我使用的是一種具有各種花哨功能的語(yǔ)言,那么我可以在離開(kāi)緩存的對(duì)象上放置某種 const 限定符,從而避免對(duì)內(nèi)存進(jìn)行克隆。即便如此,在我們的服務(wù)器上執(zhí)行的性能分析卻表明,復(fù)制并不是一個(gè)性能問(wèn)題,所以該例可能說(shuō)明,我實(shí)際上并不需要那些心心念念的更復(fù)雜的類型系統(tǒng)。通常情況下,假設(shè)很可能并不正確,性能分析才更具啟發(fā)意義。
一個(gè)障礙:索引
選擇最小可行的“nosql”的最大問(wèn)題是缺乏每個(gè)標(biāo)準(zhǔn) SQL DBMS 所提供的出色的索引系統(tǒng)。我們要么在 etcd 中存儲(chǔ)索引,要么在客戶端的內(nèi)存中管理索引。
我們使用 JSONMutexDB 在內(nèi)存中生成它們,因?yàn)楦臄?shù)據(jù)模型要容易得多。使用 etcd 的一個(gè)簡(jiǎn)單做法是將它們寫(xiě)入數(shù)據(jù)庫(kù),但這將產(chǎn)生非常復(fù)雜的數(shù)據(jù)模型。不幸的是,如果我們想要同時(shí)運(yùn)行多個(gè)控制進(jìn)程以實(shí)現(xiàn)高可用性和更好的發(fā)布管理,就意味著我們不再只有一個(gè)管理數(shù)據(jù)的進(jìn)程,因此我們的索引需要支持事務(wù)(以及回滾)。因此,我們投入了大約兩到三周的工程時(shí)間來(lái)設(shè)計(jì)事務(wù)一致的內(nèi)存索引。這一點(diǎn)描述起來(lái)有些復(fù)雜,所以筆者將在后續(xù)的博客文章中專題解釋,敬請(qǐng)期待。
遷移
而遷徙本身卻沒(méi)什么特別值得注意的,這其實(shí)件好事。我們這兩個(gè)系統(tǒng)并行運(yùn)行了一段時(shí)間,并在某個(gè)時(shí)間點(diǎn)停止了舊系統(tǒng)的使用。最令人興奮的是,當(dāng)我們關(guān)閉 JSON 寫(xiě)入時(shí),提交延遲降低了很多。在管理面板中編輯網(wǎng)絡(luò)時(shí)這一點(diǎn)尤為明顯。我們有漂亮的 Grafana 圖表,在切換之前我們就調(diào)整了 Prometheus 配置以保持更多的歷史紀(jì)錄。不論在哪種情況下,寫(xiě)操作都能從幾乎一秒(有時(shí)更糟!)的時(shí)間縮短到毫秒級(jí)。剛開(kāi)始的時(shí)候,寫(xiě)入并不是我們的第二目標(biāo)。永遠(yuǎn)不要低估“臨時(shí)”起意會(huì)產(chǎn)生多么長(zhǎng)久的影響!
未來(lái)
在這項(xiàng)工作中,除了確保 Tailscale 控制面板可以在可預(yù)見(jiàn)的未來(lái)擴(kuò)展外,最令人興奮的事情是我們發(fā)布過(guò)程的改進(jìn)。我們可以輕松地將多個(gè)控制面板實(shí)例附加到一個(gè)一致的數(shù)據(jù)庫(kù)中,這意味著我們可以切換為藍(lán)綠部署(
https://en.wikipedia.org/wiki/Blue-green_deployment)。這將讓 Tailscale 的工程師們有信心去嘗試部署特性,因?yàn)樽兏茉斐傻淖畈罱Y(jié)果是有限的。我們的目標(biāo)是將開(kāi)發(fā)速度保持在接近 JSONMutexDB 早期的水平,當(dāng)時(shí)我們可以在不到一秒的時(shí)間內(nèi)重新編譯并在本地運(yùn)行,每天部署上 10 幾次。