達(dá)達(dá)-高性能服務(wù)端優(yōu)化之路
提綱
業(yè)務(wù)場(chǎng)景
最初的技術(shù)選型
讀寫分離
垂直分庫(kù)
水平分庫(kù)(sharding)
總結(jié)
達(dá)達(dá)是全國(guó)領(lǐng)先的最后三公里物流配送平臺(tái)情萤。 達(dá)達(dá)的業(yè)務(wù)模式與滴滴以及Uber很相似,以眾包的方式利用社會(huì)閑散人力資源最爬,解決O2O最后三公里即時(shí)性配送難題铃岔。 達(dá)達(dá)業(yè)務(wù)主要包含兩部分:商家發(fā)單汪疮,配送員接單配送,如下圖所示毁习。
達(dá)達(dá)的業(yè)務(wù)規(guī)模增長(zhǎng)極大智嚷,在1年左右的時(shí)間從零增長(zhǎng)到每天近百萬(wàn)單,給后端帶來(lái)極大的訪問(wèn)壓力纺且。壓力主要分為兩類:讀壓力盏道、寫壓力。讀壓力來(lái)源于配送員在APP中搶單载碌,高頻刷新查詢周圍的訂單猜嘱,每天訪問(wèn)量幾億次,高峰期QPS高達(dá)數(shù)千次/秒嫁艇。寫壓力來(lái)源于商家發(fā)單朗伶、達(dá)達(dá)接單、取貨步咪、完成等操作论皆。達(dá)達(dá)業(yè)務(wù)讀的壓力遠(yuǎn)大于寫壓力,讀請(qǐng)求量約是寫請(qǐng)求量的30倍以上。
下圖是達(dá)達(dá)過(guò)去6個(gè)月纯丸,每天的訪問(wèn)量及QPS變化趨勢(shì)圖變化趨圖偏形,可見(jiàn)增長(zhǎng)極快
極速增長(zhǎng)的業(yè)務(wù),對(duì)技術(shù)的要求越來(lái)越高觉鼻,我們必須在架構(gòu)上做好充分的準(zhǔn)備俊扭,才能迎接業(yè)務(wù)的挑戰(zhàn)。接下來(lái)坠陈,我們一起看看達(dá)達(dá)的后臺(tái)架構(gòu)是如何演化的萨惑。
作為創(chuàng)業(yè)公司,最重要的一點(diǎn)是敏捷仇矾,快速實(shí)現(xiàn)產(chǎn)品庸蔼,對(duì)外提供服務(wù),于是我們選擇了公有云服務(wù)贮匕,保證快速實(shí)施和可擴(kuò)展性姐仅,節(jié)省了自建機(jī)房等時(shí)間。在技術(shù)選型上刻盐,為快速的響應(yīng)業(yè)務(wù)需求掏膏,業(yè)務(wù)系統(tǒng)使用python做為開(kāi)發(fā)語(yǔ)言,數(shù)據(jù)庫(kù)使用Mysql敦锌。如下圖所示馒疹,應(yīng)用層的幾大系統(tǒng)都訪問(wèn)一個(gè)數(shù)據(jù)庫(kù)。
隨著業(yè)務(wù)的發(fā)展乙墙,訪問(wèn)量的極速增長(zhǎng)颖变,上述的方案很快不能滿足性能需求。每次請(qǐng)求的響應(yīng)時(shí)間越來(lái)越長(zhǎng)听想,比如配送員在app中刷新周圍訂單腥刹,響應(yīng)時(shí)間從最初的500毫秒增加到了2秒以上。業(yè)務(wù)高峰期哗魂,系統(tǒng)甚至出現(xiàn)過(guò)宕機(jī)肛走,一些商家和配送員甚至因此而懷疑我們的服務(wù)質(zhì)量。在這生死存亡的關(guān)鍵時(shí)刻录别,通過(guò)監(jiān)控朽色,我們發(fā)現(xiàn)高期峰Mysql CPU使用率已接近80%,磁盤IO使用率接近90%组题,Slow query從每天1百條上升到1萬(wàn)條葫男,而且一天比一天嚴(yán)重。數(shù)據(jù)庫(kù)儼然已成為瓶頸崔列,我們必須得快速做架構(gòu)升級(jí)梢褐。
如下是數(shù)據(jù)庫(kù)一周的qps變化圖旺遮,可見(jiàn)數(shù)據(jù)庫(kù)壓力的增長(zhǎng)極快。
當(dāng)Web應(yīng)用服務(wù)出現(xiàn)性能瓶頸的時(shí)候盈咳,由于服務(wù)本身無(wú)狀態(tài)(stateless)耿眉,我們可以通過(guò)加機(jī)器的水平擴(kuò)展方式來(lái)解決。 而數(shù)據(jù)庫(kù)顯然無(wú)法通過(guò)簡(jiǎn)單的添加機(jī)器來(lái)實(shí)現(xiàn)擴(kuò)展鱼响,因此我們采取了Mysql主從同步和應(yīng)用服務(wù)端讀寫分離的方案鸣剪。
Mysql支持主從同步,實(shí)時(shí)將主庫(kù)的數(shù)據(jù)增量復(fù)制到從庫(kù)丈积,而且一個(gè)主庫(kù)可以連接多個(gè)從庫(kù)同步(細(xì)節(jié)參考Replication)砾莱。利用此特性丹壕,我們?cè)趹?yīng)用服務(wù)端對(duì)每次請(qǐng)求做讀寫判斷亏娜,若是寫請(qǐng)求职辅,則把這次請(qǐng)求內(nèi)的所有DB操作發(fā)向主庫(kù);若是讀請(qǐng)求唬滑,則把這次請(qǐng)求內(nèi)的所有DB操作發(fā)向從庫(kù)告唆,如下圖所示。
實(shí)現(xiàn)讀寫分離后间雀,數(shù)據(jù)庫(kù)的壓力減少了許多悔详,CPU使用率和IO使用率都降到了5%內(nèi),Slow Query也趨近于0惹挟。主從同步、讀寫分離給我們主要帶來(lái)如下兩個(gè)好處:
減輕了主庫(kù)(寫)壓力:達(dá)達(dá)的業(yè)務(wù)主要來(lái)源于讀操作缝驳,做讀寫分離后连锯,讀壓力轉(zhuǎn)移到了從庫(kù),主庫(kù)的壓力減小了數(shù)十倍用狱。
從庫(kù)(讀)可水平擴(kuò)展(加從庫(kù)機(jī)器):因系統(tǒng)壓力主要是讀請(qǐng)求运怖,而從庫(kù)又可水平擴(kuò)展,當(dāng)從庫(kù)壓力太時(shí)夏伊,可直接添加從庫(kù)機(jī)器摇展,緩解讀請(qǐng)求壓力
如下是優(yōu)化后數(shù)據(jù)庫(kù)qps的變化圖:
讀寫分離前主庫(kù)的select qps
讀寫分離后主庫(kù)的select qps
當(dāng)然,沒(méi)有一個(gè)方案是萬(wàn)能的溺忧。讀寫分離咏连,暫時(shí)解決了Mysql壓力問(wèn)題,同時(shí)也帶來(lái)了新的挑戰(zhàn)鲁森。業(yè)務(wù)高峰期祟滴,商家發(fā)完訂單,在我的訂單列表中卻看不到當(dāng)發(fā)的訂單(典型的read after write)歌溉;系統(tǒng)內(nèi)部偶爾也會(huì)出現(xiàn)一些查詢不到數(shù)據(jù)的異常垄懂。通過(guò)監(jiān)控,我們發(fā)現(xiàn),業(yè)務(wù)高峰期Mysql可能會(huì)出現(xiàn)主從延遲草慧,極端情況桶蛔,主從延遲高達(dá)10秒。
那如何監(jiān)控主從同步狀態(tài)漫谷?在從庫(kù)機(jī)器上羽圃,執(zhí)行show slave status,查看Seconds_Behind_Master值抖剿,代表主從同步從庫(kù)落后主庫(kù)的時(shí)間朽寞,單位為秒,若主從同步無(wú)延遲斩郎,這個(gè)值為0脑融。Mysql主從延遲一個(gè)重要的原因之一是主從復(fù)制是單線程串行執(zhí)行。
那如何為避免或解決主從延遲缩宜?我們做了如下一些優(yōu)化:
優(yōu)化Mysql參數(shù)肘迎,比如增大innodb_buffer_pool_size,讓更多操作在Mysql內(nèi)存中完成锻煌,減少磁盤操作妓布。
使用高性能CPU主機(jī)
數(shù)據(jù)庫(kù)使用物理主機(jī),避免使用虛擬云主機(jī)宋梧,提升IO性能
使用SSD磁盤匣沼,提升IO性能。SSD的隨機(jī)IO性能約是SATA硬盤的10倍捂龄。
業(yè)務(wù)代碼優(yōu)化释涛,將實(shí)時(shí)性要求高的某些操作,使用主庫(kù)做讀操作
讀寫分離很好的解決讀壓力問(wèn)題倦沧,每次讀壓力增加唇撬,可以通過(guò)加從庫(kù)的方式水平擴(kuò)展。但是寫操作的壓力隨著業(yè)務(wù)爆發(fā)式的增長(zhǎng)沒(méi)有很有效的緩解辦法展融,比如商家發(fā)單起來(lái)越慢窖认,嚴(yán)重影響了商家的使用體驗(yàn)。我們監(jiān)控發(fā)現(xiàn)告希,數(shù)據(jù)庫(kù)寫操作越來(lái)越慢扑浸,一次普通的insert操作,甚至可能會(huì)執(zhí)行1秒以上暂雹。
下圖是數(shù)據(jù)庫(kù)主庫(kù)的壓力首装, 可見(jiàn)磁盤IO使用率已經(jīng)非常高,高峰期IO響應(yīng)時(shí)間最大達(dá)到636毫秒杭跪,IO使用率最高達(dá)到100%仙逻。
同時(shí)驰吓,業(yè)務(wù)越來(lái)越復(fù)雜,多個(gè)應(yīng)用系統(tǒng)使用同一個(gè)數(shù)據(jù)庫(kù)系奉,其中一個(gè)很小的非核心功能出現(xiàn)Slow query檬贰,常常影響主庫(kù)上的其它核心業(yè)務(wù)功能。我們有一個(gè)應(yīng)用系統(tǒng)在MySql中記錄日志缺亮,日志量非常大翁涤,近1億行記錄,而這張表的ID是UUID萌踱,某一天高峰期葵礼,整個(gè)系統(tǒng)突然變慢,進(jìn)而引發(fā)了宕機(jī)并鸵。監(jiān)控發(fā)現(xiàn)鸳粉,這張表insert極慢,拖慢了整個(gè)MySql Master园担,進(jìn)而拖跨了整個(gè)系統(tǒng)届谈。(當(dāng)然在mysql中記日志不是一種好的設(shè)計(jì),因此我們開(kāi)發(fā)了大數(shù)據(jù)日志系統(tǒng)弯汰,敬請(qǐng)關(guān)注本博客后續(xù)文章艰山。另一方面,UUID做主鍵是個(gè)糟糕的選擇咏闪,在下文的水平分庫(kù)中曙搬,針對(duì)ID的生成,有更深入的講述)汤踏。
這時(shí)织鲸,主庫(kù)成為了性能瓶頸,我們意識(shí)到溪胶,必需得再一次做架構(gòu)升級(jí),將主庫(kù)做拆分稳诚,一方面以提升性能哗脖,另一方面減少系統(tǒng)間的相互影響,以提升系統(tǒng)穩(wěn)定性扳还。這一次才避,我們將系統(tǒng)按業(yè)務(wù)進(jìn)行了垂直拆分。如下圖所示氨距,將最初龐大的數(shù)據(jù)庫(kù)按業(yè)務(wù)拆分成不同的業(yè)務(wù)數(shù)據(jù)庫(kù)桑逝,每個(gè)系統(tǒng)僅訪問(wèn)對(duì)應(yīng)業(yè)務(wù)的數(shù)據(jù)庫(kù),避免或減少跨庫(kù)訪問(wèn)俏让。
下圖是垂直拆分后楞遏,數(shù)據(jù)庫(kù)主庫(kù)的壓力茬暇,可見(jiàn)磁盤IO使用率已降低了許多,高峰期IO響應(yīng)時(shí)間在2.33毫秒內(nèi)寡喝,IO使用率最高只到22.8%糙俗。
未來(lái)是美好的,道路是曲折的预鬓。垂直分庫(kù)過(guò)程巧骚,我們也遇到不少挑戰(zhàn),最大的挑戰(zhàn)是:不能跨庫(kù)join格二,同時(shí)需要對(duì)現(xiàn)有代碼重構(gòu)劈彪。單庫(kù)時(shí),可以簡(jiǎn)單的使用join關(guān)聯(lián)表查詢顶猜;拆庫(kù)后沧奴,拆分后的數(shù)據(jù)庫(kù)在不同的實(shí)例上,就不能跨庫(kù)使用join了驶兜。比如在CRM系統(tǒng)中扼仲,需要通過(guò)商家名查詢某個(gè)商家的所有訂單,在垂直分庫(kù)前抄淑,可以join商家和訂單表做查詢屠凶,如下如示:
select * from tb_order where supplier_id in (select id from supplier where name=‘上海海底撈’);
分庫(kù)后肆资,則要重構(gòu)代碼矗愧,先通過(guò)商家名查詢商家id,再通過(guò)商家Id查詢訂單表郑原,如下所示:
supplier_ids = select id from supplier where name=‘上海海底撈’
select * from tb_order where supplier_id in (supplier_ids )
垂直分庫(kù)過(guò)程中的經(jīng)驗(yàn)教訓(xùn)唉韭,使我們制定了SQL最佳實(shí)踐,其中一條便是程序中禁用或少用join犯犁,而應(yīng)該在程序中組裝數(shù)據(jù)属愤,讓SQL更簡(jiǎn)單。一方面為以后進(jìn)一步垂直拆分業(yè)務(wù)做準(zhǔn)備酸役,另一方面也避免了Mysql中join的性能較低的問(wèn)題住诸。
經(jīng)過(guò)一個(gè)星期緊鑼密鼓的底層架構(gòu)調(diào)整,以及業(yè)務(wù)代碼重構(gòu)涣澡,終于完成了數(shù)據(jù)庫(kù)的垂直拆分贱呐。拆分之后,每個(gè)應(yīng)用程序只訪問(wèn)對(duì)應(yīng)的數(shù)據(jù)庫(kù)入桂,一方面將單點(diǎn)數(shù)據(jù)庫(kù)拆分成了多個(gè)奄薇,分?jǐn)偭酥鲙?kù)寫壓力;另一方面抗愁,拆分后的數(shù)據(jù)庫(kù)各自獨(dú)立馁蒂,實(shí)現(xiàn)了業(yè)務(wù)隔離呵晚,不再互相影響。
讀寫分離远搪,通過(guò)從庫(kù)水平擴(kuò)展劣纲,解決了讀壓力;垂直分庫(kù)通過(guò)按業(yè)務(wù)拆分主庫(kù)谁鳍,緩存了寫壓力癞季,但系統(tǒng)依然存在以下隱患:
單表數(shù)據(jù)量越來(lái)越大。如訂單表倘潜,單表記錄數(shù)很快將過(guò)億绷柒,超出MySql的極限,影響讀寫性能涮因。
核心業(yè)務(wù)庫(kù)的寫壓力越來(lái)越大废睦,已不能再進(jìn)一次垂直拆分,Mysql 主庫(kù)不具備水平擴(kuò)展的能力
以前养泡,系統(tǒng)壓力逼迫我們架構(gòu)升級(jí)嗜湃,這一次,我們需提前做好架構(gòu)升級(jí)澜掩,實(shí)現(xiàn)數(shù)據(jù)庫(kù)的水平擴(kuò)展(sharding)购披。業(yè)務(wù)類似于我們的Uber在公司成立的5年后(2014)年才實(shí)施了水平分庫(kù)(mezzanine-migration),但我們的業(yè)務(wù)發(fā)展要求我們?cè)诔闪?8月就要開(kāi)始實(shí)施水平分庫(kù)。邏輯架構(gòu)圖如下圖所示:
水平分庫(kù)面臨的第一個(gè)問(wèn)題是肩榕,按什么邏輯進(jìn)行拆分刚陡。一種方案是按城市拆分,一個(gè)城市的所有數(shù)據(jù)在一個(gè)數(shù)據(jù)庫(kù)中株汉;另一種方案是按訂單ID平均拆分?jǐn)?shù)據(jù)筐乳。按城市拆分的優(yōu)點(diǎn)是數(shù)據(jù)聚合度比較高,做聚合查詢比較簡(jiǎn)單乔妈,實(shí)現(xiàn)也相對(duì)簡(jiǎn)單蝙云,缺點(diǎn)是數(shù)據(jù)分布不均勻,某些城市的數(shù)據(jù)量極大路召,產(chǎn)生熱點(diǎn)贮懈,而這些熱點(diǎn)以后可能還要被迫再次拆分。按訂單ID拆分則正相反优训,優(yōu)點(diǎn)是數(shù)據(jù)分布均勻,不會(huì)出現(xiàn)一個(gè)數(shù)據(jù)庫(kù)數(shù)據(jù)極大或極小的情況各聘,缺點(diǎn)是數(shù)據(jù)太分散揣非,不利于做聚合查詢。比如躲因,按訂單ID拆分后早敬,一個(gè)商家的訂單可能分布在不同的數(shù)據(jù)庫(kù)中忌傻,查詢一個(gè)商家的所有訂單,可能需要查詢多個(gè)數(shù)據(jù)庫(kù)搞监。針對(duì)這種情況水孩,一種解決方案是將需要聚合查詢的數(shù)據(jù)做冗余表,冗余的表不做拆分琐驴,同時(shí)在業(yè)務(wù)開(kāi)發(fā)過(guò)程中俘种,減少聚合查詢。
反復(fù)權(quán)衡利弊绝淡,并參考了Uber等公司的分庫(kù)方案后宙刘,我們最后決定按訂單ID做水平分庫(kù)。從架構(gòu)上牢酵,我們將系統(tǒng)分為三層:
應(yīng)用層:即各類業(yè)務(wù)應(yīng)用系統(tǒng)
數(shù)據(jù)訪問(wèn)層:統(tǒng)一的數(shù)據(jù)訪問(wèn)接口悬包,對(duì)上層應(yīng)用層屏蔽讀寫分庫(kù)、分庫(kù)馍乙、緩存等技術(shù)細(xì)節(jié)布近。
數(shù)據(jù)層:對(duì)DB數(shù)據(jù)進(jìn)行分片,并可動(dòng)態(tài)的添加shard分片丝格。
水平分庫(kù)的技術(shù)關(guān)鍵點(diǎn)在于數(shù)據(jù)訪問(wèn)層的設(shè)計(jì)撑瞧,數(shù)據(jù)訪問(wèn)層主要包含三部分:
ID生成器:生成每張表的主鍵
數(shù)據(jù)源路由:將每次DB操作路由到不同的shard數(shù)據(jù)源上
緩存: 采用Redis實(shí)現(xiàn)數(shù)據(jù)的緩存,提升性能(以后會(huì)有詳細(xì)文章)
ID生成器是整個(gè)水平分庫(kù)的核心铁追,它決定了如何拆分?jǐn)?shù)據(jù)季蚂,以及查詢存儲(chǔ)-檢索數(shù)據(jù)。ID需要跨庫(kù)全局唯一琅束,否則會(huì)引發(fā)業(yè)務(wù)層的沖突扭屁。此外,ID必須是數(shù)字且升序涩禀,這主要是考慮到升序的ID能保證Mysql的性能(若是UUID等隨機(jī)字符串料滥,在高并發(fā)和大數(shù)據(jù)量情況下,性能極差艾船。對(duì)比性能測(cè)試數(shù)據(jù)可供參考uuid-vs-int-insert-performance)葵腹。同時(shí),ID生成器必須非常穩(wěn)定屿岂,因?yàn)槿魏喂收隙紩?huì)影響所有的數(shù)據(jù)庫(kù)操作践宴。
我們的ID的生成策略借鑒了Instagram的ID生成算法(sharding-ids-at-instagram)。具體方案如下:
整個(gè)ID的二進(jìn)制長(zhǎng)度為64位
前36位使用時(shí)間戳爷怀,以保證ID是升序增加
中間13位是分庫(kù)標(biāo)識(shí)阻肩,用來(lái)標(biāo)識(shí)當(dāng)前這個(gè)ID對(duì)應(yīng)的記錄在哪個(gè)數(shù)據(jù)庫(kù)中
后15位為自增序列,以保證在同一秒內(nèi)并發(fā)時(shí)运授,ID不會(huì)重復(fù)烤惊。每個(gè)shard庫(kù)都有一個(gè)自增序列表乔煞,生成自增序列時(shí),從自增序列表中獲取當(dāng)前自增序列值柒室,并加1渡贾,做為當(dāng)前ID的后15位
水平分庫(kù)是一個(gè)極具挑戰(zhàn)的項(xiàng)目,我們也還在夜以繼日的開(kāi)發(fā)中雄右,欲知詳情空骚,敬請(qǐng)繼續(xù)關(guān)注本博客。
創(chuàng)業(yè)是與時(shí)間賽跑的過(guò)程不脯,前期為了快速滿足業(yè)務(wù)需求府怯,我們采用簡(jiǎn)單高效的方案,如使用云服務(wù)防楷、應(yīng)用服務(wù)直接訪問(wèn)單點(diǎn)DB牺丙;后期隨著系統(tǒng)壓力增大,性能和穩(wěn)定性逐漸納入考慮范圍复局,而DB最容易出現(xiàn)性能瓶頸冲簿,我們采用讀寫分離、垂直分庫(kù)亿昏、水平分庫(kù)等方案峦剔。面對(duì)高性能和高穩(wěn)定性,架構(gòu)升級(jí)需要盡可能超前完成角钩,否則吝沫,系統(tǒng)隨時(shí)可能出現(xiàn)系統(tǒng)響應(yīng)變慢甚至宕機(jī)的情況。當(dāng)然递礼,架構(gòu)升級(jí)的過(guò)程都是痛并快樂(lè)著惨险,每次通宵達(dá)旦實(shí)施系統(tǒng)架構(gòu)升級(jí),一邊享受公司準(zhǔn)備的美味佳肴夜宵脊髓,一邊感受解決技術(shù)難題辫愉,釋放公司生產(chǎn)力的成就感,不亦樂(lè)乎将硝。
更多技術(shù)分享恭朗,敬請(qǐng)關(guān)注達(dá)達(dá)博客、公眾號(hào)依疼、微信群痰腮。