針對高併發系統的解決思路與方案

整體上:html

開濤大神在博客中說過:在開發高併發系統時有三把利器用來保護系統:緩存、降級和限流。java

1.擴容

根據業務系統的類型,考慮不一樣的針對在數據庫方面的擴容:mysql

2.緩存(特別重要)

緩存設置的地方nginx

手段redis

主要是Redis、CDN、瀏覽器等,其次可能一些問題算法

2.3可能存在的問題sql

2.3.1一致性數據庫

緩存一致性的話,主要可能考慮到如下幾種可能致使一致性問題:編程

2.3.2緩存併發api

因爲併發時大量請求瞬間涌入,那麼在創建或修改緩存時(還沒完成),就可能已經打到DB或老緩存上了,形成雪崩或緩存一致性問題

解決方案:

鎖機制

2.3.3緩存穿透

緩存穿透是指查詢一個必定不存在的數據,因爲緩存是不命中時被動寫的,若是從存儲層查不到數據則不寫入緩存,這將致使這個不存在的數據每次請求都要到存儲層去查詢,失去了緩存的意義。在流量大時,可能DB就掛掉了,要是有人利用不存在的key頻繁攻擊咱們的應用,這就是漏洞。

解決方案:

最多見的則是採用布隆過濾器,將全部可能存在的數據哈希到一個足夠大的bitmap中,一個必定不存在的數據會被 這個bitmap攔截掉,從而避免了對底層存儲系統的查詢壓力。另外也有一個更爲簡單粗暴的方法(咱們採用的就是這種),若是一個查詢返回的數據爲空(不論是數 據不存在,仍是系統故障),咱們仍然把這個空結果進行緩存,但它的過時時間會很短,最長不超過五分鐘。

2.3.4雪崩

緩存雪崩是指在咱們設置緩存時採用了相同的過時時間,致使緩存在某一時刻同時失效,請求所有轉發到DB,DB瞬時壓力太重雪崩。

解決方案

大多數系統設計者考慮用加鎖或者隊列的方式保證緩存的單線 程(進程)寫,從而避免失效時大量的併發請求落到底層存儲系統上。這裏分享一個簡單方案就時講緩存失效時間分散開,好比咱們能夠在原有的失效時間基礎上增長一個隨機值,好比1-5分鐘隨機,這樣每個緩存的過時時間的重複率就會下降,就很難引起集體失效的事件。

3.消息隊列

作到了大量請求異步與解耦,好比下單與短信驗證碼請求 的處理,及早返回處理。

好比遠程調用較慢,能夠用消息隊列攢一堆去完成調用。

 

基於消息的模型,等通知便可,而不是處理(那是RPC),以及索引庫更新,只是一個通知,而不是強完成(重量級),只是通知而不是職責所在(好比上游系統對下的通知)。

僅僅最終一致性。

 

需求場景

及一些非核心業務或者多個對象、數據量小時,而

相比RPC框架(dubbo等),RPC場景是須要 強一致性、對延遲敏感、注重結果立馬返回、重量級

4.應用拆分

SOA 微服務等

我的見解,原則是:

1.業務是否適合拆分,拆分後耦合度不能高

2.是否須要拆分,拆分是爲了高併發高可用,是否解決了高併發熱點問題?

5.應用限流

好比股票系統中,收盤後,你有幾百萬數據須要入庫了,若是不作限流,瞬間將會把帶寬所有打滿,其它服務暫時GG或者主從延遲很大等等問題。

 

限流的算法

常見的限流算法有:計數器、漏桶和令牌桶算法。

計數器

計數器是最簡單粗暴的算法。好比某個服務最多隻能每秒鐘處理100個請求。咱們能夠設置一個1秒鐘的滑動窗口,窗口中有10個格子,每一個格子100毫秒,每100毫秒移動一次,每次移動都須要記錄當前服務請求的次數。內存中須要保存10次的次數。能夠用數據結構LinkedList來實現。格子每次移動的時候判斷一次,當前訪問次數和LinkedList中最後一個相差是否超過100,若是超過就須要限流了。

 

很明顯,當滑動窗口的格子劃分的越多,那麼滑動窗口的滾動就越平滑,限流的統計就會越精確。

漏桶算法

漏桶算法即leaky bucket是一種很是經常使用的限流算法,能夠用來實現流量整形(Traffic Shaping)和流量控制(Traffic Policing)。貼了一張維基百科上示意圖幫助你們理解:

 

漏桶算法的主要概念以下:

  • 一個固定容量的漏桶,按照常量固定速率流出水滴;
  • 若是桶是空的,則不需流出水滴;
  • 能夠以任意速率流入水滴到漏桶;
  • 若是流入水滴超出了桶的容量,則流入的水滴溢出了(被丟棄),而漏桶容量是不變的。

 

漏桶算法比較好實現,在單機系統中可使用隊列來實現(.Net中TPL DataFlow能夠較好的處理相似的問題,你能夠在這裏找到相關的介紹),在分佈式環境中消息中間件或者Redis都是可選的方案。

令牌桶算法

令牌桶算法是一個存放固定容量令牌(token)的桶,按照固定速率往桶裏添加令牌。令牌桶算法基本能夠用下面的幾個概念來描述:

  • 令牌將按照固定的速率被放入令牌桶中。好比每秒放10個。
  • 桶中最多存放b個令牌,當桶滿時,新添加的令牌被丟棄或拒絕。
  • 當一個n個字節大小的數據包到達,將從桶中刪除n個令牌,接着數據包被髮送到網絡上。
  • 若是桶中的令牌不足n個,則不會刪除令牌,且該數據包將被限流(要麼丟棄,要麼緩衝區等待)。

以下圖:

令牌算法是根據放令牌的速率去控制輸出的速率,也就是上圖的to network的速率。to network咱們能夠理解爲消息的處理程序,執行某段業務或者調用某個RPC。

應用級限流

 

限流總併發/鏈接/請求數

對於一個應用系統來講必定會有極限併發/請求數,即總有一個TPS/QPS閥值,若是超了閥值則系統就會不響應用戶請求或響應的很是慢,所以咱們最好進行過載保護,防止大量請求涌入擊垮系統。

若是使用過Tomcat,其Connector其中一種配置有以下幾個參數:

acceptCount:若是Tomcat的線程都忙於響應,新來的鏈接會進入隊列排隊,若是超出排隊大小,則拒絕鏈接;

maxConnections:瞬時最大鏈接數,超出的會排隊等待;

maxThreads:Tomcat能啓動用來處理請求的最大線程數,若是請求處理量一直遠遠大於最大線程數則可能會僵死。

詳細的配置請參考官方文檔。另外如MySQL(如max_connections)、Redis(如tcp-backlog)都會有相似的限制鏈接數的配置。

   

限流總資源數

若是有的資源是稀缺資源(如數據庫鏈接、線程),並且可能有多個系統都會去使用它,那麼須要限制應用;可使用池化技術來限制總資源數:鏈接池、線程池。好比分配給每一個應用的數據庫鏈接是100,那麼本應用最多可使用100個資源,超出了能夠等待或者拋異常。

   

限流某個接口的總併發/請求數

若是接口可能會有突發訪問狀況,但又擔憂訪問量太大形成崩潰,如搶購業務;這個時候就須要限制這個接口的總併發/請求數總請求數了;由於粒度比較細,能夠爲每一個接口都設置相應的閥值。可使用Java中的AtomicLong進行限流:

=================================

try {
if(atomic.incrementAndGet() > 限流數) {
//拒絕請求
   }
//處理請求
} finally {
atomic.decrementAndGet();
}

=================================

適合對業務無損的服務或者須要過載保護的服務進行限流,如搶購業務,超出了大小要麼讓用戶排隊,要麼告訴用戶沒貨了,對用戶來講是能夠接受的。而一些開放平臺也會限制用戶調用某個接口的試用請求量,也能夠用這種計數器方式實現。這種方式也是簡單粗暴的限流,沒有平滑處理,須要根據實際狀況選擇使用;

   

限流某個接口的時間窗請求數

即一個時間窗口內的請求數,如想限制某個接口/服務每秒/每分鐘/天天的請求數/調用量。如一些基礎服務會被不少其餘系統調用,好比商品詳情頁服務會調用基礎商品服務調用,可是怕由於更新量比較大將基礎服務打掛,這時咱們要對每秒/每分鐘的調用量進行限速;一種實現方式以下所示:

=================================

LoadingCache<Long, AtomicLong> counter =
        CacheBuilder.newBuilder()
                .expireAfterWrite(2, TimeUnit.SECONDS)
                .build(new CacheLoader<Long, AtomicLong>() {
                    @Override
                    public AtomicLong load(Long seconds) throws Exception {
                        return new AtomicLong(0);
                    }
                });
long limit = 1000;
while(true) {
    //獲得當前秒
    long currentSeconds = System.currentTimeMillis() / 1000;
    if(counter.get(currentSeconds).incrementAndGet() > limit) {
        System.out.println("限流了:" + currentSeconds);
        continue;
    }
    //業務處理
}

=================================

咱們使用Guava的Cache來存儲計數器,過時時間設置爲2秒(保證1秒內的計數器是有的),而後咱們獲取當前時間戳而後取秒數來做爲KEY進行計數統計和限流,這種方式也是簡單粗暴,剛纔說的場景夠用了。

 

平滑限流某個接口的請求數

以前的限流方式都不能很好地應對突發請求,即瞬間請求可能都被容許從而致使一些問題;所以在一些場景中須要對突發請求進行整形,整形爲平均速率請求處理(好比5r/s,則每隔200毫秒處理一個請求,平滑了速率)。這個時候有兩種算法知足咱們的場景:令牌桶和漏桶算法。Guava框架提供了令牌桶算法實現,可直接拿來使用。

Guava RateLimiter提供了令牌桶算法實現:平滑突發限流(SmoothBursty)和平滑預熱限流(SmoothWarmingUp)實現。

分佈式限流

分佈式限流最關鍵的是要將限流服務作成原子化,而解決方案可使使用redis+lua或者nginx+lua技術進行實現,經過這兩種技術能夠實現的高併發和高性能。

使用redis+lua實現時間窗內某個接口的請求數限流,實現了該功能後能夠改造爲限流總併發/請求數和限制總資源數。Lua自己就是一種編程語言,也可使用它實現複雜的令牌桶或漏桶算法。

6.服務降級與熔斷

1.服務降級

當服務器壓力劇增的狀況下,根據當前業務狀況及流量對一些服務和頁面有策略的降級,以此釋放服務器資源以保證核心任務的正常運行。

降級後的處理能夠設置一些默認的頁面或返回,如:

  • 服務接口拒絕服務:頁面能訪問,可是添加刪除提示服務器繁忙。頁面內容也可在Varnish或CDN內獲取。
  • 頁面拒絕服務:頁面提示因爲服務繁忙此服務暫停。跳轉到varnish或nginx的一個靜態頁面。
  • 延遲持久化:頁面訪問照常,可是涉及記錄變動,會提示稍晚能看到結果,將數據記錄到異步隊列或log,服務恢復後執行。
  • 隨機拒絕服務:服務接口隨機拒絕服務,讓用戶重試,目前較少有人採用。由於用戶體驗不佳。

 

2.服務熔斷

服務熔斷通常是指軟件系統中,因爲某些緣由使得服務出現了過載現象,爲防止形成整個系統故障,從而採用的一種保護措施,因此不少地方把熔斷亦稱爲過載保護。

 

3.對比區別和共性

相似性的:

  1. 目的很一致,都是從可用性可靠性着想,爲防止系統的總體緩慢甚至崩潰,採用的技術手段;
  2. 最終表現相似,對於二者來講,最終讓用戶體驗到的是某些功能暫時不可達或不可用;
  3. 粒度通常都是服務級別,固然,業界也有很多更細粒度的作法,好比作到數據持久層(容許查詢,不容許增刪改);
  4. 自治性要求很高,熔斷模式通常都是服務基於策略的自動觸發,降級雖然說可人工干預,但在微服務架構下,徹底靠人顯然不可能,開關預置、配置中心都是必要手段;

而二者的區別也是明顯的:

  1. 觸發緣由不太同樣,服務熔斷通常是某個服務(下游服務)故障引發,而服務降級通常是從總體負荷考慮;
  2. 管理目標的層次不太同樣,熔斷實際上是一個框架級的處理,每一個微服務都須要(無層級之分),而降級通常須要對業務有層級之分(好比降級通常是從最外圍服務開始)
  3. 實現方式不太同樣

7.數據庫分庫、分表

出現如下狀況時開始考慮

如下爲數據庫優化,參考以前寫的MySQL優化部分。

 

8.高可用-方案

相關文章
相關標籤/搜索