zuul 上传文件
在user-service
中定义一个上传接口:
@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();
}
}
配置文件配置如下:
spring:
application:
name: user-service
http:
multipart:
max-file-size: 2000Mb # Max file size,默认1M
max-request-size: 2500Mb # Max request size,默认10M
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
prefer-ip-address: true
server:
port: 8080
在user-service写一个html简单测试一下:
<form method="POST" enctype="multipart/form-data" action="/upload">
File to upload:
<input type="file" name="file">
<input type="submit" value="Upload">
</form>
点击上传成功。
那么怎么通过zuul来代理呢?
If you @EnableZuulProxy
you can use the proxy paths to upload files and it should just work as long as the files are small. For large files there is an alternative path which bypasses the Spring DispatcherServlet (to avoid multipart processing) in "/zuul/*
". I.e. if zuul.routes.customers=/customers/**
then you can POST large files to "/zuul/customers/*
". The servlet path is externalized via zuul.servletPath. Extremely large files will also require elevated timeout settings if the proxy route takes you through a Ribbon load balancer, e.g.
如果是你使用@EnableZuulProxy
你可以使用代理的路径来上传文件,并且必须是文件比较小的时候。对于大文件一个可选择的方案就是绕开Spring的DispatcherServlet
(避免多部分处理)通过/zuul/*
路径。如果zuul.routes.customers=/customers/**
你可以上传大文件通过/zuul/customers/*
路径。 如果使用Ribbon进行负载均衡,超大文件也将需要设置的超时时间。
小文件传输不需要修改zuul的配置:
zuul的配置:
spring:
application:
name: zuul-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
prefer-ip-address: true
server:
port: 6069
测试成功:
curl -v -H "Transfer-Encoding: chunked" -F "file=@Netty_in_Action最新版.pdf" localhost:6069/zuul/user-service/upload
如果是大文件传输,需要在zuul服务修改配置:
spring:
application:
name: zuul-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
prefer-ip-address: true
server:
port: 6069
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 60000
ribbon:
ConnectTimeout: 3000
ReadTimeout: 60000
参考资料
Uploading Files through Zuul
重定向问题
解决了Cookie问题之后,我们已经能够通过网关来访问并登录到我们的web应用了。但是这个时候又发现另外一个问题:虽然可以通过网关访问登录页面并发起登录请求,但是登录成功之后,我们跳转的url却是具体web应用实例的地址,而不是通过网关的路由地址。这个问题特别严重,因为使用api网关的一个重要原因就是将网关作为统一入口,从而不暴露所有内部服务细节。那么时什么原因导致了这个问题呢?
demo
比如我们在user服务定义了一个controller,重定向到hello.html
@Controller
public class UserController2 {
private Logger logger = LoggerFactory.getLogger(getClass());
@RequestMapping("/testRedirect")
public String testRedirect(){
logger.info("user2 testRedirect");
return "redirect:hello.html";
}
}
直接访问user服务跳转
http://192.168.1.57:8080/testRedirect
通过zuul代理,因为此url是跳转资源,直接跳转到web真实实例的url
http://192.168.1.57:6069/user-service/testRedirect
通过浏览器开发工具查看登录以及登录之后的请求详情,可以发现,引起问题的大致原因时由于spring secutity
或shiro
在登录完成之后,通过重定向的方式跳转到登录后的页面,此时登录后的请求结果状态吗为302,请求响应头信息中的Location指向了具体的服务实例地址,而请求头信息中的Host也指向了具体的服务实例ip地址和端口。所以,该问题的根本原因在于spring cloud zuul在路由请求时,并没有将最初的host信息设置正确,如何解决?
配置zuul.add-host-header=true
即可。
我配置了然后访问:
http://192.168.1.57:6069/user-service/testRedirect
跳转的地址是
http://192.168.1.57:6069/hello.html
很明显跳转到http://192.168.1.57:6069/user-service/hello.html
才对,没找到解决的方案。
网上根据这个问题有人在spring cloud
上提issue,自己在zuul上写个过滤器,我觉得这个可以不解决,因为现在都是前后端分离架构,不多数都不在后端进行跳转,zuul.add-host-header=true
只是为了不暴露真实的ip信息,如果要重定向到具体的前端页面可以自己可以配置zuul.routes
到指定的服务上。
参考资料
Spring Cloud实战小贴士:Zuul处理Cookie和重定向
Hystrix的路由回退
When a circuit for a given route in Zuul is tripped you can provide a fallback response by creating a bean of type ZuulFallbackProvider. Within this bean you need to specify the route ID the fallback is for and provide a ClientHttpResponse to return as a fallback. Here is a very simple ZuulFallbackProvider implementation.
当Zuul中给定路由的电路跳闸时,您可以通过创建ZuulFallbackProvider
类型的bean来提供回退响应。 在这个bean中,您需要指定回退所对应的路由ID,并提供一个ClientHttpResponse
作为后备返回。 这是一个非常简单的ZuulFallbackProvider
实现。
demo
在zuul-service
中去定义MyFallbackProvider
继承ZuulFallbackProvider
,定义了路由id为user-service
服务的回退。
@Component
public class MyFallbackProvider implements ZuulFallbackProvider {
//getRoute返回的必须要和zuul.routes.***一致,才能针对某个服务降级
@Override
public String getRoute() {
return "user-service";
}
@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"+MyFallbackProvider.this.getRoute()).getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
当访问user-service超时的时候页面上显示的是fallbackuser-service。
参考资料
官网Providing Hystrix Fallbacks For Routes
异构语言支持Sidecar
Do you have non-jvm languages you want to take advantage of Eureka, Ribbon and Config Server? The Spring Cloud Netflix Sidecar was inspired by Netflix Prana. It includes a simple http api to get all of the instances (ie host and port) for a given service. You can also proxy service calls through an embedded Zuul proxy which gets its route entries from Eureka. The Spring Cloud Config Server can be accessed directly via host lookup or through the Zuul Proxy. The non-jvm app should implement a health check so the Sidecar can report to eureka if the app is up or down.
你有没有非jvm语言你想利用Eureka
,Ribbon
和配置服务器? Spring Cloud Netflix Sidecar
的灵感来自Netflix Prana
。 它包含一个简单的http api来获取给定服务的所有实例(即主机和端口)。 您还可以通过嵌入式Zuul代理来代理服务调用,该代理从Eureka获取其路由条目。 可以通过主机查找或通过Zuul Proxy直接访问Spring Cloud Config Server
。 非jvm应用程序应该执行健康检查,以便Sidecar可以向应用程序启动或关闭时向eureka报告。
To include Sidecar in your project use the dependency with group org.springframework.cloud and artifact id spring-cloud-netflix-sidecar.
要在项目中包含Sidecar,请使用组org.springframework.cloud
和artifact id 为spring-cloud-netflix-sidecar
的依赖关系。
To enable the Sidecar, create a Spring Boot application with @EnableSidecar
. This annotation includes @EnableCircuitBreaker
, @EnableDiscoveryClient
, and @EnableZuulProxy
. Run the resulting application on the same host as the non-jvm application.
要启用Sidecar,请使用@EnableSidecar
创建一个Spring Boot应用程序。 此注释包括@EnableCircuitBreaker
,@EnableDiscoveryClient
和@EnableZuulProxy
。 在与非jvm应用程序相同的主机上运行生成的应用程序。
To configure the side car add sidecar.port and sidecar.health-uri to application.yml. The sidecar.port property is the port the non-jvm app is listening on. This is so the Sidecar can properly register the app with Eureka. The sidecar.health-uri is a uri accessible on the non-jvm app that mimicks a Spring Boot health indicator. It should return a json document like the following:
要将side car配置为sidecar.port
和sidecar.health-uri
到application.yml
。 sidecar.port
属性是非jvm应用程序正在侦听的端口。 这是因为Sidecar可以正确地注册该应用程序与eureka。 sidecar.health-uri
是一个可以在非jvm应用程序上访问的uri,它可以模仿Spring Boot健康指示器。 它应该返回一个json文档,如下所示:
{
"status":"UP"
}
demo
写一个node.js的服务,端口是8060,访问localhost:8060
返回"欢迎来到首页",访问http://localhost:8060/health.json
,将会返回{"status":"UP"}
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/,将会返回首页
if(pathname === '/'){
res.end(JSON.stringify({"index":"欢迎来到首页"}));
}
//访问http://localhost:8060/health,将会返回{"status":"UP"}
else if(pathname ==="/health.json"){
res.end(JSON.stringify({"status":"UP"}));
}
//其他情况返回404
else {
res.end("404");
}
});
//创建监听,并打印日志
server.listen(8060,function () {
console.log('listening on localhost:8060');
})
启动服务:
node node-service.js
分别访问首页和健康检查页面。
然后新建一个zuul-sidecar服务,依赖如下:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-sidecar</artifactId>
</dependency>
</dependencies>
启动类SidecarApplication,除了@SpringBootApplication
还标记有@EnableSidecar
注解:
@SpringBootApplication
@EnableSidecar
public class SidecarApplication {
public static void main(String[] args) {
SpringApplication.run(SidecarApplication.class, args);
}
}
配置文件application.yml:
server:
port: 8070
spring:
application:
name: zuul-sidecar
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
prefer-ip-address: true
sidecar:
port: 8060 # Node.js微服务的端口
health-uri: http://localhost:8060/health.json # Node.js微服务的健康检查URL
启动服务,zuul-sidecar注册到eureka上了,控制面板如下,
通过zuul访问zuul-sidecar间接访问node的服务,访问首页和健康页面
http://192.168.1.57:6069/zuul-sidecar/
http://192.168.1.57:6069/zuul-sidecar/health.json
访问sidecar的服务:
http://localhost:8070/
user服务访问node服务也可以通过sidecar来访问通过注册到zuul的服务名来访问。
在user-service中定义:
@GetMapping("/sidecar")
public String sidecar(){
String response = restTemplate.getForObject("http://zuul-sidecar/",String.class);
return response;
}
http://192.168.1.57:8080/user/sidecar
成功访问。
The api for the DiscoveryClient.getInstances()
method is /hosts/{serviceId}
. Here is an example response for /hosts/customer
that returns two instances on different hosts. This api is accessible to the non-jvm app (if the sidecar is on port 5678) at http://localhost:5678/hosts/{serviceId}
.
DiscoveryClient.getInstances()
方法的api是/hosts/{serviceId}
。 以下是/ hosts/customers
的一个示例响应,它会在不同的主机上返回两个实例。 这个api可以访问http://localhost:5678/hosts/{serviceId}
的非jvm应用程序(如果sidecar在端口5678上)。比如我上面的列子就可以根据http://localhost:8070/hosts/user-service
来查看user-service的服务信息。具体原因下面解释。
The Zuul proxy automatically adds routes for each service known in eureka to /<serviceId>
, so the customers service is available at /customers
. The Non-jvm app can access the customer service via http://localhost:5678/customers
(assuming the sidecar is listening on port 5678).
Zuul代理自动将eureka中已知的每个服务的路由添加到/<serviceId>
,以便客户可以在/客户端使用客户服务。 非jvm应用程序可以通过http://localhost:5678/customers
访问客户服务(假设边界正在侦听端口5678)。
If the Config Server is registered with Eureka, non-jvm application can access it via the Zuul proxy. If the serviceId of the ConfigServer is configserver and the Sidecar is on port 5678, then it can be accessed at http://localhost:5678/configserver
如果配置服务器在Eureka中注册,则非jvm应用程序可以通过Zuul代理访问它。 如果ConfigServer的serviceId是configserver,而Sidecar在端口5678上,则可以访问http://localhost:5678/configserver
使用sidecar也是可以访问注册到eureka上的服务,也就是使用zuul的代理,而不需要另外的起一个zuul服务器。比如下面的可以通过zuul-sidecar访问user服务。
http://localhost:8070/user-service/user/index
参考资料
官网Polyglot support with Sidecar
Hystrix和ribbon支持
spring-cloud-starter-zuul
依赖包括spring-cloud-starter-hystrix
和spring-cloud-starter-ribbon
模块的依赖,所以zuul天生就拥有线程隔离和断路器的自我保护功能,以及对服务调用的客户端负载均衡功能。
需要注意的事,当使用path与url的映射关系来配置路由规则的时候,对于路由转发的请求不会采用hystrixCommand来包装,所以这类请求没有线程隔离和断路器的保护,并且也不会有负载均衡的能力。因此,我们在使用zuul的时候尽量使用path和serviceId的组合来进行配置,这样不仅可以保证api网关的健壮和稳定,也能用到ribbon的客户端负载均衡功能,
我们在使用zuul搭建api网关的时候,可以通过hystrix和ribbon的参数来调整路由请求的各种超时时间等配置,比如下面这些参数的设置。
-
hystrix.command.default.execution.isolation.thread.timeoutInMillseconds
:该参数可以用来设置api网关中路由转发请求hystrixCommand执行超时时间,单位为毫秒。当路由转发请求的命令执行时间超过该配置值之后,hystrix会将该执行命令标记为timeout并抛出异常,zuul会对该异常进行处理并返回如下的json信息给外部调用方。
{
"timestamp":14454545234324,
"status":500,
"error":"Internal Server Error",
"exception":"com.netflix.zuul.exception.ZuulException",
"message":"TIMEOUT"
}
-
ribbon.ConnectTimeout
:该参数用来设置路由转发请求的时候,创建请求连接的超时时间。当ribbon.ConnectTimeout
的配置值小于hystrix.command.default.execttion.isolation.thread.timeoutInMilliseconds
配置值的时候,若出现路由请求连接超时时,会自动进行重试路由请求,如果请求依然失败,zuul会返回如下json信息给外部调用方。
{
"timestamp":14454545234324,
"status":500,
"error":"Internal Server Error",
"exception":"com.netflix.zuul.exception.ZuulException",
"message":"NUMBEROF_RETRIES_NEXTSERVER_EXCEEDED"
}
ribbon.ConnectTimeout
的配置值大于hystrix.command.default.execution.isolation.thread.timeoutInMillseconds
配置值的时候,当出现路由请求连接超时时,由于此时对于路由转发的请求命令已经超时,所以不会进行重试路由请求,而是直接按请求命令超时处理,返回TIMEOUT的错误信息。
-
ribbon.ReadTimeout
:该参数用来设置路由转发请求的超时时间,它的处理与ribbon.ConnectTimeout
类似,只是它的超时是对请求连接建立之后的处理时间。当ribbon.ReadTimeout
的配置值小于hystrix.command.default.execttion.isolation.thread.timeoutInMilliseconds
配置值的时候,若路由请求的处理时间超过该配置值并且依赖服务还没有响应的时候,会自动进行重试路由请求。如果重试后依然没有获得请求响应,zuul会返回NUMBEROF_RETRIES_NEXTSERVER_EXCEEDED
错误。如果ribbon.ReadTimeout
的配置值大于hystrix.command.default.execttion.isolation.thread.timeoutInMilliseconds
配置值,若路由请求的处理时间超过该配置值并且依赖服务还没有响应时,不会进行重试路由请求,而是直接按请求命令超时处理,返回timeout的错误信息。
根据上面的介绍我们知道,在使用zuul的服务路由时,如果路由转发请求发生超时(连接超时或处理超时),只要超时时间的设置小于hystrix的命令超时时间,那么它就会自动发起重试。有些背景下,我们需要关闭重试机制,那么可以通过下面的二个参数进行设置。
zuul.retryable=false
zuul.routes.<route>.retryable=false
其中,zuul.retryable
用来关闭全局的重试机制,而zuul.routes.<route>.retryable=false
指定路由关闭重试机制。
本博客代码
代码地址