API网关之Zuul

引言

Zuul is the front door for all requests from devices and web sites to the backend of the Netflix streaming application. As an edge service application, Zuul is built to enable dynamic routing, monitoring, resiliency and security. It also has the ability to route requests to multiple Amazon Auto Scaling Groups as appropriate.

—— Zuul 官方wiki

Zuul 简介

Zuul 是 Netflix OOS 中的一员,是一个基于 JVM 路由和服务端的负载均衡。提供路由、监控、弹性、安全等方面的服务框架。Zuul 能够与 Eureka 、Ribbon 、Hystrix 等组件配合使用。

Zuul 的核心是过滤器,通过这些过滤器我们可以扩展出很多功能,比如:

  • 动态路由:动态的将客户端的请求路由到后端的不同服务,做一些逻辑处理,比如聚合多个服务的数据返回。
  • 请求监控:可以对整个系统的请求进行监控,记录详细的请求响应日志,可以实时统计出当前系统的访问量以及监控状态。
  • 认证鉴权:对每一个访问的请求做认证,拒绝非法请求,保护好后端的服务。
  • 压力测试:压力测试是一项很重要的工作,像电商公司需要模拟更多的真实的用户并发量来保证重大活动时系统的稳定。通过 Zuul 可以动态地将请求转发到后端服务的集群中,还可以识别测试流量和真实流量,从而做一些特殊处理。
  • 灰度发布:灰度发布可以保证系统的稳定,在初始灰度的时候就可以发现、调整问题、以保证其影响度。

——《spring cloud 微服务 入门、进阶与实战》第103页

使用 Zuul 构建微服务网关

简单使用

建立一个 spring cloud 项目。配置如下:

  • pom添加依赖
1
2
3
4
 <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
  • properties 配置:
1
2
3
4
5
6
7
spring.application.name=zuul-demo
server.port=2103

#拦截匹配了 /demo1/** 规则的url 转发到 https://blog.wu-zy.com/
#这里 routes.demo1 的 demo1 是自定义的
zuul.routes.demo1.path=/demo1/**
zuul.routes.demo1.url=https://blog.wu-zy.com/
  • 启动类加上 @EnableZuulProxy 注解
1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableZuulProxy
public class ZuulDemoApplication {

public static void main(String[] args) {
SpringApplication.run(ZuulDemoApplication.class, args);
}
}

启动项目我们访问:http://localhost:2103/demo1/myblog

结果跳转到了我的博客:https://blog.wu-zy.com/myblog/

集成 Eurekas

结合 Eureka 实现动态路由

  • 加入 Eureka 的依赖
1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
  • 配置注册中心
1
2
#注册中心集群
eureka.client.serviceUrl.defaultZone=http://WuZhiYong:123456@localhost:8761/eureka/,http://WuZhiYong:123456@localhost:8762/eureka/
我们可通过: API网关地址 + 访问的服务实例的名称 + 服务接口的URL

我这里注册中心有个 eureka-client-service 服务 并且 该服务有 /user/hello 接口

我们可通过 网关这样调用。

http://localhost:2103/eureka-client-service/user/hello

Zuul 路由配置

  1. 指定具体服务的路由

配置:

1
zuul.routes.eureka-client-service.path=/api-user/**

访问 http://localhost:2103/api-user/user/hello 即可调用 eureka-client-service 的接口了。

以前面的配置为例:

zuul.routes.demo1.path=/demo1/**
zuul.routes.demo1.url=https://blog.wu-zy.com/

这里给我的感觉是:如果我们只配置了path zuul 会尝试将 demo1 尝试当作 服务实例名进行解析,如果我们配置了 url zuul 就会根据我们指定的 url 进行转发。

zuul.routes.${server_name}.path=/demo1/**
zuul.routes.${server_name}.url=https://blog.wu-zy.com/

  1. 路由前缀

对前缀的设置,分为 统一前缀 和每条转发路线的前缀

1
2
3
4
5
6
7
8
9
10
11
12
#统一前缀 (所有请求进来的 地址 必须满足 /user 前缀)
zuul.prefix=/user
#经过 zuul 后实际的请求 是否跳过(去除)统一前缀 默认为true
zuul.strip-prefix=false

zuul.routes.demo1.path=/demo1/**
zuul.routes.demo1.url=https://blog.wu-zy.com/

#对某个服务进行转发
zuul.routes.eureka-client-service.path=/api-user/**
#经过 zuul 后实际请求是否跳过(去除)path 所匹配的前缀 /api-user/ 默认为 true
zuul.routes.eureka-client-service.strip-prefix=true
  1. 本地跳转
  • 为了逻辑简单我们去掉统一的前缀
1
2
3
4
5
6
#统一前缀
#zuul.prefix=/user
#zuul.strip-prefix=false

zuul.routes.demo1.path=/demo1/**
zuul.routes.demo1.url=forward:/local
  • 添加本地的控制器代码
1
2
3
4
5
6
7
8
9
@RestController
public class LocalController {

@GetMapping("/local/{id}")
public String local(@PathVariable String id) {
return id;
}

}
  • 测试

image-20200511112353620

Zuul 中的过滤器

本文简介中也说到 zuul 中的很多高级功能都是通过 zuul 的过滤器实现的。与我们了解的传统 servlet 过滤器不同的是 zuul 中的过滤器分为 4 种。每一种都有对应的使用场景。

过滤器类型

pre:可以在请求被路由之前调用。适用于身份认证的场景,认证通过后再继续执行下面的流程。

route:在路由请求时被调用。适用于灰度发布场景,在将要路由的时候可以做一些自定义的逻辑。

post:在 route和eror过滤器之后被调用。这种过滤器将请求路由到达具体的服务之后执行。适用于需要添加响应头,记录响应日志等应用场景。

eror:处理请求时发生错误时被调用。在执行过程中发送错误时会进入eror过滤器,可以用来统一记录错误信息。

——《spring cloud 微服务 入门、进阶与实战》第107页

请求生命周期

img

通过上面的图可以清楚地知道整个执行的顺序,请求发过来首先到pre过滤器,再到routing过滤器,最后到post过滤器,任何一个过滤器有异常都会进入eror过滤器。

在源码中也可看到这种执行顺序:

源码包地址:com.netflix.zuul.http.ZuulServlet

使用过滤器
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
58
public class MyPreFilter extends ZuulFilter {
/** IP黑名单列表 */
private List<String> blackIpList = Arrays.asList("127.0.0.1");

/**
* 过滤器类型
* @return 可选值有pre、 route、post、 error
*/
@Override
public String filterType() {
return "pre";
}

/**
* 过滤器的执行顺序,数值越小,优先级越高。
* @return
*/
@Override
public int filterOrder() {
return 1;
}

/**
* 是否执行该过滤器,
* 这个也可以利用配置中心来实现,达到动态的开启和关闭过滤器
* @return tue为执行, false为不执行
*/
@Override
public boolean shouldFilter() {
return true;
}

/**
* 执行自己的业务逻辑,本段代码中是通过判断请求的IP是否在黑名单中,
* 决定是否进行拦截。 blacklist字段是IP的黑名单,判断条件成立之后,
* 通过设置ctx.setsendzuulresponse( false),
* 告诉Zul不需要将当前请求转发到后端的服务了。
* 通过 setresponse Body返回数据给客户端。
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
//System.err.println(2/0);
RequestContext ctx = RequestContext.getCurrentContext();
String ip = IpUtils.getIpAddr(ctx.getRequest());
// 在黑名单中禁用
if (StringUtils.isNotBlank(ip) && blackIpList.contains(ip)) {
ctx.setSendZuulResponse(false);
ctx.set("sendForwardFilter.ran", true);
ResponseData data = ResponseData.fail("非法请求", ResponseCode.NO_AUTH_CODE.getCode());
ctx.setResponseBody(JsonUtils.toJson(data));
ctx.getResponse().setContentType("application/json; charset=utf-8");
return null;
}
return null;
}
}

过滤器定义完成之后我们需要配置过滤器才能生效,

1
2
3
4
5
6
7
8
@Configuration
public class FilterConfig {

@Bean
public MyPreFilter ipFilter() {
return new MyPreFilter();
}
}
禁用过滤器
  • 利用 shouldFilter 方法中的 return false 让过滤器不再执行
  • 利用配置文件方式。格式为:“zuul.过滤器类名.过滤器类型.disable=true” 例:
1
2
#禁用过滤器
zuul.MyPreFilter.pre.disable=true
过滤器中传递数据

在前面的过滤器中我们可以这样存入数据:

1
2
3
4
5
6
7
import com.netflix.zuul.context.RequestContext;

...
...

RequestContext ctx = RequestContext.getCurrentContext();
ctx.set("msg","hello");

在后面的过滤器中我们可以这样取:

1
2
RequestContext ctx = RequestContext.getCurrentContext();
ctx.get("msg");
过滤器拦截请求

拦截和返回信息从前面代码里就可以看出:

1
2
3
4
5
6
7
8
//告诉 zuul 不需要将当前请求转发到后端服务
ctx.setSendZuulResponse(false);
//用来拦截本地转发请求的。当我们设置了 forward:/local 的路由,ctx.setSendZuulResponse(false)
//对 forward 是不起作用的,需要设置 ctx.set("sendForwardFilter.ran", true); 才行。
ctx.set("sendForwardFilter.ran", true);
ctx.setResponseBody("返回信息");
//ctx.getResponse().setContentType("application/json; charset=utf-8");
return null;

由于 zuul 拦截器和普通拦截器的逻辑的不同,如果 zuul 中有多个拦截器,即使 我们设置了 ctx.setSendZuulResponse(false); 和 ctx.set(“sendForwardFilter.ran”, true);。虽然请求最终不会转发到后端服务,但在后面的拦截器依然会执行。

如果我们不想让后面的拦截器执行,可通过 转递数据的方法 例如:

1
2
3
//在前面的拦截器处理后 设置
RequestContext ctx = RequestContext.getCurrentContext();
ctx.set("is_success",false);

在后面的拦截器 shouldFilter 方法中

1
2
3
4
5
6
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
Object success = ctx.get("is_success");
return success == null ? true : Boolean.parseBoolean(success.toString());
}
过滤器中的异常处理

对于异常来说,无论在哪个地方都需要处理。过滤器中的异常主要发生在run方法中,可以用 try catch来处理。Zuul中也为我们提供了一个异常处理的过滤器,当过滤器在执行过程中发生异常,若没有被捕获到,就会进入 error过滤器中。

我们可以定义一个eror过滤器来记录异常信息

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
public class ErrorFilter extends ZuulFilter {

private Logger log = LoggerFactory.getLogger(ErrorFilter.class);

@Override
public String filterType() {
return "error";
}

@Override
public int filterOrder() {
return 100;
}

@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
Throwable throwable = ctx.getThrowable();
log.error("Filter Erroe : {}", throwable.getCause().getMessage());
return null;
}

}

并在 ipFilter run 方法中 System.err.println(2/0); 这行代码取消注释。重启项目,在访问接口:

得到响应:

image-20200511134331169

这种情况自然不符合和统一接口规范

在 《spring cloud 微服务 入门、进阶与实战》书中说这种情况下使用 @ControllerAdvice 方式也是无效的,并解释:@ControllerAdvice 注解主要是针对 Controller 中的方法做处理的,作用于 @RequestMappping 标注的方法上,只针对与我们定义的方法有效,在 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
@RestController
public class ErrorHandlerController implements ErrorController {

@Autowired
private ErrorAttributes errorAttributes;

@Override
public String getErrorPath() {
return "/error";
}

@RequestMapping("/error")
public ResponseData error(HttpServletRequest request) {
Map<String, Object> errorAttributes = getErrorAttributes(request);
String message = (String) errorAttributes.get("message");
String trace = (String) errorAttributes.get("trace");
if (StringUtils.isNotBlank(trace)) {
message += String.format(" and trace %s", trace);
}
return ResponseData.fail(message, ResponseCode.SERVER_ERROR_CODE.getCode());
}

private Map<String, Object> getErrorAttributes(HttpServletRequest request) {
return errorAttributes.getErrorAttributes(new ServletWebRequest(request), true);
}

}

再次请求发现返回了我们想要的格式的数据了。

Zuul 容错和回退

容错机制

当某个服务不可用的时候能够切换到其他可用的服务上去。也就是需要重试机制。在 zuul 中开启重试机制需要依赖于 spring-retry

1
2
3
4
 <dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>

properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#开启重试
zuul.retryable=true
#请求链接的超时时间
ribbon.ConnectTimeout=1000
#请求处理的超时时间
ribbon.ReadTimeout=1000
#对当前实例的重试次数
ribbon.MaxAutoRetries=1
#切换实例的最大重试次数
ribbon.MaxAutoRetriesNextServer=3
#对所有操作请求都进行重试
ribbon.OkToRetryOnAllOperations=true
#对指定的 http 响应码进行重试
ribbon.retryableStatusCodes=500,404,502

测试:

服务提供方的集群我们启动多个服务,通过 zuul 不停调用,期间停掉集群中一台机器。发现确实达到了容错的目的。

回退机制

在 spring cloud 中,zuul 默认整合了 hystrix ,当后端服务异常时可以为 zuul 添加回退的功能,返回默认的数据给客户端。

实现回退机制需要实现 FallbackProvider 接口

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import com.study.base.ResponseCode;
import com.study.base.ResponseData;
import com.study.utils.JsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;

/**
* @ClassName ServiceConsumerFallbackProvider
* @Author wuzhiyong
* @Date 2020/5/11 15:07
* @Version 1.0
**/

@Component
public class ServiceConsumerFallbackProvider implements FallbackProvider {

private Logger log = LoggerFactory.getLogger(ServiceConsumerFallbackProvider.class);

/**
* 匹配回退服务
* @return 返回 * 表示对所有服务进行回退操作。如果只想对某个服务进行回退,
* 那么就返回需要回退的服务名称,这个名称一定要注册到 Eureka 中的名称
*/
@Override
public String getRoute() {
return "*";
}

/**
* 构造回退内容
* @param route
* @param cause
* @return
*/
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return new ClientHttpResponse() {
/**
* 返回响应的 状态码
* @return
* @throws IOException
*/
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}

@Override
public int getRawStatusCode() throws IOException {
return this.getStatusCode().value();
}

/**
* 返回状态码对应的文本
* @return
* @throws IOException
*/
@Override
public String getStatusText() throws IOException {
return this.getStatusCode().getReasonPhrase();
}

@Override
public void close() {

}

/**
* 返回回退的内容
* @return
* @throws IOException
*/
@Override
public InputStream getBody() throws IOException {
if (cause != null) {
log.error("", cause.getCause());
}
ResponseData data = ResponseData.fail(route+"服务内部错误", ResponseCode.SERVER_ERROR_CODE.getCode());
return new ByteArrayInputStream(JsonUtils.toJson(data).getBytes());
}

/**
* 返回响应的请求头信息
* @return
*/
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
MediaType mt = new MediaType("application", "json", Charset.forName("UTF-8"));
headers.setContentType(mt);
return headers;
}
};
}
}

重启 zuul 后我们停掉集群服务。再通过 zuul 访问接口。发现返回我们想要的数据了

1
2
3
4
5
{
"code": 500,
"message": "eureka-client-service服务内部错误",
"data": null
}

Zuul 使用小经验

结合 Actuator 查看相关信息

zuul 在结合 Actuator 使用的时候可以暴露一些端点来方面我们来查看管理 zuul 路由。

首先我们添加 Actuator 并配置暴露出所有端点。

1
2
3
4
 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
1
2
#暴露所有端点
management.endpoints.web.exposure.include=*
  • /routes 端点

访问:

1
http://localhost:2103/actuator/routes

响应:

1
2
3
4
5
{
"/demo1/**": "forward:/local",
"/api-server/**": "eureka-client-service",
"/eureka-client-service/**": "eureka-client-service"
}
  • /filters 端点

访问:

1
http://localhost:2103/actuator/filters

响应:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
{
"error": [
{
"class": "org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter",
"order": 0,
"disabled": false,
"static": true
}
],
"post": [
{
"class": "org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter",
"order": 1000,
"disabled": false,
"static": true
}
],
"pre": [
{
"class": "org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter",
"order": -2,
"disabled": false,
"static": true
},
{
"class": "org.springframework.cloud.netflix.zuul.filters.pre.ServletDetectionFilter",
"order": -3,
"disabled": false,
"static": true
},
{
"class": "org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter",
"order": 5,
"disabled": false,
"static": true
},
{
"class": "org.springframework.cloud.netflix.zuul.filters.pre.DebugFilter",
"order": 1,
"disabled": false,
"static": true
},
{
"class": "org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter",
"order": -1,
"disabled": false,
"static": true
},
{
"class": "com.study.customFilter.MyRouteFilter",
"order": 2,
"disabled": false,
"static": true
},
{
"class": "com.study.customFilter.MyPreFilter",
"order": 1,
"disabled": false,
"static": true
}
],
"route": [
{
"class": "org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter",
"order": 100,
"disabled": false,
"static": true
},
{
"class": "org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter",
"order": 10,
"disabled": false,
"static": true
},
{
"class": "org.springframework.cloud.netflix.zuul.filters.route.SendForwardFilter",
"order": 500,
"disabled": false,
"static": true
}
]
}

文件上传

创建一个 spring cloud 应用 zuul-file-demo。并注册到 Eureka

文件上传控制器接口

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class FileController {

@PostMapping("/file/upload")
public String fileUpload(@RequestParam(value = "file") MultipartFile file) throws IOException {
byte[] bytes = file.getBytes();
File fileToSave = new File(file.getOriginalFilename());
FileCopyUtils.copy(bytes, fileToSave);
return fileToSave.getAbsolutePath();
}

}

使用 postman 上传测试。并成功返回了文件路径

image-20200511160023555

然后换个大文件 35M。上传失败了

image-20200511160405668

在 zuul-demo 和 zuul-file-demo 都加上

1
2
spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=100MB

在 zuul-demo 调整

1
2
3
4
#请求链接的超时时间
ribbon.ConnectTimeout=90000
#请求处理的超时时间
ribbon.ReadTimeout=90000

重启两个服务再次测试便成功了。

  • 绕过 zuul 上传文件:

在请求地址前面加上 /zuul 就可以绕过 zuul-demo 服务,这样就可以不用配置文件大小。但接收文件的服务 zuul-file-demo 还是得配置上

访问:

1
http://localhost:2103/zuul/zuul-file-demo/file/upload

注意:在Hystrix 隔离模式为线程模式下。需要设置 Hystrix 线程的超时时间。

execution.isolation.thread.timeoutInMilliseconds (详细参看 Hystrix服务容错 文章)

请求响应详细信息输出

实际开发当中,详细的日志能够帮助我们快速排查和定位系统的问题。在 zuul 中最适合的方式就是通过post 过滤器来实现输出详细日志。

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.List;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.io.IOUtils;

import com.netflix.util.Pair;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.springframework.cloud.netflix.ribbon.RibbonHttpResponse;
/**
* @ClassName DebugRequestFilter
* @Author wuzhiyong
* @Date 2020/5/11 16:33
* @Version 1.0
**/
public class DebugRequestFilter extends ZuulFilter {

@Override
public String filterType() {
return "post";
}

@Override
public int filterOrder() {
return 1;
}

@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() {
HttpServletRequest req = (HttpServletRequest)RequestContext.getCurrentContext().getRequest();
System.err.println("REQUEST:: " + req.getScheme() + " " + req.getRemoteAddr() + ":" + req.getRemotePort());
StringBuilder params = new StringBuilder("?");
// 获取URL参数
Enumeration<String> names = req.getParameterNames();
if( req.getMethod().equals("GET") ) {
while (names.hasMoreElements()) {
String name = (String) names.nextElement();
params.append(name);
params.append("=");
params.append(req.getParameter(name));
params.append("&");
}
}
if (params.length() > 0) {
params.delete(params.length()-1, params.length());
}
System.err.println("REQUEST:: > " + req.getMethod() + " " + req.getRequestURI() + params + " " + req.getProtocol());
Enumeration<String> headers = req.getHeaderNames();
while (headers.hasMoreElements()) {
String name = (String) headers.nextElement();
String value = req.getHeader(name);
System.err.println("REQUEST:: > " + name + ":" + value);
}
final RequestContext ctx = RequestContext.getCurrentContext();
// 获取请求体参数
if (!ctx.isChunkedRequestBody()) {
ServletInputStream inp = null;
try {
inp = ctx.getRequest().getInputStream();
String body = null;
if (inp != null) {
body = IOUtils.toString(inp);
System.err.println("REQUEST:: > " + body);
}
} catch (IOException e) {
e.printStackTrace();
}
}

List<Pair<String, String>> headerList = RequestContext.getCurrentContext().getOriginResponseHeaders();
for (Pair<String, String> pair : headerList) {
System.err.println("RESPONSE HEADER:: > " +pair.second());
}
// 第一种,获取响应结果
try {
Object zuulResponse = RequestContext.getCurrentContext().get("zuulResponse");
if (zuulResponse != null) {
RibbonHttpResponse resp = (RibbonHttpResponse) zuulResponse;
String body = IOUtils.toString(resp.getBody());
System.err.println("RESPONSE:: > " + body);
resp.close();
RequestContext.getCurrentContext().setResponseBody(body);
}
} catch (IOException e) {
e.printStackTrace();
}

// 第二种,获取响应结果
// InputStream stream = RequestContext.getCurrentContext().getResponseDataStream();
// try {
// if (stream != null) {
// String body = IOUtils.toString(stream);
// System.err.println("RESPONSE:: > " + body);
// RequestContext.getCurrentContext().setResponseBody(body);
// }
//
// } catch (IOException e) {
// e.printStackTrace();
// }
return null;
}
}

控制台输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
REQUEST:: http 0:0:0:0:0:0:0:1:9596
REQUEST:: > GET /api-server/user/hello HTTP/1.1
REQUEST:: > content-type:application/json
REQUEST:: > user-agent:PostmanRuntime/7.24.1
REQUEST:: > accept:*/*
REQUEST:: > cache-control:no-cache
REQUEST:: > postman-token:9d887b8d-42d2-4787-bae8-11c63447be8c
REQUEST:: > host:localhost:2103
REQUEST:: > accept-encoding:gzip, deflate, br
REQUEST:: > connection:keep-alive
REQUEST:: > content-length:28
REQUEST:: > {"username":"22","pwd":"33"}
RESPONSE HEADER:: > eureka-client-service
RESPONSE HEADER:: > text/plain;charset=UTF-8
RESPONSE HEADER:: > 5
RESPONSE HEADER:: > Mon, 11 May 2020 08:37:33 GMT
RESPONSE HEADER:: > timeout=60
RESPONSE HEADER:: > keep-alive
RESPONSE:: > hello

Zuul 高可用

跟业务相关的服务我们都是注册到 Eureka 中,通过 Ribbon 来进行负载均衡,服务可以通过水平扩展来实现高可用。现实使用中,API网关这层往往是给 APP、 Webapp、客户来调用接口的,如果我们将 Zuul 也注册到 Eureka 中是达不到高可用的,因为你不可能让你的客户也去操作你的注册中心。这时最好的办法就是用额外的负载均衡器来实现 Zuul 的高可用,比如我们最常用的 Nginx,或者 Haproxy、F5等。

这种方式也是单体项目最常用的负载方式,当用户请求一个地址的时候,通过Ngin去做转发,当一个服务挂掉的时候, Nginx会把它排除掉。

如果想要API网关也能随时水平扩展,那么我们可以用脚本来动态修改 Nginx 的配置,通过脚本操作 Eureka,发现有新加人的网关服务或者下线的网关服务,直接修改 Nginx 的 upstream,然后通过重载( reload)配置来达到网关的动态扩容。

如果不用脚本结合注册中心去做的话,就只能提前规划好N个节点,然后手动配置上去。


参考:

《spring cloud 微服务 入门、进阶与实战》

书中本章代码:

API 网关