干货 | 携程数据库发布系统演进之路
天浩,攜程數據庫專家,專注數據庫自動化運維研發工作。
曉軍,攜程數據庫專家,主要負責運維及分布式數據庫研究。
一、前言
互聯網軟件本身具有快速迭代、持續交付等特點,加上數據庫的表結構(DDL)發布無法做到灰度發布,且回退困難、試錯成本高,一個穩定可靠的數據庫發布系統對于互聯網公司顯得尤其重要。本文將介紹攜程MySQL數據庫發布系統從無到有,版本不斷迭代的演進之路,希望對讀者有所參考和幫助。
我們先后設計了三個版本,最新的版本具有以下功能和特點:
-
發布期間只有一次表鎖,鎖定時間極短,鎖定時間不受表容量影響;
-
Master-Slave復制延遲可控,這點對有讀寫分離架構且數據實時性要求高的業務尤其重要;
-
自動避開業務高峰,自動識別熱表,確保發布期間業務基本無影響;
-
將數據庫規范加入發布前校驗,對不符合規范的發布進行攔截;
介紹整個系統之前,首先對攜程數據庫環境和發布流程做一個簡單的介紹。系統的數據庫環境主要分成Dev、測試環境(含三個子環境,功能性測試(FAT)/壓力測試(LPT)/UAT 三個環境)、Product:
1)數據庫表設計在Dev環境完成,期間包含數據庫規范檢測
2)然后發布到其它測試環境(FAT→LPT→UAT)
3)測試環境都驗證通過后,最后發布到生產環境
表發布流程圖二、初期(1.0時代)
攜程成立以來一直使用SQL Server 數據庫,2014年左右開始使用MySQL數據庫,為后面轉型MySQL做準備。這時期接入MySQL的業務量很小,數據量不大,都是非核心業務,所以整個發布過程可以概括為“簡單粗暴”:
1)開發人員通過直連DEV環境數據庫,直接對數據庫表進行修改
2)DBA通過自動化工具捕捉到表的變化,將變更同步到測試環境
3)開發測試完后,將變化同步到生產環境??????
這個階段只是簡單把表的變更傳遞到其他環境,對發布期間業務和性能方面的影響沒有考慮太多。
1.0 版本發布流程?
三、轉型期(2.0時代)
隨著業務接入MySQL不斷增加,MySQL數據庫越來越多,到2016年下半年為止,MySQL 數據庫數量已經有800+,很多核心業務也轉到MySQL,包含很多讀寫分離架構。此時原生的DDL發布已經無法滿足業務需求,這時引入了業界流行的pt-online-schema-change(pt-osc)。
2.0版本發布流程pt-osc是percona開發的一款比較成熟的產品,業界使用也較多。其采用觸發器的方式將所有的增量DML應用到了影子表,這種實現方式會加大對語句的開銷,并發過高時甚至會影響數據庫正常提供服務,因此往往會出現發布一半最后還是不得不終止發布的現象,線上遇到核心的表或者大表往往需要晚上留守來進行發布,這極大的提高了DBA的運維負擔。
?
四、引入gh-ost(3.0時代)? ? ??
為了進一步提升發布穩定性,我們在2017年調研了當時剛開源不久的gh-ost,由于產品非常新,因此做了大量的調研和測試工作,也發現提交了多個高優先級Bug(包括GBK字符集支持、bad connection以及column case-sensitive issue導致數據丟失等),都已得到作者的修復。
那么gh-ost對比pt-osc具體有哪些優勢呢?下面先簡單介紹下它的兩個最核心的特性。
4.1 Triggerless
在gh-ost出現之前第三方MySQL DDL工具均采用觸發器的方式進行實現,包括前面percona的pt-osc,Facebook的OSC等等。而gh-ost采用的機制和他們完全不同:它通過MySQL binlog來同步數據,gh-ost本身注冊為一個fake slave,可以從集群中的master或者slave上拉取binlog,并實時解析,將變更表的所有DML操作都重新apply到影子表上面。因此對于發布期間變更表上發生的DML操作,可以完全避免由于觸發器而產生的性能開銷,以及鎖的爭搶。
除此之外,一般我們選擇目標發布機器通常會選擇集群中slave節點,而slave一般不會承載業務,這樣binlog解析的開銷也不會落在提供業務的master上面,而僅僅是一次異步的DML語句重放。
4.2 Dynamically controllable
另一個最重要的特性是動態調控,這是此前其他第三方開源工具所不具備的。
之前通過pt-osc發布時,命令執行后參數就沒法修改,除非停止重來。假設發布進行到90%,突然由于其他各種原因導致服務器負載上升,為不影響業務,只能選擇將發布停掉,等性能恢復再重來。
通過pt-osc發布的表都是很大的表,耗時較長,所以遇到這類場景很尷尬。因此發布中參數如果可動態調控將變得非常重要。gh-ost另外實現了一個socket server,我們可以在發布過程中,通過socket和發布進程進行實時交互,它可以支持實時的暫停,恢復,以及很多參數的動態調整,來適應外界變化。
4.3 gh-ost如何工作?
在了解完其重要特性后,簡單介紹下其實現原理。
其原理很好理解,首先建兩張表,一張_gho的影子表,gh-ost會將原表數據以及增量數據都應用到這個表,最后會將這個表和原表做次表名切換,另一張是_ghc表,這個表是存放changelog的數據,包括信號標記,心跳等。
其次,gh-ost會開兩個goroutine,一個用于拷貝原表數據,一個用于apply增量的binlog到_gho表,并且兩個goroutine的并行在跑的,也就是不用關心數據是先拷貝過去還是先apply binlog過去。
因為這里會對insert語句做調整,首先我們拷貝的insert into會改寫成insert ignore into,而binlog內insert into會改寫成replace into,這樣可以很好的支持兩個goroutine的并行。但這樣的調整能適用所有的DDL嗎?答案是否定的,大家可以思考下,下面案例部分會給出詳細解釋。
最后,當原表數據全部拷貝完成后,gh-ost會進入到表交換階段,采用更加安全的原子交換。
Gh-ost 架構圖?
五、如何做到安全發布?
為了確保每次發布符合數據庫規范,確保發布可以順利完成,發布前我們做了很多檢查工作,發布過程中會有線程實時偵聽發布狀態。通過producer,consumer,listener如下三個組件來協同完成發布的順利進行。
任務運行架構圖5.1 運行前——是否能做發布????????
我們消費線程(consumer)會在發布前做滿足發布的前置校驗,選擇合適的目標主機進行發布。
1)MySQL環境變量的校驗:檢查當前實例變量配置是否滿足發布要求。
2)沖突表校驗:檢查集群中是否存在已發布相沖突的表,存在的話自動進行清理。
3)沖突標記文件校驗:檢查發布機器上是否存在沖突的標記文件,存在的話自動進行清理。
4)磁盤容量校驗:預估集群所有節點的磁盤空間是否足夠
5)任務并行校驗:檢查集群是否存在其他發布,多實例會檢查所有實例所屬集群是否存在發布,為避免并行發布導致的性能影響,以及磁盤容量難以預估,我們會限制單個集群只能有串行發布。
6)DRC成員狀態校驗:對于已接入DRC的DB,會在發布前先初始化所有成員狀態,并隨機選擇一個成員成為leader,僅當所有成員所屬集群均已滿足前置校驗,才會進入真正發布階段。
注:DRC(Data Replicate Center),想了解更多DRC相關的技術戳這里。這里主要負責支持多數據中心同時發起以及結束發布流程。
5.2 運行時——進展是否正常??????
整個發布過程采用的是生產消費模型,當每個消費線程運行任務時,同時會生成一個其對應的監聽線程(listener),用于監聽該任務的運行狀態。
1)磁盤容量監聽:當低于某閾值時將終止發布,并會清理發布產生的殘留表來釋放空間。
2)服務器性能監聽:當服務器負載過高,將會自動觸發throttle,等性能恢復再重新解除throttle。
3)副本延遲監聽:延遲閾值默認初始1.5s,后續在一個閾值上限內會動態增減,避免延遲一直波動時影響發布效率,但最終交換前會回置到默認1.5s。
4)時間監聽:當前時間若處于業務高峰期,會通過自動加大nice-ratio的值來進行“限流”,等業務低峰期后再做置回。
5)DRC成員狀態監聽:對于接入DRC的DB,會偵聽partner的運行狀態,等所有成員均已進入postponing狀態后,再由drc選舉出來的leader統一觸發表名交換。
6)集群拓撲監聽:線上我們往往會碰到正在發布的DB進行了變更維護,包括主從切換,DB拆分到其他集群上等等。這時我們發現gh-ost會hang在那,也不會報錯,往往會等到提交發布的人員反饋才會發現,因此我們這里加了對集群拓撲的監聽,來及時發現拓撲的變更并終止發布。
?
六、碰到了哪些問題,如何解決?
目前gh-ost接入發布系統已接近兩年,運行非常穩定。但慢慢的我們會發現原生gh-ost沒辦法滿足我們所有需求,所以做了一些二次開發。
下面通過幾個典型案例來簡要介紹下。
案例1、發布后自增列值保留
默認gh-ost 發布時新表并沒有保留原表自增值,部分業務是依賴自增列的值,這種場景會出現較大的問題。???????
要解決這個問題其實不難,只需要在建_gho表后設置一把AUTO_INCREMENT值即可。我們添加了一個- reset-original-auto-increment 參數開關,默認false,即保留原始自增值。
代碼示例如下,先查找原表的有效自增值,并應用給新的_gho表即可。
?
案例2、含唯一鍵表發布
我們知道唯一鍵發布有兩大前提,首先,表中已有的存量數據必須滿足新增的唯一鍵約束;其次,發布過程中出現的DML增量數據也需保證滿足新增的唯一鍵約束。
默認gh-ost對表添加唯一鍵是無法保證數據的完整性的。為什么呢?前面我們簡單提過gh-ost發布會做語句轉換,并且rowCopy 中insert into 會轉為 insert ignore into,而binlogApply中insert into會轉為replace into。當表結構變更中包含新增唯一鍵的話,這種轉換就顯然不夠了,它會將沖突數據全部自然抹掉,而這顯然是不合理的,是很嚴重的data integrity問題。
工具的預期應該是出現數據沖突即退出,說明這個發布并沒有發布條件。而官方并沒有做唯一索引發布的特殊支持,那我們是否可以實現這一部分邏輯?問題的關鍵在于我們要對原主鍵繼續支持insert ignore into/ replace into的邏輯保證數據一致且不失敗,另外新增唯一鍵部分又不能通過這種邏輯處理,保證沖突數據要及時發現。
后面通過分析我們想了一種方案,首先通過如下一條正則解析命令是否包含新增唯一鍵。
其次對寫入邏輯進行如下改寫:
1)原數據拷貝(rowCopy)從insert ignore into 調整為 insert into .. andnot exists PK的方式,如下示例。
2)DML增量應用(binlogApply)從 replace into 調整為 delete from + insert into的方式,如下示例。
下面對原數據拷貝(A),原表DML(B),Binlog應用到新表(C) 三個過程先后順序不固定時做下推演。首先C肯定在B后面,因此可能的順序是ABC,BCA,BAC 三種可能情況。
原表b, 2個列,col1 PK,col2 計劃新增Uniquekey,原表數據是(1,a), (3,c)。
ABC:先完成拷貝,再對原表DML,最后應用binlog
?
BCA:先原表DML,再應用binlog,最后拷貝
?
BAC:先原表DML,再拷貝,最后應用binlog
經過過程推演,我們發現這個方案可以解決新增唯一鍵時可能存在的問題。
案例3、活學活用,大表發布+數據清理
我們經常會碰到一些大表的發布,發布系統一般會對超大表做攔截,建議清理些無效數據。那這里分為兩個過程,即先清理無效數據,再進行發布。那我們是否可以將這兩個過程合并發布呢?答案是可行的,而且可以極大的提升發布效率。
邏輯可以很容易理解,見下圖,即拷貝你所需要的數據,而增量部分不做變化。我們可以加個參數-where-reserve-clause,代表你需要的數據。那這里有一個問題,拷貝范圍是先去根據-where-reserve-clause去限定,還是實際insert的時候去限定?有何區別?
發布+清理邏輯圖區別在于如果根據-where-reserve-clause去限定范圍的性能很差,往往查主鍵范圍需要花很久,如果主鍵范圍又很分散,那選擇先查這個范圍是比較差的。而如果實際insert的時候去限定實際需寫入的數據的話,則只是在每個chunk 寫入時附加上這個條件,可能一個chunk沒有一條數據符合條件,那即產生一次空跑,也沒有任何影響。
但如果用戶明確知道要保留的主鍵范圍,那先去限定范圍可以避免大量的空跑。因此添加了-force-query-migration-range-values-on-master來確定使用哪種方式,而具體選擇需具體案例具體分析。? ? ?
除此之外,我們知道數據清理表空間并不會自動瘦身,往往需要配合optimize table來進行表收縮。而添加的這個功能本身既支持數據清理,又支持表結構變更,而支持了表結構變更也就支持了表收縮。因此對-alter做了下擴展,允許noop,來支持不變更結構僅數據清理或者表收縮等場景。
下面有個線上數據清理的測試數據對比(表大小在300GB左右,需清理80%左右的數據):
| 表大小 | 總行數/保留行數 | 處理方式 | 耗時 | 備注 |
| 290GB | 68666w/17327w | 數據清理工具 | 25h | 后續還需optimize |
| 320GB | 75128w/19542w | gh-ost | 2h30m | 后續清理老表即可 |
??????通過對比,我們可以看到效率提升了10倍以上,其中還不算optimize的開銷。
?
七、結語
以上是攜程數據庫發布系統的整個演進過程,希望對讀者有所參考和幫助,新的3.0MySQL數據庫發布系統從2018年開始研發上線并持續改進,功能上已經較為完善,適應了業務快速迭代的要求,規避了發布可能造成的業務故障,覆蓋了攜程絕大多數類型的DDL。
面向未來,我們的發布系統會持續改進:更加友好的交互、更加智能的throttle,我們已經在路上。
總結
以上是生活随笔為你收集整理的干货 | 携程数据库发布系统演进之路的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 为什么 Redis 要比 Memcach
- 下一篇: 为啥不能用uuid做MySQL的主键!?