使用Zuul构建微服务网关
为什么要使用微服务网关
经过前文的讲解,微服务架构已经初具雏形,但还有一些问题——不同的微服务一般会有不同的网络地址,而外部客户端(例如手机APP)可能需要调用多个服务的接口才能完成一个业务需求。
如果让客户端直接与各个微服务通信,会有以下的问题:
- 客户端多次请求不同的微服务,增加了客户端的复杂性
- 存在跨域请求,在一定场景下处理相对复杂
- 认证复杂,每个服务都需要独立认证
- 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将很难实施。
- 某些微服务可能使用了对防火墙/浏览器不友好的协议,直接访问时会有一定的困难。
以上问题可借助微服务网关解决。微服务网关是介于客户端和服务器之间的中间层,所有的外部请求都会先经过微服务网关。
微服务网关封装了应用程序的内部结构,客户端只用跟网关交互,而无须直接调用特定微服务的接口。这样,开发就可以得到简化。不仅如此使用微服务网关还有以下优点。 - 易于监控。可在微服务网关收集监控数据并将其推送到外部系统进行分析。
- 易于认证。可在微服务网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
- 减少了客户端与各个微服务之间的交互次数。
Zuul简介
Zuul是Netflix开源的微服务网关,它可以和Eureka、Ribbon、Hystrix等组件配合使用。Zuul的核心是一系列的过滤器,这些过滤器可以完成以下功能。
- 身份认证与安全:识别每个资源的验证要求,并拒绝那些与要求不符的请求。
- 审查与监控:在边缘位置追踪有意义的数据和统计结果,从而带来精确的生产视图。
- 动态路由:动态地将请求路由到不同地后端集群。
- 压力测试:逐渐增加指向集群地流量,以了解性能。
- 负载分配:为每一种负载类型分配对应容量。并弃用超出限定值地请求。
- 静态响应处理:在边缘位置直接建立部分响应,从而避免其转发到内部集群。
- 多区域弹性:跨越AWS Region进行请求路由,旨在实现ELB使用地多样化,以及让系统的边缘更贴近系统的使用者。
Spring Cloud对Zuul进行了整合与增强。目前,Zuul使用的默认HTTP客户端是Apache HTTP Client,也可以使用RestClient或者okhttp3.OkHttpClient。如果想要使用RestClient,可以设置ribbon.restclient.enabled=true;想要使用okhttp3.OkHttpClient,可以设置ribbon.okhttp.enabled=true。
编写Zuul微服务网关
1)创建一个Maven工程,ArtifactId是microservice-gateway-zuul,并为项目添加以下依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2)在启动类上添加注解@EnableZuulProxy,声明一个Zuul代理。该代理使用Ribbon来定位注册在Eureka Server中的微服务;同时,该代理还整合了Hystrix,从而实现了容错,所有经过Zuul的请求都会在Hystrix命令中执行。
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {
public static void main(String[] args){
SpringApplication.run(ZuulApplication.class,args);
}
}
3)编写配置文件application.xml
server:
port: 8040
spring:
application:
name: microservice-gateway-zuul
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
测试一、路由规则
1)启动项目microservice-discovery-eureka。
2)启动项目microservice-provider-user。
3)启动项目microservice-consumer-movie-ribbon。
4)启动项目microservice-gateway-zuul。
5)访问http://localhost:8040/microservice-consumer-movie/user/1,请求会被转发到http://localhost:8010/user/1(电影微服务)。
6)访问http://localhost:8040/microservice-provider-user/1,请求会转发到http://localhost:8000/1(用户微服务)
说明在默认情况下,Zuul会代理所有注册到Eureka Server的微服务,并且Zuul的路由规则如下:
http://ZUUL_HOST:ZUUL_PORT/微服务在Eureka上的serviceId/**
会被转发到serviceId对应的微服务。
测试二、负载均衡
1)启动项目microservice-discovery-eureka
2)启动多个microservice-provider-user实例
3)启动项目microservice-gateway-zuul。此时,Eureka Server首页。
4)多次访问http://localhost:8040/microservice-provider-user/1,会发现两个用户微服务节点都打印了
说明Zuul可以使用Ribbon达到负载均衡的效果。
测试三、Hystrix容错与监控
1)启动项目microservice-discovery-eureka
2)启动项目microservice-provider-user
3)启动项目microservice-consumer-movie-ribbon
4)启动项目microservice-gateway-zuul
5)启动项目microservice-hystrix-dashboard
6)访问http://localhost:8040/microservice-consumer-movie/user/1,可获得预期结果。
7)在Hystrix Dashboard中输入http://localhost:8040/hystrix.stream,随意指定某个Title并单击Monitor Stream按钮后
说明Zuul已经整合了Hystrix
管理端点
当@EnableZuulProxy与Spring Boot Actuator配合使用时,Zuul会暴露两个端点:/routes和filters。借助这些端点,可方便、直观地查看以及管理Zuul。
routes端点
/routes端点的使用非常简单
- 使用GET方法访问该端点,即可返回Zuul当前映射的路由列表。
- 使用POST方法访问该端点就会强制刷新Zuul当前映射的路由列表。尽管路由会自动刷新,Spring Cloud依然提供了强制立即刷新的方式。
- Spring Cloud Edgware对/routes端点进行了进一步的增强,我们可使用/routes?format=details查看更多与路由相关的详情设置。
由于spring-cloud-starter-netfilx-zuul已经包含了spring-boot-starter-actuator,因此之前编写的microservice-gateway-zuul已具备路由管理的能力。
测试
1)启动项目microservice-discovery-eureka
2)启动项目microservice-provider-user
3)启动项目microservice-consumer-movie-ribbon
4)修改项目microservice-gateway-zuul的配置,设置management.security.enabled: false
。
5)启动项目microservice-gateway-zuul
6)使用浏览器访问http://localhost:8040/routes
,可获得如下的结果
7)访问http://localhost:8040/routes?format=details
由结果可知,此时可看到更多与路由相关的配置,例如路由id、转发路径、是否重试等,这些对于我们调试很有用
也可使用类似方式测试Zuul路由的自动刷新与强制刷新。
filters端点
从Spring Cloud Edgware版本开始,Zuul提供了/filters端点。访问该端点即可返回Zuul中当前所有过滤器的详情,并按照类型分类。
如下是/filters端点的展示结果,从中,我们可以了解当前Zuul中,error、post、pre、route四种类型的过滤器分别有哪些,每个过滤器order(执行顺序)是多少,以及是否启用等信息。这对Zuul问题的定位很有用。
路由配置详情
前文已经编写了一个简单的Zuul网关,并让该网关代理了所有注册到Eureka Server的微服务。但在现实中可能只想让Zuul代理部分微服务,又或者需要对URL进行更加精确的控制。
1)自定义指定微服务的访问路径
配置zuul.routes.指定微服务的serviceId = 指定路径
接口
zuul:
routes:
microservice-provider-user: /user/**
完成设置后,microservice-provider-user微服务就会被映射到/user/** 路径。
2)忽略指定微服务
忽略服务非常简单,可以使用zuul.ignored-services
配置需要忽略的服务,多个服务间用逗号分隔。
zuul:
ignored-services: microservice-provider-user,microservice-consumer-movie
这样就可让Zuul忽略microservice-provider-user和microservice-consumer-movie微服务,只代理其他微服务。
3)忽略所有微服务,只路由指定微服务
很多场景下,可能只想要让Zuul代理指定的微服务,此时可以将zuul.ignored-services
设为’*’
zuul:
routes:
microservice-provider-user: /user/**
ignored-services: '*'
这样就可以让Zuul只路由microservice-provider-user微服务。
4)同时指定微服务的serviceId和对应路径。
zuul:
routes:
user_route: #该配置方式中,user-routes只是给路由一个名称,可以任意起名。
service-id: microservice-provider-user
path: /user/** #service-id对应的路径
5)同时指定path和URL
zuul:
routes:
user_route:
url: http://localhost:8000/
path: /user/**
这样可以将/user/**
映射到http://localhost:8000/**
。
需要注意的是,使用这种方式配置的路由不会作为HystrixCommand执行,同时也不能使用RIbbon来负载均衡多个URL。
6)同时指定path和URL,并且不破坏Zuul的Hystrix、Ribbon特性
zuul:
routes:
user_route:
service-id: microservice-provider-user
path: /user/**
ribbon:
eureka:
enable: false
microservice-provider-user:
ribbon:
listOfServers: localhost:8000,localhost:8001
7)使用正则表达式指定Zuul的路由匹配规则
借助PatternServiceRouteMapper,实现从微服务到映射路由的正则配置
package com.example.config;
import org.springframework.cloud.netflix.zuul.filters.discovery.PatternServiceRouteMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RouteMapperConfig {
@Bean
public PatternServiceRouteMapper serviceRouteMapper(){
//调用构造函数PatternServiceRouteMapper(String servicePattern, String routePattern)
//servicePattern指定微服务的正则
//routePattern指定路由正则
return new PatternServiceRouteMapper("(?<name>^.+)-(?<version>v.+$)","${version}/${name}");
}
}
通过这段代码即可实现将诸如microservice-provider-user-v1这个微服务,映射到/v1/microservice-provider-user/**这个路径。
8)路由前缀
zuul:
prefix: /api
strip-prefix: false
routes:
microservice-provider-user: /user/**
这样,访问Zuul的/api/user/1路径,请求将被发送到microservice-provider-user的/api/1。
zuul:
routes:
microservice-provider-user:
path: /user/**
strip-prefix: false
这样,访问Zuul的/api/user/1路径,请求将被发送到microservice-provider-user的/user/1。
9)忽略某些路径
上文讲解了如何忽略微服务,但有时还需要更细粒度的路由控制。例如,想让Zuul代理某个微服务,同时又想保护该微服务的某些敏感路径。此时,可使用ignored-Patterns,指定忽略的正则。
zuul:
ignored-patterns: /**/admin/**
routes:
microservice-provider-user: /user/**
这样就可将microservice-provider-user微服务映射到/user/**路径,但会忽略该微服务中所有包含/admin/的路径。
10)本地转发
zuul:
routes:
route-name:
path: /path-a/**
url:
forward: /path-b
这样,当访问Zuul的/path-a/**
路径,将转发到Zuul的/path-b/**
注:若无法掌握Zuul路由的规律,可将com.example包的日志级别设为DEBUG。这样,Zuul就会打印转发的具体细节,从而有助于更好地理解Zuul地路由配置。
logging:
level:
com.example: DEBUG
Zuul的安全与Header
敏感Header的设置
一般来说,可在同一个系统中的服务之间共享Header。不过应尽量防止让一些敏感的Header外泄。因此,在很多场景下,需要通过为路由指定一些了敏感Header列表。
zuul:
routes:
microservice-consumer-movie:
path: /users/**
sensitive-headers: Cookie,Set-Cookie,Authorization
url: https://downstream
这样就可为microservice-consumer-movie微服务指定敏感Header了。
也可用zuul.sensitive-headers全局指定敏感Header
zuul:
sensitive-headers: Cookie,Set-Cookie,Authorization
需要注意的是,如果使用zuul.routes.*.sensitive-headers的配置方式,会覆盖全局的配置。
忽略Header
可使用zuul.ignoredHeaders
属性丢弃一些Header
zuul:
ignored-headers: Header1,Header2
这样设置后,Header1和Header2将不会传播到其他的微服务中。
默认情况下,zuul.ignored-headers是空值,但如果Spring Security在项目的classpath中,那么zuul.ignored-headers的默认值就是Pragma,Cache-Contronl,X-Frame-Options,X-Content-Type-Option,X-XSS-Protection,Expires。所以,当Spring Security在项目的classpath中,同时又需要使用下游微服务的Spring Security的Header时,可以将zuul.ignoreSecurity-Headers设置false。
使用Zuul上传文件
对于小文件(1M以内)上传,无须任何处理,即可正常上传。对于大文件(10M以上)上传,需要为上传路径添加/zuul前缀。也可使用zuul.servlet-path自定义前缀。
也就是说,假设zuul.routes.microservice-flie-upload = /microservice-file-upload/**
,如果http://{HOST}:{PORT}/upload
是微服务microservice-file-upload的上传路径,则可使用Zuul的/zuul/microservice-file-upload/upload路径上传大文件。
如果Zuul使用了Ribbon做负载均衡,那么对于超大的文件,需要提升超时设置。
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMillseconds: 60000
ribbon:
ConnectTimeout: 3000
ReadTimeout: 60000
1)创建一个Maven工程,ArtifactId是microservice-file-upload,并为项目添加以下依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2)在启动类上添加@SpringBootApplication、@EnableEurekaClient注解。
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class FileUploadApplication {
public static void main(String[] args){
SpringApplication.run(FileUploadApplication.class,args);
}
}
3)编写Controller
package com.example.controller;
import org.springframework.stereotype.Controller;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
@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();
}
}
配置文件application.yml,在其中添加如下内容
server:
port: 8050
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
spring:
application:
name: microservice-file-upload
http:
multipart:
max-file-size: 2000Mb #Max file size,默认1M
max-request-size: 2500Mb #Max request size,默认10M
由配置可知,已经将该服务注册到Eureka Server上,并配置了文件上传大小的限制。这样一个文件上传的微服务就编写完成了。
curl -F "file=@D:/user.csv" localhost:8050/upload
测试
1)准备1个小文件(1M以下),记为small.file;1个超大文件(1G以上,2G以上),记为large.file。
2)启动microservice-discovery-eureka
3)启动microservice-file-upload
4)启动microservice-gateway-zuul
5)测试直接上传到microservice-file-upload上:使用命令curl -F "file=@D:/user.csv" localhost:8050/upload
,上传大文件,发现可以正常上传。同理,小文件也可以上传。
6)测试通过Zuul上传小文件
curl -v -H "Transfer-Encoding: chunked" -F "file=@D:/user.csv" localhost:8040/microservice-file-upload/upload
7)测试通过Zuul上传大文件,不添加/zuul前缀。
curl -v -H "Transfer-Encoding: chunked" -F "file=@D:/user.csv" localhost:8040/microservice-file-upload/upload
8)测试通过Zuul上传大文件,添加/zuul前缀
curl -v -H "Transfer-Encoding: chunked" -F "file=@D:/test.exe" localhost:8040/zuul/microservice-file-upload/upload
9)因此,不妨在application.yml中添加如下内容。
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
ConnectTimeout: 3000
ReadTimeout: 60000
10)关闭microservice-gateway-zuul,启动microservice-gateway-zuul-file-upload再次使用如下命令进行测试,此时已可正常上传文件。
Zuul的过滤器
过滤器类型与请求生命周期
Zuul大部分功能都是通过过滤器来实现的。Zuul中定义了4种标准过滤器类型,这些过滤器类型对应请求的典型生命周期。
- PRE:这种过滤器在请求被路由之前调用。可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
- POUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务。
- POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
- ERROR:在其他阶段发送错误时执行该过滤器。
除了默认的过滤器类型,Zuul还允许创建自定义的过滤器类型。例如,可以定制一种STATIC类型的 过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务。
内置过滤器详解
Spring Cloud默认为Zuul编写并启用了一些过滤器,这些过滤器有什么作用呢?我们不妨结合@EnableZuulServer、@EnableZuulProxy两个注解进行讨论。
可将@EnableZuulProxy简单理解为@EnableZuulServer的增强版。事实上,当Zuul与Eureka、Ribbon等组件配合使用时,@EnableZuulProxy是我们最常用的注解,这里使用的也是@EnableZuulServer。
@RequestContext,其用于在过滤器之间传递消息。它的数据保存在每个请求的ThreadLocal中。它用于存储请求路由到哪里、错误、HttpServletRequest、HttpServletResponse等信息。RequestContext扩展了ConcurrentHashMap,所以,理论上任何数据都可以存储在RequestContext中。
@EnableZuulServer所启用的过滤器
pre类型过滤器
1)ServletDetectionFilter:该过滤器用于查看请求是否通过Spring Dispatcher。检查后,通过FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY设置布尔值。
2)FormBodyWrapperFilter:解析表单数据,并为请求重新编码。
3)DebugFilter:顾名思义,调试用的过滤器。当设置zuul.includer-debug-header=true抑或设置zuul.debug.request=true,并在请求时加上了debug=true的参数,例如$ZUUL_HOST:ZUUL_PORT/some-path?debug=true,就会开启该过滤器。该过滤器会把RequestContext.setDebugRouting()以及RequestContext.setDebugRequest()设为true。
route类型过滤器
SendForwardFilter:该过滤器使用Servlet RequestDispatcher转发请求,转发位置存储在RequestContext的属性FilterConstants.FORWARD_TO_KEY中。这对转发到Zuul自身的端点很有用。
zuul:
routes:
abc:
path: /path-a/**
url: forward:/path-b
post类型过滤器
SendResponseFilter:将代理请求的响应写入当前响应。
error类型过滤器
SendErrorFilter:若RequestContext.getThrowable()不为null,则默认转发到/error,也可以设置error.path属性来修改默认的转发路径。
@EnableZuulProxy所启用的过滤器
如果使用注解@EnableZuulProxy,那么除上述过滤器外,Spring Cloud还会安装以下过滤器。
pre类型过滤器
PreDecorationFilter:该过滤器根据提供的RouteLocator 确定路由到的地址,以及怎样去路由。同时,该过滤器还为下游请求设置各种代理相关的header。
route类型过滤器
1)RibbonRoutingFilter:该过滤器使用Ribbon、Hystrix和可插拔的HTTP客户端发送请求。serviceId在RequestContext的属性FilterConstants.SERVICE_ID_KEY中。该过滤器可使用如下这些不同的HTTP客户端。
- Apache HttpClient:默认的HTTP客户端
- Squareup OkHttpClient v3:若需使用该客户端,需保证com.squareup.okhttp3的依赖在classpath中,并设置ribbon.okhttp.enable=true。
- Netflix RibbonHTTP Client:设置ribbon.restclient.enable=true即可启用该HTTP客户端。该客户端有一定限制,例如不支持PATCH方法,另外,它有内置的重试机制。
2)SimpleHostRoutingFilter:该过滤器通过Apache HttpClient向指定的URL发送请求。URL在RequestContext.getRouteHost()中。
编写Zuul过滤器
1)复制项目microservice-gateway-zuul,将ArtifactId修改为microservice-gateway-zuul-filter。
2)编写自定义Zuul过滤器
package com.example.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import javax.servlet.http.HttpServletRequest;
public class PreRequestLogFilter extends ZuulFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(PreRequestLogFilter.class);
@Override
public String filterType() {
//pre类型的过滤器
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
//在org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter之前执行
return FilterConstants.PRE_DECORATION_FILTER_ORDER -1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request=ctx.getRequest();
PreRequestLogFilter.LOGGER.info(String.format("send %s request to %s",request.getMethod(),request.getRequestURI().toString()));
return null;
}
}
由代码可知,自定义的Zuul Filter需实现以下几个方法。
- filterType:返回过滤器的类型。有pre、route、post、error等几种取值,分别对应上文的几种过滤器。
- filterOrder:返回一个int值来指定过滤器的执行顺序,不同的过滤器允许返回相同的数字。
- shouldFilter:返回一个boolean值来判断该过滤器是否要执行,true表示执行,false表示不执行。
- run:过滤器的具体逻辑。本例中让它打印了请求的HTTP方法以及请求的地址。
3)创建配置类
package com.example.config;
import com.example.filter.PreRequestLogFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Bean
public PreRequestLogFilter preRequestLogFilter(){
return new PreRequestLogFilter();
}
}
测试
1)启动microservice-discovery-eureka
2)启动microservice-provider-user
3)启动microservice-gateway-zuul-filter
4)访问http://localhost:8040/microservice-provider-user/1,可获得类似如下的日志
说明编写的自定义Zuul过滤器被执行了。
注:这里只是演示了一个非常简单的Zuul过滤器。事实上,我们可使用过滤器做很多事,例如安全认证、灰度发布、限流。
禁用Zuul过滤器
Spring Cloud默认为Zuul编写并启用了一些过滤器,例如DebugFilter、FormBodyWrapperFilter、PreDecorationFilter等。这些过滤器都存放在spring-cloud-netflix-core这个Jar包的org.springframework.cloud.netflix.zuul.filters包中。
如果想禁用部分过滤器,只需设置zuul.<SimpleClassName>.<filterType>.disable=true,即可禁用SimpleClassName所对应的过滤器。以过滤器org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter为例,只需设置zuul.SendResponseFilter.post.disable=true,接口禁用该过滤器。
Zuul的容错与回退
在Spring Cloud中,Zuul默认已经整合了Hystrix。首先来做一个简单的实验。
1)启动项目microservice-discovery-eureka。
2)启动项目microservice-provider-user。
3)启动项目microservice-gateway-zuul。
4)启动项目microservice-hystrix-dashboard。
5)访问http://localhost:8040/microservice-provider-user/1,可正常获得结果。
6)访问http://localhost:8040/hystrix.stream,可获得Hystrix的监控数据。
7)访问http://localhost:8030/hystrix,并在监控地址一栏填入http://localhost:8040/hystrix.stream
由图可知,Zuul的Hustrix监控的粒度是微服务,而不是某个API;同时也说明,所有经过Zuul的请求,都会被Hystrix保护起来。
8)关闭项目microservice-provider-user,再次访问http://localhost:8040/microservice-provider-user/1,会看到页面输出类似如下的异常:
Zuul实现回退
想要为Zuul添加回退,需要实现ZuulFallbackProvider接口。在实现类中,指定为哪个微服务提供回退,并提供一个ClientHttpResponse作为回退响应。
1)复制项目microservice-gateway-zuul,将ArtifactId修改为microservice-gateway-zuul-fallback。
2)编写Zuul的回退类:
package com.example.entity;
import com.netflix.hystrix.exception.HystrixTimeoutException;
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;
@Component
public class MyFallbackProvider implements FallbackProvider {
private ClientHttpResponse response(final HttpStatus status){
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return status;
}
@Override
public int getRawStatusCode() throws IOException {
return status.value();
}
@Override
public String getStatusText() throws IOException {
return status.getReasonPhrase();
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("服务不可用,请稍后再试。".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
MediaType mt = new MediaType("application","json", Charset.forName("UTF-8"));
headers.setContentType(mt);
return headers;
}
};
}
@Override
public ClientHttpResponse fallbackResponse(Throwable cause) {
if (cause instanceof HystrixTimeoutException){
return response(HttpStatus.GATEWAY_TIMEOUT);
}else{
return this.fallbackResponse();
}
}
@Override
public String getRoute() {
//表示是为哪个微服务提供回退,*表示为所有微服务提供回退
return "*";
}
@Override
public ClientHttpResponse fallbackResponse() {
return this.response(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
添加回退之后,重复之前的实验,当Zuul所代理的任意微服务无法正常响应时,会返回
饥饿加载
我们知道,Zuul整合了Ribbon实现负载均衡,而Ribbon默认是懒加载的。
zuul:
ribbon:
eager-load:
enable: true
Query String编码
当处理请求时,query param会被编码,因此,可在Zuul过滤器中进行一些适当的修改。这些参数在route过滤器中构建请求时,将被重新编码。如果qurey param使用JavaScript的encodeURLComponent()方法进行编码,那么重新编码后的结果可能与原始值不同——尽管在大多数情况下不会引起问题,但某些Web服务器可能会对复杂的query string进行编码。
要强制让query string与HttpServletRequest.getQueryString()保持一致,可使用如下配置:
zuul:
force-original-query-string-encoding: true
Hystrix隔离策略与线程池
隔离策略
1)为项目microservice-gateway-zuul设置zuul.ribbon-isolation-strategy=thread。
2)按照之前的操作查看图标。
可以发现,之前Thread Pools一栏没有数据,这说明了两个问题:
1)默认情况下,Zuul的Hystrix隔离策略是SEMAPHORE。
2)可使用zuul.ribbon-isolation-strategy=thread将隔离策略改为THREAD。
线程池配置
当zuul.ribbon-isolation-strategy=thread时,Hystrix的线程隔离策略将作用于所有路由,HystrixThreadPoolKey默认为RibbonCommand,这意味着,所有路由的HystrixCommand都会在相同的Hystrix线程池中执行。
可使用以下配置,让每个路由使用独立的线程池:
zuul:
thread-pool:
use-separate-thread-pools: true
这样,HystrixThreadPoolkey将与路由的服务标识相同。对于本例,则是microservice-provider-user。
如果想为HystrixThreadPoolKey添加前缀,可使用类似如下的配置。
zuul:
thread-pool:
use-separate-thread-pools: true
thread-pool-key-prefix: prefix-
这样,HystrixThreadPoolkey将变成${prefix}-{服务标识}。对于本例,则是prefix-microservice-provider-user。
Zuul的高可用
Zuul的高可用非常关键,因为外部请求到后端微服务的流量都会经过Zuul。故而在生产环境中一般都需要部署高可用的Zuul以避免单点故障。
注:这里的Zuul客户端是指广义上的“客户端”,即向Zuul发送的浏览器、手机等终端。
Zuul客户端也注册到了Eureka Server上
这种情况下,Zuul的高可用非常简单,只需将多个Zuul节点注册到Eureka Server上,就可实现Zuul的高可用。此时,Zuul的高可用与其他微服务的高可用没什么区别。
当Zuul客户端也注册到Eureka Server上时,只需部署多个Zuul节点即可实现其高可用。Zuul客户端会自动从Eureka Server中查询Zuul Server的列表,并使用Ribbon负载均衡地请求Zuul集群。
Zuul客户端未注册到Eureka Server上
实现中,这种场景往往更常见,例如,Zuul客户端是一个手机APP——不可能让所有的手机终端都注册到Eureka Server上。这种情况下,可借助一个额外的负载均衡器来实现Zuul的高可用,例如Nginx、HAProxy、F5等。
Zuul客户端将请求发送到负载均衡器,负载均衡器将请求转发到其代理的其中一个Zuul节点。这样,就可以实现Zuul的高可用。
使用Sidecar整合非JVM微服务
非JVM微服务可操作Eureka的REST端点,从而实现注册与发现。事实上,也可使用Sidecar更加方便地整合非JVM微服务。
Spring Cloud Netflix Sidecar的灵感来自Netflix Prana,它包括了一个简单的HTTP API来获取指定服务所有实例的信息(例如主机和端口)。不仅如此,还可通过内嵌的Zuul来代理服务调用,改代理从Eureka Server中获取信息。非JVM微服务需要实现健康检查,以便Sidecar将它的状态上报给Eureka Server,健康检查的形式如下:
{
"status":"UP"
}
其中,status用于描述微服务的状态,常见取值有UP、DOWN、OUT_OF_SERVICE以及UNKNOWN等。
编写Node.js微服务
先来编写一个非JVM微服务,以Node.js为例进行演示。
var http = require("http");
var url = require("url");
var path = require("path");
// 创建server
var server = http.createServer(function(req, res){
// 获取请求的路径
var pathname = url.parse(req.url).pathname;
res.writeHead(200,{ "Content-Type" : "application/json; charset=utf-8" });
//访问http://localhost:8060/,会返回{"index":"欢迎来到首页"}
if(pathname === '/'){
res.end(JSON.stringify({"index":"欢迎来到首页"}));
}
//访问http://localhost:8060/health,会返回{"status":"UP"}
else if (pathname === '/health.json'){
res.end(JSON.stringify({"status":"UP"}));
}
else{
res.end("404");
}
});
//创建监听,并打印日志
server.listen(8060,function(){
console.log('listening on localhost:8060');
})
这是一个非常简单的Node.js服务。
测试
1)使用node node-service.js
命令即可启动该服务。
2)访问http://localhost:8060/health.json
3)访问http://localhost:8060/
4)访问其他端点会返回404
编写Sidear
1)创建一个Maven工程,ArtifactId是microservice-sidecar,并为项目添加以下依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-sidecar</artifactId>
</dependency>
2)在启动类上添加@EnableSidecar注解,声明这是一个Sidecar。
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.sidecar.EnableSidecar;
@SpringBootApplication
@EnableSidecar
public class SidecarApplication {
public static void main(String[] args){
SpringApplication.run(SidecarApplication.class);
}
}
@EnableSidecar是一个组合注解,它整合了@EnableCiruitBreaker和@EnableZuulProxy。
3)在项目的配置文件application.yml
server:
port: 8070
spring:
application:
name: microservice-sidercar-node-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
sidecar:
port: 8060 #Node.js微服务的端口
health-uri: http://localhost:8060/health.json #Node.js微服务的健康检查URL
由配置可知,现在已经把Sidecar注册到了Eureka Server上,并用sidercar.port属性指定了非JVM微服务所监听的端口,用sidecar.health-uri属性指定了非JVM微服务的健康检查URL。
这样一个Sidecar就编写完成了。
测试一、非JVM微服务访问JVM微服务
1)启动microservice-discovery-eureka。
2)启动microservice-provider-user。
3)启动node-service
4)启动microservice-sidecar。
5)访问http://localhost:8070/microservice-provider-user/1
测试二、JVM微服务调用非JVM微服务的接口
使用Sidecar微服务的serviceId,即可调用非JVM微服务的接口。
package com.example.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class SidecarController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/test")
public String findById(){
return this.restTemplate.getForObject("http://microservice-sidecar-node-service/",String.class);
}
}
Sidecar的端点
Sidecar提供了一些端点,这些端点有助于管理Sidecar。
- /:该端点返回一个测试页面,该页面展示Sidecar的常用端点。
- /host/{serviceId}:该端点返回DiscoveryClient.getInstances(serviceId),即指定微服务在Eureka上的是列表。
- /ping:该端点返回"OK"字符串。
- /{serviceId}:由于Sidecar整合了Zuul,因此可使用该端点,将请求转发到serviceId所对应的微服务。
Sidecar与Node.js微服务分离部署
前文是将Sidecar与非JVM微服务部署在同一台主机上。现实中,常常会将Sidecar与JVM微服务分离部署,例如部署在不同的主机或者容器中。
方法一
eureka:
instance:
hostname: 非JVM微服务的hostname
方法二
对于Spring Cloud Dalston及更高版本,除方法一外,也可通过以下属性实现分离。
sidecar:
hostname: 非JVM微服务的hostname
ip-address: 非JVM微服务的IP地址
Sidecar原理分析
1)访问Eureka Server的路径:http://localhost:8761/eureka/apps/microservice-sidecar-node-service
由结果可知,注册到Eureka Server上的端口是8060,homePageUrl是http://192.168.2.107:8060/
,恰恰是node-service的端口和首页。因此,注册到Eureka Server上的微服务可使用microservice-sidecar-node-service这个名称请求node-serviec的接口。
2)由于@EnableSidecar整合了注解@EnableZuulProxy,可尝试访问Sidecar的/routes端点:http://localhost:8070/routes
因此,非JVM微服务可通过Sidecar请求其他注册在Eureka Server的微服务。
3)可尝试将node-service多次启停,并观察Sidecar的/health端点。Sidercar会获取node-service的健康状态,并将该状态传播到Eureke Server。使用这种方式,Eureka Server就能感知到非JVM微服务的健康状态。
使用Zuul聚合微服务
许多场景下,外部请求需要查询Zuul后端的多个微服务。举个例子,一个电影售票手机APP,在购票订单页上,既需要查询"电影微服务"获得电影相关信息,又需要查询"用户微服务"获得当前用户的信息。如果让手机端直接请求各个微服务,那么网络开销、流量耗费、耗费时长可能都无法令我们满意。那么对于这种场景,可使用Zuul聚合微服务请求——手机APP只需发送一个请求给Zuul,由Zuul请求用户微服务以及电影微服务,并组织好数据给手机APP。
使用这种方式,在手机端只需发送一次请求即可,简化了客户端侧的开发;不仅如此,由于Zuul、用户微服务、电影微服务一般都在同一个局域网中,因此速度会非常快,效率会非常高。
1)复制项目microservice-gateway-zuul,将ArtifactId修改为microservice-gateway-zuul-aggregation。
2)修改启动类ZuulApplication:
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableZuulProxy
public class ZuulAggregationApplication {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args){
SpringApplication.run(ZuulAggregationApplication.class,args);
}
}
3)创建实体类:
package com.example.entity;
import java.math.BigDecimal;
public class User {
private Long id;
private String username;
private String name;
private Integer age;
private BigDecimal balance;
//省略getter和setter
}
4)创建Java类,名为AggregationService
package com.example.service;
import com.example.entity.User;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
@Service
public class AggregationService {
@Autowired
private RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "fallback")
public Observable<User> getUserById(Long id) {
//创建一个被观察者
return Observable.create(observer -> {
//请求用户微服务的/{id}端点
User user = restTemplate.getForObject("http://microservice-provider-user/{id}", User.class);
observer.onNext(user);
observer.onCompleted();
});
}
@HystrixCommand(fallbackMethod = "fallback")
public Observable<User> getMovieUserByUserId(Long id) {
return Observable.create(observer -> {
// 请求电影微服务的/user/{id}端点
User movieUser = restTemplate.getForObject("http://microservice-consumer-movie/user/{id}", User.class);
observer.onNext(movieUser);
observer.onCompleted();
});
}
public User fallback(Long id) {
User user = new User();
user.setId(-1L);
return user;
}
}
5)创建Controller,在Controller中聚合多个请求
package com.example.controller;
import com.example.ZuulAggregationApplication;
import com.example.entity.User;
import com.example.service.AggregationService;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import rx.Observable;
import rx.Observer;
import java.util.HashMap;
@RestController
public class AggregationController {
public static final Logger LOGGER = LoggerFactory.getLogger(ZuulAggregationApplication.class);
@Autowired
private AggregationService aggregationService;
@GetMapping("/aggregate/{id}")
public DeferredResult<HashMap<String, User>> aggregate(@PathVariable Long id){
Observable<HashMap<String,User>> result = this.aggregateObservable(id);
return this.toDeferredResult(result);
}
public Observable<HashMap<String,User>> aggregateObservable(Long id){
//合并两个或者多个Observables发射出的数据项,根据指定的函数变换它们
return Observable.zip(
this.aggregationService.getUserById(id),
this.aggregationService.getMovieUserByUserId(id),
(user,movieUser)->{
HashMap<String,User>map = Maps.newHashMap();
map.put("user",user);
map.put("movieUser",movieUser);
return map;
}
)
}
public DeferredResult<HashMap<String,User>> toDeferredResult(Observable<HashMap<String,User>> details){
DeferredResult<HashMap<String,User>> result=new DeferredResult<>();
//订阅
details.subscribe(new Observer<HashMap<String, User>>() {
@Override
public void onCompleted() {
LOGGER.info("完成...");
}
@Override
public void onError(Throwable e) {
LOGGER.error("发生错误...",e);
}
@Override
public void onNext(HashMap<String, User> stringUserHashMap) {
result.setResult(stringUserHashMap);
}
});
return result;
}
}
测试一、微服务聚合测试
1)启动项目microservice-discovery-eureka
2)启动项目microservice-provider-user
3)启动项目microservice-consumer-movie
4)启动项目microservice-gateway-zuul-aggregation
5)访问http://localhost:8040/aggregate/1
6)停掉microservice-provider-user和microservice-consumer-movie
7)访问http://localhost:8040/aggregate/1