絲般順滑!X-DB如何實現Online DDL
本文作者是阿里巴巴資料庫專家 蔡暢 ( @天士夢 )
“關係”在資料庫中的主要表現形式是表,包含表屬性,列屬性,索引以及約束等。通過“關係”來規範儲存,使得使用者通過SQL標準規範存取資料。藉助SQL語句豐富的表達能力,在資料庫層面就能搞定複雜的業務。當然,為了做到這一點,“關係”定義(schema)的維護至關重要。當schema需要發生變更時,如何能做到Online(不影響業務的讀寫操作)?業內資料庫Online schema變更的方案有哪些?X-DB是如何實現的,設計背後的原因是什麼?本文將詳細討論。
MySQL Online DDL的演進
MySQL毫無疑問是最流行的開源單機資料庫。在5.5版本以前,MySQL不支援Online DDL。需要做schema變更時(例如:修改列、加索引等),要麼鎖表變更(禁寫),要麼通過主備切換的方式來進行。第二種方式的缺陷在於:需要DBA人工介入,而且主備的schema變更先後生效,無法嚴格一致。
既然資料庫內部搞不定,那就在資料庫外部做。pt-online-schema-change, gh-ost等變更工具,通過建立目標schema的影子表,藉助觸發器雙寫或者拉取binlog雙寫,最終通過rename影子表操作來達到變更的效果。
5.6版本以後,MySQL內部開始支援Online DDL。主要原理是將資料分為基線和增量兩部分,開啟一個單獨執行緒變更基線資料,同時增量實時記錄到row-log裡。基線變更結束後,通過回放row-log,實現增量同步。整個過程中有幾個關鍵點:第一,開始變更時獲取快照,這個階段需要禁寫,確保獲取snapshot對應的基線,和後續增量(row-log)是一份完整的資料;第二,在基線變更完成後,開始回放row-log,由於row-log隨著業務的寫入在不斷追加,因此這裡基於一個前提:row-log的回放速度高於業務寫入的速度,否則可能一直追不上,schema變更也就無法完成;第三,schema生效階段同樣需要禁寫,確保不會有新的寫進來,新的schema開始生效。
目前,MySQL8.0在對於部分加列等schema變更操作做了優化,支援instant ddl,有點類似X-DB Fast DDL。其餘Online DDL的基本原理仍保持不變。對於MySQL的Online DDL方案,需要說明的是:MySQL主備副本之間通過binlog同步,主的schema變更成功後,才會寫binlog同步給備庫,然後備庫才開始做ddl。假設一個ddl變更需要1個小時,那麼備庫最多可能會延遲2倍的變更時間。若變更期間,主庫發生故障,備庫資料還未追平,則無法提供服務的。
NewSQL的Online DDL
NewSQL時代,Online DDL依然是一個繞不過去的問題。以兩個優秀的開源資料庫CockroachDB和TiDB為例,它們的Online DDL實現均參考論文 Online, Asynchronous Schema Change in F1 。F1-Server叢集中每個server都是無狀態的,多副本複製靠儲存層Spanner保證。對於Spanner而言,F1-Server相當於一個客戶端。資料庫的schema通過Spanner持久化儲存,每個F1-Server在本地維護一份schema的快取,並通過lease機制保證快取的時效性。任何一個F1-Server都可以接收讀寫請求,如果schema快取不正確,就無法保證存取資料正確性。
DDL最簡單的實現方法:執行變更過程中,所有F1-Server禁寫,變更完成後,等待lease時間,確保所有F1-Server都擁有最新的Schema。但是,通常Schema變更都伴隨著資料遷移,比如新增索引操作,需要依據主表build一份索引出來,這個時間可能很長,長時間禁寫肯定是不可接受的。
既然禁寫不可行,那麼在DDL發生時,必然存在多個F1-Server存在多個不同版本的schema的情況。下面將以一個加索引的例子簡單描述F1-Spanner架構下DDL執行過程,如為表Relation(Pk,C1)新加索引Index(C1)。首先選舉一個F1-Servers作為Owner,記為F1-Server1,執行DDL後擁有了new-schema,同時假設F1-Server2仍然使用old-schema。
對於某個記錄,F1-Server1會同時寫入主表和索引資料;如果該記錄後續被F1-Server2刪除,那麼只會刪除主表記錄,索引資料就會殘留在系統中,這就產生了不一致。未解決這類問題,論文通過引入多箇中間狀態schema,並且證明如果任意兩個相鄰狀態下,資料的一致性能得到保證,通過一系列中間狀態變更,就最終能保證整個變更過程正確性。Online DDL變更過程分為四個步驟:absent->delete-only->write-only->public,相應地,schema版本包括4個狀態:
- S1(absent): 變更之前的狀態
- S2(delete-only): 只允許刪除新二級索引,忽略新二級索引寫入,不允許讀新二級索引
- S3(write-only): 當所有F1-Server都達到S2狀態後,開始進入這一階段,允許刪除/寫入新二級索引進KV層,不允許讀新二級索引,並開始掃描基線資料,構造新的二級索引<key,value>到KV層
- S4(public):新二級索引對外可見(可讀)
F1論文詳細論述了經過這4個狀態的轉變,如何保證一致性,過程較為複雜,這裡不做詳述,感興趣的朋友可以翻看原文。
資料庫架構對Online-ddl影響
引入中間狀態的目的是為了解決多個F1-Servers無法在schema變更期間,保證資料一致性。但這個前提是基於F1+Spanner框架(如圖1)。

F1 架構
對於F1來說,Spanner是一個共享分散式儲存層,而對Spanner來說,F1就是一個client。正在這種徹底的解耦、徹底的儲存計算分離導致了schema-change的複雜性,當然架構的選擇各有利弊的權衡,這裡只是從Online DDL的角度考慮。
如果採用類似X-DB這種原生的Share-Nothing架構,對於每一個分片資料,都有三副本,並且有唯一的Leader,所有這個分片的寫請求都會路由到這個Leader。因此不會存在對於一個分片,多個Server採用多個不同的schema來寫的情況。

X-DB 架構
那麼X-DB是如何實現Online DDL的呢?
X-DB Online DDL原理
X-DB是一個原生Shared-Nothing的分散式關係資料庫,具備高效能、高可用、強一致、可全球部署、高可擴充套件特點。X-DB基於自研儲存引擎X-Engine,採用類LSM的分層架構,資料按照時序邏輯分成多層,每一層資料有序,新資料在較高的層次,最老的歷史資料在最底層。對於X-Engine來說,每個主表和二級所有資料都是一顆分層的LSM-tree結構,總共分為4層,memtable,L0,L1和L2,每一層都保持有序,資料按新舊順序依次往更高的層次遷移,其中memtable在記憶體中,其它幾層按需可以在不同的儲存介質上。
X-DB Online DDL充分利用X-Engine的特性來設計。我們將資料分為兩部分,基線資料和增量資料。基線資料是指變更開始時,通過拿snapshot能遍歷得到的資料。增量資料是指,變更開始後,使用者寫入的新資料。當然拿Snapshot過程需要禁寫,因為我們強依賴這個一致性位點,確保基線+增量資料的完整性。
具體而言,Online DDL主要流程包括以下幾步:
- 禁寫,獲取Snapshot,在schema中新增新索引元資訊;
- 通過Snapshot遍歷主表,構建二級索引的基線資料,資料直接寫入到L2(不經過memtable);
- 與步驟2同時進行,放開寫,產生二級索引增量資料,這段時間禁止merge到L2;
- 待步驟2結束,合併新索引基線(L2)+增量資料(memtable,L0,L1),禁寫,變更結束。
- 放開禁止merge到L2的限制。
可以看到,雖然X-DB也需要一個一致性快照,但是並沒有像MySQL一樣,需要回放row-log,而是充分利用了X-Engine分層儲存的特性,將所有基線資料產生的二級索引仍然作為基線直接寫到L2。而所有增量資料產生的索引資料,則分佈在memtable,L0,L1(通過禁止到L2的compaction邏輯實現)。由於資料之間有天然的分層時序關係,所有基線+增量就是一份完整的二級索引資料。通過這個方式,也避免寫入量大的情況下和回放row-log追不上的問題。
下圖分別描述了MySQL方案的二級索引build過程,X-DB方案二級索引build過程。t0時刻開始獲取snapshot,build基線資料。t1時刻表示基線資料的二級索引build完成。
相比於X-DB,MySQL方案還有一個回放追row-log的過程,假設t2時間點能追上,t2以後新的schema生效,那麼索引就build完成了。

MySQL Online build-index
X-DB中build索引並沒有追row-log的過程,這主要是因為X-Engine是一個append-only的儲存引擎,資料天然多版本儲存。因此update並不需要像Innodb儲存引擎一樣在原地更新,增量可以實時維護。在基線build完成後,只需要將增量和基線合併在一起,並確保基線資料的version比增量資料version舊即可。如果基線中的資料有被更新的情況,也沒有關係,因為最終增量中的新版本會覆蓋老版本的資料。

X-DB Online build-index
至此,我們完成一個副本建索引的任務,那麼其它副本如何構建呢?在步驟1禁寫過程中,我們還會寫一條ddl-start日誌,然後在第4步禁寫的時候,再寫一條ddl-end日誌。X-DB三副本通過同步X-Engine的redo日誌來實現資料同步,ddl-start/ddl-end也是redo日誌的一種型別。follower接收到ddl-start日誌後,獲取snapshot,並開始構建基線資料二級索引,然後接收到ddl-end日誌後,則合併基線+增量資料,ddl完成。所以從使用者的視角來看,leader和follower的ddl是同步進行的,避免了MySQL方案的主備延遲問題,任何時候三副本的多數派都是強一致的。
對比F1+Spanner的方案,一個重要區別在於:Spanner基本不感知DDL操作。對於Spanner來說,所有的操作都是PUT/GET/DELETE,所以副本間的變更也與普通DML沒有差異。任何時候,只要底層Paxos協議能正常work,三副本高可用和強一致就能得到保證。這個方案雖然Schema變更比較麻煩,但對於儲存層特別友好,不用感知DDL,共用一套機制保證高可用和強一致。為什麼X-DB需要引入ddl-start/ddl-end日誌呢?一方面,可以使leader和follower同時做ddl操作。另一方面,基線資料build產生的二級索引並不會產生redo日誌。而F1+Spanner方案,在Data-reorganization階段,基線的每一條資料,都需要產生對應的redo日誌同步到多副本,而且對於基線遍歷的每條記錄都需要重新check,來決定是否需要為這行資料執行變更。因此X-DB的Online DDL方案更簡單,也更省成本。
總結
本文首先介紹了Online DDL的重要性和MySQL Online DDL方案的演進。然後介紹了F1+Spanner的Online DDL方案,相較於單機資料庫,分散式資料庫做DDL的同時需要保證三副本的高可用和強一致,不能出現類似MySQL主備延遲和主備schema不一致問題。最後介紹了X-DB的Online DDL方案,通過對比,我們的方案簡單清晰,也更節省成本。這個過程中,也間接說明了資料庫整體架構和儲存引擎的結構對Online DDL方案的影響。