演講嘉賓:宜人貸架構(gòu)師孫軍,擁有10 年的 Java 開發(fā)經(jīng)驗(yàn),先后在人民銀行、1 號(hào)店、人人網(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ā)”活動(dòng)嘉賓演講實(shí)錄
宜人貸系統(tǒng)版本的迭代
1.0 版本——簡(jiǎn)單的煩惱
迭代之前宜人貸的系統(tǒng),其實(shí)就是一個(gè)前臺(tái),一個(gè)后臺(tái),一個(gè) DB ,前臺(tái)采用的是多機(jī)部署的方式。軟件層也是跟最傳統(tǒng)的軟件一樣分三層,第一層是 Controller ,第二層是 Service ,第三層是 DAO 。顯然這個(gè)系統(tǒng)并不適合互聯(lián)網(wǎng),有一些難以避免的問題。首先當(dāng)用戶過萬,在線用戶上千的時(shí)候,這樣的部署方式會(huì)產(chǎn)生一些瓶頸,包括服務(wù)器和數(shù)據(jù)庫(kù)兩方面。第二個(gè)就是團(tuán)隊(duì)規(guī)模變大,所有開發(fā)人員集中開發(fā)同一個(gè)系統(tǒng),沖突嚴(yán)重。
1.5 版本——“吃大補(bǔ)”試試!
針對(duì)上面的問題我們做了一些修改,我把它定義成“吃大補(bǔ)”。吃大補(bǔ)通常有一個(gè)很明顯的特點(diǎn),就是立馬見效,但是副作用也很大。
首先,我們?cè)谝巳速J的頁(yè)面層更加關(guān)注性能的提升,比如說使用瀏覽器緩存,壓縮傳輸,頁(yè)面都經(jīng)過了 YSlow 的優(yōu)化,鏈路層增加了 CDN ,做了靜態(tài)化甚至反向代理,這樣可以抵擋 80% 的流量。應(yīng)用服務(wù)器與數(shù)據(jù)庫(kù)層增加了一個(gè)緩存集群,這個(gè)緩存集群基本上又可以擋掉 80% 流量,最后系統(tǒng)層按照業(yè)務(wù)垂直拆分成多個(gè)系統(tǒng)。數(shù)據(jù)庫(kù)也有一些變化,開始只是一臺(tái)主機(jī),一臺(tái)數(shù)據(jù)庫(kù),現(xiàn)在變成了主從,甚至一主多從。用戶可以撐到過百萬,在線用戶上萬。即便如此,我們的制約因素依然在數(shù)據(jù)庫(kù),優(yōu)化的兩層雖然擋掉了大約 95% 的流量,但是業(yè)務(wù)發(fā)展依然超過了數(shù)據(jù)庫(kù)所能承受的負(fù)載,所以數(shù)據(jù)庫(kù)依然是一個(gè)很大的瓶頸。
第二個(gè)問題就是團(tuán)隊(duì)劃分,其實(shí)每個(gè)團(tuán)隊(duì)都做自己的系統(tǒng),但是大家仍然使用同一個(gè)數(shù)據(jù)庫(kù),這個(gè)時(shí)候?qū)τ谠O(shè)計(jì)和修改數(shù)據(jù)庫(kù),都非常麻煩。甚至每次都要問一下其他團(tuán)隊(duì),我這么改行不行,對(duì)你有什么影響等等。
第三個(gè)問題也非常棘手,大量使用了緩存,數(shù)據(jù)的時(shí)效性和一致性的問題越來越嚴(yán)重。
2.0 版本——“開小灶”精細(xì)化
為了解決 1.5 版本存在的問題,我們需要做精細(xì)化的優(yōu)化,我把它定義成開小灶。首先,合理規(guī)劃數(shù)據(jù)歸屬、優(yōu)化查詢效率、縮短數(shù)據(jù)庫(kù)事務(wù)時(shí)間;其次,分系統(tǒng),每個(gè)系統(tǒng)用固定的表。我們每天都要做的事,就是讓運(yùn)維找出線上最慢的SQL有哪些,對(duì)它們做優(yōu)化。第三,做去事務(wù),或者盡可能地縮短事務(wù)的時(shí)間。
然后開始關(guān)注代碼質(zhì)量,提高執(zhí)行效率,并且開始關(guān)注并發(fā)問題。用戶達(dá)到這個(gè)量的時(shí)候,就會(huì)有用戶幫我們測(cè)試并發(fā)問題。舉一個(gè)例子,同一個(gè)用戶用同一個(gè)帳戶登錄了兩個(gè)客戶端,他同時(shí)點(diǎn)取現(xiàn),這個(gè)時(shí)候如果程序處理的不好,很有可能讓他提現(xiàn)兩次。
最后,要區(qū)分強(qiáng)一致性與最終一致性的請(qǐng)求,合理使用緩存與讀寫分離來解決這些問題。
2.0 解決了很多性能問題,但還是會(huì)有新的問題——系統(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è)線上的問題,通過日志的形式會(huì)非常難查。
所以在這個(gè)基礎(chǔ)上我們做了幾件事。一是限流。限流通常基于兩點(diǎn):最大活動(dòng)線程數(shù)和每秒運(yùn)行次數(shù);活動(dòng)最大線程數(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)后會(huì)更便于查找。
關(guān)于軟件方面,宜人貸統(tǒng)一使用 SLF4J+Logback 來輸出日志,然后業(yè)務(wù)系統(tǒng)間實(shí)現(xiàn)日志串聯(lián),所有服務(wù)端和客戶端之間都隱含地傳遞一些參數(shù),這些參數(shù)會(huì)隨著調(diào)用鏈一步一步往下傳,通過 AOP 來實(shí)現(xiàn)。日志串聯(lián)需要傳遞哪些參數(shù),或者日志中到底要打哪些參數(shù)呢?第一個(gè)是時(shí)間,這個(gè)時(shí)間應(yīng)該到毫秒級(jí),第二個(gè)是流水號(hào),流水號(hào)就是每次請(qǐng)求生成的一個(gè)唯一的值。然后是用戶 Session 、設(shè)備號(hào)、調(diào)用者時(shí)間( APP 使用手機(jī)本地時(shí)間)、本機(jī) IP 、客戶端 IP 、用戶真實(shí) IP 、跨越系統(tǒng)次數(shù)——如果我們發(fā)現(xiàn)了一個(gè)錯(cuò)誤,根據(jù)錯(cuò)誤日志可以找到流水號(hào),再通過流水號(hào)可以到日志查詢平臺(tái)查詢出這次請(qǐng)求途徑的所有系統(tǒng)和每個(gè)系統(tǒng)對(duì)這次請(qǐng)求的日志。有了這些找問題就非常容易。
做到 2.0 之后,宜人貸的網(wǎng)站基本能支撐中大型網(wǎng)站的規(guī)模,短時(shí)間內(nèi)不會(huì)有太多的性能問題了,但是我們依然會(huì)繼續(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ù)可以拆分成后臺(tái)、 Web 、合作渠道等,這個(gè)系統(tǒng)之下會(huì)有一個(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í)際操作問題會(huì)很多。拆分的辦法我總結(jié)了如下幾個(gè):
第一,適當(dāng)冗余,冗余可以確保數(shù)據(jù)庫(kù)依然可以進(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è)比較適合于上文說的全局配置,比如說基本上所有公司都會(huì)有幾張表,記錄了全國(guó)的省市區(qū)縣,這些在每個(gè)系統(tǒng)中都會(huì)用,不一定每個(gè)系統(tǒng)都以接口的形式調(diào)用它,可以在每個(gè)系統(tǒng)里面都冗余一份數(shù)據(jù)。
第三,小技巧——如何驗(yàn)證數(shù)據(jù)庫(kù),并不一定非把它拆分成兩個(gè)物理的數(shù)據(jù)庫(kù)來驗(yàn)證,可以一個(gè)數(shù)據(jù)庫(kù)上建兩個(gè)帳號(hào),這兩個(gè)帳號(hào)分別的權(quán)限指向拆分之后的表,這樣就可以通過帳號(hào)來直接驗(yàn)證拆分效果。
第四,提前規(guī)劃服務(wù),拆分之前確定一下服務(wù)類型是讀多還是寫多的服務(wù),是快請(qǐng)求還是慢請(qǐng)求服務(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 版本是不是要做云平臺(tái),異地部署的方案,表很大的時(shí)候是不是要做垂直拆分,去 IOE 或者使用 Docker 快速部署等等這些,這些其實(shí)都是我們做 4.0 或者 5.0 將來要考慮的事情。
宜人貸理財(cái)系統(tǒng)的優(yōu)化
合理預(yù)估流量——強(qiáng)一致與最終一致
圖中這三個(gè)界面分別為首頁(yè)、列頁(yè),詳情頁(yè)。
在做優(yōu)化之前,首先要合理預(yù)估流量,常用方法有下面兩個(gè)。
評(píng)估方法一:平日 PV / 熱度時(shí)間;
評(píng)估方法二:熱度時(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)一致性要求請(qǐng)求的數(shù)據(jù)必須是當(dāng)時(shí)最準(zhǔn)確的數(shù)據(jù),這個(gè)數(shù)據(jù)不能用讀寫分離或緩存。最終一致性的數(shù)據(jù)時(shí)效性沒有那么高,只要最后的結(jié)果是正確的就可以。
假設(shè)這 M 次操作包含:注冊(cè)、注冊(cè)驗(yàn)證碼、登錄、解鎖手勢(shì)密碼、首頁(yè)、瀏覽產(chǎn)品列表等等這些操作,這里面其中有一些操作,比如說產(chǎn)品余額、生成訂單、支付短信、付款,這些都是強(qiáng)一致的要求。
針對(duì)最終一致的方案非常簡(jiǎn)單,增加機(jī)器就可以解決,實(shí)時(shí)性較高的可以直接使用數(shù)據(jù)庫(kù)的讀寫分離,如果使用 cache 的話,可以縮短 cache 時(shí)間;實(shí)時(shí)性較低的應(yīng)當(dāng)使用較長(zhǎng)時(shí)間的 cache 。
強(qiáng)一致性的流量處理方案,總的來說就是加鎖,可以使用數(shù)據(jù)庫(kù)的鎖,也可以使用 ZK ( Zookeeper )這樣的分布式鎖,或者直接使用隊(duì)列,因?yàn)殛?duì)列總得來說也是一種鎖。如果使用數(shù)據(jù)庫(kù)的鎖,基本上可以支持到并發(fā)在 2000 次每秒上下。使用數(shù)據(jù)庫(kù)的鎖來處理并發(fā),第一個(gè)方法就是有事務(wù)的處理并發(fā)。先開啟事務(wù),加鎖共享資源,然后再更新共享資源,最后再查詢一次共享資源,然后判斷一下結(jié)果。假如說這個(gè)結(jié)果是成立的,就直接繼續(xù)執(zhí)行,假如說這個(gè)結(jié)果是不成立的,直接回滾事務(wù)。第二個(gè)方法就是無事務(wù)的處理并發(fā),在數(shù)據(jù)庫(kù) 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ā)過大,使用合適的方式拆分鎖的粒度。
第三,增加降級(jí)需求,不影響用戶正常使用情況下可以適當(dāng)降低服務(wù)質(zhì)量。適當(dāng)修改需求、適當(dāng)增加用戶等待結(jié)果的時(shí)間;如果讓用戶多等一倍的時(shí)間,可能就能承受之前兩倍的并發(fā),這個(gè)可以在交互上優(yōu)化,讓用戶有更好的體驗(yàn)。
最后,適當(dāng)調(diào)整運(yùn)營(yíng)策略,分散用戶的集中活躍時(shí)間。