vatt'ghern jaskier's ballads

PlanetScale 把「相容 Postgres」的分散式資料庫形容成會「hide the shard key from you, take away your extensions, and add complexity and latency」的東西。他們給自己新產品 Neki 定的規則反過來寫:「stick to Postgres, don't work around it, fake it, or turn away from it」。

每個 shard 都還是真的 Postgres

Postgres 資料庫養大之後那些老症頭——table 大到 vacuum、建 index 會拖累線上流量,備份要跑好幾個鐘頭,連線數頂到上限,改 schema 得留維護窗口,還有 transaction wraparound——PlanetScale 在 9 月 10 日把 Neki 放進 platform preview 的那篇文章裡,一次列完,等於先把問題攤開來講,擺在解法前面。Neki 要處理的就是這一串問題,做法是把一個 Postgres 資料庫橫著切成很多份,但每一份都堅持是貨真價實的 Postgres:沒有自訂儲存引擎,extension、SQL 支援、效能表現都跟原生 Postgres 一致,不會因為被切成 shard 就縮水或走樣。這也不是 PlanetScale 第一次碰 Postgres——一年半以前他們就推出過 PlanetScale Postgres,Neki 接在那之後,補的正是「分散式」這一塊原本沒做完的功課。

這句話聽起來像一句口號,拆開看其實是四塊具體工程各自的分工結果:一層 router 擋在應用程式前面,一群真正的 Postgres 被組成 shard 跟 shard group,一個 sidecar 貼著每個 Postgres instance 管連線,還有一個 control plane 在背後盯著節點健康、協調拓樸變化。四塊各自負責一段,合起來才撐得住「橫著切但每一份都還是真 Postgres」這句話——拿掉任何一塊,剩下的三塊都得重新想過。下面這張圖把四塊放在一起,點哪一塊,右邊就換成那一塊的職責說明;先看一次全貌,再往下逐段拆開講每一塊實際在做什麼。

點選各元件看各自負責什麼 · 4 個元件

Neki Router 1 個 shard(屬於某個 shard group) AZ-1 · primary 真 Postgres AZ-2 · replica AZ-3 · replica sidecar sidecar sidecar Control plane

Neki Router · 負責什麼

應用程式連的第一個東西,講標準 Postgres wire protocol,現有 driver、ORM 不用改,單一連線字串就能用。

裡面有完整的 query parser、分散式 query planner,還有 query buffering。

可以垂直、也可以水平擴充,避免自己變成新的單一瓶頸。

shard · 負責什麼

一個 shard 是一整個 Postgres cluster:1 個 primary、至少 2 個 replica,橫跨 3 個可用區。

shard 被分進 shard group,不同 table、不同 workload 可以落在不同的 shard 集合上。

shard index 定義路由要看哪個欄位、值要怎麼 hash。

pooling sidecar · 負責什麼

跟每一個 Postgres instance 一起跑,專門處理連線池。

同時卡在 router 端跟 Postgres 端,pool 大小是量出來的,不是外部估的。

官方原話:這是讓連線處理比「在資料庫前面擺一個 PgBouncer」有意義地更好的那一塊。

control plane · 負責什麼

追蹤每個節點的健康狀態,跑計畫內的 switchover、計畫外的 failover。

協調 reshard、schema 變更、版本升級這些工作流。

把這一切串起來的,是一份叫 data topology 的 JSON,負責把邏輯 table 對應到實體 shard。

Neki Router:查詢先到誰手上

應用程式連的第一個東西是 Neki router,講的是標準 Postgres wire protocol,所以現有的 driver、ORM 完全不用改,連線字串照樣用一條就好。這件事看似只是相容性細節,其實決定了遷移的門檻高不高——應用程式端不需要因為底層從單一 Postgres 換成一整套 sharded 系統,就跟著改連線邏輯、換 driver 版本或重寫查詢層,這一整層差異被 router 吸收掉了,對寫應用程式的人來說,資料庫還是「一個」資料庫。

router 裡面塞了一整套 query parser、一個做分散式規劃的 query planner,還有 query buffering 這些模組。官方把這四步講得比這幾個名詞更具體:router「解析查詢、規劃出哪些 shard 該跑這條查詢、把工作送出去,然後把回來的結果合併成一條串流」。就算一筆查詢牽涉到好幾個 shard,應用程式收到的還是一份結果,不用自己在應用層寫程式碼去拼裝多個 shard 各自回傳的片段——這很可能就是「query buffering and more」裡「and more」實際在做的事,也是為什麼只要一條連線字串就夠用:合併這一步已經在 router 內部做完了。

query buffering 這個字眼也值得停一下:router 介入了查詢的執行路徑本身,不只是轉發封包的代理,該等結果、該合併結果的地方,就在這一層處理掉。從 driver 的角度看,這種多 shard 查詢回來的,跟平常收到的 Postgres wire protocol 回應是同一種格式,都是合併好的單一結果串流。

這一層可以垂直、也可以水平擴充,理由很直接:如果整個叢集只靠一個 router,它自己就會變成新的瓶頸,跟一開始想解決的「單一 Postgres 撐不住」是同一種問題,只是換了個位置重新發生一次。router 因此可以疊很多台——文章沒有講前面實際上怎麼分流,這裡合理的推測是搭配某種負載平衡機制。這跟 shard 本身能不能撐住流量是兩件事,router 層的擴充能力不會被某一個 shard 的容量卡住,兩層各自有各自的伸縮空間,出問題時也比較好分開排查。對正在規劃容量的維運工程師來說,「router 夠不夠」跟「shard 夠不夠」或許可以拆成兩個獨立問題來想:router 主要做計算跟轉發(文章也提到它有 query buffering,不算完全無狀態),加一台大概就是加一台運算力;shard 要擴充,動的則是資料本身要不要重新分佈。把這兩件事混在一起想,很容易把「流量尖峰」誤判成「資料該重新分片了」,其實多數時候可能只是 router 這一層不夠用而已。

shard 與 shard group:真 Postgres 怎麼分成幾組

每個 shard 是一整個 Postgres cluster:1 個 primary、至少 2 個 replica,橫跨 3 個可用區。這代表「shard」這個字在 Neki 裡本身就帶著高可用,壞掉一個可用區還有另外兩個撐著——拓樸設計當下就把這件事算進去了,用不著等 primary 真的掛了才臨時去找 replica 頂上。官方原話寫的是「at least two replicas」,2 個是下限不是上限,理論上可以往上加;至於固定的「3 個可用區」這個數字能不能調整,文章沒有講。官方接著補了一句:「Each shard uses a configuration profile that defines its instance size, replica count, storage, Postgres parameters, and extensions, so you can size each group for its own traffic.」——也就是說,不同的 shard group 本來就可以配不同的複製數,不是所有 shard 都得套同一組數字;至於多開的 replica 會不會反映在計費上,或者會不會影響 reshard 時「catch up with replication」要花多久,文章都沒有提到。

AZ-1 · primary
AZ-2 · replica
AZ-3 · replica
1 個 shard:1 個 primary、至少 2 個 replica,橫跨 3 個可用區——壞一個 AZ,另外兩個還撐著。

shard 之上還有一層 shard group,作用是把不同 table 或不同 workload 分配到不同的 shard 集合,流量大的表跟流量小的表可以各自被切成不一樣的份數,不用被綁在一起搬動。定義這一切用兩種設定物件:shard index 決定路由要看哪個欄位、值要怎麼 hash;shard group 決定一組 table 要攤開成幾個 shard、攤在哪幾個 shard 上。這兩個設定是同一組 table 底下的兩顆旋鈕,各自管各自的事——換 hash 方式動的是 shard index,要分成幾份動的是 shard group;文章沒有明講改其中一個是否完全不影響另一個,但兩者被定義成獨立的物件,看起來是設計成可以分開調整。不同的 table 集合則各自有自己的一對 shard index、shard group,流量大的表要拆成更多份,不需要連帶去動另一組表的設定。文章沒有進一步說明如果一開始選錯 shard index 的路由欄位,或者一筆查詢真的跨了好幾個 shard,實際要付出什麼代價——這部分官方沒有寫,這裡不替它猜。reshard workflow 講的「provision → catch up → switch traffic → retire」,讀起來是針對 shard_group 底下的 shard 數量調整;日後要換 shard_index 那個路由欄位本身,是不是也能走同一套 workflow,文章沒有另外交代。

「沒有自訂儲存引擎」這句話在 shard 這一層的意義也值得說清楚:extension、SQL 支援、效能表現既然都跟原生 Postgres 一致,每一個 shard 都可以照舊裝上應用程式原本依賴的 extension,不用先確認「這個相容層支不支援某個 extension」才敢升級或上線。少一層要驗證的相容性清單,對維運的人來說是實際的差別。同樣的邏輯也延伸到監控、備份這類維運工具上:底層行為跟原生 Postgres 沒有差異,維運團隊原本用來查看單一 Postgres 的工具、寫的巡檢腳本,理論上可以直接對著每一個 shard 用。

官方文件另外補了一句:不一定要一開始就決定好怎麼分片。可以先用一個 unsharded 的 cluster 起步,之後再依實際的流量、存取模式、資料量決定要不要切、切成幾份。shard index 跟 shard group 這兩顆旋鈕不是建表當下就得押注的決定,可以留到真的看到瓶頸之後再轉——合理的推測是,router、sidecar、control plane 這幾層本身早就在跑,只是還沒有把資料真正切開——文章沒有明講 unsharded cluster 底下的拓樸長什麼樣,這是從「可以先不分片、之後再決定」這句話推出來的;不論如何,這跟「一開始就要設計好整套分片方案」是完全不同的門檻。把「可以先 unsharded 起步」跟「shard 之上還有 shard group」兩件事放在一起看,Neki 想要的效果其實是把「要不要分片」跟「分片要怎麼設計」拆成兩個時間點分別決定,不用在專案剛起步、還不知道哪些 table 會先長大的時候,就把整套 shard index、shard group 的設計一次定案。

connection pooling sidecar:量出來的 pool,不是猜出來的

每個 Postgres instance 旁邊都跟著一個 sidecar,專門處理連線池。PlanetScale 自己講得很直白:這是讓連線處理「比在資料庫前面擺一個 PgBouncer 有意義地更好」的那一塊。差別在於 sidecar 同時卡在 router 端跟 Postgres 端——因為兩端都在它控制之下,pool 該開多大是照兩端實際的餘裕量出來的。

外部 pooler 通常只看得到自己這一側的連線行為,得靠經驗值或保守估計去設 pool size:設少了,底層 Postgres 明明還有餘裕卻被浪費;設多了,又可能撞上 Postgres 自己扛不住的連線上限,結果是一堆連線卡在排隊或直接被拒絕。Neki 把量測搬進 process 內部,router 端跟 Postgres 端的狀態同時可見,理論上就不用在「浪費容量」跟「打爆連線上限」之間賭一個數字——這不是外部 pooler 做得不好,而是它站在另一個獨立部署的 process 上,位置本來就拿不到 Postgres 那一側的真實餘裕;Neki 把 sidecar 貼著 Postgres instance 跑,等於是把量測點搬到資訊本來就在的地方。這張圖把兩種畫法放在一起,按鈕可以切換著看。

點按鈕切換兩種 pool 畫法 · 示意圖,非實測資料

Postgres 實際撐得住的連線數上限 連線數增加 → 目前:sidecar 雙端量測

這個設計跟每個 shard 的高可用結構其實是同一套邏輯的延伸:sidecar 貼的是每一個 Postgres instance,不是每一個 shard——一個 shard 有 1 個 primary、至少 2 個 replica,代表底下至少同時跑著 3 個 sidecar,各自量各自那台機器的連線狀況。連線管理因此變成跟著 shard 一起佈署、一起擴散的基礎設施。把這件事講白一點:pool 大小是照「這個 instance 實際能撐住多少」量出來的,維運團隊原則上不用再為每一種 workload 組合單獨跑一輪連線壓測,才敢決定 pool size 該設多少。

control plane 與 data topology:誰在背後動刀

control plane 追蹤每個節點的健康狀態,跑計畫內的 switchover 跟計畫外的 failover,還協調 reshard、schema 變更、版本升級這些工作流——這些原本要工程師手動盯著、甚至半夜被叫起來處理的事,被收進同一層,變成一套可以重複執行的流程。「計畫內的 switchover」跟「計畫外的 failover」被放在同一句話裡講——兩者至少都收進 control plane 這一層處理,是不是走同一條程式路徑,文章沒有明講。「追蹤每個節點的健康狀態」具體用什麼指標判定健康、要撐多久才觸發 failover,文章同樣沒有寫——這幾個問號,大概要等 Neki 走出 preview、有更多人實際跑過 reshard 跟 failover 之後才會有答案。

reshard 這個工作流本身官方也講得很具體,是「你在既有叢集上執行的一個 workflow」:先把新的目標節點準備好,接著用 replication 讓新節點追上舊節點目前的資料進度,然後透過一個叫 __neki 的 metafunction 把流量切過去,最後才把舊節點退役。順序值得注意——流量是在新節點已經追上進度之後才切過去,舊節點也是流量確認切完才退役。「用 replication 追上」這一步也值得多想一層:看起來用的字眼,跟每個 shard 本來就要用來維持 primary、replica 一致的機制是同一個詞,但文章沒有明講兩者是不是同一套實作。如果是,這也呼應了官方一開頭那句「不要在 preview 期間跑 production workload」——合理的推測是,reshard 真正還沒被時間驗證過的地方,會集中在「切流量」跟「退役」這兩個比較新的步驟上。下面這個滑桿可以逐一切過這四個階段。

拖曳滑桿看 reshard workflow 的四個階段 · 4 個階段

1 · provision new target nodes

先把新的目標節點準備好——這是 workflow 的第一步,流量要等到後面的步驟才會切過去。

1 / 4

把整個拓樸串起來的是 data topology,一份 JSON 設定,負責把邏輯上的 table 對應到實體的 shard。router 怎麼知道一筆查詢該送去哪個 shard,答案就寫在這份 JSON 裡;shard index 定義的路由欄位跟 hash 規則,也是這份設定的一部分——前面提到的「先用 unsharded cluster 起步,之後再決定怎麼切」,實際上動的也是這份設定,改的範圍就停在這裡。官方也把這幾件事並列講清楚:原本「通常要排一個維護窗口」才能做的事——schema 變更、版本升級、計畫內跟計畫外的 failover、匯入資料,還有 reshard——在 Neki 裡全部變成內建 workflow 在跑。這句話剛好接回文章一開頭列的那串老症頭:「改 schema 得留維護窗口」正是其中一條,等於 control plane 這一層就是衝著那份症頭清單設計的。

這也代表 reshard、schema 變更、版本升級這幾件事,在 Neki 裡被歸到同一種操作模式,由 control plane 協調執行。對正在評估要不要導入的工程師來說,reshard 因此不再是那種半夜緊急介入、手動搬資料的操作,而是一個交給 control plane 執行的既定流程——只是這個流程眼下還沒有經過大規模、長時間的正式驗證。這幾條工作流目前也還在 platform preview 的範圍內——文章開頭那句「不要拿去跑 production workload」,同樣適用在 reshard、failover 這些由 control plane 執行的操作上,不是只限於一般查詢路徑;至於這條路徑在 platform preview 階段到底多穩,PlanetScale 自己沒有把話說滿:理由正是產品本身、包括這幾條工作流,都還在變動中,有些變動不會向下相容。

跟 Vitess、跟「相容 Postgres」的分散式資料庫放在一起看

PlanetScale 自己在文章裡寫,Neki 是「過去八年經營全世界數一數二大的 sharded MySQL 叢集」累積下來的經驗做出來的,這篇文章本身沒有點名是哪個專案;但 Neki 文件另一頁把話講得很直接,標題就是「From the maintainers of Vitess」,內文寫「Neki is built by PlanetScale, the maintainers of Vitess」——指的就是 Vitess,PlanetScale 長年主力經營的 MySQL 水平擴充專案。同一份文件也把界線劃清楚:「Neki is not a Vitess port. It is built from scratch to specifically solve the scalability and operational challenges of Postgres」,兩者共享的是團隊跟設計經驗,不是共用程式碼。Vitess 官方對自己的定義是一套「for horizontal scaling of MySQL」的叢集系統,用 VTGate 當 MySQL protocol 的 proxy 接應用程式、分流到對的 shard,VTTablet 則跟著每一個 MySQL instance 跑,管查詢、連線、replication。

拿這兩層對照 Neki 的 router/sidecar/control plane 三層,角色大致對得上:VTGate 對應 router,VTTablet 原本一手包辦的查詢、連線、replication,被 Neki 拆成 sidecar(專管連線)跟 control plane(管節點健康、reshard、schema)兩塊,分工切得更細一點。Vitess 官方也提到自己能「migrate tables into different databases and scale the number of shards up or down」——這跟 Neki control plane 裡「協調 reshard」的工作流,講的其實是同一種能力,只是換了一個資料庫引擎、換了一層分工方式。Vitess 有「served all YouTube database traffic for over five years」這種實績撐腰,Neki 現在還停在 platform preview 階段,兩者不是同一個成熟度的東西,只是同一條設計路線的延伸。

至於另一條常見路線——那些號稱「相容 Postgres」的分散式資料庫——PlanetScale 自己的評價不客氣:會「hide the shard key from you, take away your extensions, and add complexity and latency which becomes difficult to handle and debug」。這句話本身是廠商對別的路線的評價,不是第三方評測,讀的時候要記得這層立場;但拿它對照 Neki 文件列出的能力清單——sharding、zero-downtime operations、online DDL、replication workflows,外加「sophisticated cluster management capabilities, all using real Postgres」——至少可以看出 PlanetScale 想強調的重點:清單上每一項都在講「用真 Postgres 做到」,不是「做出跟 Postgres 相容的另一套東西」。

Neki

  • shard 是真 Postgres,沒有自訂儲存引擎
  • router 講標準 Postgres wire protocol
  • sidecar 同時控制兩端連線,pool 大小是量出來的

Vitess(MySQL)

  • shard 是真 MySQL,VTGate 講 MySQL protocol
  • VTTablet 跟著每個 MySQL instance 管查詢、連線、replication
  • 已經扛了 YouTube 全站流量超過五年

「相容 Postgres」的分散式資料庫

  • PlanetScale 的講法:hide the shard key from you
  • take away your extensions
  • add complexity and latency which becomes difficult to handle and debug
第三張卡是 PlanetScale 自己對另一條路線的評價,不是第三方評測結果,讀的時候要記得這層立場。

想試的話,步驟寫得很直接:登入 PlanetScale,選擇加入 platform preview,建一個 Neki cluster。這裡有一層要分清楚——PlanetScale 說自己過去八年撐過「thousands of production workloads with millions of queries per second」的規模,這句話講的是他們既有的 MySQL 艦隊,不是剛進 preview 的 Neki 本身,兩者的成熟度不能混為一談,讀到這個數字時要記得它指的是誰,不要拿來當成 Neki 已經驗證過的容量數字。

What this enables:router、shard、sidecar、control plane 湊在一起,換來的是不用丟掉 Postgres 語義、也不用換一顆相容它的新資料庫,就能把單一 Postgres 撐不住的規模繼續往外推。這份保證目前只做到 platform preview 這一步:PlanetScale 自己把「不要拿去跑 production workload」寫進了這篇發布文章裡,理由是產品本身還在改,其中一些改動不會向下相容。