Dubbo之限流TpsLimitFilter原始碼分析

weixin_33895657發表於2018-08-24

本文基於incubator-dubbo 2.7.0版本

前言

在分散式系統中,限流和熔斷是處理併發的兩大利器。關於限流和熔斷,需要記住一句話,客戶端熔斷,服務端限流。本文我會講解Dubbo框架對限流的支援。

限流的作用

我個人理解限流的作用,保護應用,防止雪崩。每個應用都有自己處理請求的上限,一旦應用承受過多請求,首先會對正在處理中的請求造成影響,如果更嚴重,對上下游也會造成雪崩效應。

TpsLimitFilter分析

Dubbo中的限流通過TpsLimitFilter來實現,會在invoker執行實際業務邏輯前進行攔截,判斷單位時間請求數是否超過上限,如果超過,丟擲異常阻斷呼叫。
TpsLimitFilter原始碼如下

@Activate(group = Constants.PROVIDER, value = Constants.TPS_LIMIT_RATE_KEY)
public class TpsLimitFilter implements Filter {

    private final TPSLimiter tpsLimiter = new DefaultTPSLimiter();

    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {

        if (!tpsLimiter.isAllowable(invoker.getUrl(), invocation)) {
            throw new RpcException(
                    new StringBuilder(64)
                            .append("Failed to invoke service ")
                            .append(invoker.getInterface().getName())
                            .append(".")
                            .append(invocation.getMethodName())
                            .append(" because exceed max service tps.")
                            .toString());
        }

        return invoker.invoke(invocation);
    }

}

從TpsLimitFilter的原始碼中可以看到,因為是擴充套件點自動啟用配置,首先TpsLimitFilter只對provider端有效,其次provider url的需要包括tps=xxx這個配置才能生效。

通過TPSLimiter的isAllowable實現限流 ,其內部採用了計數器演算法,單位時間內限制多少呼叫次數,超過限制,返回false。

public class DefaultTPSLimiter implements TPSLimiter {

    /**
     * 每個Service維護一個計數器
     */
    private final ConcurrentMap<String, StatItem> stats
            = new ConcurrentHashMap<String, StatItem>();

    @Override
    public boolean isAllowable(URL url, Invocation invocation) {
        int rate = url.getParameter(Constants.TPS_LIMIT_RATE_KEY, -1);
        long interval = url.getParameter(Constants.TPS_LIMIT_INTERVAL_KEY,
                Constants.DEFAULT_TPS_LIMIT_INTERVAL);
        //servicekey並沒有和方法繫結,只能限流介面
        String serviceKey = url.getServiceKey();
        if (rate > 0) {
            StatItem statItem = stats.get(serviceKey);
            if (statItem == null) {
                stats.putIfAbsent(serviceKey,
                        new StatItem(serviceKey, rate, interval));
                statItem = stats.get(serviceKey);
            }
            return statItem.isAllowable();
        } else {
            StatItem statItem = stats.get(serviceKey);
            if (statItem != null) {
                stats.remove(serviceKey);
            }
        }

        return true;
    }

}

TPSLimiter 針對每個service都建立一個計數器StatItem,通過StatItem的isAllowable方法判斷請求是否有效

class StatItem {

    //介面名
    private String name;

    //計數週期開始
    private long lastResetTime;

    //計數間隔
    private long interval;

    //剩餘計數請求數
    private AtomicInteger token;

    //總共允許請求數
    private int rate;

    StatItem(String name, int rate, long interval) {
        this.name = name;
        this.rate = rate;
        this.interval = interval;
        this.lastResetTime = System.currentTimeMillis();
        this.token = new AtomicInteger(rate);
    }

    public boolean isAllowable() {
        long now = System.currentTimeMillis();
        if (now > lastResetTime + interval) {
            token.set(rate);
            lastResetTime = now;
        }

        int value = token.get();
        boolean flag = false;
        while (value > 0 && !flag) {
            //樂觀鎖增加計數
            flag = token.compareAndSet(value, value - 1);
            //失敗重新獲取
            value = token.get();
        }

        return flag;
    }

    long getLastResetTime() {
        return lastResetTime;
    }

    int getToken() {
        return token.get();
    }

    @Override
    public String toString() {
        return new StringBuilder(32).append("StatItem ")
                .append("[name=").append(name).append(", ")
                .append("rate = ").append(rate).append(", ")
                .append("interval = ").append(interval).append("]")
                .toString();
    }

}

StatItem內的邏輯很簡單,針對每段時間(lastResetTime,lastResetTime+interval)允許rate次呼叫,只要計數器達不到上限,返回true。如果超過lastResetTime+interval,重置計數器。

使用TpsLimitFilter

令人費解的是,Dubbo框架並沒有預設通過配置檔案啟動這個Filter,所以我們需要在classpath的META-INF/dubbo/目錄下增加com.alibaba.dubbo.rpc.Filter檔案

tps=com.alibaba.dubbo.rpc.filter.TpsLimitFilter

就算加上了這個配置,其實也還是生效不了,我們的provider url需要有tps=xxx引數

問題就來了,怎麼加這個配置呢,答案就是override,這個功能的官方介紹如下


9919411-2f3971dedd50464f.png

override的原理是,其實在RegistryProtocol使用export方法對服務進行本地暴露以及註冊Provider Url到zk後,還做了另外一個操作,監聽服務對應的 /dubbo/interface/configurations目錄,一旦configurations目錄下節點發生變化,就會重新生成暴露的url,然後進行reexport。
具體相關原始碼大家可以細細品味下,我覺得這個設計是dubbo服務治理的核心。
註冊監聽程式碼如下

 //得到override url,用於監聽configurations目錄
        final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registedProviderUrl);
        //構造監聽器,用於provider url被override時 重新發布exporter
        //監聽路徑為 /dubbo/interface/configurations
        final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
        overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
        //向registry訂閱這個url路徑
        registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);

回到正題,那麼我們怎麼讓tps生效呢?

在zk的configurations目錄下,增加一個目錄,目錄名如下

override://10.111.27.41:20880/com.alibaba.dubbo.demo.DemoService?tps=5&category=configurators

zk操作命令如下

create -e /dubbo/com.alibaba.dubbo.demo.DemoService/configurators/override%3a%2f%2f10.111.27.41%3a20880%2fcom.alibaba.dubbo.demo.DemoService%3ftps%3d5%26category%3dconfigurators 1

注意overrider後面這端url需要進行URLEncode,因為裡面包含了/符號,zk會誤識別為目錄。 -e用於建立臨時目錄,客戶端斷開後這個目錄會失效,也就是限流會失效。建立zk目錄的時候需要注意下。最好設定成永久。

我通過以上方式設定tps=5之後,超過第六次呼叫後,就對客戶端丟擲異常了


9919411-062598574f28bdc8.png

限流演算法

Dubbo的限流演算法使用了最簡單的計數器演算法,如果併發流量剛好在上個計數器最後一秒和下個計數器第一秒來臨,也不能完全預防突發流量,所以推薦自己使用令牌桶演算法或漏桶演算法實現自定義限流Filter,並且也可以考慮分散式限流。

關於限流演算法,下面這篇文章還不錯。
https://blog.csdn.net/tianyaleixiaowu/article/details/74942405

總結

Dubbo設計擴充套件性真的很強,我們可以通過對Dubbo原始碼的學習,學習到各個方面的知識,舉一反三,應用到實際專案中去,也會有助於對其他框架的原始碼理解。

最後

希望大家關注下我的公眾號


9919411-cdb3cba0f4d6d039..jpg
image

相關文章