在高并發(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過濾器有疑問的可參考我的博客:
Spring Cloud內(nèi)置的Zuul過濾器詳解:http://www.itmuch.com/spring-cloud/zuul/zuul-filter-in-spring-cloud
Spring Cloud Zuul過濾器詳解:http://www.itmuch.com/spring-cloud/zuul/spring-cloud-zuul-filter
在過濾器中褪测,我們使用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)參閱我的博客:
- Spring Cloud內(nèi)置的Zuul過濾器詳解:http://www.itmuch.com/spring-cloud/zuul/zuul-filter-in-spring-cloud
- Spring Cloud Zuul過濾器詳解:http://www.itmuch.com/spring-cloud/zuul/spring-cloud-zuul-filter
搞清楚這點(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)系宣谈。
參考文檔
- 分布式環(huán)境下限流方案的實(shí)現(xiàn):http://blog.csdn.net/Justnow_/article/details/53055299
本文首發(fā)
http://www.itmuch.com/spring-cloud-sum/spring-cloud-ratelimit/