1.简介
Zuul是Netflix开源的微服务网关,它可以和Eureka、Ribbon、Hystrix等组件配合使用。Zuul的核心是一系列的过滤器,这些过滤器可以完成一下功能。
- 身份认证与安全:识别每个资源的验证要求,并拒绝那些与要求不符的请求。
- 审查与监控:在边缘位置追踪有意义的数据和统计结果,从而带来精确的生产视图。
- 动态路由:动态的将请求路由到不同的后端集群。
- 压力测试:逐渐增加指向集群的流量,以了解性能。
- 负载分配:为每一个负载类型分配对应的容量,并弃用超出限定值的请求。
- 静态响应处理:在边缘位置直接建立部分响应,从而避免其转发到内部集群。
- 多区域弹性:跨域AWS Region进行请求的路由,旨在实现ELB(Elastic Load Balancing)使用的多样化,以及让系统的边缘更贴近系统的使用者。
Spring Cloud对Zuul进行了整合与增强。目前,Zuul使用的默认HTTP客户端是Apache HTTP Client,也可以使用RestClient或者okhttp3.OkHttpClient。如果想要使用RestClient,可以设置ribbon.restclient.enabled=true;想要使用okhttp3.OkHttpClient,可以设置ribbon.okhttp.enabled=true。
2.代码示例
这里新建一个模块microservice-gateway-zuul。
引入zuul和eureka client的依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
spring cloud文档有说,Zuul starter没有包含discovery client,所以我们在上面增加了eureka client的依赖。
在Spring boot的主类上增加注解@EnableZuulProxy
@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication
{
public static void main( String[] args )
{
SpringApplication.run(ZuulApplication.class,args);
}
}
application.yml配置
spring:
application:
name: microservice-gateway-zuul
server:
port: 8808
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
prefer-ip-address: true
测试
1.启动Eureka server;
2.启动user微服务;
3.启动zuul模块。
在user模块,有/sample/1获取用户信息的接口。
浏览器请求http://10.41.3.149:8808/microservice-springcloud-user/sample/1。
可以看到,请求成功。
我们看Zuul模块的控制台日志,可以看到下面的日志:
Mapped URL path [/microservice-springcloud-user/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
自定义请求路径
通过Eureka server中的serviceID可以请求成功,但名字太长,如何自定义?
比如我们想通过/user/sample/1访问,如何做到?
在application.yml增加下面的配置:
zuul:
routes:
microservice-springcloud-user: /user/**
Zuul忽略某些服务
比如有user和movie2个服务,但我只想Zuul代理user服务。
zuul:
ignoredServices: '*'
routes:
microservice-springcloud-user: /user/**
ignoredServices:*忽略所有的服务,然后在routes中指定了user,所以最终就是Zuul代理user服务。
或者:
zuul:
# 多个服务id之间用逗号分隔
ignoredServices: microservice-springcloud-movie
routes:
microservice-springcloud-user: /user/**
Zuul指定path和serviceId
zuul:
routes:
# 下面的user1只是一个标识,保证唯一即可
user1:
# 映射的路径
path: /user/**
# 服务id
serviceId: microservice-springcloud-user
上面的配置意思是:让Zuul代理microservice-springcloud-user,路径为/user/**,user1可以随便写,只要保证唯一即可。
然后通过http://10.41.3.149:8808/user/sample/1请求即可。
Zuul指定path+url
除了上面说的指定path+serviceId外,还可以使用path+url的配置。
zuul:
routes:
user1:
path: /user/**
# url为user服务的url
url: http://10.41.3.149:7902
然后通过http://10.41.3.149:8808/user/sample/1请求即可。
Zuul指定可用服务的负载均衡
在spring cloud的文档中有写,如果使用上面的path+url配置,不会作为HystrixCommand执行,也不会使用Ribbon对多个URL进行负载均衡。 要实现此目的,您可以使用静态服务器列表指定serviceId。
zuul:
routes:
user1:
path: /user/**
serviceId: microservice-springcloud-user
在Ribbon中禁用eureka
ribbon:
eureka:
enabled: false
microservice-springcloud-user:
ribbon:
listOfServers: localhost:7901,localhost:7902
如上,需要在ribbon中禁用Eureka。然后指定了2个user服务,端口分别为7901,7902。
仍然是通过http://localhost:8808/user/sample/1来访问。访问多次,可以在控制台看到SQL打印,是轮询调用7901和7902的。
Zuul使用正则表达式指定路由规则
您可以使用regexmapper在serviceId和路由之间提供约定。 它使用名为groups的正则表达式从serviceId中提取变量并将它们注入路由模式。
将user服务id修改为
spring:
application:
name: microservice-springcloud-user-v1
zuul模块application.yml
spring:
application:
name: microservice-gateway-zuul
server:
port: 8808
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
prefer-ip-address: true
Zuul主类:
@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication
{
public static void main( String[] args )
{
SpringApplication.run(ZuulApplication.class,args);
}
@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
// 第一个参数:servicePattern,第2个参数routePattern
return new PatternServiceRouteMapper(
"(?<name>^.+)-(?<version>v.+$)",
"${version}/${name}");
}
}
上面的PatternServiceRouteMapper意思是myusers-v1将映射为/v1/myusers/**。
接受任何正则表达式,但所有命名组必须同时出现在servicePattern和routePattern中。
如果servicePattern与serviceId不匹配,则使用默认行为。比如user的serviceId为microservice-springcloud-user,那么实际上最终是通过http://localhost:zuul端口/microservice-springcloud-user/**来访问。
在上面的示例中,serviceId“myusers”将映射到路由“/myusers/**”(在未检测到版本时)默认情况下禁用此功能,仅适用于已发现的服务。
然后浏览器可以通过[http://localhost:zuul端口]/v1/microservice-springcloud-user/sample/1来访问user服务。
为所有映射增加前缀
要为所有映射添加前缀,请将zuul.prefix设置为值,例如/ api。 在默认情况下转发请求之前,会从请求中删除代理前缀(使用zuul.stripPrefix = false关闭此行为)。
在application.yml增加
zuul:
prefix: /api
然后通过http://localhost:Zuul端口/api/microservice-springcloud-user/sample/1来访问。
上面是一种全局的设置方式。可以通过zuul.stripPrefix=true/false来设置在请求具体的服务时是否剥离前缀。比如访问/api/sample/1,如果zuul.stripPrefix设置为true(默认为true),则实际请求用户服务的是/sample/1,相反请求路径是/api/sample/1.
您还可以关闭从各个路由中剥离特定于服务的前缀,例如:
假定user服务中指定了context-path为/user,我们访问/sample/1是通过/user/sample/1来访问的。现在我想通过http://localhost:zuul端口/user/sample/1来访问,可以这样做:
zuul:
routes:
microservice-springcloud-user:
path: /user/**
stripPrefix: false
或者:
zuul:
routes:
microservice-springcloud-user:
prefix: /user
path: /**
stripPrefix: false
stripPrefix是剥离前缀的意思,设置为false就是不剥离前缀,Zuul默认是剥离前缀的。比如我们设置path=/user/**,比如访问/user/sample/1,实际请求用户服务的是/sample/1。
stripPrefix比较实用的场景是服务带有context-path。
zuul.stripPrefix仅适用于zuul.prefix中设置的前缀。 它对给定路由的路径中定义的前缀没有任何影响。
Zuul忽略指定的路径
上面说过了,通过ignoredServices可以指定忽略某些服务,这是比较粗粒度的控制。如果想细粒度的控制忽略某些路径,可以通过下面的方式:
zuul:
ignoredPatterns: /**/admin/**
routes:
users: /myusers/**
这意味着所有诸如“/myusers/101”之类的请求将被转发到“users”服务上的“/101”。 但包括/admin/”在内的请求则不会处理。
Zuul指定路由的顺序
zuul:
routes:
microservice-springcloud-user:
path: /user/**
stripPrefix: false
legacy:
path: /**
上面配置的意思是/user**的请求转发到microservice-springcloud-user去处理,其他的请求按默认的方式处理(即通过http://zuulHost:zuulPort/服务名/请求路径)。
比如我们启动了microservice-springcloud-user和microservice-springcloud-movie-feign-without-hystrix2个服务。
通过Zuul访问microservice-springcloud-user,可以这样访问:
通过Zuul访问microservice-springcloud-movie-feign-without-hystrix需要如下方式访问:
如果你需要保证路由的顺序,则需要使用YAML文件,因为使用属性文件就会丢失顺序。所以,如果你用properties文件配置,可能会导致/user/**访问不到microservice-springcloud-user。
Zuul Http Client
Zuul默认使用的是Apache的http client。之前使用的是RestClient。如果你还是想使用RestClient,可以设置ribbon.restclient.enabled=true;如果你想使用OkHttp3,可以设置ribbon.okhttp.enabled=true。
如果要自定义Apache HTTP客户端或OK HTTP客户端,请提供ClosableHttpClient或OkHttpClient类型的bean。
Cookie和敏感的Headers
在同一系统中的服务之间共享Headers是可以的,但您可能不希望敏感Headers向下游泄漏到外部服务器。您可以在路由配置中指定忽略的Headers列表。 Cookie起着特殊的作用,因为它们在浏览器中具有明确定义的语义,并且它们始终被视为敏感。如果您的代理的消费者是浏览器,那么下游服务的cookie也会给用户带来问题,因为它们都会混乱(所有下游服务看起来都来自同一个地方)。
如果您对服务的设计非常小心,例如,如果只有一个下游服务设置了cookie,那么您可以让它们从后端一直流到调用者。此外,如果您的代理设置了cookie并且您的所有后端服务都是同一系统的一部分,那么简单地共享它们就很自然(例如使用Spring Session将它们链接到某个共享状态)。除此之外,由下游服务设置的任何cookie都可能对调用者不是很有用,因此建议您(至少)将“Set-Cookie”和“Cookie”放入敏感的标头中不属于您的域名。即使对于属于您域的路由,在允许cookie在它们与代理之间流动之前,请仔细考虑它的含义。
可以将敏感报头配置为每个路由的逗号分隔列表,例如,
zuul:
routes:
users:
path: /myusers/**
sensitiveHeaders: Cookie,Set-Cookie,Authorization
url: https://downstream
这是sensitiveHeaders的默认值,因此除非您希望它不同,否则无需进行设置。注: 这是Spring Cloud Netflix 1.1中的新功能(在1.0中,用户无法控制Headers,所有Cookie都在所有方向上流动)。
sensitiveHeaders是黑名单,默认不为空,因此要使Zuul发送所有Headers(“忽略”的Headers除外),您必须将其明确设置为空列表。 如果要将cookie或授权Headers传递给后端,则必须执行此操作。 例:
zuul:
routes:
users:
path: /myusers/**
sensitiveHeaders:
url: https://downstream
也可以通过设置zuul.sensitiveHeaders来全局设置敏感的Headers。 如果在路由上设置了sensitiveHeaders,则会覆盖全局sensitiveHeaders设置。
忽略Headers
除了每个路由规则上面的敏感头部信息设置,我们还可以在网关与外部服务交互的时候,用一个全局的设置zuul.ignoredHeaders,去除那些我们不想要的http头部信息(包括请求和响应的)。在默认情况下,zuul是不会去除这些信息的。如果Spring Security不在类路径上的话,它们就会被初始化为一组众所周知的“安全”头部信息(例如,涉及缓存),这是由Spring Security指定的。在这种情况下,假设请求网关的服务也会添加头部信息,我们又要得到这些代理头部信息,就可以设置zuul.ignoreSecurityHeaders为false,同时保留Spring Security的安全头部信息和代理的头部信息。当然,我们也可以不设置这个值,仅仅获取来自代理的头部信息。
路由端点
Actuator提供了一个可以查看路由规则的端点/routes,我们在Zuul中引入Actuator依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
再把安全验证关闭,让我们可以访问到这个端点:
management:
security:
enabled: false
这里,遗留请求的路由规则会影响到我们访问这个端点,先注释掉这个路由规则:
#legacy:
#path: /**
重启Zuul项目,我们便能看到zuul网关的路由规则了
如果想知道路由的详细细节,可以增加参数?format=details
Strangulation Patterns (绞杀者模式)
迁移现有应用程序或API时的常见模式是“扼杀”旧的端点,慢慢地用不同的实现替换它们。 Zuul代理是一个有用的工具,因为可以使用它来处理来自旧端点的客户端的所有流量,但重定向一些请求到新的端点。
zuul:
routes:
first:
path: /first/**
url: http://first.example.com
second:
path: /second/**
url: forward:/second
third:
path: /third/**
url: forward:/3rd # 本地的转发
legacy: # 老系统的请求
path: /**
url: http://legacy.example.com
在这个例子中,我们正在扼杀“遗留”应用程序,该应用程序映射到与其他模式之一不匹配的所有请求。 /first/**中的路径已被提取到具有外部URL的新服务中。 并转发/second/**中的路径,以便可以在本地处理它们,例如, 使用正常的Spring @RequestMapping。 /third/**中的路径也被转发,但具有不同的前缀(即/third/foo被转发到/3rd/foo)。
忽略的模式不会被完全忽略,它们只是不由代理处理(因此它们也可以在本地有效转发)。
通过Zuul上传文件
新建一个模块microservice-file-upload,该模块用于文件上传。
application.yml
server:
port: 9999
spring:
application:
name: microservice-file-upload
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
prefer-ip-address: true
上传文件的Controller
@Controller
@RequestMapping("/file")
public class FileUploadController {
@RequestMapping(value = "/upload",method = RequestMethod.POST)
@ResponseBody
public String uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
String uploadDir = "E:/test/";
String originName = file.getOriginalFilename();
String uploadPath = uploadDir+originName;
File destDir = new File(uploadDir);
if (!destDir.exists()) {
destDir.mkdirs();
}
File dest = new File(uploadPath);
if (dest.exists()) {
dest.delete();
}
file.transferTo(dest);
System.out.println("文件上传路径:" + uploadPath);
return uploadPath;
}
}
这里将服务注册到Eureka,是为了后面使用Zuul代理文件上传功能。
这里用curl测试。
curl -F “file=@d:/luckystar88/books/java_bloomfilter.rar”
http://localhost:9999/file/upload
可以看到,请求成功。
刚刚上传的文件大小14Kb,我们上传一个大点的文件(文件大小18.9M)。
出错,看错误信息,提示文件大小19M,超过了配置的最大大小10M。
解决办法:
spring:
application:
name: microservice-file-upload
http:
multipart:
# 单个文件大小
max-file-size: 1024Mb
# 总上传数据的大小
max-request-size: 2048Mb
配置上面2项设置文件大小即可。
重新上传:
现在我们使用Zuul测试。
修改Zuul的application.yml
zuul:
routes:
microservice-file-upload:
path: /upload-api/**
将/upload-api/**的请求交给microservice-file-upload处理。
启动Eureka Server,Zuul和file-upload模块。
curl -F "file=@d:/360极速浏览器下载/111.mp4" http://localhost:8808/zuul/upload-api/file/upload
可以看到上传成功。
我们准备一个大点的文件(175M)测试下上传超时。
curl -F "file=@C:\Users\Administrator\Downloads\Spring+Cloud微服务实战.pdf" http://localhost:8808/zuul/upload-api/file/upload
可以看到Zuul报超时了。
解决办法:
在Zuul增加配置:
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
ConnectTimeout: 3000
ReadTimeout: 60000
重新请求,可以看到上传成功了(文件名乱码就暂时不管了)。
禁用Zuul Filters
默认会使用很多filters,可采用如下方式禁止
zuul.SendResponseFilter.post.disable=true
Zuul的回退
当Zuul中给定路径的电路跳闸时,您可以通过创建ZuulFallbackProvider类型的bean来提供回退响应。 在此bean中,您需要指定回退所针对的路由ID,并提供ClientHttpResponse作为回退返回。
我们创建一个模块microservice-gateway-zuul-fallback。
application.yml
spring:
application:
name: microservice-gateway-zuul-fallback
server:
port: 8808
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
prefer-ip-address: true
zuul:
routes:
microservice-springcloud-user:
path: /user/**
stripPrefix: false
由于在microservice-springcloud-user服务中指定了context-path,所以这里设置stripPrefix=false。
spring boot主类:
@SpringBootApplication
@EnableZuulProxy
public class ZuulFallbackApplication
{
public static void main( String[] args )
{
SpringApplication.run(ZuulFallbackApplication.class,args);
}
}
回退类:
@Component
public class UserFallbackProvider implements ZuulFallbackProvider {
@Override
public String getRoute() {
return "microservice-springcloud-user";
}
@Override
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.BAD_REQUEST;
}
@Override
public int getRawStatusCode() throws IOException {
return HttpStatus.BAD_REQUEST.value();
}
@Override
public String getStatusText() throws IOException {
return HttpStatus.BAD_REQUEST.getReasonPhrase();
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream((getRoute() + "==》fallback").getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
回退的类中指定了路由为microservice-springcloud-user,同时指定了响应码,响应内容,响应类型等信息。
测试:
启动Eureka server,microservice-springcloud-user和microservice-gateway-zuul-fallback。
user服务正常时访问:
关闭user服务,再次访问:
通过/routes访问路由信息:
注意:FallbackProvider类中的routes必须与配置文件中的一致。
如果想为所有的路由设置一个默认的fallback,可以创建一个ZuulFallbackProvider类型的Bean,并且getRoute返回*或null。
比如:
@Component
public class MyFallbackProvider implements ZuulFallbackProvider {
@Override
public String getRoute() {
return "*";
}
@Override
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
@Override
public String getStatusText() throws IOException {
return "OK";
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("fallback".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
如果您想根据失败原因选择响应,请使用FallbackProvider,它将取代未来版本中的ZuulFallbackProvder。
@Component
public class MyFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
return "*";
}
@Override
public ClientHttpResponse fallbackResponse(final Throwable cause) {
if (cause instanceof HystrixTimeoutException) {
return response(HttpStatus.GATEWAY_TIMEOUT);
} else {
return fallbackResponse();
}
}
@Override
public ClientHttpResponse fallbackResponse() {
return response(HttpStatus.INTERNAL_SERVER_ERROR);
}
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("fallback".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}