之前利用 Hystrix,通過(guò)熔斷器實(shí)現(xiàn)了通過(guò)某個(gè)閾值來(lái)對(duì)異常流量進(jìn)行降級(jí)處理。除了對(duì)異常流量進(jìn)行降級(jí)之外,還可以通過(guò) 流量排隊(duì)、限流、分流等操作,防止系統(tǒng)出錯(cuò)。
限流算法
限流算法一般分為 漏桶、令牌桶 兩種。
漏桶
漏桶的圓形是一個(gè)底部有漏孔的桶,桶的上方有一個(gè)入水口,水不斷流進(jìn)桶內(nèi),桶下方的漏孔會(huì)以一個(gè)相對(duì)恒定的速度漏水,在入大于出的情況下,桶在一段時(shí)間內(nèi)就會(huì)被裝滿,這時(shí)候多余的水就會(huì)溢出;而在入小于出的情況下,漏桶起不到任何作用。
當(dāng)請(qǐng)求或者具有一定體量的數(shù)據(jù)進(jìn)入系統(tǒng)時(shí),在漏桶作用下,流量被整形,不能滿足要求的部分被削減掉,漏桶算法能強(qiáng)制限定流量速度。溢出的流量可以被再次利用起來(lái),并非完全丟棄,可以把溢出的流量收集到一個(gè)隊(duì)列中,做流量排隊(duì),盡量合理利用所有資。

令牌桶
令牌桶與漏桶的區(qū)別是,桶里放的是令牌而不是流量,令牌以一個(gè)恒定的速度被加入桶內(nèi),可以積壓,可以溢出。當(dāng)流量涌入時(shí),量化請(qǐng)求用于獲取令牌,如果取到令牌則方形,同時(shí)桶內(nèi)丟掉這個(gè)令牌;如果取不到令牌,則請(qǐng)求被丟棄。
由于桶內(nèi)可以存一定量的令牌,那么就可能會(huì)解決一定程度的流量突發(fā)。這個(gè)也是漏桶與令牌桶的適用場(chǎng)景不同之處。

限流實(shí)例
在 Zuul 中實(shí)現(xiàn)限流,最簡(jiǎn)單的方式是使用 Filter 加上相關(guān)的限流算法,其中可能會(huì)考慮到 Zuul 多節(jié)點(diǎn)部署。因?yàn)樗惴ǖ脑?,這是需要一個(gè) K/V 存儲(chǔ)工具(Redis等)。
spring-cloud-zuul-ratelimit 是一個(gè)針對(duì) Zuul 的限流庫(kù)
限流粒度的策略:
- user:認(rèn)證用戶名或匿名,針對(duì)某用戶粒度進(jìn)行限流
- origin:客戶機(jī) ip,針對(duì)請(qǐng)求客戶機(jī) ip 粒度進(jìn)行限流
- url:特定 url,針對(duì)某個(gè)請(qǐng)求 url 粒度進(jìn)行限流
- serviceId:特定服務(wù),針對(duì)某個(gè)服務(wù) id 粒度進(jìn)行限流
限流粒度臨時(shí)變量存儲(chǔ)方式:
- IN_MEMORY:基于本地內(nèi)存,底層是 ConcurrentHashMap
- REDIS:基于 Redis K/V 存儲(chǔ)
- CONSUL:基于 Consul K/V 存儲(chǔ)
- JPA:基于 SpringData JPA,數(shù)據(jù)庫(kù)存儲(chǔ)
- BUKET4J:使用 Java 編寫的基于令牌桶算法的限流庫(kù),四種模:
JCache、Hazelcast、Apache Ignite、Inifinispan,后面三種支持異步
源碼:https://gitee.com/laiyy0728/spring-cloud/tree/master/spring-cloud-zuul/spring-cloud-zuul-ratelimit
Zuul Server
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>com.marcosbarbero.cloud</groupId>
<artifactId>spring-cloud-zuul-ratelimit</artifactId>
<version>2.0.6.RELEASE</version>
</dependency>
</dependencies>
server:
port: 5555
spring:
application:
name: spring-cloud-ratelimit-zuul-server
eureka:
instance:
instance-id: ${spring.application.name}:${server.port}
prefer-ip-address: true
client:
service-url:
defaultZone: http://localhost:8761/eureka/
zuul:
routes:
spring-cloud-ratelimit-provider-service:
path: /provider/**
serviceId: spring-cloud-ratelimit-provider-service
ratelimit:
key-prefix: springcloud # 按粒度拆分的臨時(shí)變量 key 的前綴
enabled: true # 啟用開關(guān)
repository: in_memory # key 的存儲(chǔ)類型,默認(rèn)是 in_memory
behind-proxy: true # 表示代理之后
default-policy:
limit: 2 # 在一個(gè)單位時(shí)間內(nèi)的請(qǐng)求數(shù)量
quota: 1 # 在一個(gè)單位時(shí)間內(nèi)的請(qǐng)求時(shí)間限制
refresh-interval: 3 # 單位時(shí)間窗口
type:
- user # 可指定用戶粒度
- origin # 可指定客戶端地址粒度
- url # 可指定 url 粒度
Provider
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
spring:
application:
name: spring-cloud-ratelimit-provider-service
server:
port: 7070
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
instance-id: ${spring.application.name}:${server.port}
驗(yàn)證
快速訪問(wèn)幾次 http://localhost:5555/provider/get-result ,返回值如下:
{
"timestamp": "2019-02-20T06:52:51.220+0000",
"status": 429,
"error": "Too Many Requests",
"message": "429"
}
控制臺(tái)打印異常如下:
com.netflix.zuul.exception.ZuulException: 429
at com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.RateLimitExceededException.<init>(RateLimitExceededException.java:13) ~[spring-cloud-zuul-ratelimit-core-2.0.6.RELEASE.jar:2.0.6.RELEASE]
at com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.filters.RateLimitPreFilter.lambda$run$0(RateLimitPreFilter.java:106) ~[spring-cloud-zuul-ratelimit-core-2.0.6.RELEASE.jar:2.0.6.RELEASE]
at java.util.ArrayList.forEach(ArrayList.java:1257) ~[na:1.8.0_171]
at com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.filters.RateLimitPreFilter.run(RateLimitPreFilter.java:79) ~[spring-cloud-zuul-ratelimit-core-2.0.6.RELEASE.jar:2.0.6.RELEASE]
at com.netflix.zuul.ZuulFilter.runFilter(ZuulFilter.java:117) ~[zuul-core-1.3.1.jar:1.3.1]
at com.netflix.zuul.FilterProcessor.processZuulFilter(FilterProcessor.java:193) ~[zuul-core-1.3.1.jar:1.3.1]
at com.netflix.zuul.FilterProcessor.runFilters(FilterProcessor.java:157) ~[zuul-core-1.3.1.jar:1.3.1]
...
正常訪問(wèn)結(jié)果如下:
zuul rate limit result !
動(dòng)態(tài)路由
之前配置路由映射規(guī)則的方式,為“靜態(tài)路由”。如果在迭代過(guò)程中,可能需要?jiǎng)討B(tài)將路由映射規(guī)則寫入內(nèi)存。在“靜態(tài)路由”配置中,需要重啟 Zuul 應(yīng)用。
不需要重啟 Zuul,又能修改映射規(guī)則的方式,稱為“動(dòng)態(tài)路由”。
- SpringCloud Config + Bus,動(dòng)態(tài)刷新配置文件。好處是不用 Zuul 維護(hù)映射規(guī)則,可以隨時(shí)修改,隨時(shí)生效。缺點(diǎn)是需要單獨(dú)集成一些使用并不頻繁的組件。SpringCloud Config 沒(méi)有可視化界面,維護(hù)也麻煩
- 重寫 Zuul 配置讀取方式,采用事件刷新機(jī)制,從數(shù)據(jù)庫(kù)讀取路由映射規(guī)則。此方式基于數(shù)據(jù)庫(kù),可輕松實(shí)現(xiàn)管理頁(yè)面,靈活度高。
動(dòng)態(tài)路由實(shí)現(xiàn)原理

DiscoveryClientRouteLocator
public class DiscoveryClientRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {
// 省略其他方法
// 路由
protected LinkedHashMap<String, ZuulRoute> locateRoutes() {
// 省略方法實(shí)現(xiàn)
}
// 刷新
public void refresh() {
this.doRefresh();
}
}
locateRoutes 方法繼承自 SimpleRouteLocator 類,并重寫規(guī)則,該方法主要的功能就是將配置文件中的映射規(guī)則信息包裝成 LinkedHashMap<String, ZuulRoute>,鍵為路徑 path,值 ZuulRoute 是配置文件的封裝類。之前的映射配置信息就是使用 ZuulRoute 封裝的。
refresh 實(shí)現(xiàn)自 RefreshableRouteLocator 接口,添加刷新功能必須實(shí)現(xiàn)此方法,doRefresh 方法來(lái)自 SimpleRouteLocator 類
SimpleRouteLocator
SimpleRouteLocator 是 DiscoveryClientRouteLocator 的父類,此類基本實(shí)現(xiàn)了 RouteLocator 接口,對(duì)讀取配置文件信息做一些處理,提供方法 doRefresh、locateRoutes 供子類實(shí)現(xiàn)刷新策略與映射規(guī)則加載策略
/**
* Calculate all the routes and set up a cache for the values. Subclasses can call
* this method if they need to implement {@link RefreshableRouteLocator}.
*/
protected void doRefresh() {
this.routes.set(locateRoutes());
}
/**
* Compute a map of path pattern to route. The default is just a static map from the
* {@link ZuulProperties}, but subclasses can add dynamic calculations.
*/
protected Map<String, ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<>();
for (ZuulRoute route : this.properties.getRoutes().values()) {
routesMap.put(route.getPath(), route);
}
return routesMap;
}
這兩個(gè)方法都是 protectted 修飾,是為了讓子類不用維護(hù)此類一些成員變量就能實(shí)現(xiàn)刷新或讀取路由的功能。從注釋上可以看到,調(diào)用 doRedresh 方法需要實(shí)現(xiàn) RefreshableRouteLocator;locateRoutes 默認(rèn)是一個(gè)靜態(tài)的映射讀取方法,如果需要?jiǎng)討B(tài)記載映射,需要子類重寫此方法。
ZuulServerAutoConfiguration
ZuulServerAutoConfiguration 是 Spring Cloud Zuul 的配置類,主要目的是注冊(cè)各種過(guò)濾器、監(jiān)聽器以及其他功能。Zuul 在注冊(cè)中心新增服務(wù)后刷新監(jiān)聽器也是在這個(gè)類中注冊(cè)的,底層是 Spring 的 ApplicationListener
@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass(ZuulServlet.class)
@ConditionalOnBean(ZuulServerMarkerConfiguration.Marker.class)
public class ZuulServerAutoConfiguration {
// 省略其他功能注冊(cè)
// Zuul 刷新監(jiān)聽器
private static class ZuulRefreshListener
implements ApplicationListener<ApplicationEvent> {
@Autowired
private ZuulHandlerMapping zuulHandlerMapping;
private HeartbeatMonitor heartbeatMonitor = new HeartbeatMonitor();
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextRefreshedEvent
|| event instanceof RefreshScopeRefreshedEvent
|| event instanceof RoutesRefreshedEvent
|| event instanceof InstanceRegisteredEvent) {
reset();
}
else if (event instanceof ParentHeartbeatEvent) {
ParentHeartbeatEvent e = (ParentHeartbeatEvent) event;
resetIfNeeded(e.getValue());
}
else if (event instanceof HeartbeatEvent) {
HeartbeatEvent e = (HeartbeatEvent) event;
resetIfNeeded(e.getValue());
}
}
private void resetIfNeeded(Object value) {
if (this.heartbeatMonitor.update(value)) {
reset();
}
}
private void reset() {
this.zuulHandlerMapping.setDirty(true);
}
}
}
其中,由方法 onApplicationEvent可知,Zuul 會(huì)接收 4 種事件通知 ContextRefreshedEvent、RefreshScopeRefreshedEvent、RoutesRefreshedEvent、InstanceRegisteredEvent,這四種通知都會(huì)去刷新路由映射配置信息,此外,心跳續(xù)約監(jiān)視器 HeartbeatEvent 也會(huì)觸發(fā)這個(gè)動(dòng)作
ZuulHandlerMapping
在 ZuulServerAutoConfiguration#ZuulRefreshListener 中,注入了 ZuulHandlerMapping,此類是將本地配置的映射關(guān)系,映射到遠(yuǎn)程的過(guò)程控制器
/**
* MVC HandlerMapping that maps incoming request paths to remote services.
*/
public class ZuulHandlerMapping extends AbstractUrlHandlerMapping {
// 省略其他配置
private volatile boolean dirty = true;
public void setDirty(boolean dirty) {
this.dirty = dirty;
if (this.routeLocator instanceof RefreshableRouteLocator) {
((RefreshableRouteLocator) this.routeLocator).refresh();
}
}
}
dirty 屬性很重要,它是用來(lái)控制當(dāng)前是否需要重新加載映射配置信息的標(biāo)記,在 Zuul 每次進(jìn)行路由操作的時(shí)候都會(huì)檢查這個(gè)值。如果為 true,則會(huì)觸發(fā)配置信息的重新加載,同時(shí)再將其審核制為 false。由 setDirty 方法體可知,啟動(dòng)刷新動(dòng)作必須實(shí)現(xiàn) RefreshableRouteLocator,否則會(huì)出現(xiàn)類轉(zhuǎn)換異常。