微博資料庫3個變遷階段背後的設計思想
資料庫專家的成長感觸
“與 MySQL 結緣主要也是源於興趣。第一份工作是在一家小公司,由於人手有限,各個領域的工作都要接觸,相比之下我發現還是對資料庫最感興趣,所以就一直從事和資料庫相關的技術工作了。而隨著工作年限的增加,在資料庫方面積累的經驗也逐漸增多,越來越覺得資料庫管理員(DBA)是一個偏實踐的工種,很多理論上的東西在現實中會有各種的變化,比如“反正規化”設計等。因此,如果想成為資料庫方面的專家,建議大家一定要挑選好環境,大平臺很多時候會由於量變引發質變產生很多有挑戰的問題,而解決這些問題是成為技術專家的必經之路。” —— 肖鵬
微博資料庫經歷的變遷
首先為大家分享微博資料庫經歷的幾個重要的階段。
初創階段
初期微博作為一個內部創新產品,功能比較簡潔,資料庫架構採用的是標準 1M/2S/1MB 結構,按照讀寫分離設計,主庫承擔寫入,而從庫承擔訪問。如果訪問壓力過大,可以通過擴容從庫的數量獲得 scale out 的能力。
上圖紅色代表寫入、綠色代表讀取、黑色對映到內部結構,由圖可知,業務僅僅進行了垂直拆分,也就是按照業務模組比如使用者、內容、關係等進行區分,並單獨使用了資料庫。在初期這其實是非常好的架構,在功能模組上提供瞭解耦的基礎,出現問題也可以很方便地進行定位、在最開始就可以做到按照不同的功能模組進行降級。
個人認為,在初期,這種架構其實就可以滿足業務的增長了,沒有必要進行過度設計,開始就搞得過於複雜可能會導致喪失敏捷的可能。
爆發階段
隨著微博上線之後使用者活躍度的增高,資料庫的壓力也與日俱增,我們首先通過採購高效能的硬體裝置來對單機效能進行 scale up,以達到支撐業務高速發展的需求。然後,通過使用高效能裝置爭取來的時間對微博進行整體上的業務垂直拆分,將使用者、關係、博文、轉發、評論等功能模組分別獨立儲存,並在垂直拆分的基礎上,對於一些預期會產生海量資料的業務模組再次進行了二次拆分。
對於使用硬體這裡多說幾句,由於微博最開始的時候就出現了一個很高的使用者增長峰值,在這個階段我們在技術上的積累不是很豐富,而且最主要的是沒有時間進行架構改造,所以通過購買 PCIE-Flash 裝置來支援的很多核心業務,我現在還清楚記得最開始的 feed 系統是重度依賴 MySQL 的,在 2012 年的春晚當天 MySQL 寫入 QPS 曾經飆到過 35000,至今記憶猶新。
雖然看上去高效能硬體的價格會比普通硬體高很多,但是爭取來的時間是最寶貴的,很有可能在產品生命的初期由於一些效能上的問題引發產品故障,直接導致使用者流失,更加得不償失。所以,個人認為在前期的爆發階段,暴力投入資金解決問題其實反而是最划算的。
繼續說資料庫拆分,以博文為例。博文是微博使用者主要產生的內容,可預見會隨著時間維度不斷增大,最終會變得非常巨大,如何在滿足業務效能需求的情況下,儘可能地使用較少的成本儲存,這是我們面臨的一個比較有挑戰性的問題。
- 首先,我們將索引同內容進行了拆分,因為索引所需儲存空間較少,而內容儲存所需空間較大,且這兩者的使用需求也不盡相同,訪問頻次也會不同,需要區別對待。
- 然後,分別對索引和內容採用先 hash,再按照時間維度拆分的方式進行水平拆分,儘量保障每張表的容量在可控範圍之內,以保證查詢的效能指標。
- 最後,業務先通過索引獲得實際所需內容的 id,再通過內容庫獲得實際的內容,並通過部署 memcached 來加速整個過程,雖然看上去步驟變多,但實際效果完全可以滿足業務需求。
上圖乍一看和上一張圖一樣,但這其實只是一個博文功能模組的資料庫架構圖,我們可以看到索引和內容各自分了很多的埠,每個埠中又分了很多的 DB,每個 DB 下的表先 hash 後按照時間維度進行了拆分,這樣就可以讓我們在後期遇到容量瓶頸或者效能瓶頸的時候,可以選擇做歸檔或者調整部署結構,無論選擇那種都非常的方便。另外,在做歸檔之後,還可以選擇使用不同的硬體來承擔不同業務,提高硬體的利用率、降低成本。
在這個階段,我們對很多的微博功能進行了拆分改造,比如使用者、關係、博文、轉發、評論、贊等,基本上將核心的功能都進行了資料拆分,以保障在遇到瓶頸的時候可以按照預案進行改造和調整。
沉澱階段
在上一個階段,微博的資料庫經歷了很多的拆分改造,這也就直接造成了規模成倍增長的狀況,而業務經歷了高速增長之後,也開始趨於穩定。在這個階段,我們開始著重進行自動化的建設,將之前在快速擴張期間積攢下來的經驗用自動化工具加以實現,對外形成標準化和流程化的平臺服務。我們相繼建設改造了備份系統、監控系統、AutoDDL 系統、MHA 系統、巡檢系統、慢查系統、maya 中介軟體系統。並且為了提高業務使用效率、降低溝通成倍,相對於內部管理系統,重新開發了 iDB 系統供資料庫平臺的使用者使用。通過 iDB 系統,使用者可以很便捷地瞭解自己業務資料庫的執行狀態,並可以直接提交對資料庫的 DDL 修改需求,DBA 僅需點選稽核通過,即可交由 Robot 在線上執行,不但提高了工作效率,也提高了安全性和規範性。
由於涉及的自動化系統比較多,就不一一展開描述了,其實個人理解,在產品發展到一定階段之後無論如何運維都會進入到自動化階段,因為前期活兒少,人工足夠支援變更和其中操作,且有很多特殊情況需要人,尤其是人腦的介入判斷和處理。
這裡要額外重點說一下規範的重要性。以 MySQL 開發規範來說,如果提前做好約定,並進行好限制,雖然開發人員在使用的過程中會感覺受到的約束,但是這可以避免線上發生完全不可控的故障,並且有些問題由於規範的存在就永遠不會發生了。
舉個例子。MySQL 的慢查是導致線上效能慢的罪魁禍首,但是很多時候並不是沒有 index,只是由於程式碼寫得有問題,引起了隱式轉換等問題。在這種情況下,我們一般建議所有的 where 條件都加上雙引號,這樣就可以直接消除隱式轉換的可能性了,開發人員在寫程式碼的時候也不用刻意去考慮到底是字元型還是 int 型。
繼續說自動化。過了初期階段、規模擴大之後,就會出現活多人少的情況,這種壓力會促使大家自動去尋求解決方案,也就自然而然地進行了自動化改造了。當然,業務穩定後有時間開發了,其實是一個更重要的原因。
個人認為自動化分為兩個階段。第一個階段是機器替代人工,也就是將大部分機械勞動交給程式來實現,解決的是批量操作,重複性勞動的問題;第二個階段是機器替人,也就是機器可以替人進行一定的判斷之後進行自我選擇,解放的是人力。不過第二個階段是我們一直追求的理想狀態,至今也僅完成了很簡單的一些小功能,比如動態調整 max mem 等邏輯非常簡單的功能。
微博資料庫的優化和設計
接下來介紹一下微博資料庫平臺最近做的一些改進和優化。
資料庫平臺並不僅有 MySQL 還有 Redis、Memcached、HBase 等資料庫服務,而在快取為王的趨勢下,微博 2015 年重點將研發精力投入在 Redis 上。
微博使用 Redis 的時間較早,並且一開始量就很大,於是在實際使用過程中遇到了很多實際的問題,我們的內部分支版本都是針對這些實際問題進行優化的,比較有特點的有如下幾個。
- 增加基於 pos 位同步功能。在 2.4 版本中,Redis 的同步一旦出現中斷就會重新將主庫的資料”全部”傳輸到從庫上,這會造成瞬時的網路頻寬峰值,並且對於資料量較大的業務來說,從庫恢復的時間較慢,為此我們聯合架構組的同學借鑑 MySQL 的主從同步複製機制,將 Redis 的 aof 改造為記錄 pos 位,並讓從庫記錄已經同步的 pos 位,這樣在網路出現波動的時候即使重傳,也僅是一部分資料,並不會影響業務。
- 線上熱升級。在使用初期,由於很多新功能的加入,Redis 版本不斷升級,為了不影響業務, 每次升級都需要進行主庫切換,給運維帶來了很大的挑戰,於是開發了熱升級機制,通過動態載入 libredis.so 來實現版本的改變,不再需要進行主庫切換,極大地提升了運維效率,也降低了變更帶來的風險。
- 定製化改造。在使用 Redis 的後期,由於微博產品上技術類的需求非常多,為此專門開發了相容 Redis 的 redisscounter,用以專門儲存技術類資料,通過使用 array 替換 hash table 極大地降低記憶體佔用。而在此之後,開發了基於 bloom filter 的 phantom 解決判斷類場景需求。
Redis 中介軟體
在 2015 年我們自研的 Redis 中介軟體 tribe 系統完成了開發和上線,tribe 採用有中心節點的 proxy 架構設計,通過 configer server 管理叢集節點,並借鑑官方 Redis cluster 的 slot 分片的設計思路來完成資料儲存,最終實現了路由、分片、自動遷移、fail over 等功能,並且預留了操作和監控的 API 介面,以便同其他的自動化運維繫統對接。
我們開發 tribe 最主要的目的是解決自動遷移的問題,由於 Redis 記憶體使用會呈現波動性變化,很多時候前一天還是 10%,第二天有可能就變成了 80%,這種時候人工去遷移肯定無法響應業務的變化,而且如果這時候恰巧還碰到了實體記憶體上的瓶頸,那就更麻煩了,涉及業務進行重構資料 hash 都有可能導致故障的發生。
基於 slot 的動態遷移,首先對業務無感知,其次不再需要整臺伺服器,只需找有可用記憶體的伺服器就可以將部分 slot 遷移過去,直接解決擴容遷移問題,可以極大地提高伺服器的利用率、降低成本。
提供的路由功能,可以降低開發門檻,再也不用將資源邏輯配置寫到程式碼或者前端配置檔案中了,每次更改變更的時候也不用再進行上線了,這極大地提高了開發效率,也降低了線上變更引發的故障風險,畢竟 90% 的故障是主動變更引發的。
補充一點,關於重複造輪子,個人認為每個公司都有自己的場景,開源軟體可以給我們提供一個很好的解決思路,但並不能百分百適應應用場景,所以重構並不是堅決不可接受的,有些事情你總要妥協。
Databus
由於我們先有 MySQL 後有 Redis 和 HBase 等資料庫,故存在一種場景就是目前資料已經寫入 MySQL 中,但是需要將這些資料同步到其他的資料庫中,我們為此開發了 Databus,可以基於 MySQL 的 binlog 將資料同步到其他異構的資料庫中,並且支援自定義業務邏輯。目前已經實現了 MySQL 到 Redis 和 MySQL 到 HBase 的資料流向,下一步計劃開發 Redis 到 MySQL 的資料流向。
我們開發 databus 最初的初衷是解決寫 Redis 的問題,由於有些資料即需要寫入 MySQL 中,也需要寫入 Redis 中。如果在前端開啟雙寫也是可以解決的,但是這會造成程式碼複雜現象;如果在後端實現一個數據鏈路,會讓程式碼更加清晰,同時也可以保障資料的最終一致性。後來在實際應用中,databus 慢慢也開始承擔導資料的功能。
下面說一下目前微博積累下來的資料庫的設計習慣。通常來說我們都會採用一些“反正規化”的設計思路,而“反正規化“設計帶來便利的同時確實也帶來了一些問題,尤其是在資料規模變大之後。有如下幾種解決方案。
- 預拆分。在接到需求的時候提前針對於容量進行評估,並按照先垂直後水平進行拆分,如果可以按照時間維度設計,那就納入歸檔機制。通過資料庫的庫表拆分,解決容量儲存問題。
- 引入訊息佇列。利用佇列的一寫多讀特性或多佇列來滿足冗餘資料的多份寫入需求,但僅能保障最終一致性,中間可能會出現資料延遲。
- 引入介面層。通過不同業務模組的介面將資料進行彙總之後再返回給應用層,降低應用層開發的編碼複雜度。
另外一點就是,如果資料庫預估量比較大的話,我們會參考博文的設計思路,在最開始的時候進行索引和內容的分離,並設計好 hash 和時間維度的分表,最大可能地減少後續拆分時可能遇到的問題和困難。
微博資料庫平臺未來的計劃
最後,我想分享一下微博資料庫平臺發展的一點思考,希望可以給大家提供一些思路,當然,更希望大家也給我提出一些建議和意見,讓我少走彎路、少掉坑。
隨著業務的發展,會遇到越來越多的場景,我們希望可以引進最適合的資料庫來解決場景問題,比如 PostgreSQL、SSDB 等。同時,利用 MySQL 新版本的特性,比如 MySQL 5.7 的並行複製、GTID、動態調整 BP,不斷優化現有服務的效能和穩定性。
另外,推進現有 NoSQL 服務的服務化,通過使用 proxy 將儲存節點進行組織之後對外提供服務,對外降低開發人員的開發複雜度和獲取資源的細度,對內提高單機利用率並解決資源層橫向擴充套件的瓶頸問題。
同時,嘗試利用各大雲端計算資源,實現 cache 層的動態擴縮容,充分利用雲端計算的彈性資源,解決業務訪問波動的問題。
Q & A
1. 資料和索引分離是業務層做還是中介軟體做?感覺中介軟體做了很多工作,這部分能不能稍微展開一下?
由於在當初拆分改造的時候並沒有考慮中介軟體的方案,故目前是業務邏輯上實現的索引和內容的分離。以我個人的經驗來看,即使使用中介軟體的解決方案,依然應該在業務邏輯層將索引和內容分割。
中介軟體最核心的功能就是讓程式和後端資源隔離,後端不管有多少資源,對於程式來說都是一個統一的入口,所以中介軟體解決的是水平拆分的問題,而索引和內容分離屬於垂直拆分的範圍,個人認為不應該由中介軟體解決。
2. 印象最深的一次資料庫服務故障能否回憶並說幾點注意事項?
要說印象最深的一次就是資料庫服務的故障,當時有個同事不小心執行了 drop table 命令,瞭解資料庫的人都知道這個命令有多大的威力,我們利用架構上面爭取來的時候緊急進行了單表恢復,雖然降級了一段時間,但是整體上並沒有影響使用者。
對此我要說的注意事項就是規範。自那之後,我們修改了所有刪表需求的流程,並保證嚴格執行,無論多麼緊急的刪除需求都必須進行24小時的冷卻,具體如下。
- 執行 rename table 操作,將 table rename 成 table—will-drop。
- 等待 24 小時之後再執行 drop 操作。
3. 在微博暴漲階段,對錶進行拆分的部分,“對索引和內容進行hash,之後再按著時間緯度進行拆分”,這個做hash的部分是否能展開說一下?
這個其實沒有那麼複雜。首先我們會預估一下一年大概的數量級別,然後計算需要拆分的表數目,並且儘量將每個表控制在 3 千萬行記錄以內(當然這只是希望,現實證明計劃趕不上變化)。比如我們使用模 1024 的辦法,根據博文 id 將所有產生的博文分到 1024 張表中(這個博文 id 又涉及我們的 uuid 全域性發號器就不展開了)。
由於微博大部分使用者產生的內容都會和時間掛鉤,所以時間維度對我們來說是強屬性,基本都會有,假設每個月都建表,這樣就等於每個月會生產 1024 張表。如果資料庫的容量出現瓶頸了,我們就可以根據時間維度來解決,比如將 2010 年的所有表都遷移到其他的資料庫中。
4. Linkedin 多年前出過一個類似 databus 的專案,後續也有一些開源專案支援 MySQL 到 HBase、ES 等的資料同步,微博的做法能不能展開一下?
這個異構資料的同步確實很多公司都有,據我所知阿里的 DRC 也是做同樣的事情。我們的實現思路主要就是依賴於 MySQL 的 binlog,大家都知道在 MySQL 的 binlog 設定成 row 格式的情況下,它會將所有受到影響的資料都記錄到日誌中,這樣就提供了所有資料的變化。
我們通過解析 row 格式的 MySQL binglog 將資料的變化讀取到 databus 中,然後將實際需要的業務邏輯通過。so 檔案 load 到 databus 中,databus 會根據業務邏輯重新再處理一下資料的變化,然後輸出給下游資源。
databus 這個專案我們已經開源,大家可以直接在 GitHub 上搜“MyBus”即可。
5. 問題 1 中說的索引是指什麼,如何找到對應內容的演算法?
索引其實指的並不是內容的演算法,舉個例子,如果你需要儲存博文,那麼必然會儲存一個唯一的 id 來進行區分,然後會儲存一些這個博文發表時候的狀態,比如誰發的、什麼時候發的、我們認為這些狀態和 id 都是索引;而博文內容就是實際的內容,由於內容比較大,和索引儲存在一起會導致 MySQL 的效能下降,而且很多查詢只需要最終拿到博文 id 其實就等於拿到了實際的博文。
我們可以對索引進行各種過濾之後得到一個最終要輸出給使用者的索引 list,再根據這個 list 從內容庫中查詢實際內容,這樣就符合 MySQL 的返回結果集越小效能越高的規律,從而起到優化的效果。
6. NoSQL發揮著哪些作用?
在微博NoSQL發揮了越來越重要的作用,比如說 rediscounter,我們自研的計數服務,當初計數存貯在 MySQL 中,由於所有計數都是 update set count = count +1 這種高併發對單行資料的寫操作,會引發MySQL多種鎖,而且併發越高鎖得越厲害。
由下圖可見,如果對單行的併發操作達到500 以上,tps就會從上萬變成幾百,所以MySQL無論怎麼優化都無法很好地支援這個業務場景,而Redis就可以。
我個人認為,NoSQL 就像瑞士軍刀,在最適合的地方它就是最優的方案,個人認為這也是 NoSQL 未來發展的方向,每一個都有一個最佳場景。
7. 我們公司將來會有很多智慧裝置,今年或許就兩萬個以上的裝置,一年後可能再翻很多倍,要收集資料,都是 JSON 報文格式,JSON 裡欄位標識不同型別的報文,看似不太適合以字串存進 varchar 或 longtext 欄位,DB 儲存是否可充分利用 MYSQL 5.7 的原生JSON,存一列就搞定,而不必用無法擴充套件的“寬列”進行儲存,或者POSTGRES 還是其他的 DB 能提供更方便的儲存嗎?前期還用的時候,MySQL 5.7 還沒 FINAL, 利用 mariadb 多主分擔N多裝置接進來,DB 寫入層的負載,對這種大量裝置一直要傳資料的寫入場景有什麼指導?
我們其實也在看 MySQL 5.7 的新特性,其中 JSON 對於資料庫設計會帶來比較大的衝擊,按照你的說法,確實不應該使用 varchar 或者 text 之類的欄位,不過我個人建議智慧裝置這種場景,如果有可能,最好直接上 HBase,因為遲早資料量會變得 MySQL 難以支撐,這屬於天生沒有優勢。
8. “資料和索引分離”是什麼意思呢,是資料檔案和索引檔案分開存放到不同機器上嗎?
應該是內容和索引,這樣更好理解,大家把這個理解為業務層上的垂直拆分就好。由於進行了垂直拆分,必然存在於不同的資料庫例項中,所以可以放在一臺物理機上,也可以放到不同的物理機上,我們按照習慣都是放在不同的物理機上的,以避免互相干擾。
9. 哪些資訊存MySQL?哪些存NoSQL?用的是哪種NoSQL?
這就涉及一個分層儲存的問題了,目前我們主流是 MySQL + Redis+mc,mc 和 Redis 都用來抗熱點和峰值,而 MySQL 則為資料落地,保障最終有原始資料可查。大部分請求會到 mc 或者 Redis 層就返回了,只有不到 1% 的資料會到 MySQL上。
當然也有特殊的,比如之前說的計數服務,我們就把 rediscounter 當初落地儲存使用,後面並沒有在 MySQL 中再儲存一份資料了。
最後,個人認為 NoSQL 最大的優勢是開發的便捷性,開發人員使用 NoSQL 會比使用 MySQL 更簡單,因為就是 KV 結構,所有查詢都是主鍵查詢,不用考慮 index 優化的問題,也不用考慮如何建表,這對於現在的網際網路速度來說是有致命誘惑的。
10. 請問全域性唯一發號器和自動生成id對業務來說優劣分別是什麼?
全域性唯一發號器有很多實現的方案,我們是用 mc 協議改的 Redis,主要追求效能。我也見過使用 MySQL 的自增 id 作為發號器,但是由於 MySQL 的鎖太重,業務起量之後經常會出現問題,所以就放棄了。
再說另外一個好處,全域性唯一發號器的開發難度並不高,可以將一些你想要的屬性封裝到 uuid 中,比如 id 的格式可以是這樣“時間戳 + 業務flog + 自增序列數”,這樣你拿到這個 id 的時候直接就可以知道時間和對應的業務了,與 MySQL 自身發出來的簡單的一個沒意義的序列號相比,可以做更多的事情。