從零到千萬用戶,我是如何一步步優(yōu)化MySQL數(shù)據(jù)庫的?
作者個人研發(fā)的在高并發(fā)場景下,提供的簡單、穩(wěn)定、可擴展的延遲消息隊列框架,具有精準(zhǔn)的定時任務(wù)和延遲隊列處理功能。自開源半年多以來,已成功為十幾家中小型企業(yè)提供了精準(zhǔn)定時調(diào)度方案,經(jīng)受住了生產(chǎn)環(huán)境的考驗。為使更多童鞋受益,現(xiàn)給出開源框架地址:https://github.com/sunshinelyz/mykit-delay
寫在前面
很多小伙伴留言說讓我寫一些工作過程中的真實案例,寫些啥呢?想來想去,寫一篇我在以前公司從零開始到用戶超千萬的數(shù)據(jù)庫架構(gòu)升級演變的過程吧。
本文記錄了我之前初到一家創(chuàng)業(yè)公司,從零開始到用戶超千萬,系統(tǒng)壓力暴增的情況下是如何一步步優(yōu)化MySQL數(shù)據(jù)庫的,以及數(shù)據(jù)庫架構(gòu)升級的演變過程。升級的過程極具技術(shù)挑戰(zhàn)性,也從中收獲不少。希望能夠為小伙伴們帶來實質(zhì)性的幫助。
業(yè)務(wù)背景
我之前呆過一家創(chuàng)業(yè)工作,是做商城業(yè)務(wù)的,商城這種業(yè)務(wù),表面上看起來涉及的業(yè)務(wù)簡單,包括:用戶、商品、庫存、訂單、購物車、支付、物流等業(yè)務(wù)。但是,細(xì)分下來,還是比較復(fù)雜的。這其中往往會牽扯到很多提升用戶體驗的潛在需求。例如:為用戶推薦商品,這就涉及到用戶的行為分析和大數(shù)據(jù)的精準(zhǔn)推薦。如果說具體的技術(shù)的話,那肯定就包含了:用戶行為日志埋點、采集、上報,大數(shù)據(jù)實時統(tǒng)計分析,用戶畫像,商品推薦等大數(shù)據(jù)技術(shù)。
公司的業(yè)務(wù)增長迅速,僅僅2年半不到的時間用戶就從零積累到千萬級別,每天的訪問量幾億次,高峰QPS高達上萬次每秒,雙十一期間的訪問量和QPS是平時的幾倍。數(shù)據(jù)的寫壓力來源于用戶下單,支付等操作,尤其是趕上雙十一大促期間,系統(tǒng)的寫壓力會成倍增長。然而,讀業(yè)務(wù)的壓力會遠遠大于寫壓力,據(jù)不完全統(tǒng)計,讀業(yè)務(wù)的請求量是寫業(yè)務(wù)的請求量的50倍左右。
接下來,我們就一起來看看數(shù)據(jù)庫是如何升級的。
最初的技術(shù)選型
作為創(chuàng)業(yè)公司,最重要的一點是敏捷,快速實現(xiàn)產(chǎn)品,對外提供服務(wù),于是我們選擇了公有云服務(wù),保證快速實施和可擴展性,節(jié)省了自建機房等時間。整體后臺采用的是Java語言進行開發(fā),數(shù)據(jù)庫使用的MySQL。整體如下圖所示。
讀寫分離
隨著業(yè)務(wù)的發(fā)展,訪問量的極速增長,上述的方案很快不能滿足性能需求。每次請求的響應(yīng)時間越來越長,比如用戶在H5頁面上不斷刷新商品,響應(yīng)時間從最初的500毫秒增加到了2秒以上。業(yè)務(wù)高峰期,系統(tǒng)甚至出現(xiàn)過宕機。在這生死存亡的關(guān)鍵時刻,通過監(jiān)控,我們發(fā)現(xiàn)高峰期MySQL CPU使用率已接近80%,磁盤IO使用率接近90%,slow query(慢查詢)從每天1百條上升到1萬條,而且一天比一天嚴(yán)重。數(shù)據(jù)庫儼然已成為瓶頸,我們必須得快速做架構(gòu)升級。
當(dāng)Web應(yīng)用服務(wù)出現(xiàn)性能瓶頸的時候,由于服務(wù)本身無狀態(tài),我們可以通過加機器的水平擴展方式來解決。而數(shù)據(jù)庫顯然無法通過簡單的添加機器來實現(xiàn)擴展,因此我們采取了MySQL主從同步和應(yīng)用服務(wù)端讀寫分離的方案。
MySQL支持主從同步,實時將主庫的數(shù)據(jù)增量復(fù)制到從庫,而且一個主庫可以連接多個從庫同步。利用此特性,我們在應(yīng)用服務(wù)端對每次請求做讀寫判斷,若是寫請求,則把這次請求內(nèi)的所有DB操作發(fā)向主庫;若是讀請求,則把這次請求內(nèi)的所有DB操作發(fā)向從庫,如下圖所示。
實現(xiàn)讀寫分離后,數(shù)據(jù)庫的壓力減少了許多,CPU使用率和IO使用率都降到了5%以內(nèi),Slow Query(慢查詢)也趨近于0。主從同步、讀寫分離給我們主要帶來如下兩個好處:
- 減輕了主庫(寫)壓力:商城業(yè)務(wù)主要來源于讀操作,做讀寫分離后,讀壓力轉(zhuǎn)移到了從庫,主庫的壓力減小了數(shù)十倍。
- 從庫(讀)可水平擴展(加從庫機器):因系統(tǒng)壓力主要是讀請求,而從庫又可水平擴展,當(dāng)從庫壓力太時,可直接添加從庫機器,緩解讀請求壓力。
當(dāng)然,沒有一個方案是萬能的。讀寫分離,暫時解決了MySQL壓力問題,同時也帶來了新的挑戰(zhàn)。業(yè)務(wù)高峰期,用戶提交完訂單,在我的訂單列表中卻看不到自己提交的訂單信息(典型的read after write問題);系統(tǒng)內(nèi)部偶爾也會出現(xiàn)一些查詢不到數(shù)據(jù)的異常。通過監(jiān)控,我們發(fā)現(xiàn),業(yè)務(wù)高峰期MySQL可能會出現(xiàn)主從復(fù)制延遲,極端情況,主從延遲高達數(shù)秒。這極大的影響了用戶體驗。
那如何監(jiān)控主從同步狀態(tài)?在從庫機器上,執(zhí)行show slave status,查看Seconds_Behind_Master值,代表主從同步從庫落后主庫的時間,單位為秒,若主從同步無延遲,這個值為0。MySQL主從延遲一個重要的原因之一是主從復(fù)制是單線程串行執(zhí)行(高版本MySQL支持并行復(fù)制)。
那如何避免或解決主從延遲?我們做了如下一些優(yōu)化:
- 優(yōu)化MySQL參數(shù),比如增大innodb_buffer_pool_size,讓更多操作在MySQL內(nèi)存中完成,減少磁盤操作。
- 使用高性能CPU主機。
- 數(shù)據(jù)庫使用物理主機,避免使用虛擬云主機,提升IO性能。
- 使用SSD磁盤,提升IO性能。SSD的隨機IO性能約是SATA硬盤的10倍甚至更高。
- 業(yè)務(wù)代碼優(yōu)化,將實時性要求高的某些操作,強制使用主庫做讀操作。
- 升級高版本MySQL,支持并行主從復(fù)制。
垂直分庫
讀寫分離很好的解決了讀壓力問題,每次讀壓力增加,可以通過加從庫的方式水平擴展。但是寫操作的壓力隨著業(yè)務(wù)爆發(fā)式的增長沒有得到有效的緩解,比如用戶提交訂單越來越慢。通過監(jiān)控MySQL數(shù)據(jù)庫,我們發(fā)現(xiàn),數(shù)據(jù)庫寫操作越來越慢,一次普通的insert操作,甚至可能會執(zhí)行1秒以上。
另一方面,業(yè)務(wù)越來越復(fù)雜,多個應(yīng)用系統(tǒng)使用同一個數(shù)據(jù)庫,其中一個很小的非核心功能出現(xiàn)延遲,常常影響主庫上的其它核心業(yè)務(wù)功能。這時,主庫成為了性能瓶頸,我們意識到,必須得再一次做架構(gòu)升級,將主庫做拆分,一方面以提升性能,另一方面減少系統(tǒng)間的相互影響,以提升系統(tǒng)穩(wěn)定性。這一次,我們將系統(tǒng)按業(yè)務(wù)進行了垂直拆分。如下圖所示,將最初龐大的數(shù)據(jù)庫按業(yè)務(wù)拆分成不同的業(yè)務(wù)數(shù)據(jù)庫,每個系統(tǒng)僅訪問對應(yīng)業(yè)務(wù)的數(shù)據(jù)庫,盡量避免或減少跨庫訪問。
垂直分庫過程,我們也遇到不少挑戰(zhàn),最大的挑戰(zhàn)是:不能跨庫join,同時需要對現(xiàn)有代碼重構(gòu)。單庫時,可以簡單的使用join關(guān)聯(lián)表查詢;拆庫后,拆分后的數(shù)據(jù)庫在不同的實例上,就不能跨庫使用join了。
例如,通過商家名查詢某個商家的所有訂單,在垂直分庫前,可以join商家和訂單表做查詢,也可以直接使用子查詢,如下如示:
- select * from tb_order where supplier_id in (select id from supplier where name=’商家名稱’);
分庫后,則要重構(gòu)代碼,先通過商家名查詢商家id,再通過商家id查詢訂單表,如下所示:
- select id from supplier where name=’商家名稱’
- select * from tb_order where supplier_id in (supplier_ids )
垂直分庫過程中的經(jīng)驗教訓(xùn),使我們制定了SQL最佳實踐,其中一條便是程序中禁用或少用join,而應(yīng)該在程序中組裝數(shù)據(jù),讓SQL更簡單。一方面為以后進一步垂直拆分業(yè)務(wù)做準(zhǔn)備,另一方面也避免了MySQL中join的性能低下的問題。
經(jīng)過近十天加班加點的底層架構(gòu)調(diào)整,以及業(yè)務(wù)代碼重構(gòu),終于完成了數(shù)據(jù)庫的垂直拆分。拆分之后,每個應(yīng)用程序只訪問對應(yīng)的數(shù)據(jù)庫,一方面將單點數(shù)據(jù)庫拆分成了多個,分?jǐn)偭酥鲙鞂憠毫?另一方面,拆分后的數(shù)據(jù)庫各自獨立,實現(xiàn)了業(yè)務(wù)隔離,不再互相影響。
水平分庫
讀寫分離,通過從庫水平擴展,解決了讀壓力;垂直分庫通過按業(yè)務(wù)拆分主庫,緩存了寫壓力,但系統(tǒng)依然存在以下隱患:
- 單表數(shù)據(jù)量越來越大。如訂單表,單表記錄數(shù)很快就過億,超出MySQL的極限,影響讀寫性能。
- 核心業(yè)務(wù)庫的寫壓力越來越大,已不能再進一次垂直拆分,此時的系統(tǒng)架構(gòu)中,MySQL 主庫不具備水平擴展的能力。
此時,我們需要對MySQL進一步進行水平拆分。
水平分庫面臨的第一個問題是,按什么邏輯進行拆分。一種方案是按城市拆分,一個城市的所有數(shù)據(jù)在一個數(shù)據(jù)庫中;另一種方案是按訂單ID平均拆分?jǐn)?shù)據(jù)。按城市拆分的優(yōu)點是數(shù)據(jù)聚合度比較高,做聚合查詢比較簡單,實現(xiàn)也相對簡單,缺點是數(shù)據(jù)分布不均勻,某些城市的數(shù)據(jù)量極大,產(chǎn)生熱點,而這些熱點以后可能還要被迫再次拆分。按訂單ID拆分則正相反,優(yōu)點是數(shù)據(jù)分布均勻,不會出現(xiàn)一個數(shù)據(jù)庫數(shù)據(jù)極大或極小的情況,缺點是數(shù)據(jù)太分散,不利于做聚合查詢。比如,按訂單ID拆分后,一個商家的訂單可能分布在不同的數(shù)據(jù)庫中,查詢一個商家的所有訂單,可能需要查詢多個數(shù)據(jù)庫。針對這種情況,一種解決方案是將需要聚合查詢的數(shù)據(jù)做冗余表,冗余的表不做拆分,同時在業(yè)務(wù)開發(fā)過程中,減少聚合查詢。
經(jīng)過反復(fù)思考,我們最后決定按訂單ID做水平分庫。從架構(gòu)上,將系統(tǒng)分為三層:
- 應(yīng)用層:即各類業(yè)務(wù)應(yīng)用系統(tǒng)
- 數(shù)據(jù)訪問層:統(tǒng)一的數(shù)據(jù)訪問接口,對上層應(yīng)用層屏蔽讀寫分庫、分表、緩存等技術(shù)細(xì)節(jié)。
- 數(shù)據(jù)層:對DB數(shù)據(jù)進行分片,并可動態(tài)的添加shard分片。
水平分庫的技術(shù)關(guān)鍵點在于數(shù)據(jù)訪問層的設(shè)計,數(shù)據(jù)訪問層主要包含三部分:
- 分布式緩存
- 數(shù)據(jù)庫中間件
- 數(shù)據(jù)異構(gòu)中間件
而數(shù)據(jù)庫中間件需要包含如下重要的功能:
- ID生成器:生成每張表的主鍵
- 數(shù)據(jù)源路由:將每次DB操作路由到不同的分片數(shù)據(jù)源上
ID生成器
ID生成器是整個水平分庫的核心,它決定了如何拆分?jǐn)?shù)據(jù),以及查詢存儲-檢索數(shù)據(jù)。ID需要跨庫全局唯一,否則會引發(fā)業(yè)務(wù)層的沖突。此外,ID必須是數(shù)字且升序,這主要是考慮到升序的ID能保證MySQL的性能(若是UUID等隨機字符串,在高并發(fā)和大數(shù)據(jù)量情況下,性能極差)。同時,ID生成器必須非常穩(wěn)定,因為任何故障都會影響所有的數(shù)據(jù)庫操作。
我們系統(tǒng)中ID生成器的設(shè)計如下所示。
- 整個ID的二進制長度為64位
- 前36位使用時間戳,以保證ID是升序增加
- 中間13位是分庫標(biāo)識,用來標(biāo)識當(dāng)前這個ID對應(yīng)的記錄在哪個數(shù)據(jù)庫中
- 后15位為自增序列,以保證在同一秒內(nèi)并發(fā)時,ID不會重復(fù)。每個分片庫都有一個自增序列表,生成自增序列時,從自增序列表中獲取當(dāng)前自增序列值,并加1,做為當(dāng)前ID的后15位
- 下一秒時,后15位的自增序列再次從1開始。
水平分庫是一個極具挑戰(zhàn)的項目,我們整個團隊也在不斷的迎接挑戰(zhàn)中快速成長。
為了適應(yīng)公司業(yè)務(wù)的不斷發(fā)展,除了在MySQL數(shù)據(jù)庫上進行相應(yīng)的架構(gòu)升級外,我們還搭建了一套完整的大數(shù)據(jù)實時分析統(tǒng)計平臺,在系統(tǒng)中對用戶的行為進行實時分析。
關(guān)于如何搭建大數(shù)據(jù)實時分析統(tǒng)計平臺,對用戶的行為進行實時分析,我們后面再詳細(xì)介紹。