不少小夥伴留言說讓我寫一些工做過程當中的真實案例,寫些啥呢?想來想去,寫一篇我在之前公司從零開始到用戶超千萬的數據庫架構升級演變的過程吧。本文記錄了我以前初到一家創業公司,從零開始到用戶超千萬,系統壓力暴增的狀況下是如何一步步優化MySQL數據庫的,以及數據庫架構升級的演變過程。升級的過程極具技術挑戰性,也從中收穫很多。但願可以爲小夥伴們帶來實質性的幫助。程序員
我以前呆過一家創業工做,是作商城業務的,商城這種業務,表面上看起來涉及的業務簡單,包括:用戶、商品、庫存、訂單、購物車、支付、物流等業務。可是,細分下來,仍是比較複雜的。這其中每每會牽扯到不少提高用戶體驗的潛在需求。例如:爲用戶推薦商品,這就涉及到用戶的行爲分析和大數據的精準推薦。若是說具體的技術的話,那確定就包含了:用戶行爲日誌埋點、採集、上報,大數據實時統計分析,用戶畫像,商品推薦等大數據技術。面試
公司的業務增加迅速,僅僅2年半不到的時間用戶就從零積累到千萬級別,天天的訪問量幾億次,高峯QPS高達上萬次每秒。數據的寫壓力來源於用戶下單,支付等操做,尤爲是遇上雙十一大促期間,系統的寫壓力會成倍增加。然而,讀業務的壓力會遠遠大於寫壓力,據不徹底統計,讀業務的請求量是寫業務的請求量的50倍左右。sql
接下來,咱們就一塊兒來看看數據庫是如何升級的。數據庫
做爲創業公司,最重要的一點是敏捷,快速實現產品,對外提供服務,因而咱們選擇了公有云服務,保證快速實施和可擴展性,節省了自建機房等時間。總體後臺採用的是Java語言進行開發,數據庫使用的MySQL。總體以下圖所示。
緩存
隨着業務的發展,訪問量的極速增加,上述的方案很快不能知足性能需求。每次請求的響應時間愈來愈長,好比用戶在H5頁面上不斷刷新商品,響應時間從最初的500毫秒增長到了2秒以上。業務高峯期,系統甚至出現過宕機。在這生死存亡的關鍵時刻,經過監控,咱們發現高期峯MySQL CPU使用率已接近80%,磁盤IO使用率接近90%,slow query(慢查詢)從天天1百條上升到1萬條,並且一天比一天嚴重。數據庫儼然已成爲瓶頸,咱們必須得快速作架構升級。微信
當Web應用服務出現性能瓶頸的時候,因爲服務自己無狀態,咱們能夠經過加機器的水平擴展方式來解決。 而數據庫顯然沒法經過簡單的添加機器來實現擴展,所以咱們採起了MySQL主從同步和應用服務端讀寫分離的方案。架構
MySQL支持主從同步,實時將主庫的數據增量複製到從庫,並且一個主庫能夠鏈接多個從庫同步。利用此特性,咱們在應用服務端對每次請求作讀寫判斷,如果寫請求,則把此次請求內的全部DB操做發向主庫;如果讀請求,則把此次請求內的全部DB操做發向從庫,以下圖所示。併發
實現讀寫分離後,數據庫的壓力減小了許多,CPU使用率和IO使用率都降到了5%之內,Slow Query(慢查詢)也趨近於0。主從同步、讀寫分離給咱們主要帶來以下兩個好處:分佈式
固然,沒有一個方案是萬能的。讀寫分離,暫時解決了MySQL壓力問題,同時也帶來了新的挑戰。業務高峯期,用戶提交完訂單,在個人訂單列表中卻看不到本身提交的訂單信息(典型的read after write問題);系統內部偶爾也會出現一些查詢不到數據的異常。經過監控,咱們發現,業務高峯期MySQL可能會出現主從複製延遲,極端狀況,主從延遲高達數秒。這極大的影響了用戶體驗。微服務
那如何監控主從同步狀態?在從庫機器上,執行show slave status,查看Seconds_Behind_Master值,表明主從同步從庫落後主庫的時間,單位爲秒,若主從同步無延遲,這個值爲0。MySQL主從延遲一個重要的緣由之一是主從複製是單線程串行執行(高版本MySQL支持並行複製)。
那如何避免或解決主從延遲?咱們作了以下一些優化:
讀寫分離很好的解決了讀壓力問題,每次讀壓力增長,能夠經過加從庫的方式水平擴展。可是寫操做的壓力隨着業務爆發式的增加沒有獲得有效的緩解,好比用戶提交訂單愈來愈慢。經過監控MySQL數據庫,咱們發現,數據庫寫操做愈來愈慢,一次普通的insert操做,甚至可能會執行1秒以上。
另外一方面,業務愈來愈複雜,多個應用系統使用同一個數據庫,其中一個很小的非核心功能出現延遲,經常影響主庫上的其它核心業務功能。這時,主庫成爲了性能瓶頸,咱們意識到,必需得再一次作架構升級,將主庫作拆分,一方面以提高性能,另外一方面減小系統間的相互影響,以提高系統穩定性。這一次,咱們將系統按業務進行了垂直拆分。以下圖所示,將最初龐大的數據庫按業務拆分紅不一樣的業務數據庫,每一個系統僅訪問對應業務的數據庫,儘可能避免或減小跨庫訪問。
垂直分庫過程,咱們也遇到很多挑戰,最大的挑戰是:不能跨庫join,同時須要對現有代碼重構。單庫時,能夠簡單的使用join關聯表查詢;拆庫後,拆分後的數據庫在不一樣的實例上,就不能跨庫使用join了。
例如,經過商家名查詢某個商家的全部訂單,在垂直分庫前,能夠join商家和訂單表作查詢,也能夠直接使用子查詢,以下如示:
select * from tb_order where supplier_id in (select id from supplier where name=’商家名稱’);
分庫後,則要重構代碼,先經過商家名查詢商家id,再經過商家id查詢訂單表,以下所示:
select id from supplier where name=’商家名稱’ select * from tb_order where supplier_id in (supplier_ids )
垂直分庫過程當中的經驗教訓,使咱們制定了SQL最佳實踐,其中一條即是程序中禁用或少用join,而應該在程序中組裝數據,讓SQL更簡單。一方面爲之後進一步垂直拆分業務作準備,另外一方面也避免了MySQL中join的性能低下的問題。
通過近十天加班加點的底層架構調整,以及業務代碼重構,終於完成了數據庫的垂直拆分。拆分以後,每一個應用程序只訪問對應的數據庫,一方面將單點數據庫拆分紅了多個,分攤了主庫寫壓力;另外一方面,拆分後的數據庫各自獨立,實現了業務隔離,再也不互相影響。
讀寫分離,經過從庫水平擴展,解決了讀壓力;垂直分庫經過按業務拆分主庫,緩存了寫壓力,但系統依然存在如下隱患:
此時,咱們須要對MySQL進一步進行水平拆分。
水平分庫面臨的第一個問題是,按什麼邏輯進行拆分。一種方案是按城市拆分,一個城市的全部數據在一個數據庫中;另外一種方案是按訂單ID平均拆分數據。按城市拆分的優勢是數據聚合度比較高,作聚合查詢比較簡單,實現也相對簡單,缺點是數據分佈不均勻,某些城市的數據量極大,產生熱點,而這些熱點之後可能還要被迫再次拆分。按訂單ID拆分則正相反,優勢是數據分佈均勻,不會出現一個數據庫數據極大或極小的狀況,缺點是數據太分散,不利於作聚合查詢。好比,按訂單ID拆分後,一個商家的訂單可能分佈在不一樣的數據庫中,查詢一個商家的全部訂單,可能須要查詢多個數據庫。針對這種狀況,一種解決方案是將須要聚合查詢的數據作冗餘表,冗餘的表不作拆分,同時在業務開發過程當中,減小聚合查詢。
通過反覆思考,咱們最後決定按訂單ID作水平分庫。從架構上,將系統分爲三層:
水平分庫的技術關鍵點在於數據訪問層的設計,數據訪問層主要包含三部分:
而數據庫中間件須要包含以下重要的功能:
ID生成器是整個水平分庫的核心,它決定了如何拆分數據,以及查詢存儲-檢索數據。ID須要跨庫全局惟一,不然會引起業務層的衝突。此外,ID必須是數字且升序,這主要是考慮到升序的ID能保證MySQL的性能(如果UUID等隨機字符串,在高併發和大數據量狀況下,性能極差)。同時,ID生成器必須很是穩定,由於任何故障都會影響全部的數據庫操做。
咱們系統中ID生成器的設計以下所示。
水平分庫是一個極具挑戰的項目,咱們整個團隊也在不斷的迎接挑戰中快速成長。
爲了適應公司業務的不斷髮展,除了在MySQL數據庫上進行相應的架構升級外,咱們還搭建了一套完整的大數據實時分析統計平臺,在系統中對用戶的行爲進行實時分析。
關於如何搭建大數據實時分析統計平臺,對用戶的行爲進行實時分析,咱們後面再詳細介紹。
好了,今天就到這兒吧,我是冰河,咱們下期見!!
微信搜一搜【冰河技術】微信公衆號,關注這個有深度的程序員,天天閱讀超硬核技術乾貨,公衆號內回覆【PDF】有我準備的一線大廠面試資料和我原創的超硬核PDF技術文檔,以及我爲你們精心準備的多套簡歷模板(不斷更新中),但願你們都能找到心儀的工做,學習是一條時而鬱鬱寡歡,時而開懷大笑的路,加油。若是你經過努力成功進入到了心儀的公司,必定不要懈怠放鬆,職場成長和新技術學習同樣,不進則退。若是有幸咱們江湖再見!
另外,我開源的各個PDF,後續我都會持續更新和維護,感謝你們長期以來對冰河的支持!!
若是你以爲冰河寫的還不錯,請微信搜索並關注「 冰河技術 」微信公衆號,跟冰河學習高併發、分佈式、微服務、大數據、互聯網和雲原生技術,「 冰河技術 」微信公衆號更新了大量技術專題,每一篇技術文章乾貨滿滿!很多讀者已經經過閱讀「 冰河技術 」微信公衆號文章,吊打面試官,成功跳槽到大廠;也有很多讀者實現了技術上的飛躍,成爲公司的技術骨幹!若是你也想像他們同樣提高本身的能力,實現技術能力的飛躍,進大廠,升職加薪,那就關注「 冰河技術 」微信公衆號吧,天天更新超硬核技術乾貨,讓你對如何提高技術能力再也不迷茫!