轉自: http://blog.csdn.net/zl1zl2zl3/article/details/78683855
在高並發的應用中,限流往往是一個繞不開的話題。本文詳細探討在Spring Cloud中如何實現限流。
在 Zuul 上實現限流是個不錯的選擇,只需要編寫一個過濾器就可以了,關鍵在於如何實現限流的算法。常見的限流算法有漏桶算法以及令牌桶算法。這個可參考 https://www.cnblogs.com/LBSer/p/4083131.html ,寫得通俗易懂,你值得擁有,我就不拽文了。
GoogleGuava 為我們提供了限流工具類 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() { -
// 這里可以考慮弄個限流開啟的開關,開啟限流返回true,關閉限流返回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; -
} -
}
如上,我們編寫了一個 pre 類型的過濾器。對Zuul過濾器有疑問的可參考我的博客:
-
Spring Cloud內置的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
在過濾器中,我們使用 GuavaRateLimiter 實現限流,如果已經達到最大流量,就拋異常。
分布式場景下的限流
以上單節點Zuul下的限流,但在生產中,我們往往會有多個Zuul實例。對於這種場景如何限流呢?我們可以借助Redis實現限流。
使用redis實現,存儲兩個key,一個用於計時,一個用於計數。請求每調用一次,計數器增加1,若在計時器時間內計數器未超過閾值,則可以處理任務
-
if(!cacheDao.hasKey(TIME_KEY)) { -
cacheDao.putToValue(TIME_KEY, 0, 1, TimeUnit.SECONDS); -
} -
if(cacheDao.hasKey(TIME_KEY) && cacheDao.incrBy(COUNTER_KEY, 1) > 400) { -
// 拋個異常什么的 -
}
實現微服務級別的限流
一些場景下,我們可能還需要實現微服務粒度的限流。此時可以有兩種方案:
方式一:在微服務本身實現限流。
和在Zuul上實現限流類似,只需編寫一個過濾器或者攔截器即可,比較簡單,不作贅述。個人不太喜歡這種方式,因為每個微服務都得編碼,感覺成本很高啊。
加班那么多,作為程序猿的我們,應該學會偷懶,這樣才可能有時間孝順父母、抱老婆、逗兒子、遛狗養鳥、聊天打屁、追求人生信仰。好了不扯淡了,看方法二吧。
方法二:在Zuul上實現微服務粒度的限流。
在講解之前,我們不妨模擬兩個路由規則,兩種路由規則分別代表Zuul的兩種路由方式。
-
zuul: -
routes: -
microservice-provider-user: /user/** -
user2: -
url: http://localhost:8000/ -
path: /user2/**
如配置所示,在這里,我們定義了兩個路由規則, microservice-provider-user 以及 user2 ,其中 microservice-provider-user 這個路由規則使用到Ribbon + Hystrix,走的是 RibbonRoutingFilter ;而 user2 這個路由用不上Ribbon也用不上Hystrix,走的是 SipleRoutingFilter 。如果你搞不清楚這點,請參閱我的博客:
-
Spring Cloud內置的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
搞清楚這點之后,我們就可以擼代碼了:
-
@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等數據。 -
return Ordered.LOWEST_PRECEDENCE; -
} -
-
@Override -
public boolean shouldFilter() { -
// 這里可以考慮弄個限流開啟的開關,開啟限流返回true,關閉限流返回false,你懂的。 -
return true; -
} -
-
@Override -
public Object run() { -
try { -
RequestContext context = RequestContext.getCurrentContext(); -
HttpServletResponse response = context.getResponse(); -
-
String key = null; -
// 對於service格式的路由,走RibbonRoutingFilter -
String serviceId = (String) context.get(SERVICE_ID_KEY); -
if (serviceId != null) { -
key = serviceId; -
map.putIfAbsent(serviceId, RateLimiter.create(1000.0)); -
} -
// 如果壓根不走RibbonRoutingFilter,則認為是URL格式的路由 -
else { -
// 對於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; -
} -
}
簡單講解一下這段代碼:
對於 microservice-provider-user 這個路由,我們可以用 context.get(SERVICE_ID_KEY); 獲取到serviceId,獲取出來就是 microservice-provider-user;
而對於 user2 這個路由,我們使用 context.get(SERVICE_ID_KEY); 獲得是null,但是呢,可以用 context.getRouteHost() 獲得路由到的地址,獲取出來就是 http://localhost:8000/ 。接下來的事情,你們懂的。
改進與提升
實際項目中,除以上實現的限流方式,還可能會:
一、在上文的基礎上,增加配置項,控制每個路由的限流指標,並實現動態刷新,從而實現更加靈活的管理
二、基於CPU、內存、數據庫等壓力限流(感謝平安常浩智)提出。。
下面,筆者借助Spring Boot Actuator提供的 Metrics 能力進行實現基於內存壓力的限流——當可用內存低於某個閾值就開啟限流,否則不開啟限流。
-
@Component -
public class RateLimitZuulFilter extends ZuulFilter { -
@Autowired -
private SystemPublicMetrics systemPublicMetrics; -
@Override -
public boolean shouldFilter() { -
// 這里可以考慮弄個限流開啟的開關,開啟限流返回true,關閉限流返回false,你懂的。 -
Collection<Metric<?>> metrics = systemPublicMetrics.metrics(); -
Optional<Metric<?>> freeMemoryMetric = metrics.stream() -
.filter(t -> "mem.free".equals(t.getName())) -
.findFirst(); -
// 如果不存在這個指標,穩妥起見,返回true,開啟限流 -
if (!freeMemoryMetric.isPresent()) { -
return true; -
} -
long freeMemory = freeMemoryMetric.get() -
.getValue() -
.longValue(); -
// 如果可用內存小於1000000KB,開啟流控 -
return freeMemory < 1000000L; -
} -
// 省略其他方法 -
}
三、實現不同維度的限流,例如:
-
對請求的目標URL進行限流(例如:某個URL每分鍾只允許調用多少次)
-
對客戶端的訪問IP進行限流(例如:某個IP每分鍾只允許請求多少次)
-
對某些特定用戶或者用戶組進行限流(例如:非VIP用戶限制每分鍾只允許調用100次某個API等)
-
多維度混合的限流。此時,就需要實現一些限流規則的編排機制。與、或、非等關系。
參考文檔
-
分布式環境下限流方案的實現:http://blog.csdn.net/Justnow_/article/details/53055299
