一、认识Zuul
1.什么是Zuul
不同的微服务一般会有不同的网络地址,而外部APP可能需要调用多个服务的接口才能完成一个业务需求。
让客户端和各个微服务通信肯定不行。
微服务网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过微服务网关。这样的好处是易于监控、认证,也减少了客户端与各个微服务之间的交互次数。
2. Zuul的作用
Zuul可以和Eureka、Ribbon、Hystrix等组件配合使用。核心是一系列过滤器,这些过滤器可以完成以下功能:
----身份认证与安全
----审查与监控
----动态路由
----压力测试
----负载分配
----静态相应处理
----多区域弹性
3.编写Zuul微服务网关(添加zuul依赖,并将Zuul注册到Eureka)
3.1 添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
注:其中,spring-cloud-starter-netflix-zuul包含了spring-boot-starter-actoator
3.2 配置启动类
@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
3.3 修改配置文件
server.port=8040
spring.application.name=gateway-zuul
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
四、管理端点
当@EnableZuulProxy和Spring Boot Actuator配合使用时,Zuul会暴露两个端点:/routes和/filters
4.1 route端点
4.1.1 使用GET方法访问该端点,即可返回Zuul当前映射的路由列表
4.1.2 使用POST方法访问该端点就会强制刷新Zuul当前映射的路由列表。尽管路由会自动刷新,Spring Cloud依然提供了强制立刻刷新的方式
4.1.3 SpringCloudEdgware对/routes端点进行了进一步的增强,我们可以使用/routes?format=details查看更多与路由相关的详细设置
4.2 filters端点:访问该端点即可返回Zuul中当前所有过滤器的详情,并按照类型分类
4.2.1 一共有四种类型过滤器:error、post、pre、route
五、路由配置详解
5.1 自定义指定微服务的访问路径
zuul.routes.<serviceId>=/user/** #指定路径
5.2 忽略指定微服务
zuul.ignored-services=provider-user,consumer-user
5.3 忽略所有微服务,只路由指定微服务
zuul.ignored-services='*'
zuul.routes.<serviceId>=/user/** #指定路径
5.4 同时指定微服务的serviceId和对应路径
zuul.routes.<路由名称>.service-id=user
zuul.routes.<路由名称>.path=/user/**
5.5 同时指定path和URL
zuul.routes.<路由名称>.url=http://localhost:8000/
zuul.routes.<路由名称>.path=/user/**
注:使用这种方式配置的路由不会作为HystrixCommand执行,同时也不能使用Ribbon来负载均衡多个URL。5.6可以解决这些问题
5.6 同时指定path和URL,并且不破坏Zuul的Hystrix、ribbon特性
zuul.routes.<路由名称>.path=/user/**
zuul.routes.<路由名称>.service-id=user
ribbon.eureka.enabled=false
user.ribbon.listOfServers=localhost:8000,localhost:8001
5.7 使用正则表达式指定Zuul的路由匹配规则
@Bean
public PatternServiceRouteMapper serviceRouteMapper(){
return new PatternServiceRouteMapper("(?<name>^.+)-(?<version>v.+$)", "${version}/${name}");
}
5.8 路由前缀
zuul.prefix=/api
zuul.strip-prefix=false
zuul.routes.<路由名称>=/user/**
zuul.routes.<路由名称>.path=/user/**
zuul.routes.<路由名称>.strip-prefix=false
5.9 更细粒度忽略某些路径
zuul.ignoredPatterns=/**/admin/**
zuul.routes.<路由名称>=/user/**
5.10 本地转发
zuul.routes.route-name.path=/path-a/**
zuul.routes.route-name.url=forward:/path-b
六、Zuul的安全与Header
6.1 敏感Header的设置
6.1.1 一般来说,同一个系统中的服务共享Header,但是对于一些敏感的Header,可以这样设置
zuul.routes.<路由名称>.path=/user/**
zuul.routes.<路由名称>.sensitive-headers=Cookie,Set-Cookie,Authorization
zuul.routes.<路由名称>.url=https://downstream
6.1.2 设置全局指定敏感Header
zuul.sensitive-headers=Cookie,Set-Cookie,Authorization
6.2 忽略Header
zuul.ignored-headers=Header1,Header2
七、使用Zuul上传文件
7.1 对于1M以内的文件上传,无须任何处理
7.2 对于10M以上的需要为上传路径添加/zuul前缀。也可以使用zuul.servlet-path自定义前缀
7.3 上传实例
7.3.1 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
7.3.2 启动类上添加@SpringBootApplication,@EnableEurekaClient
7.3.3 编写Controller
@Controller
public class FileUploadController {
@RequestMapping(value="/upload",method=RequestMethod.POST)
public @ResponseBody String handleFileUpload(@RequestParam(value="file",required=true) MultipartFile file) throws IOException{
byte[] bytes = file.getBytes();
File fileToSave = new File(file.getOriginalFilename());
FileCopyUtils.copy(bytes, fileToSave);
return fileToSave.getAbsolutePath();
}
}
7.3.4 配置文件
server.port=8050
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/
eureka.instance.prefer-ip-address=true
spring.application.name=file-upload
spring.http.multipart.max-file-size=2000Mb #默认1M
spring.http.multipart.max-request-size=2500Mb #默认10M
7.3.5 测试文件上传
curl -F "file=@文件全名" localhost:8050/upload
八、Zuul的过滤器
8.1 过滤器类型和请求生命周期
标准过滤器类型:PRE、ROUTE、POST、ERROR
定制过滤器:STATIC
8.2 内置过滤器详解
RequestContext其用于在过滤器之间传递消息。它的数据保存在每个请求的ThreadLocal中。它用于存储请求路由到哪里、错误、HttpServletRequest、HttpServletResponse等信息。RequestContext扩展了ConcurrentHashMap,所以理论上任何数据都可以存储在RequestContext中。
8.3 @EnableZuulServer所启用的过滤器
8.3.1 pre类型过滤器
8.3.1.1 ServletDetectionFilter:该过滤器用于检查请求是否通过Spring Dispatcher。检查后,通过FilterConstants.IS_DISPATCHER_SERVLET_KEY设置布尔值。
8.3.1.2 FormBodyWrapperFilter:解析表单数据,并为请求重新编码
8.3.1.3 DebugFilter:调试用的过滤器。当设置zuul.include-debug-header=true或者zuul.debug.request=true,并在请求时加上debug=true的参数,就会开启该过滤器。该过滤器会把RequestContext.setDebugRouting()以及RequestContext.setDebugRequest()设为true
8.3.2 route类型过滤器
8.3.2.1 SendForwardFilter:该过滤器使用Servlet RequestDispatcher转发请求,转发位置存储在RequestContext的属性FilterContants.FORWARD_TO_KEY中。
zuul.routes.<路由名称>.path=/path-a/**
zuul.routes.<路由名称>.url=forward:/path-b
8.3.3 post类型过滤器
8.3.3.1 SendResponseFilter:将代理请求的响应写入当前响应
8.3.4 error类型过滤器
8.3.4.1 SendErrorFilter:若RequestContext.getThrowable()不为null,则默认转发到/error,也可设置error.path属性来修改默认的转发路径
8.4 @EnableZuulProxy所启用的过滤器
如果使用@EnableZuulProxy,那么除上述过滤器外,Spring Cloud还会安装以下过滤器:
8.4.1 pre类型过滤器
PreDecorationFilter:该过滤器根据提供的RouteLocator确定路由到的地址,以及怎么去路由。同时,该过滤器还为下游请求设置各种代理相关的header
8.4.2 route类型过滤器
8.4.2.1 RibbonRoutingFilter:该过滤器使用Ribbon、Hystrix和可插拔的HTTP客户端发送请求。serviceId在RequestContext的属性FilterContants.SERVICE_ID_KEY中。该过滤器可使用如下这些不同的HTTP客户端
* Apache HttpClient:默认的HTTP客户端
* Squareup OkHttpClient v3:若需使用该客户端,需保证com.squareup.okhttp3的依赖在classpath中,并设置ribbon.okhttp.enabled=true
* Netflix Ribbon HTTP Client:设置ribbon.restclient.enabled=true即可启用该HTTP客户端。该客户端有一定限制,例如不支持PATCH方法。另外,它还有内置的重试机制
8.4.2.2 SimpleHostRoutingFilter:该过滤器通过Apache HttpClient向指定的URL发送请求。URL在RequestContext.getRouteHost()中
九、编写Zuul过滤器
public class PreRequestLogFilter extends ZuulFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(PreRequestLogFilter.class);
/**
* 返回过滤器的类型,一共有四种:pre,route,post,error
*/
@Override
public String filterType() {
//pre类型的过滤器
return FilterConstants.PRE_TYPE;
}
/**
* 返回一个int来指定过滤器的执行顺序
*/
@Override
public int filterOrder() {
//在org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter之前执行
return FilterConstants.PRE_DECORATION_FILTER_ORDER -1;
}
/**
* 返回一个boolean判断该过滤器是否要执行
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 过滤器的具体逻辑
*/
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
PreRequestLogFilter.LOGGER.info(String.format("send %s request to %s", request.getMethod(),request.getRequestURL().toString()));
return null;
}
}
十、禁用过滤器
zuul.<SimpleClassName>.<filterType>.disable=true
十一、Zuul的容错和回退
想要为Zuul添加回退,需要实现ZuulFallbackProvider接口。在实现类中,指定为哪个微服务提供回退,并提供一个ClientHttpResponse作为回退响应。
11.1 编写zuul的回退类
@Component
public class MyFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
// 表明为哪个微服务提供回退,*表示为所以微服务提供回退
return "*";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
if(cause instanceof HystrixTimeoutException){
return response(org.springframework.http.HttpStatus.GATEWAY_TIMEOUT);
}else{
return this.fallbackResponse(null, null);
}
}
private ClientHttpResponse response(final org.springframework.http.HttpStatus status) {
return new ClientHttpResponse() {
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
MediaType mType = new MediaType("application","json",Charset.forName("UTF-8"));
headers.setContentType(mType);
return headers;
}
@Override
public InputStream getBody() throws IOException {
// TODO Auto-generated method stub
return new ByteArrayInputStream("服务不可用,请稍后再试".getBytes());
}
@Override
public String getStatusText() throws IOException {
// TODO Auto-generated method stub
return status.getReasonPhrase();
}
@Override
public org.springframework.http.HttpStatus getStatusCode()
throws IOException {
// TODO Auto-generated method stub
return status;
}
@Override
public int getRawStatusCode() throws IOException {
// TODO Auto-generated method stub
return status.value();
}
@Override
public void close() {
// TODO Auto-generated method stub
}
};
}
}
十二、饥饿加载
因为zuul整合了ribbon实现负载均衡,而ribbon默认是懒加载的,可能会导致首次请求较慢。可以使用以下配置进行饥饿加载
zuul.ribbon.eager-load.enabled=true
十三、QueryString编码
当处理请求时,query param会被解码,因此,可在Zuul过滤器中进行一些适当的修改。
这些参数在route过滤器中构建请求时,将被重新编码。如果query param使用js的encodeURIComponent()方法进行编码,那么重新编码后的结果可能与原始值不同(一般情况下不会有问题,但是某些web服务器可能会对复杂的query string进行编码)
如果强制让query string与HttpServletRequest.getQueryString()保持一致,可使用如下配置:
zuul.forceOriginalQueryStringEncoding=true
十四、Hystrix隔离策略与线程池
14.1 在Hystrix那一节说过,Hystrix有两种隔离策略:THREAD、SEMAPHORE
默认情况下,Zuul的Hystrix隔离策略是SEMAPHORE,可使用如下方法将隔离策略变成THREAD
zuul.ribbon-isolation-strategy=thread
14.2
14.2.1 当Hystrix的线程隔离级别是THREAD时,将作用于所有路由,HystrixThreadPoolKey默认是RibbonCommand。这意味着所有的路由HystrixCommand都会在相同的Hystrix线程池中执行。
14.2.2 可使用如下配置,让每个路由使用独立的线程池:
zuul.threadPool.useSeparateThreadPools=true
14.2.3 如果想为HystrixThreadPoolKey添加前缀,使用如下配置:
zuul.threadPool.useSeparateThreadPools=true
zuul.threadPool.threadPoolKeyPrefix=prefix-
十五、Zuul的高可用
Zuul的高可用很关键,因为外部请求到后端微服务的流量都会经过Zuul。所以需要避免单点故障
15.1 将Zuul客户端也注册到了Eureka Server上
只需将Zuul节点注册到Eureka Server上
15.2 Zuul客户端未注册到Eureka Server上
借助额外的负载均衡器来实现Zuul的高可用,比如Nginx。Zuul客户端将请求发送到负载均衡器,负载均衡器将请求转发到其代理的其中一个Zuul节点。
十六、使用Zuul聚合微服务
场景:外部请求需要查询Zuul后端的多个微服务,我们想让手机APP只发送一个请求给Zuul,由Zuul请求其他多个微服务,并将数据组织好返回APP。别让手机APP自己去访问多个微服务。
使用RxJava