Spring Cloud限流詳解(內(nèi)含源碼)

在高并發(fā)的應(yīng)用中晨继,限流往往是一個(gè)繞不開的話題俺孙。本文詳細(xì)探討在Spring Cloud中如何實(shí)現(xiàn)限流凰盔。

Zuul 上實(shí)現(xiàn)限流是個(gè)不錯(cuò)的選擇墓卦,只需要編寫一個(gè)過濾器就可以了倦春,關(guān)鍵在于如何實(shí)現(xiàn)限流的算法户敬。常見的限流算法有漏桶算法以及令牌桶算法。這個(gè)可參考 https://www.cnblogs.com/LBSer/p/4083131.html 睁本,寫得通俗易懂尿庐,你值得擁有,我就不拽文了呢堰。

Google Guava 為我們提供了限流工具類RateLimiter 抄瑟,于是乎,我們可以擼代碼了枉疼。

代碼示例

@Component
public class RateLimitZuulFilter extends ZuulFilter {

    private final RateLimiter rateLimiter = RateLimiter.create(1000.0);

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

    @Override
    public boolean shouldFilter() {
        // 這里可以考慮弄個(gè)限流開啟的開關(guān)皮假,開啟限流返回true,關(guān)閉限流返回false骂维,你懂的惹资。
        return true;
    }

    @Override
    public Object run() {
        try {
            RequestContext currentContext = RequestContext.getCurrentContext();
            HttpServletResponse response = currentContext.getResponse();
            if (!rateLimiter.tryAcquire()) {
                HttpStatus httpStatus = HttpStatus.TOO_MANY_REQUESTS;

                response.setContentType(MediaType.TEXT_PLAIN_VALUE);
                response.setStatus(httpStatus.value());
                response.getWriter().append(httpStatus.getReasonPhrase());

                currentContext.setSendZuulResponse(false);

                throw new ZuulException(
                        httpStatus.getReasonPhrase(),
                        httpStatus.value(),
                        httpStatus.getReasonPhrase()
                );
            }
        } catch (Exception e) {
            ReflectionUtils.rethrowRuntimeException(e);
        }
        return null;
    }
}

如上,我們編寫了一個(gè)pre 類型的過濾器航闺。對(duì)Zuul過濾器有疑問的可參考我的博客:

在過濾器中褪测,我們使用Guava RateLimiter 實(shí)現(xiàn)限流,如果已經(jīng)達(dá)到最大流量潦刃,就拋異常侮措。

分布式場景下的限流

以上單節(jié)點(diǎn)Zuul下的限流,但在生產(chǎn)中乖杠,我們往往會(huì)有多個(gè)Zuul實(shí)例分扎。對(duì)于這種場景如何限流呢?我們可以借助Redis實(shí)現(xiàn)限流胧洒。

使用redis實(shí)現(xiàn)畏吓,存儲(chǔ)兩個(gè)key,一個(gè)用于計(jì)時(shí)略荡,一個(gè)用于計(jì)數(shù)庵佣。請(qǐng)求每調(diào)用一次,計(jì)數(shù)器增加1汛兜,若在計(jì)時(shí)器時(shí)間內(nèi)計(jì)數(shù)器未超過閾值巴粪,則可以處理任務(wù)

if(!cacheDao.hasKey(TIME_KEY)) {
    cacheDao.putToValue(TIME_KEY, 0, 1, TimeUnit.SECONDS);
}       
if(cacheDao.hasKey(TIME_KEY) && cacheDao.incrBy(COUNTER_KEY, 1) > 400) {
    // 拋個(gè)異常什么的
}

實(shí)現(xiàn)微服務(wù)級(jí)別的限流

一些場景下,我們可能還需要實(shí)現(xiàn)微服務(wù)粒度的限流。此時(shí)可以有兩種方案:

方式一:在微服務(wù)本身實(shí)現(xiàn)限流肛根。

和在Zuul上實(shí)現(xiàn)限流類似辫塌,只需編寫一個(gè)過濾器或者攔截器即可,比較簡單派哲,不作贅述臼氨。個(gè)人不太喜歡這種方式,因?yàn)槊總€(gè)微服務(wù)都得編碼芭届,感覺成本很高啊储矩。

加班那么多,作為程序猿的我們褂乍,應(yīng)該學(xué)會(huì)偷懶持隧,這樣才可能有時(shí)間孝順父母、抱老婆逃片、逗兒子屡拨、遛狗養(yǎng)鳥、聊天打屁褥实、追求人生信仰呀狼。好了不扯淡了,看方法二吧损离。

方法二:在Zuul上實(shí)現(xiàn)微服務(wù)粒度的限流哥艇。

在講解之前,我們不妨模擬兩個(gè)路由規(guī)則草冈,兩種路由規(guī)則分別代表Zuul的兩種路由方式她奥。

zuul:
  routes:
    microservice-provider-user: /user/**
    user2:
      url: http://localhost:8000/
      path: /user2/**

如配置所示,在這里怎棱,我們定義了兩個(gè)路由規(guī)則哩俭,microservice-provider-user 以及user2 ,其中microservice-provider-user 這個(gè)路由規(guī)則使用到Ribbon + Hystrix拳恋,走的是RibbonRoutingFilter 凡资;而user2 這個(gè)路由用不上Ribbon也用不上Hystrix,走的是SipleRoutingFilter 谬运。如果你搞不清楚這點(diǎn)隙赁,請(qǐng)參閱我的博客:

搞清楚這點(diǎn)之后,我們就可以擼代碼了:

@Component
public class RateLimitZuulFilter extends ZuulFilter {

    private Map<String, RateLimiter> map = Maps.newConcurrentMap();

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        // 這邊的order一定要大于org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter的order
        // 也就是要大于5
        // 否則梆暖,RequestContext.getCurrentContext()里拿不到serviceId等數(shù)據(jù)伞访。
        return Ordered.LOWEST_PRECEDENCE;
    }

    @Override
    public boolean shouldFilter() {
        // 這里可以考慮弄個(gè)限流開啟的開關(guān),開啟限流返回true轰驳,關(guān)閉限流返回false厚掷,你懂的弟灼。
        return true;
    }

    @Override
    public Object run() {
        try {
            RequestContext context = RequestContext.getCurrentContext();
            HttpServletResponse response = context.getResponse();

            String key = null;
            // 對(duì)于service格式的路由,走RibbonRoutingFilter
            String serviceId = (String) context.get(SERVICE_ID_KEY);
            if (serviceId != null) {
                key = serviceId;
                map.putIfAbsent(serviceId, RateLimiter.create(1000.0));
            }
            // 如果壓根不走RibbonRoutingFilter冒黑,則認(rèn)為是URL格式的路由
            else {
                // 對(duì)于URL格式的路由田绑,走SimpleHostRoutingFilter
                URL routeHost = context.getRouteHost();
                if (routeHost != null) {
                    String url = routeHost.toString();
                    key = url;
                    map.putIfAbsent(url, RateLimiter.create(2000.0));
                }
            }
            RateLimiter rateLimiter = map.get(key);
            if (!rateLimiter.tryAcquire()) {
                HttpStatus httpStatus = HttpStatus.TOO_MANY_REQUESTS;

                response.setContentType(MediaType.TEXT_PLAIN_VALUE);
                response.setStatus(httpStatus.value());
                response.getWriter().append(httpStatus.getReasonPhrase());

                context.setSendZuulResponse(false);

                throw new ZuulException(
                        httpStatus.getReasonPhrase(),
                        httpStatus.value(),
                        httpStatus.getReasonPhrase()
                );
            }
        } catch (Exception e) {
            ReflectionUtils.rethrowRuntimeException(e);
        }
        return null;
    }
}

簡單講解一下這段代碼:

對(duì)于microservice-provider-user 這個(gè)路由,我們可以用context.get(SERVICE_ID_KEY); 獲取到serviceId抡爹,獲取出來就是microservice-provider-user 掩驱;

而對(duì)于user2 這個(gè)路由,我們使用context.get(SERVICE_ID_KEY); 獲得是null冬竟,但是呢欧穴,可以用context.getRouteHost() 獲得路由到的地址,獲取出來就是http://localhost:8000/ 诱咏。接下來的事情苔可,你們懂的缴挖。

改進(jìn)與提升

實(shí)際項(xiàng)目中袋狞,除以上實(shí)現(xiàn)的限流方式,還可能會(huì):

一映屋、在上文的基礎(chǔ)上苟鸯,增加配置項(xiàng),控制每個(gè)路由的限流指標(biāo)棚点,并實(shí)現(xiàn)動(dòng)態(tài)刷新早处,從而實(shí)現(xiàn)更加靈活的管理

二、基于CPU瘫析、內(nèi)存砌梆、數(shù)據(jù)庫等壓力限流(感謝平安常浩智)提出。贬循。

下面咸包,筆者借助Spring Boot Actuator提供的Metrics 能力進(jìn)行實(shí)現(xiàn)基于內(nèi)存壓力的限流——當(dāng)可用內(nèi)存低于某個(gè)閾值就開啟限流,否則不開啟限流杖虾。

@Component
public class RateLimitZuulFilter extends ZuulFilter {
    @Autowired
    private SystemPublicMetrics systemPublicMetrics;
    @Override
    public boolean shouldFilter() {
        // 這里可以考慮弄個(gè)限流開啟的開關(guān)烂瘫,開啟限流返回true,關(guān)閉限流返回false奇适,你懂的坟比。
        Collection<Metric<?>> metrics = systemPublicMetrics.metrics();
        Optional<Metric<?>> freeMemoryMetric = metrics.stream()
                .filter(t -> "mem.free".equals(t.getName()))
                .findFirst();
        // 如果不存在這個(gè)指標(biāo),穩(wěn)妥起見嚷往,返回true葛账,開啟限流
        if (!freeMemoryMetric.isPresent()) {
            return true;
        }
        long freeMemory = freeMemoryMetric.get()
                .getValue()
                .longValue();
        // 如果可用內(nèi)存小于1000000KB,開啟流控
        return freeMemory < 1000000L;
    }
    // 省略其他方法
}

三皮仁、實(shí)現(xiàn)不同維度的限流籍琳,例如:

  • 對(duì)請(qǐng)求的目標(biāo)URL進(jìn)行限流(例如:某個(gè)URL每分鐘只允許調(diào)用多少次)
  • 對(duì)客戶端的訪問IP進(jìn)行限流(例如:某個(gè)IP每分鐘只允許請(qǐng)求多少次)
  • 對(duì)某些特定用戶或者用戶組進(jìn)行限流(例如:非VIP用戶限制每分鐘只允許調(diào)用100次某個(gè)API等)
  • 多維度混合的限流茄茁。此時(shí),就需要實(shí)現(xiàn)一些限流規(guī)則的編排機(jī)制巩割。與裙顽、或、非等關(guān)系宣谈。

參考文檔

本文首發(fā)

http://www.itmuch.com/spring-cloud-sum/spring-cloud-ratelimit/

干貨分享

全是干貨
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末愈犹,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子闻丑,更是在濱河造成了極大的恐慌漩怎,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,406評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嗦嗡,死亡現(xiàn)場離奇詭異勋锤,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)侥祭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門浮梢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人夕凝,你說我怎么就攤上這事柠偶。” “怎么了胎署?”我有些...
    開封第一講書人閱讀 167,815評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵吆录,是天一觀的道長。 經(jīng)常有香客問我琼牧,道長恢筝,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,537評(píng)論 1 296
  • 正文 為了忘掉前任巨坊,我火速辦了婚禮撬槽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘抱究。我一直安慰自己恢氯,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,536評(píng)論 6 397
  • 文/花漫 我一把揭開白布鼓寺。 她就那樣靜靜地躺著勋拟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪妈候。 梳的紋絲不亂的頭發(fā)上敢靡,一...
    開封第一講書人閱讀 52,184評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音苦银,去河邊找鬼啸胧。 笑死赶站,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的纺念。 我是一名探鬼主播贝椿,決...
    沈念sama閱讀 40,776評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼陷谱!你這毒婦竟也來了烙博?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,668評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤烟逊,失蹤者是張志新(化名)和其女友劉穎渣窜,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宪躯,經(jīng)...
    沈念sama閱讀 46,212評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡乔宿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,299評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了访雪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片详瑞。...
    茶點(diǎn)故事閱讀 40,438評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖冬阳,靈堂內(nèi)的尸體忽然破棺而出蛤虐,到底是詐尸還是另有隱情,我是刑警寧澤肝陪,帶...
    沈念sama閱讀 36,128評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站刑顺,受9級(jí)特大地震影響氯窍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蹲堂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,807評(píng)論 3 333
  • 文/蒙蒙 一狼讨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧柒竞,春花似錦政供、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至稼虎,卻和暖如春衅檀,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背霎俩。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評(píng)論 1 272
  • 我被黑心中介騙來泰國打工哀军, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留沉眶,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,827評(píng)論 3 376
  • 正文 我出身青樓杉适,卻偏偏與公主長得像谎倔,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子猿推,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,446評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容