服務溶斷、降級、限流(1):概念

服務熔斷

在介紹熔斷機制之前,我們需要了解微服務的雪崩效應。在微服務架構中,微服務是完成一個單一的業務功能,這樣做的好處是可以做到解耦,每個微服務可以獨立演進。但是,一個應用可能會有多個微服務組成,微服務之間的數據交互通過遠程過程調用完成。這就帶來一個問題,假設微服務A調用微服務B和微服務C,微服務B和微服務C又調用其它的微服務,這就是所謂的“扇出”。如果扇出的鏈路上某個微服務的調用響應時間過長或者不可用,對微服務A的調用就會佔用越來越多的系統資源,進而引起系統崩潰,所謂的“雪崩效應”。 
這裏寫圖片描述 
熔斷機制是應對雪崩效應的一種微服務鏈路保護機制。我們在各種場景下都會接觸到熔斷這兩個字。高壓電路中,如果某個地方的電壓過高,熔斷器就會熔斷,對電路進行保護。股票交易中,如果股票指數過高,也會採用熔斷機制,暫停股票的交易。同樣,在微服務架構中,熔斷機制也是起着類似的作用。當扇出鏈路的某個微服務不可用或者響應時間太長時,會進行服務的降級,進而熔斷該節點微服務的調用,快速返回錯誤的響應信息。當檢測到該節點微服務調用響應正常後,恢復調用鏈路。

在Spring Cloud框架裏,熔斷機制通過Hystrix實現。Hystrix會監控微服務間調用的狀況,當失敗的調用到一定閾值,缺省是5秒內20次調用失敗,就會啓動熔斷機制。

在dubbo中也可利用nio超時+失敗次數做熔斷。 
dubbo可以通過擴展Filter的方式引入Hystrix,具體代碼如下:

package com.netease.hystrix.dubbo.rpc.filter;

import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.extension.Activate;
import com.alibaba.dubbo.rpc.Filter;
import com.alibaba.dubbo.rpc.Invocation;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.Result;
import com.alibaba.dubbo.rpc.RpcException;

@Activate(group = Constants.CONSUMER)
public class HystrixFilter implements Filter {

    @Override
    public Result invoke(Invoker invoker, Invocation invocation) throws RpcException {
        DubboHystrixCommand command = new DubboHystrixCommand(invoker, invocation);
        return command.execute();
    }

}

DubboHystrixCommand

package com.netease.hystrix.dubbo.rpc.filter;

import org.apache.log4j.Logger;

import com.alibaba.dubbo.common.URL;
import com.alibaba.dubbo.rpc.Invocation;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.Result;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandProperties;
import com.netflix.hystrix.HystrixThreadPoolProperties;

public class DubboHystrixCommand extends HystrixCommand {

    private static Logger    logger                       = Logger.getLogger(DubboHystrixCommand.class);
    private static final int DEFAULT_THREADPOOL_CORE_SIZE = 30;
    private Invoker       invoker;
    private Invocation       invocation;

    public DubboHystrixCommand(Invoker invoker,Invocation invocation){
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(invoker.getInterface().getName()))
                    .andCommandKey(HystrixCommandKey.Factory.asKey(String.format("%s_%d", invocation.getMethodName(),
                                                                                 invocation.getArguments() == null ? 0 : invocation.getArguments().length)))
              .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                                            .withCircuitBreakerRequestVolumeThreshold(20)//10秒鐘內至少19此請求失敗,熔斷器才發揮起作用
                                            .withCircuitBreakerSleepWindowInMilliseconds(30000)//熔斷器中斷請求30秒後會進入半打開狀態,放部分流量過去重試
                                            .withCircuitBreakerErrorThresholdPercentage(50)//錯誤率達到50開啓熔斷保護
                                            .withExecutionTimeoutEnabled(false))//使用dubbo的超時,禁用這裏的超時
              .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter().withCoreSize(getThreadPoolCoreSize(invoker.getUrl()))));//線程池爲30


        this.invoker=invoker;
        this.invocation=invocation;
    }

    /**
     * 獲取線程池大小
     * 
     * @param url
     * @return
     */
    private static int getThreadPoolCoreSize(URL url) {
        if (url != null) {
            int size = url.getParameter("ThreadPoolCoreSize", DEFAULT_THREADPOOL_CORE_SIZE);
            if (logger.isDebugEnabled()) {
                logger.debug("ThreadPoolCoreSize:" + size);
            }
            return size;
        }

        return DEFAULT_THREADPOOL_CORE_SIZE;

    }

    @Override
    protected Result run() throws Exception {
        return invoker.invoke(invocation);
    }
}

線程池大小可以通過dubbo參數進行控制,當前其他的參數也可以通過類似的方式進行配置

 

代碼添加好後在,resource添加加載文本

|-resources 
|-META-INF 
|-dubbo 
|-com.alibaba.dubbo.rpc.Filter (純文本文件,內容爲:hystrix=com.netease.hystrix.dubbo.rpc.filter.HystrixFilter

由於Filter定義爲自動激活的,所以啓動代碼所有消費者都被隔離起來啦!

熔段解決如下幾個問題: 
    當所依賴的對象不穩定時,能夠起到快速失敗的目的 
    快速失敗後,能夠根據一定的算法動態試探所依賴對象是否恢復

參考:http://www.roncoo.com/article/detail/126834 
https://www.cnblogs.com/lvgg/p/7843809.html

服務降級

降級是指自己的待遇下降了,從RPC調用環節來講,就是去訪問一個本地的僞裝者而不是真實的服務。

    當雙11活動時,把無關交易的服務統統降級,如查看螞蟻深林,查看歷史訂單,商品歷史評論,只顯示最後100條等等。

區別

相同點: 
目的很一致,都是從可用性可靠性着想,爲防止系統的整體緩慢甚至崩潰,採用的技術手段; 
最終表現類似,對於兩者來說,最終讓用戶體驗到的是某些功能暫時不可達或不可用; 
粒度一般都是服務級別,當然,業界也有不少更細粒度的做法,比如做到數據持久層(允許查詢,不允許增刪改); 
自治性要求很高,熔斷模式一般都是服務基於策略的自動觸發,降級雖說可人工干預,但在微服務架構下,完全靠人顯然不可能,開關預置、配置中心都是必要手段;

區別: 
觸發原因不太一樣,服務熔斷一般是某個服務(下游服務)故障引起,而服務降級一般是從整體負荷考慮; 
管理目標的層次不太一樣,熔斷其實是一個框架級的處理,每個微服務都需要(無層級之分),而降級一般需要對業務有層級之分(比如降級一般是從最外圍服務開始) 
實現方式不太一樣;服務降級具有代碼侵入性(由控制器完成/或自動降級),熔斷一般稱爲自我熔斷。

服務限流

在開發高併發系統時有三把利器用來保護系統:緩存、降級和限流。緩存的目的是提升系統訪問速度和增大系統能處理的容量,可謂是抗高併發流量的銀彈;而降級是當服務出問題或者影響到核心流程的性能則需要暫時屏蔽掉,待高峯或者問題解決後再打開;而有些場景並不能用緩存和降級來解決,比如稀缺資源(秒殺、搶購)、寫服務(如評論、下單)、頻繁的複雜查詢(評論的最後幾頁),因此需有一種手段來限制這些場景的併發/請求量,即限流。

限流的目的是通過對併發訪問/請求進行限速或者一個時間窗口內的的請求進行限速來保護系統,一旦達到限制速率則可以拒絕服務(定向到錯誤頁或告知資源沒有了)、排隊或等待(比如秒殺、評論、下單)、降級(返回兜底數據或默認數據,如商品詳情頁庫存默認有貨)。

一般開發高併發系統常見的限流有:限制總併發數(比如數據庫連接池、線程池)、限制瞬時併發數(如nginx的limit_conn模塊,用來限制瞬時併發連接數)、限制時間窗口內的平均速率(如Guava的RateLimiter、nginx的limit_req模塊,限制每秒的平均速率);其他還有如限制遠程接口調用速率、限制MQ的消費速率。另外還可以根據網絡連接數、網絡流量、CPU或內存負載等來限流。

限流算法

常見的限流算法有:令牌桶、漏桶。計數器也可以進行粗暴限流實現。

漏桶(Leaky Bucket)算法思路很簡單,水(請求)先進入到漏桶裏,漏桶以一定的速度出水(接口有響應速率),當水流入速度過大會直接溢出(訪問頻率超過接口響應速率),然後就拒絕請求,可以看出漏桶算法能強行限制數據的傳輸速率.示意圖如下: 
這裏寫圖片描述

令牌桶算法(Token Bucket)和 Leaky Bucket 效果一樣但方向相反的算法,更加容易理解.隨着時間流逝,系統會按恆定1/QPS時間間隔(如果QPS=100,則間隔是10ms)往桶裏加入Token(想象和漏洞漏水相反,有個水龍頭在不斷的加水),如果桶已經滿了就不再加了.新請求來臨時,會各自拿走一個Token,如果沒有Token可拿了就阻塞或者拒絕服務. 
這裏寫圖片描述 
令牌桶的另外一個好處是可以方便的改變速度. 一旦需要提高速率,則按需提高放入桶中的令牌的速率. 一般會定時(比如100毫秒)往桶中增加一定數量的令牌, 有些變種算法則實時的計算應該增加的令牌的數量.

應用級限流

對於一個應用系統來說一定會有極限併發/請求數,即總有一個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();
}

參考:https://blog.csdn.net/g_hongjin/article/details/51649246

分佈式限流

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

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

有人會糾結如果應用併發量非常大那麼redis或者nginx是不是能抗得住;不過這個問題要從多方面考慮:你的流量是不是真的有這麼大,是不是可以通過一致性哈希將分佈式限流進行分片,是不是可以當併發量太大降級爲應用級限流;對策非常多,可以根據實際情況調節;像在京東使用Redis+Lua來限流搶購流量,一般流量是沒有問題的。

對於分佈式限流目前遇到的場景是業務上的限流,而不是流量入口的限流;流量入口限流應該在接入層完成,而接入層筆者一般使用Nginx。

基於Redis功能的實現限流

參考:https://www.cnblogs.com/exceptioneye/p/4783904.html 
簡陋的設計思路:假設一個用戶(用IP判斷)每分鐘訪問某一個服務接口的次數不能超過10次,那麼我們可以在Redis中創建一個鍵,並此時我們就設置鍵的過期時間爲60秒,每一個用戶對此服務接口的訪問就把鍵值加1,在60秒內當鍵值增加到10的時候,就禁止訪問服務接口。在某種場景中添加訪問時間間隔還是很有必要的。

基於令牌桶算法的實現

令牌桶算法最初來源於計算機網絡。在網絡傳輸數據時,爲了防止網絡擁塞,需限制流出網絡的流量,使流量以比較均勻的速度向外發送。令牌桶算法就實現了這個功能,可控制發送到網絡上數據的數目,並允許突發數據的發送。

令牌桶算法是網絡流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一種算法。典型情況下,令牌桶算法用來控制發送到網絡上的數據的數目,並允許突發數據的發送。

大小固定的令牌桶可自行以恆定的速率源源不斷地產生令牌。如果令牌不被消耗,或者被消耗的速度小於產生的速度,令牌就會不斷地增多,直到把桶填滿。後面再產生的令牌就會從桶中溢出。最後桶中可以保存的最大令牌數永遠不會超過桶的大小。

傳送到令牌桶的數據包需要消耗令牌。不同大小的數據包,消耗的令牌數量不一樣。

令牌桶這種控制機制基於令牌桶中是否存在令牌來指示什麼時候可以發送流量。令牌桶中的每一個令牌都代表一個字節。如果令牌桶中存在令牌,則允許發送流量;而如果令牌桶中不存在令牌,則不允許發送流量。因此,如果突發門限被合理地配置並且令牌桶中有足夠的令牌,那麼流量就可以以峯值速率發送。

 
算法描述:

假如用戶配置的平均發送速率爲r,則每隔1/r秒一個令牌被加入到桶中(每秒會有r個令牌放入桶中);

假設桶中最多可以存放b個令牌。如果令牌到達時令牌桶已經滿了,那麼這個令牌會被丟棄;

當一個n個字節的數據包到達時,就從令牌桶中刪除n個令牌(不同大小的數據包,消耗的令牌數量不一樣),並且數據包被髮送到網絡;

如果令牌桶中少於n個令牌,那麼不會刪除令牌,並且認爲這個數據包在流量限制之外(n個字節,需要n個令牌。該數據包將被緩存或丟棄);

算法允許最長b個字節的突發,但從長期運行結果看,數據包的速率被限制成常量r。對於在流量限制外的數據包可以以不同的方式處理:(1)它們可以被丟棄;(2)它們可以排放在隊列中以便當令牌桶中累積了足夠多的令牌時再傳輸;(3)它們可以繼續發送,但需要做特殊標記,網絡過載的時候將這些特殊標記的包丟棄。

Java實現

我們可以使用Guava 的 RateLimiter 來實現基於令牌桶的流控,RateLimiter 令牌桶算法是單桶實現。RateLimiter 對簡單的令牌桶算法做了一些工程上的優化,具體的實現是 SmoothBursty。需要注意的是,RateLimiter 的另一個實現SmoothWarmingUp,就不是令牌桶了,而是漏桶算法。也許是出於簡單起見,RateLimiter 中的時間窗口能且僅能爲 1s。

SmoothBursty 有一個可以放 N 個時間窗口產生的令牌的桶,系統空閒的時候令牌就一直攢着,最好情況下可以扛 N 倍於限流值的高峯而不影響後續請求。RateLimite允許某次請求拿走超出剩餘令牌數的令牌,但是下一次請求將爲此付出代價,一直等到令牌虧空補上,並且桶中有足夠本次請求使用的令牌爲止。當某次請求不能得到所需要的令牌時,這時涉及到一個權衡,是讓前一次請求乾等到令牌夠用才走掉呢,還是讓它先走掉後面的請求等一等呢?Guava 的設計者選擇的是後者,先把眼前的活幹了,後面的事後面再說。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章