阿里二面:如何設計一個高併發系統?

苏三说技术發表於2024-11-25

大家好,我是蘇三,又跟大家見面了。

前言

最近有位粉絲問了我一個問題:如何設計一個高併發系統?

這是一個非常高頻的面試題,面試官可以從多個角度,考查技術的廣度和深度。

今天這篇文章跟大家一起聊聊高併發系統設計一些關鍵點,希望對你會有所幫助。

圖片

1 頁面靜態化

對於高併發系統的頁面功能,我們必須要做靜態化設計。

如果併發訪問系統的使用者非常多,每次使用者訪問頁面的時候,都透過伺服器動態渲染,會導致服務端承受過大的壓力,而導致頁面無法正常載入的情況發生。

我們可以使用FreemarkerVelocity模板引擎,實現頁面靜態化功能。

以商城官網首頁為例,我們可以在Job中,每隔一段時間,查詢出所有需要在首頁展示的資料,彙總到一起,使用模板引擎生成到html檔案當中。

然後將該html檔案,透過shell指令碼,自動同步到前端頁面相關的伺服器上。

2 CDN加速

雖說頁面靜態化可以提升網站網頁的訪問速度,但還不夠,因為使用者分佈在全國各地,有些人在北京,有些人在成都,有些人在深圳,地域相差很遠,他們訪問網站的網速各不相同。

如何才能讓使用者最快訪問到活動頁面呢?

這就需要使用CDN,它的全稱是Content Delivery Network,即內容分發網路。

圖片

使使用者就近獲取所需內容,降低網路擁塞,提高使用者訪問響應速度和命中率。

CDN加速的基本原理是:將網站的靜態內容(如圖片、CSS、JavaScript檔案等)複製並儲存到分佈在全球各地的伺服器節點上。

當使用者請求訪問網站時,CDN系統會根據使用者的地理位置,自動將內容分發給離使用者最近的伺服器,從而實現快速訪問。

國內常見的CDN提供商有阿里雲CDN、騰訊雲CDN、百度雲加速等,它們提供了全球分佈的節點伺服器,為全球範圍內的網站加速服務。

3 快取

在高併發的系統中,快取可以說是必不可少的技術之一。

目前快取有兩種:

  1. 基於應用伺服器的記憶體快取,也就是我們說的二級快取。
  2. 使用快取中介軟體,比如:Redis、Memcached等,這種是分散式快取。

這兩種快取各有優缺點。

二級快取的效能更好,但因為是基於應用伺服器記憶體的快取,如果系統部署到了多個伺服器節點,可能會存在資料不一致的情況。

而Redis或Memcached雖說效能上比不上二級快取,但它們是分散式快取,避免多個伺服器節點資料不一致的問題。

快取的用法一般是這樣的:圖片使用快取之後,可以減輕訪問資料庫的壓力,顯著的提升系統的效能。

有些業務場景,甚至會分散式快取和二級快取一起使用。

比如獲取商品分類資料,流程如下:圖片

不過引入快取,雖說給我們的系統效能帶來了提升,但同時也給我們帶來了一些新的問題,比如:《資料庫和快取雙向資料庫一致性問題》、《快取穿透、擊穿和雪崩問題》等。

我們在使用快取時,一定要結合實際業務場景,切記不要為了快取而快取。

4 非同步

有時候,我們在高併發系統當中,某些介面的業務邏輯,沒必要都同步執行。

比如有個使用者請求介面中,需要做業務操作,發站內通知,和記錄操作日誌。為了實現起來比較方便,通常我們會將這些邏輯放在介面中同步執行,勢必會對介面效能造成一定的影響。

介面內部流程圖如下:

圖片

這個介面表面上看起來沒有問題,但如果你仔細梳理一下業務邏輯,會發現只有業務操作才是核心邏輯,其他的功能都是非核心邏輯。

在這裡有個原則就是:核心邏輯可以同步執行,同步寫庫。非核心邏輯,可以非同步執行,非同步寫庫。

上面這個例子中,發站內通知和使用者操作日誌功能,對實時性要求不高,即使晚點寫庫,使用者無非是晚點收到站內通知,或者運營晚點看到使用者操作日誌,對業務影響不大,所以完全可以非同步處理。

通常非同步主要有兩種:多執行緒 和 mq。

4.1 執行緒池

使用執行緒池改造之後,介面邏輯如下:圖片

發站內通知和使用者操作日誌功能,被提交到了兩個單獨的執行緒池中。

這樣介面中重點關注的是業務操作,把其他的邏輯交給執行緒非同步執行,這樣改造之後,讓介面效能瞬間提升了。

但使用執行緒池有個小問題就是:如果伺服器重啟了,或者是需要被執行的功能出現異常了,無法重試,會丟資料。

那麼這個問題該怎麼辦呢?

4.2 mq

使用mq改造之後,介面邏輯如下:圖片對於發站內通知和使用者操作日誌功能,在介面中並沒真正實現,它只傳送了mq訊息到mq伺服器。然後由mq消費者消費訊息時,才真正的執行這兩個功能。

這樣改造之後,介面效能同樣提升了,因為傳送mq訊息速度是很快的,我們只需關注業務操作的程式碼即可。

5 多執行緒處理

在高併發系統當中,使用者的請求量很大。

假如我們現在用mq處理業務邏輯。

一下子有大量的使用者請求,產生了大量的mq訊息,儲存到了mq伺服器。

而mq的消費者,消費速度很慢。

可能會導致大量的訊息積壓問題。

從而嚴重影響資料的實時性。

我們需要對訊息的消費者做最佳化。

最快的方式是使用多執行緒消費訊息,比如:改成執行緒池消費訊息。

當然核心執行緒數、最大執行緒數、佇列大小 和 執行緒回收時間,一定要做成配置的,後面可以根據實際情況動態調整。

這樣改造之後,我們可以快速解決訊息積壓問題。

除此之外,在很多資料匯入場景,用多執行緒匯入資料,可以提升效率。

溫馨提醒一下:使用多執行緒消費訊息,可能會出現訊息的順序問題。如果你的業務場景中,需要保證訊息的順序,則要用其他的方式解決問題。感興趣的小夥伴,可以找我私聊。

6 分庫分表

有時候,高併發系統的吞吐量受限的不是別的,而是資料庫。

當系統發展到一定的階段,使用者併發量大,會有大量的資料庫請求,需要佔用大量的資料庫連線,同時會帶來磁碟IO的效能瓶頸問題。

此外,隨著使用者數量越來越多,產生的資料也越來越多,一張表有可能存不下。由於資料量太大,sql語句查詢資料時,即使走了索引也會非常耗時。

這時該怎麼辦呢?

答:需要做分庫分表

如下圖所示:圖片圖中將使用者庫拆分成了三個庫,每個庫都包含了四張使用者表。

如果有使用者請求過來的時候,先根據使用者id路由到其中一個使用者庫,然後再定位到某張表。

路由的演算法挺多的:

  • 根據id取模,比如:id=7,有4張表,則7%4=3,模為3,路由到使用者表3。
  • 給id指定一個區間範圍,比如:id的值是0-10萬,則資料存在使用者表0,id的值是10-20萬,則資料存在使用者表1。
  • 一致性hash演算法

分庫分表主要有兩個方向:垂直水平

說實話垂直方向(即業務方向)更簡單。

在水平方向(即資料方向)上,分庫和分表的作用,其實是有區別的,不能混為一談。

  • 分庫:是為了解決資料庫連線資源不足問題,和磁碟IO的效能瓶頸問題。
  • 分表:是為了解決單表資料量太大,sql語句查詢資料時,即使走了索引也非常耗時問題。此外還可以解決消耗cpu資源問題。
  • 分庫分表:可以解決 資料庫連線資源不足、磁碟IO的效能瓶頸、檢索資料耗時 和 消耗cpu資源等問題。

如果在有些業務場景中,使用者併發量很大,但是需要儲存的資料量很少,這時可以只分庫,不分表。

如果在有些業務場景中,使用者併發量不大,但是需要儲存的數量很多,這時可以只分表,不分庫。

如果在有些業務場景中,使用者併發量大,並且需要儲存的數量也很多時,可以分庫分表。

關於分庫分表更詳細的內容,可以看看我另一篇文章,裡面講的更深入《阿里二面:為什麼分庫分表?

7 池化技術

其實不光是高併發系統,為了效能考慮,有些低併發的系統,也在使用池化技術,比如:資料庫連線池、執行緒池等。

池化技術是多例設計模式的一個體現。

我們都知道建立銷燬資料庫連線是非常耗時耗資源的操作。

如果每次使用者請求,都需要建立一個新的資料庫連線,勢必會影響程式的效能。

為了提升效能,我們可以建立一批資料庫連線,儲存到記憶體中的某個集合中,快取起來。

這樣的話,如果下次有需要用資料庫連線的時候,就能直接從集合中獲取,不用再額外建立資料庫連線,這樣處理將會給我們提升系統效能。圖片

當然用完之後,需要及時歸還。

目前常用的資料庫連線池有:Druid、C3P0、hikari和DBCP等。

8 讀寫分離

不知道你有沒有聽說過二八原則,在一個系統當中可能有80%是讀資料請求,另外20%是寫資料請求。

不過這個比例也不是絕對的。

我想告訴大家的是,一般的系統讀資料請求會遠遠大於寫資料請求。

如果讀資料請求和寫資料請求,都訪問同一個資料庫,可能會相互搶佔資料庫連線,相互影響。

我們都知道,一個資料庫的資料庫連線數量是有限,是非常寶貴的資源,不能因為讀資料請求,影響到寫資料請求吧?

這就需要對資料庫做讀寫分離了。

於是,就出現了主從讀寫分離架構:圖片考慮剛開始使用者量還沒那麼大,選擇的是一主一從的架構,也就是常說的一個master,一個slave

所有的寫資料請求,都指向主庫。一旦主庫寫完資料之後,立馬非同步同步給從庫。這樣所有的讀資料請求,就能及時從從庫中獲取到資料了(除非網路有延遲)。

但這裡有個問題就是:如果使用者量確實有些大,如果master掛了,升級slave為master,將所有讀寫請求都指向新master。

但此時,如果這個新master根本扛不住所有的讀寫請求,該怎麼辦?

這就需要一主多從的架構了:圖片上圖中我列的是一主兩從,如果master掛了,可以選擇從庫1或從庫2中的一個,升級為新master。假如我們在這裡升級從庫1為新master,則原來的從庫2就變成了新master的的slave了。

調整之後的架構圖如下:

圖片

這樣就能解決上面的問題了。

除此之外,如果查詢請求量再增大,我們還可以將架構升級為一主三從、一主四從...一主N從等。

9 索引

在高併發的系統當中,使用者經常需要查詢資料,對資料庫增加索引,是必不可少的一個環節。

尤其是表中資料非常多時,加了索引,跟沒加索引,執行同一條sql語句,查詢相同的資料,耗時可能會相差N個數量級。

雖說索引能夠提升SQL語句的查詢速度,但索引也不是越多越好。

在insert資料時,需要給索引分配額外的資源,對insert的效能有一定的損耗。

我們要根據實際業務場景來決定建立哪些索引,索引少了,影響查詢速度,索引多了,影響寫入速度。

很多時候,我們需要經常對索引做最佳化。

  1. 可以將多個單個索引,改成一個聯合索引。
  2. 刪除不要索引。
  3. 使用explain關鍵字,查詢SQL語句的執行計劃,看看哪些走了索引,哪些沒有走索引。
  4. 要注意索引失效的一些場景。
  5. 必要時可以使用force index來強制查詢sql走某個索引。

如果你想進一步瞭解explain的詳細用法,可以看看我的另一篇文章《explain | 索引最佳化的這把絕世好劍,你真的會用嗎?》。

如果你想進一步瞭解哪些情況下索引會失效,可以看看我的另一篇文章《聊聊索引失效的10種場景,太坑了》。

10 批處理

有時候,我們需要從指定的使用者集合中,查詢出有哪些是在資料庫中已經存在的。

實現程式碼可以這樣寫:

public List<User> queryUser(List<User> searchList) {
if (CollectionUtils.isEmpty(searchList)) {
return Collections.emptyList();
}

List<User> result = Lists.newArrayList();
searchList.forEach(user -> result.add(userMapper.getUserById(user.getId())));
return result;
}

這裡如果有50個使用者,則需要迴圈50次,去查詢資料庫。我們都知道,每查詢一次資料庫,就是一次遠端呼叫。

如果查詢50次資料庫,就有50次遠端呼叫,這是非常耗時的操作。

那麼,我們如何最佳化呢?

答:批處理

具體程式碼如下:

public List<User> queryUser(List<User> searchList) {
if (CollectionUtils.isEmpty(searchList)) {
return Collections.emptyList();
}
List<Long> ids = searchList.stream().map(User::getId).collect(Collectors.toList());
return userMapper.getUserByIds(ids);
}

提供一個根據使用者id集合批次查詢使用者的介面,只遠端呼叫一次,就能查詢出所有的資料。

這裡有個需要注意的地方是:id集合的大小要做限制,最好一次不要請求太多的資料。要根據實際情況而定,建議控制每次請求的記錄條數在500以內。

11 叢集

系統部署的伺服器節點,可能會down機,比如:伺服器的磁碟壞了,或者作業系統出現記憶體不足問題。

為了保證系統的高可用,我們需要部署多個節點,構成一個叢集,防止因為部分伺服器節點掛了,導致系統的整個服務不可用的情況發生。

叢集有很多種:

  1. 應用伺服器叢集
  2. 資料庫叢集
  3. 中介軟體叢集
  4. 檔案伺服器叢集

我們以中介軟體Redis為例。

在高併發系統中,使用者的資料量非常龐大時,比如使用者的快取資料總共大小有40G,一個伺服器節點只有16G的記憶體。

這樣需要部署3臺伺服器節點。

該業務場景,使用普通的master/slave模式,或者使用哨兵模式都行不通。

40G的資料,不能只儲存到一臺伺服器節點,需要均分到3個master伺服器節點上,一個master伺服器節點儲存13.3G的資料。

當有使用者請求過來的時候,先經過路由,根據使用者的id或者ip,每次都訪問指定的伺服器節點。圖片這用就構成了一個叢集。

但這樣有風險,為了防止其中一個master伺服器節點掛掉,導致部分使用者的快取訪問不了,還需要對資料做備份。

這樣每一個master,都需要有一個slave,做資料備份。圖片如果master掛了,可以將slave升級為新的master,而不影響使用者的正常使用。

12 負載均衡

如果我們的系統部署到了多臺伺服器節點。那麼哪些使用者的請求,訪問節點a,哪些使用者的請求,訪問節點b,哪些使用者的請求,訪問節點c?

我們需要某種機制,將使用者的請求,轉發到具體的伺服器節點上。

這就需要使用負載均衡機制了。

在linux下有NginxLVSHaproxy等服務可以提供負載均衡服務。

在SpringCloud微服務架構中,大部分使用的負載均衡元件就是RibbonOpenFeginSpringCloud Loadbalancer

硬體方面,可以使用F5實現負載均衡。它可以基於交換機實現負載均衡,效能更好,但是價格更貴一些。

常用的負載均衡策略有:

  1. 輪詢:每個請求按時間順序逐一分配到不同的伺服器節點,如果伺服器節點down掉,能自動剔除。
  2. weight權重:weight代表權重預設為1,權重越高,伺服器節點被分配到的機率越大。weight和訪問比率成正比,用於伺服器節點效能不均的情況。
  3. ip hash:每個請求按訪問ip的hash結果分配, 這樣每個訪客固定訪問同一個伺服器節點,它是解訣Session共享的問題的解決方案之一。
  4. 最少連線數:把請求轉發給連線數較少的伺服器節點。輪詢演算法是把請求平均的轉發給各個伺服器節點,使它們的負載大致相同;但有些請求佔用的時間很長,會導致其所在的伺服器節點負載較高。這時least_conn方式就可以達到更好的負載均衡效果。
  5. 最短響應時間:按伺服器節點的響應時間來分配請求,響應時間短的伺服器節點優先被分配。

當然還有其他的策略,在這裡就不給大家一一介紹了。

13 限流

對於高併發系統,為了保證系統的穩定性,需要對使用者的請求量做限流

特別是秒殺系統中,如果不做任何限制,絕大部分商品可能是被機器搶到,而非正常的使用者,有點不太公平。

所以,我們有必要識別這些非法請求,做一些限制。那麼,我們該如何現在這些非法請求呢?

目前有兩種常用的限流方式:

  • 基於nginx限流
  • 基於redis限流

13.1 對同一使用者限流

為了防止某個使用者,請求介面次數過於頻繁,可以只針對該使用者做限制。圖片

限制同一個使用者id,比如每分鐘只能請求5次介面。

13.2 對同一ip限流

有時候只對某個使用者限流是不夠的,有些高手可以模擬多個使用者請求,這種nginx就沒法識別了。

這時需要加同一ip限流功能。

限制同一個ip,比如每分鐘只能請求5次介面。

但這種限流方式可能會有誤殺的情況,比如同一個公司或網咖的出口ip是相同的,如果裡面有多個正常使用者同時發起請求,有些使用者可能會被限制住。

13.3 對介面限流

別以為限制了使用者和ip就萬事大吉,有些高手甚至可以使用代理,每次都請求都換一個ip。

這時可以限制請求的介面總次數。

圖片

在高併發場景下,這種限制對於系統的穩定性是非常有必要的。但可能由於有些非法請求次數太多,達到了該介面的請求上限,而影響其他的正常使用者訪問該介面。看起來有點得不償失。

13.4 加驗證碼

相對於上面三種方式,加驗證碼的方式可能更精準一些,同樣能限制使用者的訪問頻次,但好處是不會存在誤殺的情況。

圖片

通常情況下,使用者在請求之前,需要先輸入驗證碼。使用者發起請求之後,服務端會去校驗該驗證碼是否正確。只有正確才允許進行下一步操作,否則直接返回,並且提示驗證碼錯誤。

此外,驗證碼一般是一次性的,同一個驗證碼只允許使用一次,不允許重複使用。

普通驗證碼,由於生成的數字或者圖案比較簡單,可能會被破解。優點是生成速度比較快,缺點是有安全隱患。

還有一個驗證碼叫做:移動滑塊,它生成速度比較慢,但比較安全,是目前各大網際網路公司的首選。

14 服務降級

前面已經說過,對於高併發系統,為了保證系統的穩定性,需要做限流。

但光做限流還不夠。

我們需要合理利用伺服器資源,保留核心的功能,將部分非核心的功能,我們可以選擇遮蔽或者下線掉。

我們需要做服務降級

我們在設計高併發系統時,可以預留一些服務降級的開關。

比如在秒殺系統中,核心的功能是商品的秒殺,對於商品的評論功能,可以暫時遮蔽掉。

在服務端的分散式配置中心,比如:apollo中,可以增加一個開關,配置是否展示評論功能,預設是true。

前端頁面透過伺服器的介面,獲取到該配置引數。

如果需要暫時遮蔽商品評論功能,可以將apollo中的引數設定成false。

此外,我們在設計高併發系統時,還可以預留一些兜底方案。

比如某個分類查詢介面,要從redis中獲取分類資料,返回給使用者。但如果那一條redis掛了,則查詢資料失敗。

這時候,我們可以增加一個兜底方案。

如果從redis中獲取不到資料,則從apollo中獲取一份預設的分類資料。

目前使用較多的熔斷降級中介軟體是:HystrixSentinel

  • Hystrix是Netflix開源的熔斷降級元件。
  • Sentinel是阿里中介軟體團隊開源的一款不光具有熔斷降級功能,同時還支援系統負載保護的元件。

二者的區別如下圖所示:圖片

15 故障轉移

在高併發的系統當中,同一時間有大量的使用者訪問系統。

如果某一個應用伺服器節點處於假死狀態,比如CPU使用率100%了,使用者的請求沒辦法及時處理,導致大量使用者出現請求超時的情況。

如果這種情況下,不做任何處理,可能會影響系統中部分使用者的正常使用。

這時我們需要建立故障轉移機制。

當檢測到經常介面超時,或者CPU打滿,或者記憶體溢位的情況,能夠自動重啟那臺伺服器節點上的應用。

在SpringCloud微服務當中,可以使用Ribbon做負載均衡器。

Ribbon是Spring Cloud中的一個負載均衡器元件,它可以檢測服務的可用性,並根據一定規則將請求分發至不同的服務節點。在使用Ribbon時,需要注意以下幾個方面:

  1. 設定請求超時時間,當請求超時時,Ribbon會自動將請求轉發到其他可用的服務上。
  2. 設定服務的健康檢查,Ribbon會自動檢測服務的可用性,並將請求轉發至可用的服務上。

此外,還需要使用Hystrix做熔斷處理。

Hystrix是SpringCloud中的一個熔斷器元件,它可以自動地監測所有透過它呼叫的服務,並在服務出現故障時自動切換到備用服務。在使用Hystrix時,需要注意以下幾個方面:

  1. 設定斷路器的閾值,當故障率超過一定閾值後,斷路器會自動切換到備用服務上。
  2. 設定服務的超時時間,如果服務在指定的時間內無法返回結果,斷路器會自動切換到備用服務上。到其他的能夠正常使用的伺服器節點上。

16 異地多活

有些高併發系統,為了保證系統的穩定性,不只部署在一個機房當中。

為了防止機房斷電,或者某些不可逆的因素,比如:發生地震,導致機房掛了。

需要把系統部署到多個機房。

我們之前的遊戲登入系統,就部署到了深圳、天津和成都,這三個機房。

這三個機房都有使用者的流量,其中深圳機房佔了40%,天津機房佔了30%,成都機房佔了30%。

如果其中的某個機房突然掛了,流量會被自動分配到另外兩個機房當中,不會影響使用者的正常使用。

這就需要使用異地多活架構了。圖片使用者請求先經過第三方的DNS伺服器解析,然後該使用者請求到達路由伺服器,部署在雲伺服器上。

路由伺服器,根據一定的演算法,會將該使用者請求分配到具體的機房。

異地多活的難度是多個機房需要做資料同步,如何保證資料的一致性?

17 壓測

高併發系統,在上線之前,必須要做的一件事是做壓力測試

我們先要預估一下生產環境的請求量,然後對系統做壓力測試,之後評估系統需要部署多少個伺服器節點。

比如預估有10000的qps,一個伺服器節點最大支援1000pqs,這樣我們需要部署10個伺服器節點。

但假如只部署10個伺服器節點,萬一突增了一些新的使用者請求,伺服器可能會扛不住壓力。

因此,部署的伺服器節點,需要把預估使用者請求量的多一些,比如:按3倍的使用者請求量來計算。

這樣我們需要部署30個伺服器節點。

壓力測試的結果跟環境有關,在dev環境或者test環境,只能壓測一個大概的趨勢。

想要更真實的資料,我們需要在pre環境,或者跟生產環境相同配置的專門的壓測環境中,進行壓力測試。

目前市面上做壓力測試的工具有很多,比如開源的有:Jemter、LoaderRunnder、Locust等等。

收費的有:阿里自研的雲壓測工具PTS。

18 監控

為了出現系統或者SQL問題時,能夠讓我們及時發現,我們需要對系統做監控。

目前業界使用比較多的開源監控系統是:Prometheus

它提供了 監控預警 的功能。

架構圖如下:圖片

我們可以用它監控如下資訊:

  • 介面響應時間
  • 呼叫第三方服務耗時
  • 慢查詢sql耗時
  • cpu使用情況
  • 記憶體使用情況
  • 磁碟使用情況
  • 資料庫使用情況

等等。。。

它的介面大概長這樣子:

圖片

可以看到mysql當前qps,活躍執行緒數,連線數,快取池的大小等資訊。

如果發現資料量連線池佔用太多,對介面的效能肯定會有影響。

這時可能是程式碼中開啟了連線忘了關,或者併發量太大了導致的,需要做進一步排查和系統最佳化。

截圖中只是它一小部分功能,如果你想了解更多功能,可以訪問Prometheus的官網:https://prometheus.io/

其實,高併發的系統中,還需要考慮安全問題,比如:

  1. 遇到使用者不斷變化ip刷介面怎辦?
  2. 遇到使用者大量訪問快取中不存在的資料,導致快取雪崩怎麼辦?
  3. 如果使用者發起ddos攻擊怎麼辦?
  4. 使用者併發量突增,導致伺服器扛不住了,如何動態擴容?

感興趣的小夥伴可以找我私聊。

最後說一句(求關注,別白嫖我)

如果這篇文章對您有所幫助,或者有所啟發的話,幫忙關注一下我的同名公眾號:蘇三說技術,您的支援是我堅持寫作最大的動力。

求一鍵三連:點贊、轉發、在看。

關注公眾號:【蘇三說技術】,在公眾號中回覆:進大廠,可以免費獲取我最近整理的10萬字的面試寶典,好多小夥伴靠這個寶典拿到了多家大廠的offer。

相關文章