在Spring Cloud微服务系统中,一种常见的负载均衡方式是,客户端的请求首先经过负载均衡(apache、Ngnix),再到达服务网关(zuul集群),然后再到具体的服。

Zuul的主要功能是路由转发和过滤器。路由功能是微服务的一部分,比如/api/user转发到到user服务,/api/shop转发到到shop服务。zuul默认和Ribbon结合实现了负载均衡的功能

在前文的基础上,重新创建一个工程,加入以下依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>com.marcosbarbero.cloud</groupId>
<artifactId>spring-cloud-zuul-ratelimit</artifactId>
<version>1.3.4.RELEASE</version>
</dependency>

在主类上加入注解@EnableZuulProxy@EnableEurekaClient,开启zuul的功能

1
2
3
4
5
6
7
8
@EnableZuulProxy
@EnableEurekaClient
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}

编辑配置文件application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
port: 8083
spring:
application:
name: gateway
zuul:
routes:
service-sample:
path: /sample/**
serviceId: sample
service-1:
path: /service1/**
serviceId: service1

在上面配置中,指定网关服务名为gateway,运行在8083端口,配置了两条路由规则。以/sample/开头的请求都转发到sample服务,以/service1/的请求都转发到service1服务。
在浏览器访问localhost:8083/sample/param?a=1,返回结果

1
{"a":1,"msg":"请求成功"}

证明zuul起到了路由的作用。

过滤器
zuul的过滤器不是web服务的过滤器,它能实现如权限验证,身份校验,负载均衡、限流等一系列功能,一个简单的过滤器如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Component
public class AccessFilter extends ZuulFilter{
@Value("${resource.exception.extension}")
private String exceptionExtension;
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
if(StringUtils.isBlank(exceptionExtension)){
return true;
}
RequestContext ctx = RequestContext.getCurrentContext();
String requestPath = ctx.getRequest().getRequestURI();
String extension = UriUtils.extractFileExtension(requestPath);
if(StringUtils.isBlank(extension)||StringUtils.equals(extension,"action")){
return true;
}
String []extensions = exceptionExtension.split(",");
for(String ex:extensions){
if(StringUtils.equals(ex,extension)){
return false;
}
}
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
try {
HttpServletRequest request = ctx.getRequest();
String token = request.getParameter("token");
IMapEntry params = HttpServletUtil.getRequestParameters(ctx);
if(StringUtils.isBlank(token)){
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(200);
IMapEntry result = ResultUtil.resultFailure(CommonConstant.RESULT_CODE_FAIL,"token不能为空");
ctx.setResponseBody(JSONUtil.toJSONString(result));
}
}catch (Exception e){
DICLogger.error("在权限验证时发生错误",e);
ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
ctx.set("error.exception", e);
ctx.set("error.message", "在权限验证时发生错误");
}
return null;
}
}

上面实现的是一个访问的过滤器,作用是拦截接口请求,对rest请求和后缀是.action的请求做拦截,校验token。局部变量exceptionExtension是例外列表,是从配置文件读取的,指定的文件不做过滤如jsp,css,js等静态文件。请求的判断逻辑在shouldFilter方法中。在run方法中写token校验逻辑,这里只是简单的判断了以下非空。

熔断处理

zuul转发请求是通过ribbon请求指定的服务,也会存在服务不可用的情况,zuul自己做了熔断,但是返回的数据格式不一定是我们需要的格式,我们可以自己写熔断逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Component
public class HystrixFallback implements ZuulFallbackProvider{
@Override
public String getRoute() {
return "*";
}
@Override
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
MediaType type = new MediaType("application","json",Charset.forName("UTF-8"));
headers.setContentType(type);
return headers;
}
@Override
public InputStream getBody() throws IOException {
IMapEntry result = ResultUtil.resultFailure(CommonConstant.RESULT_CODE_FAIL,"服务不可用,请稍后重试");
return new ByteArrayInputStream(JSONUtil.toJSONString(result).getBytes("UTF-8"));
}
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return this.getStatusCode().value();
}
@Override
public String getStatusText() throws IOException {
return this.getStatusCode().getReasonPhrase();
}
@Override
public void close() {
}
};
}
}

getRoute方法返回一个路由名(如上面的service-sample或service-1),指定该熔断是处理哪一个服务的请求,返回null*表示处理所有的服务请求。

统一错误处理

zuul同样统一处理了错误,在网上的一些版本中,需要写一个error过滤器,在新版本中已经不需要了,只需要实现一个bean就行,具体可参考 Spring Cloud Zuul中异常处理细节

1
2
3
4
5
6
7
8
9
10
@Component
public class CustomerErrorMsg extends DefaultErrorAttributes{
@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
Map<String, Object> result = super.getErrorAttributes(requestAttributes,includeStackTrace);
result.put("code",result.get("status"));
result.put("msg",result.get("error"));
return result;
}
}

在上面的例子中,重写了getErrorAttributes方法,在原来的错误消息格式中,新加了两个字段code和msg。通过过滤器的方式可以参考这篇博客Spring Cloud实战小贴士:Zuul统一异常处理(三)【Dalston版】

限流

zuul限流使用了GitHub上的一个模块spring-cloud-zuul-ratelimit,具体可参考文档

最后附上我的zuul配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#路由
zuul:
add-host-header: true
routes:
gatewayService:
path: /gateway/**
url: forward:/gateway #将请求转到mapping为gateway的本地controller
service1:
path: /service1/**
serviceId: service1
#限流
ratelimit:
enabled: true
default-policy: #默认限流策略
limit: 10 #限制每个IP、用户、url每分钟只能请求10次
quota: 1000 #限制每个路径每分钟只能被请求1000次
policies:
service1: #针对service1限流
type: url #通过请求路径区分
quota: 2000
max:
host:
connections: 1000 #网关最大连接数量
host:
socket-timeout-millis: 3000
connect-timeout-millis: 1000
#负载均衡
ribbon:
ReadTimeout: 1000
ConnectTimeout: 500
OkToRetryOnAllOperations: true #对所有操作进行重试
MaxAutoRetries: 0 #不对当前实例重试
MaxAutoRetriesNextServer: 1 #对下个实例进行一次重试
#断路器
hystrix:
command:
default:
execution:
timeout:
enabled: true
isolation:
thread:
timeoutInMilliseconds: 5000 #配置断路器的超时时间,必须超过ribbon的超时时间,否则重试将失效
resource:
exception:
#不被拦截的资源后缀
extension: html,jsp,css,js,json,txt,jpg,jpeg,png,bmp,svg,woff,eot,ttf,md,zip,doc,docx,ppt,pptx,xls,xlsx