有人會(huì)問過我,我們是怎么抗住百萬日活的?其實(shí)這個(gè)問題很難回答,因?yàn)橹С诌@樣的高并發(fā)并沒有什么難度,如果一定要講的話,我會(huì)把一個(gè)鏈路里的請(qǐng)求進(jìn)行一下拆解,來分段講解下思路
先看這張圖:

- DDOS:有很短的延時(shí)、不可優(yōu)化
- 一般早期公司不用做這個(gè),畢竟這個(gè)東西的成本是超高的,而且?guī)缀醪豢勺越?;如果你被DDOS攻擊了,別想節(jié)省成本,趕緊上吧。
- WAF:較短延時(shí)、不可優(yōu)化
- 網(wǎng)關(guān):可優(yōu)化、收益一般、風(fēng)險(xiǎn)高
我們的網(wǎng)關(guān)是用的java生態(tài)的,選型spring cloud gateway,java語言的特性會(huì)使得這個(gè)網(wǎng)關(guān)顯得比較笨重,優(yōu)點(diǎn)是團(tuán)隊(duì)都會(huì)java,并且和我們的整體架構(gòu)嚴(yán)絲合縫。
-
優(yōu)化代替方案:apache apisix,一款nginx + etcd組合的網(wǎng)關(guān) https://github.com/apache/apisix,功能十分強(qiáng)大,插件很豐富,但它有兩個(gè)問題
- 身邊沒有公司有這樣的經(jīng)驗(yàn),網(wǎng)上的資料不夠多
- 本身是C語言寫的,如果要寫具體邏輯,比如風(fēng)控、驗(yàn)簽等,一旦插件不支持需要自己開發(fā),則要學(xué)lua腳本,這個(gè)團(tuán)隊(duì)會(huì)的人很少。
- image.png
sign、jwt token、黑白名單、風(fēng)控旁路,這幾個(gè)基本是沒有優(yōu)化空間的。
- BFF(plate)層:可優(yōu)化,風(fēng)險(xiǎn)低
-
要提到BFF層是做什么的,從功能上來說,可以叫它,請(qǐng)求分發(fā)器;按功能來說,可以叫它,模塊化;我畫了個(gè)時(shí)序圖,簡(jiǎn)單理解下,BFF層就是圖中的plate + gourpService。
image.png 你看到這個(gè)模型會(huì)不會(huì)想到,為什么你再BFF層不使用NIO技術(shù)(tomcat、redis、dubbo),為什么不用響應(yīng)式編程(node、rxjava),我的回答是,因?yàn)楹?jiǎn)單;我的核心不在于性能,性能我可以堆幾臺(tái)高性能的服務(wù)器,一年也就多個(gè)小幾千塊,但我去找一個(gè)擅長網(wǎng)絡(luò)編程的人,來專門做個(gè)中間件并維護(hù)的價(jià)格來說,就太貴了。
-
- 應(yīng)用程序:重點(diǎn)優(yōu)化
- 應(yīng)用程序的優(yōu)化方案要case by case,不能一概而論;一般互聯(lián)網(wǎng)項(xiàng)目的請(qǐng)求都是CPU密集的,而非IO密集的,所以,一般來說只要程序?qū)懙臎]問題,一般不會(huì)對(duì)網(wǎng)絡(luò)帶寬有較大的沖擊(但實(shí)際上我們有一次壓測(cè)還真碰到了這個(gè)問題,壓測(cè)的時(shí)候把redis的帶寬給打滿了,對(duì)redis的利用太粗暴了),優(yōu)化基本都是在流程和sql里。
- 那我給出幾個(gè)常見的優(yōu)化方式
- 異步處理:我們?cè)谝粋€(gè)類里,基本都是串行的模式(為什么不用并行?以為并行不僅要面臨線程控制不好,導(dǎo)致OOM線程創(chuàng)建失敗的風(fēng)險(xiǎn);還要合并結(jié)果集,代碼難度高處一個(gè)檔次),那我們一般來說最好的方法就是把如果沒有上下文強(qiáng)依賴的外部請(qǐng)求(不需要等結(jié)果集,比如發(fā)送短信,失敗重試)變成異步請(qǐng)求;異步的方式有以下幾種
- 消息隊(duì)列:這種是最常見的,也是最好用的,對(duì)系統(tǒng)沒有額外負(fù)擔(dān),只需要保證消息服務(wù)落盤OK,即可馬上返回成功。
- 掃表&定時(shí)任務(wù):數(shù)據(jù)量小的時(shí)候,這類的處理也是比較簡(jiǎn)單的,一旦數(shù)據(jù)量變大,給DB帶來的壓力會(huì)很大。
- 異步線程調(diào)用:這個(gè)是我們現(xiàn)在在用的方法,框架是hystrix,它的原理是異步轉(zhuǎn)同步;如果你不懂什么是異步轉(zhuǎn)同步,可以查一下dubbo里的異步轉(zhuǎn)同步,原理是一樣的;我在這里簡(jiǎn)單的提一下什么是異步轉(zhuǎn)同步:
- 從主線程創(chuàng)建子線程,來執(zhí)行遠(yuǎn)程調(diào)用
- 主線程通過lock里的condition.await來阻塞,等待子線程返回結(jié)果。
- 子線程拿到結(jié)果集之后,因?yàn)槭荖IO,他并不知道如何找到主線程,那怎么辦呢?主線程創(chuàng)建一個(gè)Map<requestId,response>,創(chuàng)建子線程的時(shí)候,將requestId傳遞給主線程;這樣主線程只需要定期循環(huán)map,查詢是否有自己的requestId的結(jié)果集,一旦發(fā)現(xiàn)有,則取出值,則sigal喚醒,返回給上層。
- 快速失敗 fastover
- 這其實(shí)是一個(gè)思想,它是說如果這個(gè)請(qǐng)求的是有可能被一些條件限定攔住的,那應(yīng)該優(yōu)先去找到那些限定范圍,第一時(shí)間判斷失敗返回。
- 舉一個(gè)例子 一個(gè)程序的按照業(yè)務(wù)邏輯的思路是 A→B→C→D,A\B\C\D本身沒有參數(shù)依賴,假設(shè)如果D中有一個(gè)條件,如果滿足了條件,則整個(gè)處理失敗。那我應(yīng)該把整個(gè)邏輯調(diào)整為 D→A→B→C,他雖然不符合我們平時(shí)思考的邏輯,但只要不影響全局邏輯的處理,先調(diào)用哪個(gè)微服務(wù),又怎么會(huì)出錯(cuò)呢?
- 緩存
-
緩存是一個(gè)非常值得討論的,只要讀的比例比寫的比例高很多,那緩存就是一個(gè)最好的場(chǎng)景。不過,我給你出一下一個(gè)題目,你來思考一下怎么思考緩存的邏輯;“我的訂單”這個(gè)頁面中,要調(diào)用用戶信息和訂單列表,如果你是整個(gè)系統(tǒng)的架構(gòu)師,你要怎么設(shè)計(jì)呢?有以下幾種思路?這道題大伙自己探討吧,是個(gè)非常值得通過場(chǎng)景來深度思考的題
image.png- 用戶信息的接口緩存、訂單列表的接口緩存,我的訂單的頁面接口不緩存。
- 我的訂單頁面接口緩存,另外兩個(gè)接口不緩存。
- 全緩存
- 緩存的真正含義,很多人把緩存理解為 redis、memcache,我覺得理解的還是比較片面,實(shí)際上,ES、MYSQL里的undo log、hbase里的memtable,我覺得都是緩存,這里涉及了一些概念,你如果不了解的話,建議去百度一下。
- 緩存的核心能力:我理解是HA(高可用)和TPS(吞吐量),如果是分布式緩存,要符合BASE理論。redis本身單機(jī)能擋住10W并發(fā),redis的HA有哨兵、cluster、還有codis(阿里)、Twenproxy(Twitter)等高可用方案,這里不再深入細(xì)節(jié);ES擁有無限擴(kuò)容的集群能力,以及通過LSMtree協(xié)議產(chǎn)生的高并發(fā)寫和強(qiáng)大的HA能力,這兩個(gè)都是抗住高并發(fā)的利器,尤其是ES,幾乎能承接所有來自于用戶查詢&篩選的能力。
-
- 限流
-
限流有很多種業(yè)務(wù)場(chǎng)景
- 上游限流:這是一個(gè)標(biāo)準(zhǔn)的熔斷場(chǎng)景,比如當(dāng)A調(diào)用B,A擔(dān)心B的并發(fā)能力不夠,把B打死,因此A自身做了限流處理。
-
下游限流:當(dāng)A同時(shí)調(diào)用B\C\D,這里只有B的吞吐量是最差的,但A是沒辦法了解所有下游系統(tǒng)的能力,并針對(duì)下游系統(tǒng)去限制自身的吞吐,聽起來就是一個(gè)很不合理的需求。所以B就要考慮自己的HA問題了,因?yàn)锽清晰的知道自己的承載量,所以B可以自身做限流。image.png
-
限流的種類
- QPS
- 并發(fā)線程數(shù)
- RT時(shí)間
-
限流的工具
- 分布式限流nginx、sentinal,這兩種是常用的,尤其是sentinal支持無縫配置生效
- 單機(jī)限流hystrix,這塊spring還提供了一個(gè)監(jiān)控。
-
限流的技術(shù)
- 靜態(tài)窗口,一般用redis可實(shí)現(xiàn),缺點(diǎn)是兩個(gè)臨界點(diǎn)直接會(huì)出現(xiàn)double的流量。
- 滑動(dòng)窗口,hystrix默認(rèn)的模式、sentinal默認(rèn)的模式
- 漏桶,redis4.0的cell模式的模式
- 令牌桶,RateLimter可以配置此模式
-
- 熔斷
- 熔斷及容災(zāi),它的核心的犧牲局部保全整體。比如:在商品詳情頁,最重要的服務(wù)是商品服務(wù)和下單服務(wù);而其他的圖片服務(wù)、評(píng)價(jià)服務(wù)如果出現(xiàn)了異常,是不影響用戶購買的主流程的;因此、圖片服務(wù)和評(píng)價(jià)服務(wù)是可以接受熔斷的。在一般電商大促的時(shí)候,我們都會(huì)對(duì)核心的接口做流程分析,將非核心的服務(wù)做降級(jí)服務(wù),一旦接口性能出現(xiàn)問題,則壯士斷腕,直接拋棄部分功能,保全核心能力。
- 現(xiàn)在常用的熔斷技術(shù)就是spring cloud的hystrix。
- 異步處理:我們?cè)谝粋€(gè)類里,基本都是串行的模式(為什么不用并行?以為并行不僅要面臨線程控制不好,導(dǎo)致OOM線程創(chuàng)建失敗的風(fēng)險(xiǎn);還要合并結(jié)果集,代碼難度高處一個(gè)檔次),那我們一般來說最好的方法就是把如果沒有上下文強(qiáng)依賴的外部請(qǐng)求(不需要等結(jié)果集,比如發(fā)送短信,失敗重試)變成異步請(qǐng)求;異步的方式有以下幾種
- 數(shù)據(jù)庫
- 隨著計(jì)算機(jī)技術(shù)的快速發(fā)展,我們的CPU、內(nèi)存性能越來越強(qiáng),價(jià)格也越來越便宜;再加上我們的框架,天然支持服務(wù)器的水平擴(kuò)容,因此,幾乎90%以上的性能瓶頸,都是在數(shù)據(jù)庫的處理上。
- 關(guān)于讀的能力瓶頸
- 困難&解決方案:雖然讀可擴(kuò)展,一主多從的形式,可以讓讀性能得到很好的擴(kuò)容;但是單表的能力一樣受到很大的限制,造成這樣問題的元兇是因?yàn)閙ysql在設(shè)計(jì)的時(shí)候,采用的就是全量副本的概念,這種定位,就會(huì)導(dǎo)致不可能會(huì)有很強(qiáng)的水平擴(kuò)容能力。那如何解決單標(biāo)數(shù)據(jù)量太大的問題呢?
- 表的長度不適合設(shè)置的太大;
- 這里要懂得mysql的檢索邏輯,mysql是B+樹的邏輯,最大高度一般是4以內(nèi);一般默認(rèn)單個(gè)B+樹的節(jié)點(diǎn)大小是16K,這么設(shè)置的原因是,mysql自己定義了數(shù)據(jù)頁的概念(其實(shí)mysql模仿的linux的pagecache的一些設(shè)定),一頁大小是16K;所以,單個(gè)節(jié)點(diǎn)上裝的節(jié)點(diǎn)數(shù)是優(yōu)先的(可以計(jì)算出來,比如索引是BIGINT,那是8個(gè)字節(jié),用16K*1024/8就是能裝的個(gè)數(shù));通過對(duì)非葉子節(jié)點(diǎn)的尋址,一直找到最后的葉子節(jié)點(diǎn)。
- 如果找到結(jié)果,就需要去回表(通過聚簇索引找到放數(shù)據(jù)的地址),再將數(shù)據(jù)加載到內(nèi)存里返回。這個(gè)過程當(dāng)中,你的一行的數(shù)據(jù)越大,每個(gè)數(shù)據(jù)頁裝的就越少,比如一行數(shù)據(jù)2K,數(shù)據(jù)頁能裝8條,用limit 80的時(shí)候,需要掃描10個(gè)數(shù)據(jù)頁;而如果一行數(shù)據(jù)只有512字節(jié),一頁數(shù)據(jù)頁能裝32條,那limit80的查詢只需要掃描3個(gè)數(shù)據(jù)頁就好了。
- 那這里我想進(jìn)一步挖一下,MYSQL是怎么把這些數(shù)據(jù)拉出來的,實(shí)際上當(dāng)mysql通過內(nèi)存尋址去訪問具體的磁盤地址的時(shí)候,linux會(huì)將數(shù)據(jù)放到buffcache,再裝入pageche,mysql再讀pagecache將數(shù)據(jù)加載到自己的內(nèi)存;這個(gè)流程本身也是十分消耗性能,因此,對(duì)數(shù)據(jù)行的大小控制一定要慎重。
- 索引不適合建太多;
- 一般mysql會(huì)優(yōu)先劃分一下buffer pool,這個(gè)你可以理解為是一個(gè)萬能用內(nèi)存,這個(gè)內(nèi)存里使用LRU進(jìn)行淘汰的;我們?cè)谶M(jìn)行查詢的時(shí)候,都是先要把索引加載到buffer pool中,如果你的索引足夠少,就能在內(nèi)存中存活的時(shí)間很長;但如果你的索引非常多,經(jīng)常會(huì)被淘汰重新加載,那就會(huì)浪費(fèi)很多資源。PS:了解一下系統(tǒng)態(tài)→用戶態(tài)對(duì)CPU的消耗邏輯。
- mysql內(nèi)部的patition
- 這個(gè)常規(guī)技術(shù),mysql很早就有了,原理就和分庫分表是一樣的。
- 分庫分表
- 這是我目前最不推崇的方案,技術(shù)發(fā)展,為了解決這個(gè)問題,給業(yè)務(wù)帶來了很多約束,目前這類的框架也有很多
- 客戶端分庫分表:sharding sphere(sharding jdbc)
- 代理模式:DRDS(阿里云商業(yè)版)、MYCAT
- 分布式數(shù)據(jù)庫:polarDB、oceanbase、spanner(最早的谷歌的)、cockroachdb、hybrid for mysql、tidb
- 這是我目前最不推崇的方案,技術(shù)發(fā)展,為了解決這個(gè)問題,給業(yè)務(wù)帶來了很多約束,目前這類的框架也有很多
- 通過ES擋住流量
- ES本身支持sharding模式,因此,ES本身有很強(qiáng)的水平擴(kuò)容的能力,每個(gè)分片上只保存部分?jǐn)?shù)據(jù);mysql只當(dāng)做元數(shù)據(jù)來用。
- 表的長度不適合設(shè)置的太大;
- 困難&解決方案:雖然讀可擴(kuò)展,一主多從的形式,可以讓讀性能得到很好的擴(kuò)容;但是單表的能力一樣受到很大的限制,造成這樣問題的元兇是因?yàn)閙ysql在設(shè)計(jì)的時(shí)候,采用的就是全量副本的概念,這種定位,就會(huì)導(dǎo)致不可能會(huì)有很強(qiáng)的水平擴(kuò)容能力。那如何解決單標(biāo)數(shù)據(jù)量太大的問題呢?
- 關(guān)于寫的能力瓶頸
- 這里我要提到分布式數(shù)據(jù)庫的概念,實(shí)際上分布式數(shù)據(jù)庫并不代表就是new sql,現(xiàn)在市面上的一些主流產(chǎn)品大概分兩類
- 云原生數(shù)據(jù)庫:代表做POLAR DB:
- 云原生書庫并沒有解決高并發(fā)寫的問題,他的寫入還是單點(diǎn)的,因此,這類數(shù)據(jù)庫想提升寫的瓶頸,只能升級(jí)CPU核數(shù)來達(dá)到目的。
- 這類數(shù)據(jù)庫的特點(diǎn)是,上層是由一個(gè)主來寫的,下層是計(jì)算節(jié)點(diǎn)和存儲(chǔ)分離的,因此,在數(shù)據(jù)量方面的擴(kuò)容能力是相當(dāng)強(qiáng)的,一個(gè)底層存儲(chǔ)節(jié)點(diǎn)10M大??;應(yīng)用的協(xié)議是quorum nwr + raft,來保證寫多、讀多的前提下,可以拿到最新值。
- 基于sharding模式的new sql:代表作一大堆
這類的產(chǎn)品太過于復(fù)雜了,并且大多數(shù)都是閉源的,最早的是2012年google的Spanner,國內(nèi)最少開始出現(xiàn)的,大概是2018年,有阿里的oceanbase,也有開源和商業(yè)版TiDB,這類數(shù)據(jù)庫因?yàn)槭莝harding模式,底層數(shù)據(jù)是由多個(gè)節(jié)點(diǎn)來進(jìn)行寫入,因此對(duì)于寫支持很好。
分布式數(shù)據(jù)庫的缺點(diǎn),那自然就是分布式事務(wù)了,現(xiàn)在的new sql每個(gè)產(chǎn)品對(duì)于分布式事務(wù)的支持都是不一樣的,互相之間有細(xì)微的變化,從這單看來,分布式事務(wù)并沒有業(yè)界一個(gè)特別成熟和穩(wěn)定的打法。
-
一般來說分布式事務(wù)有兩種,一種是2PC提交,另一種的percolator;2PC不用多說了,看看percolator的原理:(摘錄的文章)
- 事務(wù)提交前,在客戶端 buffer 所有的 update/delete 操作。
- Prewrite 階段:
首先在所有行的寫操作中選出一個(gè)作為 primary,其他的為 secondaries。
PrewritePrimary: 對(duì) primaryRow 寫入L列(上鎖),L列中記錄本次事務(wù)的開始時(shí)間戳。寫入L列前會(huì)檢查:
是否已經(jīng)有別的客戶端已經(jīng)上鎖 (Locking)。
是否在本次事務(wù)開始時(shí)間之后,檢查 W 列,是否有更新 [startTs, +Inf) 的寫操作已經(jīng)提交 (Conflict)。
在這兩種種情況下會(huì)返回事務(wù)沖突。否則,就成功上鎖。將行的內(nèi)容寫入 row 中,時(shí)間戳設(shè)置為 startTs。
將 primaryRow 的鎖上好了以后,進(jìn)行 secondaries 的 prewrite 流程:
類似 primaryRow 的上鎖流程,只不過鎖的內(nèi)容為事務(wù)開始時(shí)間及 primaryRow 的 Lock 的信息。
檢查的事項(xiàng)同 primaryRow 的一致。
當(dāng)鎖成功寫入后,寫入 row,時(shí)間戳設(shè)置為 startTs。 - 以上 Prewrite 流程任何一步發(fā)生錯(cuò)誤,都會(huì)進(jìn)行回滾:刪除 Lock,刪除版本為 startTs 的數(shù)據(jù)。
- 當(dāng) Prewrite 完成以后,進(jìn)入 Commit 階段,當(dāng)前時(shí)間戳為 commitTs,且 commitTs> startTs :
commit primary:寫入 W 列新數(shù)據(jù),時(shí)間戳為 commitTs,內(nèi)容為 startTs,表明數(shù)據(jù)的最新版本是 startTs 對(duì)應(yīng)的數(shù)據(jù)。
刪除L列。
如果 primary row 提交失敗的話,全事務(wù)回滾,回滾邏輯同 prewrite。如果 commit primary 成功,則可以異步的 commit secondaries, 流程和 commit primary 一致, 失敗了也無所謂。
image.png
image.png
- 云原生數(shù)據(jù)庫:代表做POLAR DB:
- 這里我要提到分布式數(shù)據(jù)庫的概念,實(shí)際上分布式數(shù)據(jù)庫并不代表就是new sql,現(xiàn)在市面上的一些主流產(chǎn)品大概分兩類





