演講嘉賓:宜人貸架構(gòu)師孫軍糖权,擁有10 年的 Java 開發(fā)經(jīng)驗(yàn)厕隧,先后在人民銀行奔脐、1 號店、人人網(wǎng)吁讨、當(dāng)當(dāng)網(wǎng)從事軟件開發(fā)與技術(shù)架構(gòu)工作。本次分享以宜人貸的系統(tǒng)迭代發(fā)展過程為主峦朗,著重介紹系統(tǒng)發(fā)展過程中遇到的實(shí)際問題和解決的辦法建丧,并重點(diǎn)介紹宜人貸理財(cái)系統(tǒng)的高并發(fā)解決方案。
以下為數(shù)人云“高并發(fā)”活動嘉賓演講實(shí)錄
宜人貸系統(tǒng)版本的迭代
1.0 版本——簡單的煩惱
迭代之前宜人貸的系統(tǒng)波势,其實(shí)就是一個(gè)前臺翎朱,一個(gè)后臺橄维,一個(gè) DB ,前臺采用的是多機(jī)部署的方式拴曲。軟件層也是跟最傳統(tǒng)的軟件一樣分三層争舞,第一層是 Controller ,第二層是 Service 澈灼,第三層是 DAO 竞川。顯然這個(gè)系統(tǒng)并不適合互聯(lián)網(wǎng),有一些難以避免的問題叁熔。首先當(dāng)用戶過萬委乌,在線用戶上千的時(shí)候,這樣的部署方式會產(chǎn)生一些瓶頸荣回,包括服務(wù)器和數(shù)據(jù)庫兩方面遭贸。第二個(gè)就是團(tuán)隊(duì)規(guī)模變大,所有開發(fā)人員集中開發(fā)同一個(gè)系統(tǒng)心软,沖突嚴(yán)重壕吹。
1.5 版本——“吃大補(bǔ)”試試!
針對上面的問題我們做了一些修改删铃,我把它定義成“吃大補(bǔ)”算利。吃大補(bǔ)通常有一個(gè)很明顯的特點(diǎn),就是立馬見效泳姐,但是副作用也很大效拭。
首先,我們在宜人貸的頁面層更加關(guān)注性能的提升胖秒,比如說使用瀏覽器緩存缎患,壓縮傳輸,頁面都經(jīng)過了 YSlow 的優(yōu)化阎肝,鏈路層增加了 CDN 挤渔,做了靜態(tài)化甚至反向代理,這樣可以抵擋 80% 的流量风题。應(yīng)用服務(wù)器與數(shù)據(jù)庫層增加了一個(gè)緩存集群判导,這個(gè)緩存集群基本上又可以擋掉 80% 流量,最后系統(tǒng)層按照業(yè)務(wù)垂直拆分成多個(gè)系統(tǒng)沛硅。數(shù)據(jù)庫也有一些變化眼刃,開始只是一臺主機(jī),一臺數(shù)據(jù)庫摇肌,現(xiàn)在變成了主從擂红,甚至一主多從。用戶可以撐到過百萬围小,在線用戶上萬昵骤。即便如此树碱,我們的制約因素依然在數(shù)據(jù)庫,優(yōu)化的兩層雖然擋掉了大約 95% 的流量变秦,但是業(yè)務(wù)發(fā)展依然超過了數(shù)據(jù)庫所能承受的負(fù)載成榜,所以數(shù)據(jù)庫依然是一個(gè)很大的瓶頸。
第二個(gè)問題就是團(tuán)隊(duì)劃分蹦玫,其實(shí)每個(gè)團(tuán)隊(duì)都做自己的系統(tǒng)赎婚,但是大家仍然使用同一個(gè)數(shù)據(jù)庫,這個(gè)時(shí)候?qū)τ谠O(shè)計(jì)和修改數(shù)據(jù)庫钳垮,都非常麻煩惑淳。甚至每次都要問一下其他團(tuán)隊(duì),我這么改行不行饺窿,對你有什么影響等等歧焦。
第三個(gè)問題也非常棘手,大量使用了緩存肚医,數(shù)據(jù)的時(shí)效性和一致性的問題越來越嚴(yán)重绢馍。
2.0 版本——“開小灶”精細(xì)化
為了解決 1.5 版本存在的問題,我們需要做精細(xì)化的優(yōu)化肠套,我把它定義成開小灶舰涌。首先,合理規(guī)劃數(shù)據(jù)歸屬你稚、優(yōu)化查詢效率瓷耙、縮短數(shù)據(jù)庫事務(wù)時(shí)間;其次刁赖,分系統(tǒng)搁痛,每個(gè)系統(tǒng)用固定的表。我們每天都要做的事宇弛,就是讓運(yùn)維找出線上最慢的SQL有哪些鸡典,對它們做優(yōu)化。第三枪芒,做去事務(wù)彻况,或者盡可能地縮短事務(wù)的時(shí)間。
然后開始關(guān)注代碼質(zhì)量舅踪,提高執(zhí)行效率纽甘,并且開始關(guān)注并發(fā)問題。用戶達(dá)到這個(gè)量的時(shí)候硫朦,就會有用戶幫我們測試并發(fā)問題贷腕。舉一個(gè)例子,同一個(gè)用戶用同一個(gè)帳戶登錄了兩個(gè)客戶端咬展,他同時(shí)點(diǎn)取現(xiàn)泽裳,這個(gè)時(shí)候如果程序處理的不好,很有可能讓他提現(xiàn)兩次破婆。
最后涮总,要區(qū)分強(qiáng)一致性與最終一致性的請求,合理使用緩存與讀寫分離來解決這些問題祷舀。
2.0 解決了很多性能問題瀑梗,但還是會有新的問題——系統(tǒng)越來越多,系統(tǒng)間依賴關(guān)系變得復(fù)雜裳扯,這個(gè)時(shí)候很容易出現(xiàn) A 調(diào) B 抛丽, B 調(diào) C , C 再調(diào)回 A 的循環(huán)調(diào)用饰豺。第二個(gè)是系統(tǒng)間互相調(diào)用增多亿鲜,上游系統(tǒng)壓垮下游系統(tǒng);第三個(gè)也是非常頭疼的問題冤吨,系統(tǒng)很多蒿柳,查找線上問題變得越來越困難——試想一下多個(gè)系統(tǒng)部署到很多機(jī)器上,想找一個(gè)線上的問題漩蟆,通過日志的形式會非常難查垒探。
所以在這個(gè)基礎(chǔ)上我們做了幾件事。一是限流怠李。限流通郴穑基于兩點(diǎn):最大活動線程數(shù)和每秒運(yùn)行次數(shù);活動最大線程數(shù)適合于高消耗的任務(wù)捺癞,每秒運(yùn)行次數(shù)適合于低消耗的任務(wù)夷蚊。第二,我建議在這個(gè)時(shí)期盡可能統(tǒng)一內(nèi)部系統(tǒng)間的返回值翘簇,返回值中一定要記錄返回狀態(tài)(業(yè)務(wù)正常撬码、業(yè)務(wù)異常、程序異常)和錯(cuò)誤說明版保;第三呜笑,可重用RPC框架或在原框架基礎(chǔ)上繼續(xù)開發(fā)完成限流工作。
再說一下關(guān)于查找日志的問題彻犁。圖中為宜人貸日志系統(tǒng)部署框架叫胁,最左側(cè)的是我們的業(yè)務(wù)系統(tǒng),在業(yè)務(wù)系統(tǒng)上把日志收集到 Kafka 隊(duì)列汞幢,然后把 Kafka 隊(duì)列日志放到 ES 集群做索引驼鹅,最終采用 Kibana 和我們自己研發(fā)的日志查詢系統(tǒng)去查看日志,這樣日志被集中到一個(gè)點(diǎn)后會更便于查找。
關(guān)于軟件方面输钩,宜人貸統(tǒng)一使用 SLF4J+Logback 來輸出日志豺型,然后業(yè)務(wù)系統(tǒng)間實(shí)現(xiàn)日志串聯(lián),所有服務(wù)端和客戶端之間都隱含地傳遞一些參數(shù)买乃,這些參數(shù)會隨著調(diào)用鏈一步一步往下傳姻氨,通過 AOP 來實(shí)現(xiàn)。日志串聯(lián)需要傳遞哪些參數(shù)剪验,或者日志中到底要打哪些參數(shù)呢肴焊?第一個(gè)是時(shí)間,這個(gè)時(shí)間應(yīng)該到毫秒級功戚,第二個(gè)是流水號娶眷,流水號就是每次請求生成的一個(gè)唯一的值。然后是用戶 Session 啸臀、設(shè)備號届宠、調(diào)用者時(shí)間( APP 使用手機(jī)本地時(shí)間)、本機(jī) IP 壳咕、客戶端 IP 席揽、用戶真實(shí) IP 、跨越系統(tǒng)次數(shù)——如果我們發(fā)現(xiàn)了一個(gè)錯(cuò)誤谓厘,根據(jù)錯(cuò)誤日志可以找到流水號石蔗,再通過流水號可以到日志查詢平臺查詢出這次請求途徑的所有系統(tǒng)和每個(gè)系統(tǒng)對這次請求的日志钧大。有了這些找問題就非常容易肚菠。
做到 2.0 之后沦补,宜人貸的網(wǎng)站基本能支撐中大型網(wǎng)站的規(guī)模,短時(shí)間內(nèi)不會有太多的性能問題了他爸,但是我們依然會繼續(xù)往下走聂宾,進(jìn)一步提升系統(tǒng)版本。
3.0 版本——拆分做服務(wù)化
3.0 總結(jié)下來就是要做服務(wù)化诊笤,通俗一點(diǎn)說就是拆分系谐,包括業(yè)務(wù)上的垂直拆分,以及垂直拆分基礎(chǔ)上的系統(tǒng)之上的水平拆分讨跟,那么服務(wù)化要怎么做呢纪他?
首先,做業(yè)務(wù)拆分的時(shí)候晾匠,可以按照基礎(chǔ)服務(wù)和業(yè)務(wù)服務(wù)先做一個(gè)大的服務(wù)拆分茶袒,然后基礎(chǔ)服務(wù)又包括無業(yè)務(wù)型的基礎(chǔ)服務(wù)和有業(yè)務(wù)型的基礎(chǔ)服務(wù),無業(yè)務(wù)型的系統(tǒng)非常明顯跟其他的系統(tǒng)沒有太大的關(guān)系凉馆。而業(yè)務(wù)型基礎(chǔ)服務(wù)跟業(yè)務(wù)之間的關(guān)系很小薪寓,基本上跟業(yè)務(wù)系統(tǒng)之間的關(guān)聯(lián)關(guān)系僅限于主鍵和外鍵的關(guān)聯(lián)關(guān)系亡资。
宜人貸可以天然地拆卸分成兩大系統(tǒng),一個(gè)是借款業(yè)務(wù)向叉,一個(gè)是理財(cái)業(yè)務(wù)锥腻,借款業(yè)務(wù)可以拆分成后臺、 Web 植康、合作渠道等旷太,這個(gè)系統(tǒng)之下會有一個(gè)基礎(chǔ)服務(wù)展懈,就是提供一些基礎(chǔ)服務(wù)和接口的一層系統(tǒng)销睁。而基礎(chǔ)服務(wù)又拆成了兩部分,一個(gè)是基礎(chǔ)服務(wù)的進(jìn)件存崖,一個(gè)是基礎(chǔ)服務(wù)的貸后冻记。在拆分過程中我們又發(fā)現(xiàn)一個(gè)問題,理財(cái)和借款有兩個(gè)業(yè)務(wù)怎么拆都拆不開来惧,就是撮合業(yè)務(wù)和債券關(guān)系冗栗,這種拆不開的可以單獨(dú)再提成一個(gè)系統(tǒng)來提供服務(wù)。
拆分系統(tǒng)看起來好像很容易供搀,但是實(shí)際操作問題會很多隅居。拆分的辦法我總結(jié)了如下幾個(gè):
第一,適當(dāng)冗余葛虐,冗余可以確保數(shù)據(jù)庫依然可以進(jìn)行關(guān)聯(lián)查詢胎源。大部分重構(gòu)過程并不是做一個(gè)全新的系統(tǒng),而是在原來系統(tǒng)之上進(jìn)行修改屿脐,這個(gè)時(shí)候可以做一些冗余涕蚤,避免修改代碼。
第二的诵,數(shù)據(jù)復(fù)制万栅,但必須保證數(shù)據(jù)歸屬系統(tǒng)有修改和發(fā)起復(fù)制的權(quán)限。這個(gè)比較適合于上文說的全局配置西疤,比如說基本上所有公司都會有幾張表烦粒,記錄了全國的省市區(qū)縣,這些在每個(gè)系統(tǒng)中都會用代赁,不一定每個(gè)系統(tǒng)都以接口的形式調(diào)用它扰她,可以在每個(gè)系統(tǒng)里面都冗余一份數(shù)據(jù)。
第三管跺,小技巧——如何驗(yàn)證數(shù)據(jù)庫义黎,并不一定非把它拆分成兩個(gè)物理的數(shù)據(jù)庫來驗(yàn)證,可以一個(gè)數(shù)據(jù)庫上建兩個(gè)帳號豁跑,這兩個(gè)帳號分別的權(quán)限指向拆分之后的表廉涕,這樣就可以通過帳號來直接驗(yàn)證拆分效果泻云。
第四,提前規(guī)劃服務(wù)狐蜕,拆分之前確定一下服務(wù)類型是讀多還是寫多的服務(wù)宠纯,是快請求還是慢請求服務(wù),不同服務(wù)需要分開部署层释。
最后婆瓜,同一數(shù)據(jù)不能由超過一個(gè)以上的系統(tǒng)控制,同一系統(tǒng)不能由超過一個(gè)以上的團(tuán)隊(duì)負(fù)責(zé)贡羔。
4.0 版本——云的展望
做到以上幾點(diǎn)廉白, 3.0 版本已經(jīng)做的差不多了,但是后面宜人貸依然還有很多要做的乖寒,4.0 版本是不是要做云平臺猴蹂,異地部署的方案,表很大的時(shí)候是不是要做垂直拆分楣嘁,去 IOE 或者使用 Docker 快速部署等等這些磅轻,這些其實(shí)都是我們做 4.0 或者 5.0 將來要考慮的事情。
宜人貸理財(cái)系統(tǒng)的優(yōu)化
合理預(yù)估流量——強(qiáng)一致與最終一致
圖中這三個(gè)界面分別為首頁逐虚、列頁聋溜,詳情頁。
在做優(yōu)化之前叭爱,首先要合理預(yù)估流量撮躁,常用方法有下面兩個(gè)。
評估方法一:平日 PV / 熱度時(shí)間涤伐;
評估方法二:熱度時(shí)間內(nèi)在線用戶數(shù) * 平均每人操作次數(shù)/熱度時(shí)間馒胆。以宜人貸理財(cái)端為例,假設(shè)在高峰時(shí)期有 N 萬人凝果,然后平均做 M 次操作祝迂,在R分鐘左右基本上就把所有的債券搶光,計(jì)算出來大概是 N * M / R 萬次 / 秒器净。
預(yù)估完以后要做更細(xì)的預(yù)估型雳,區(qū)分什么是強(qiáng)一致,什么是最終一致山害,這兩個(gè)流量分別是多少纠俭。強(qiáng)一致性要求請求的數(shù)據(jù)必須是當(dāng)時(shí)最準(zhǔn)確的數(shù)據(jù),這個(gè)數(shù)據(jù)不能用讀寫分離或緩存浪慌。最終一致性的數(shù)據(jù)時(shí)效性沒有那么高冤荆,只要最后的結(jié)果是正確的就可以。
假設(shè)這 M 次操作包含:注冊权纤、注冊驗(yàn)證碼钓简、登錄乌妒、解鎖手勢密碼、首頁外邓、瀏覽產(chǎn)品列表等等這些操作撤蚊,這里面其中有一些操作,比如說產(chǎn)品余額损话、生成訂單侦啸、支付短信、付款丧枪,這些都是強(qiáng)一致的要求光涂。
針對最終一致的方案非常簡單,增加機(jī)器就可以解決豪诲,實(shí)時(shí)性較高的可以直接使用數(shù)據(jù)庫的讀寫分離顶捷,如果使用 cache 的話,可以縮短 cache 時(shí)間屎篱;實(shí)時(shí)性較低的應(yīng)當(dāng)使用較長時(shí)間的 cache 。
強(qiáng)一致性的流量處理方案葵蒂,總的來說就是加鎖交播,可以使用數(shù)據(jù)庫的鎖,也可以使用 ZK ( Zookeeper )這樣的分布式鎖践付,或者直接使用隊(duì)列秦士,因?yàn)殛?duì)列總得來說也是一種鎖。如果使用數(shù)據(jù)庫的鎖永高,基本上可以支持到并發(fā)在 2000 次每秒上下隧土。使用數(shù)據(jù)庫的鎖來處理并發(fā),第一個(gè)方法就是有事務(wù)的處理并發(fā)命爬。先開啟事務(wù)曹傀,加鎖共享資源,然后再更新共享資源饲宛,最后再查詢一次共享資源皆愉,然后判斷一下結(jié)果。假如說這個(gè)結(jié)果是成立的艇抠,就直接繼續(xù)執(zhí)行幕庐,假如說這個(gè)結(jié)果是不成立的,直接回滾事務(wù)家淤。第二個(gè)方法就是無事務(wù)的處理并發(fā)异剥,在數(shù)據(jù)庫 SQL 的 where 條件加上判斷條件,如果 update 條數(shù)為 1 則更新成功絮重,如果為 0 則更新失敗冤寿,這時(shí)需要用寫代碼的形式回滾數(shù)據(jù)错妖。
如果流量依然承受不住該怎么辦?
做到這些其實(shí)已經(jīng)能夠承受非常大的流量疚沐,但是業(yè)務(wù)可能繼續(xù)發(fā)展暂氯,還承受不住怎么辦呢?
首先的一個(gè)原則就是亮蛔,沒有任何一個(gè)分布式算法適合并發(fā)操作痴施,最好的方法就是單點(diǎn)并排隊(duì)進(jìn)行處理。
第二究流,單點(diǎn)并發(fā)過大辣吃,使用合適的方式拆分鎖的粒度。
第三芬探,增加降級需求神得,不影響用戶正常使用情況下可以適當(dāng)降低服務(wù)質(zhì)量。適當(dāng)修改需求偷仿、適當(dāng)增加用戶等待結(jié)果的時(shí)間哩簿;如果讓用戶多等一倍的時(shí)間,可能就能承受之前兩倍的并發(fā)酝静,這個(gè)可以在交互上優(yōu)化节榜,讓用戶有更好的體驗(yàn)。
最后别智,適當(dāng)調(diào)整運(yùn)營策略宗苍,分散用戶的集中活躍時(shí)間。