本文是在DTCC2019第十屆中國資料庫大會上所做的「網易MyRocks使用和優化實踐」PPT講稿。由於不少同學看過PPT後,又線下聯繫希望獲取更多的信息。於是將講稿放出來。可評論進一步交流。如有疑問或錯誤之處,敬請指正。


MyRocks 是 Facebook 引入 RocksDB 後的 MySQL 分支版本,在 Facebook 內部得到了大規模使用,得益於 RocksDB 優秀的寫入性能和數據壓縮等特點,MyRocks 也受到了 MySQL 社區的極大關注,網易杭研在 InnoSQL 5.7.20 版本增加了 MyRocks 存儲引擎。本次分享將為大家分析 MyRocks 的優點,介紹其在網易互聯網核心產品的不同業務場景上的使用、參數調優、問題定位和功能優化。

本次分享主要從3個方面著手,分別是MyRocks/RocksDB實現和優點分析,MyRocks在網易的使用案例分析。

MyRocks技術實現

MyRocks是使用RocksDB替換InnoDB作為存儲引擎的MySQL版本。下面簡單介紹下RocksDB的技術實現。

RocksDB是一個kv存儲引擎,基於Log Structured Merge Tree作為數據存儲方式。這是跟InnoDB最大的不同。在RocksDB中,LSM-Tree默認是多層的,每層都會獨立進行compaction操作。

RocksDB還有個概念是列族,column famliy。一個列族可以是一個表,也可以是表的一個索引。一個列族裡面可以包括多個表的數據。每個CF單獨有一顆LSM-Tree和多個MemTable。

Memtable是內存中的寫buffer,緩存著已經提交但還未持久化的事務數據。其包括active和immutable 2種。Memtable可通過參數配置大小和個數。

在RocksDB中,所有的列族共用write ahead log文件。

下面看下RocksDB的寫流程。

每個事務都有一個writebatch對象,用於緩存該事務在提交前修改的所有數據。在事務提交時,其修改的數據先寫入WAL日誌buffer,根據配置參數選擇是否持久化。然後將修改的數據有序寫入到active memtable中。當active memtable達到閾值後會變為immutable,再新產生一個active memtable。

數據寫入memtable後就意味著事務已經提交。數據的持久化和compaction都是非同步進行的。當immutable memtable個數達到所設置的參數閾值後,會被回刷成L0 SST文件。在L0文件個數達到閾值後,合併到L1上並依次往下刷。RocksDB中可以配置多個線程用於對每層數據文件進行compaction。

讀操作分為當前讀和快照讀。這裡以當前讀為例。

首先查找本事務對應的writebatch中是否存在請求的數據。接著查找memtable,包括active和immutable,然後基於sst文件元數據查找所需的數據對應的文件是否緩存在block cache中。如果沒有被緩存,那麼需將對應的SST文件載入到block cache中。

在整個查找過程中,為了提高查找效率,會藉助布隆過濾器。可以避免無效的數據IO和遍歷操作。

上面3頁ppt簡單介紹了RocksDB及其讀寫流程。接著我們看看基於RocksDB的MyRocks都有哪些特性,有哪些不足。

首先,在特性方面,支持最常使用的RC和RR隔離級別,與InnoDB的RR級別解決了幻讀問題不同,MyRocks的RR是標準的,存在幻讀;實現了記錄級別的鎖粒度,支持MVCC提高讀效率;通過WAL日誌實現crash-safe;有相比InnoDB強大很多的壓縮性能。

我們的MyRocks支持多種高效率的物理備份方式。包括myrocks_hotbackup和mariabackup等;MyRocks對主從複製機制也進行了優化,提高回放效率。除此之外,還有一些優秀的特性,比如更加高效的TTL等,在此不一一舉例。

不足和限制方面:相比InnoDB,MyRocks在功能和穩定性上還存在較大差距。比如在線DDL,所支持的索引類型等功能。Bug也明顯更多,尤其在XA事務、TTL、分區表等方面。

MyRocks優點分析

分析了實現和特性後,下面說下我們認為MyRocks相比InnoDB的核心競爭力,因為InnoDB已經足夠成熟和強大,為什麼還需要MyRocks,下面我們要講的就是MyRocks存在的價值。

InnoDB是典型的B/B+樹存取方式,即使是順序插入場景,也會在一個page還未完全填滿時觸發分裂。變為2個半空的page。或者說InnoDB的page一定存在頁內碎片。

而RocksDB由於是Append方式,都是文件順序寫行為。每寫滿一個memtable就flush到磁碟成為獨立的sst文件,sst文件和sst文件的merge操作也是順序讀寫,整個過程均不會產生內部碎片。

即使在非壓縮模式下,RocksDB記錄也進行了前綴編碼,默認每16條記錄纔有一條完整的,這意味著節省了隨後15條記錄的共同前綴所需的空間。

此外,RocksDB每個索引佔用7+1 bytes(sequence number + flag)的元數據開銷,InnoDB是每記錄6+7 bytes (trx_id + undo ptr)空間開銷,似乎存在多個索引的場景下RocksDB更加費空間,但Ln SST文件seq id可置0,經過壓縮等操作大部分元數據開銷是可節省的。

在壓縮效率方面,顯然也是RocksDB更佔據優勢。

首先說下InnoDB壓縮,包括兩種,一種是使用最廣泛的記錄壓縮,也就是通過在create table或alter table時設置key_block_size或compressed來啟用。這種壓縮並不是記錄透明的,壓縮前它會提取每條記錄的索引信息。相對來說壓縮比會降低。另一種是透明頁壓縮,在5.7版本中支持,使用較少,其壓縮操作時在InnoDB的文件IO層實現,對InnoDB上層是透明的。該壓縮需要依賴稀疏文件(sparse file)和操作系統的(hole punching)特性。不管是哪一種,都以page/block為單位來獨立保持壓縮後的數據,而文件系統是需要塊對齊的,一般都是4KB,這就意味著一個16KB的頁,即使壓縮為5KB,也需要使用2個文件block,即8K保存。其中的3KB空間填空。

RocksDB很好得解決了這個問題。每個block壓縮後,先組合成一個SST文件,保存時只需要SST文件對齊到4KB即可。

與HDD不同,SSD基於NAND Flash介質存儲數據,其可擦寫的次數有限,每個NAND Flash塊寫數據前都需要先擦除置位後才能寫入新數據,因此寫放大越小有利於延長SSD盤的使用壽命

InnoDB在隨機寫時,每個page寫入或更新一條記錄意味著需要完整寫至少一個page,而且由於存在doublewrite,還需要再寫一遍page。

相對來說,RocksDB會好一些。可以通過公式:1 + 1 + fanout * ( n – 2) / 2(n為LSM層數,fanout為每層存儲增長倍數))計算出大概的寫入放大。

1:從memtable回刷到L0 (1,20)

1:從L0到L1 (1,20)

從L1到L2開始,每層有fanout(10)倍放大。那就意味著 L2上面的數據有可能因為L1數據compaction的時候參與了compaction。參與次數最少0次,最多有fanout次。平均下來是fanout/2次。

而L2到L4一共3層。總層數是5層。那麼假設層數是n,就是n-2層有 fanout/2次放大

最少0次的案例是:如果寫入是順序的,那麼就不需要參與合併。比如每個sst文件2條記錄。那麼第一次寫1,2,第二次寫3,4,第三次寫5,6。這樣基於sst文件的campaction時,每層的數據都直接往下寫就行了,不需要跟其他sst文件合併。

最多fanout次的案例是:如果第一次寫入(1,40),第二次寫入(20,21),那麼第一次和第二次就得先merge然後拆為2個新文件(1,20)和(21,40),如果第三次寫入是(10,11),那麼(1,20)這個文件還得繼續參與merge和拆分。一直下去,直到該層的數據達到了fanout閾值,不能再合併上層的數據了,就被寫到下一層去。這樣子,1就被重複寫了fanout次了

相應地,RocksDB也提供了較為精確的寫入放大統計可供實時查看,在MyRocks中可以通過show engine rocksdb status查看。

MyRocks應用案例

目前網易互聯網這塊有不少業務在使用MyRocks,包括雲音樂、雲計算等。接下來介紹幾個在這些業務上的使用案例。除了MyRocks本身的優勢外,在MyRocks落地過程中,我們跟DBA和業務三方進行了非常良好地配合,這也為MyRocks成功落地打下了很好的基礎。

大數據量場景

第一個案例是大數據量業務,這可以是用戶的行為,動態,歷史聽歌記錄等。業務特點包括寫多讀少,數據量很大而且都是有價值的,不好基於時間進行刪除,需要佔用不少SSD盤。而且由於用戶基數很大且在增長,所以數據增長也很快。導致需要頻繁進行實例擴容/拆分。

目前採用DDB+MySQL/InnoDB的方式,這裡舉其中一個場景,該場景是一個DDB集羣,下面掛著16個MySQL主從高可用實例,通過設置key_block_size對數據進行壓縮。每個實例算1TB數據。總數據大概32TB。

寫比較多,數據量大。這看起來是一個比較適合MyRocks的場景。

那麼最簡單的測試方式就是搭個MyRocks節點作為高可用實例的slave了。測試比較順利,我們採用snappy壓縮演算法,1TB左右的InnoDB壓縮數據,在MyRocks下只有300G多一點。換算到32個mysqld節點,可以節省20TB的SSD盤空間。而且由於盤空下來了,計算資源本來就比較空,那麼本來一個伺服器部署2個mysqld節點,現在有能力部署3~4個節點。同時,使用MyRocks後,數據的增長幅度也變低,對實例進行拆分/擴容的頻率也就相應減低了。

當然,在使用MyRocks的時候也需要關注其與InnoDB的不同之處,比如內存使用方面。

RocksDB會比InnoDB佔用更多的內存空間。這是因為RocksDB除了有與InnoDB buffer pool相對應的block cache外。我們在前面提到過他還有write buffer,就是memtable。而且每個column family都有好幾個memtable。如果column family比較多的話,這部分佔據的內存空間是很可觀的。

目前RocksDB本身已經提供了實例總的memtable內存大小和未flush部分的大小,為了能夠更加直觀得了解每個column family的memtable內存情況,我們增加了一些指標,比如每個column family下memtable使用的總內存等。

另外,我們也發現在MyRocks上使用tcmalloc和jemalloc比使用glibc中默認的內存分配器高效很多。剛開始使用默認分配器,在寫入壓力很大的情況下,內存極有可能爆掉。

在block cache配置方面,需要了解rocksdb_cache_index_and_filter_blocks設置為0和1的不同之處,設置為1會將index和filter block保存在block cache裡面。這樣比較好控制內存的實際使用量。

最後,需要合理規劃一個實例下column family的個數。每個cf的memtable個數也需要根據實際寫入場景來配置,主要有圖中所列的參數再加上每個memtable的大小write_buffer_size。

寫密集型業務

下面說下第二個案例。這個案例與第一個案例不同的是,寫的壓力比第一個大很多,使用InnoDB根本就扛不住。而且讀也很多,且讀延遲比較敏感。這種業務場景,目前一般使用redis。但用redis也會遇到不少問題,比如持久化之類的,這裡不展開。在本案例中主要還是關心成本。因為數據量比較大,需要大量的內存,而且隨著推薦的實時化改進和推薦場景的增多,內存使用成本急劇增加,甚至可能出現內存採購來不及的情況。。。

顯然,單個MyRocks肯定扛不住全部的壓力,於是仍採用DDB+MyRocks的方式將壓力拆分到多個實例上。但馬上就發現新的問題。那就是MyRocks實例的從庫複製跟不上。我們這個場景下,tps達到5000以上就不行了。為此也做了下調優,比如啟用slave端跳過事務api等,雖然有點效果,但解決不了問題。 另外也嘗試過在DDB層將庫拆細,這樣每個實例會有數十個DB,然後將複製模式改為基於DATABASE,效果是比較好的。但這樣會導致DDB這層出現瓶頸,所以看起來也不是好的方案。

不過好在業務對數據一致性有一定的容忍度,所以,最終落地的方案是業務層雙寫。

這是雙寫部署框圖。圖中把演算法相關的單元用Flink來表示。

演算法單元從DDB1讀取上一週期的推薦數據,跟當前實時變化的新數據相作用產生本週期的推薦數據,然後將其分別寫入DDB1和DDB2。推薦使用方從DDB2上讀取所需的推薦數據

這個系統灰度上線後效果還挺不錯,業務方也較滿意。但隨著不斷往上面加推薦場景,遇到了急需解決的問題是如何在高效利用伺服器資源的情況下扛住寫入壓力。

也就是說,均衡利用現有伺服器的cpu、內存和IO資源。不要出現IO滿了但CPU很空,或者CPU爆了但IO還有不少剩餘。而且在IO和CPU負載處於合理區間時得保證mysqld不會OOM。

在不斷加推薦場景的過程中,首先出現的是數據compaction來不及。寫性能出現大幅度波動。因為剛開始配置的compaction線程是8個,所以很自然得將其翻倍。效果還是不錯的。但受cpu和io能力影響,再往上增加線程數就沒什麼效果了。 根據RocksDB暴露的一些統計信息,我們將觸發write stall的閾值調高。

先是增加了觸發write stall的等待compaction數據量閾值。Write stall觸發週期明顯變長。而且並不是數據量,而是l0層的文件個數閾值被觸發。

於是又將l0層的文件閾值調高。瓶頸有回到了等待compaction的數據量。

最終在這幾組參數上找到了一個平衡。講到這裡,可能有些同學有疑問,隨著數據不斷寫入,如果compaction速度跟不上,設置的write stall閾值總是會被觸發的。這就跟具體的業務場景有關了。因為這個場景的寫入壓力每天都有很強的週期性。到了凌晨時非常明顯的低谷期。那麼只要確保在低谷期compaction線程能夠把累計的數據merge掉就不會有問題。如此循環。

當然,除了這組參數調優,其實還可以通過調節memtable的大小和個數。Memtable越大,意味著可以合併更多對同一條記錄的DML操作,memtable越多意味著在flush時可以合併更多寫操作。但這個調優需要考慮伺服器的內存使用情況。如果內存本來就比較緊張,那就不可取了。

對於我們這個業務,在上一組參數調優下,效果比較好。伺服器資源使用均衡,同時性能上也支撐住了業務的要求。

目前MyRocks已經成功得替換了好幾個業務場景的Redis服務。用SSD盤換內存。再藉助MyRocks高效的壓縮能力,進一步減少SSD盤消耗。

當然,這並不是說所有的Redis都可用MyRocks取代。只能說對於哪些寫入壓力明顯超過InnoDB能力但還沒有到非用全內存不可而且數據量又相對較大的業務來說。可以嘗試使用MyRocks。

延遲從庫

第三個案例是MyRocks在解決數據誤刪除方面的嘗試。數據誤刪除是個非常頭痛的問題,現有的主從複製或基於paxos/raft的高可用方案都無法解決這個問題。目前潛在可選的方案包括基於全量+增量備份向前恢復,基於當前數據+flashback向後回退,基於延遲從庫+待回放relay-log來恢復。

基於flashback的方案一般來說是最快的。對於DML操作,如果複製是基於row格式的,那麼可以通過delete改insert,insert改delete,update操作改變前後項的方式來回滾。但對於DDL操作,目前MySQL官方版本是做不到的,社區裡面也有開源的DDL flashback方案,但存在兼容性等問題。我們網易杭研這邊維護的MySQL版本目前已經在不影響binlog兼容性的情況下支持DDL的flashback,在此不展開。

基於延遲從庫的方案跟基於全量+增量的方式是類似的,但由於MySQL原生提供了延遲複製,而且是基於運行中的實例進行數據恢復,所以可靠性更高,相對來說性能也更好。但存儲開銷比較大,而且需要一定的計算資源。而MyRocks可以緩解存儲成本的問題。

目前一些業務的核心庫已經部署了基於MyRocks延遲從。在落地過程遇到過一些問題。主要是XA事務相關的,包括XA事務的回放問題,從InnoDB遷移數據到RocksDB等。

首先說聊下第一個問題。MySQL在5.7版本之前有個著名的xa prepare bug。就是說,按照正常的語義,xa prepare成功後,其數據雖然不可見,但它是持久化的。而在5.7之前,xa prepare的數據是不持久化的,如果xa commit前session退出,或者mysqld crash了,那麼數據也就沒了。

為瞭解決這個問題,5.7版本增加了一個新的binlog event類型,用來記錄xa prepare操作。並配套修改了slave端xa事務回放的邏輯。詳細的分析可以查看mysql worklog 6860 (dev.mysql.com/worklog/t)。這裡簡單說明下:

大家知道,一個session執行了xa prepare後,是不能執行其他事務語句的,只能執行xa commit或xa rollback。但在slave端,mysql使用固定的幾個worker線程來回放事務的,必須需要解決xa prepare後無法執行其他事務的問題。那麼為什麼無法執行其他事務呢,就是因為xa prepare後,session中該xa事務對應的上下文得保持到xa commit或rollback。很顯然,只要將對應的事務上下文保存起來就可以了,在5.7中,mysql在server層引入了全局事務對象,通過在執行xa prepare前後進行detach和attach的操作將xa prepare事務上下文緩存起來。

但不僅僅是mysql server層有事務上下文,對於事務引擎也有上下文。而MyRocks的問題就在於並沒有實現這套xa事務回放框架要求的API介面。比如在session退出時的引擎層面處理介面close_connection,將xa prepare事務的引擎層事務對象從當前worker線程detach掉的介面replace_native_transactioni_in_thd。

當然,上面只是解決了大框架的問題。在我們實現了所欠缺的介面開始用的時候,還是碰到xa事務回放的不少問題。包括因為回放xa事務導致更新gtid_executed和rpl_slave_info後未釋放innodb事務對象而導致內存泄露問題。Xa prepare後因為rocksdb redolog刷新機制原因導致crash後數據丟失。解決與引擎無關的rocksdb和innodb都存在的因為mysql xa prepare操作先記錄binlog在進行引擎層prepare導致數據丟失問題。等等。

由於時間關係,這裡都不展開來講了。感興趣的同學可以線下交流。總之,如果沒有較強的mysql源碼修改能力。那就不要把myrocks用在有xa事務的業務場景上。

那麼,除了myrocks代碼本身的問題外,第二個問題是如何將innodb的數據遷到myrocks從庫上。可能大家會疑問這有什麼難的。做個備份不就行了嗎。但這個問題確實給我們帶來了一些麻煩。首先是因為物理備份然後通過alter table轉成rocksdb存儲引擎這種方法不行。因為rocksdb的ddl效率太差。所以只能通過邏輯備份了。網易內部有個數據遷移服務叫NDC,類似阿里的DTS,可以進行全量遷移,然後再通過解析binlog進行增量遷移。在遷移了全量數據然後基於gtid_executed起複制後很快就會報複製出錯。提示xa commit對應的xid找不到。有2個原因,一是xa prepare的數據是不可見的,無法通過select出來導致的。二是NDC在停止增量遷移時,不會將binlog中的xa prepare執行掉。

所以,正確的方式應該是NDC在開始全量遷移前,獲取gtid_executed,然後執行下xa recover,等待這些xa事務都commit掉後再開始遷移數據。結束增量遷移時,獲取此時源節點的gtid_executed,並執行掉xa prepare後再起複制。

第三個問題相對來說好辦,就是需要在複製時的ddl建表語句從innodb轉為rocksdb。可採用的方案是週期性查詢mysql系統表,將所有新產生的InnoDB業務錶轉為RocksDB。第二種是設置myrocks節點只能創建rocksdb引擎的表。這樣會導致複製失敗,通過腳本來將建表sql改為使用rocksdb。

延遲從庫這個項目效果還是不錯的。基本上一個物理伺服器就搞定一個業務所有的核心庫。花比較小的代價來提高了業務核心庫的數據安全性,達到了預期的效果。

截止目前,通過將一些業務場景的InnoDB或Redis實例替換為MyRocks已經為業務節省了超過100w+的成本,但這還只是在小部分業務場景上使用。在網易內部還有很大的潛在使用空間。

接下來,我們在MyRocks上的計劃是將MyRocks上到網易雲資料庫服務RDS上,可以大大提高MyRocks運維自動化,接入更多的業務場景。在MySQL 8.0上支持MyRocks,改造優化MyRocks非常糟糕的在線DDL性能等。


推薦閱讀:
相關文章