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進行負載均衡,超大文件也將需要設(shè)置的超時時間。
小文件傳輸不需要修改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服務(wù)修改配置:
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問題之后,我們已經(jīng)能夠通過網(wǎng)關(guān)來訪問并登錄到我們的web應(yīng)用了。但是這個時候又發(fā)現(xiàn)另外一個問題:雖然可以通過網(wǎng)關(guān)訪問登錄頁面并發(fā)起登錄請求,但是登錄成功之后,我們跳轉(zhuǎn)的url卻是具體web應(yīng)用實例的地址,而不是通過網(wǎng)關(guān)的路由地址。這個問題特別嚴重,因為使用api網(wǎng)關(guān)的一個重要原因就是將網(wǎng)關(guān)作為統(tǒng)一入口,從而不暴露所有內(nèi)部服務(wù)細節(jié)。那么時什么原因?qū)е铝诉@個問題呢?
demo
比如我們在user服務(wù)定義了一個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服務(wù)跳轉(zhuǎn)
http://192.168.1.57:8080/testRedirect
通過zuul代理,因為此url是跳轉(zhuǎn)資源,直接跳轉(zhuǎn)到web真實實例的url
http://192.168.1.57:6069/user-service/testRedirect
通過瀏覽器開發(fā)工具查看登錄以及登錄之后的請求詳情,可以發(fā)現(xiàn),引起問題的大致原因時由于spring secutity或shiro在登錄完成之后,通過重定向的方式跳轉(zhuǎn)到登錄后的頁面,此時登錄后的請求結(jié)果狀態(tài)嗎為302,請求響應(yīng)頭信息中的Location指向了具體的服務(wù)實例地址,而請求頭信息中的Host也指向了具體的服務(wù)實例ip地址和端口。所以,該問題的根本原因在于spring cloud zuul在路由請求時,并沒有將最初的host信息設(shè)置正確,如何解決?
配置zuul.add-host-header=true即可。
我配置了然后訪問:
http://192.168.1.57:6069/user-service/testRedirect
跳轉(zhuǎn)的地址是
http://192.168.1.57:6069/hello.html
很明顯跳轉(zhuǎn)到http://192.168.1.57:6069/user-service/hello.html才對,沒找到解決的方案。
網(wǎng)上根據(jù)這個問題有人在spring cloud上提issue,自己在zuul上寫個過濾器,我覺得這個可以不解決,因為現(xiàn)在都是前后端分離架構(gòu),不多數(shù)都不在后端進行跳轉(zhuǎn),zuul.add-host-header=true只是為了不暴露真實的ip信息,如果要重定向到具體的前端頁面可以自己可以配置zuul.routes到指定的服務(wù)上。
參考資料
Spring Cloud實戰(zhàn)小貼士: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中給定路由的電路跳閘時,您可以通過創(chuàng)建ZuulFallbackProvider類型的bean來提供回退響應(yīng)。 在這個bean中,您需要指定回退所對應(yīng)的路由ID,并提供一個ClientHttpResponse作為后備返回。 這是一個非常簡單的ZuulFallbackProvider實現(xiàn)。
demo
在zuul-service中去定義MyFallbackProvider繼承ZuulFallbackProvider,定義了路由id為user-service服務(wù)的回退。
@Component
public class MyFallbackProvider implements ZuulFallbackProvider {
//getRoute返回的必須要和zuul.routes.***一致,才能針對某個服務(wù)降級
@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。
參考資料
官網(wǎng)Providing Hystrix Fallbacks For Routes
異構(gòu)語言支持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和配置服務(wù)器? Spring Cloud Netflix Sidecar的靈感來自Netflix Prana。 它包含一個簡單的http api來獲取給定服務(wù)的所有實例(即主機和端口)。 您還可以通過嵌入式Zuul代理來代理服務(wù)調(diào)用,該代理從Eureka獲取其路由條目。 可以通過主機查找或通過Zuul Proxy直接訪問Spring Cloud Config Server。 非jvm應(yīng)用程序應(yīng)該執(zhí)行健康檢查,以便Sidecar可以向應(yīng)用程序啟動或關(guān)閉時向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的依賴關(guān)系。
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創(chuàng)建一個Spring Boot應(yīng)用程序。 此注釋包括@EnableCircuitBreaker,@EnableDiscoveryClient和@EnableZuulProxy。 在與非jvm應(yīng)用程序相同的主機上運行生成的應(yīng)用程序。
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應(yīng)用程序正在偵聽的端口。 這是因為Sidecar可以正確地注冊該應(yīng)用程序與eureka。 sidecar.health-uri是一個可以在非jvm應(yīng)用程序上訪問的uri,它可以模仿Spring Boot健康指示器。 它應(yīng)該返回一個json文檔,如下所示:
{
"status":"UP"
}
demo
寫一個node.js的服務(wù),端口是8060,訪問localhost:8060返回"歡迎來到首頁",訪問http://localhost:8060/health.json,將會返回{"status":"UP"}
var http = require('http');
var url = require('url');
var path = require('path');
//創(chuàng)建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");
}
});
//創(chuàng)建監(jiān)聽,并打印日志
server.listen(8060,function () {
console.log('listening on localhost:8060');
})
啟動服務(wù):
node node-service.js
分別訪問首頁和健康檢查頁面。
然后新建一個zuul-sidecar服務(wù),依賴如下:
<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微服務(wù)的端口
health-uri: http://localhost:8060/health.json # Node.js微服務(wù)的健康檢查URL
啟動服務(wù),zuul-sidecar注冊到eureka上了,控制面板如下,

通過zuul訪問zuul-sidecar間接訪問node的服務(wù),訪問首頁和健康頁面
http://192.168.1.57:6069/zuul-sidecar/
http://192.168.1.57:6069/zuul-sidecar/health.json
訪問sidecar的服務(wù):
http://localhost:8070/

user服務(wù)訪問node服務(wù)也可以通過sidecar來訪問通過注冊到zuul的服務(wù)名來訪問。
在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的一個示例響應(yīng),它會在不同的主機上返回兩個實例。 這個api可以訪問http://localhost:5678/hosts/{serviceId}的非jvm應(yīng)用程序(如果sidecar在端口5678上)。比如我上面的列子就可以根據(jù)http://localhost:8070/hosts/user-service來查看user-service的服務(wù)信息。具體原因下面解釋。
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中已知的每個服務(wù)的路由添加到/<serviceId>,以便客戶可以在/客戶端使用客戶服務(wù)。 非jvm應(yīng)用程序可以通過http://localhost:5678/customers訪問客戶服務(wù)(假設(shè)邊界正在偵聽端口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
如果配置服務(wù)器在Eureka中注冊,則非jvm應(yīng)用程序可以通過Zuul代理訪問它。 如果ConfigServer的serviceId是configserver,而Sidecar在端口5678上,則可以訪問http://localhost:5678/configserver
使用sidecar也是可以訪問注冊到eureka上的服務(wù),也就是使用zuul的代理,而不需要另外的起一個zuul服務(wù)器。比如下面的可以通過zuul-sidecar訪問user服務(wù)。
http://localhost:8070/user-service/user/index
參考資料
官網(wǎng)Polyglot support with Sidecar
Hystrix和ribbon支持
spring-cloud-starter-zuul依賴包括spring-cloud-starter-hystrix和spring-cloud-starter-ribbon模塊的依賴,所以zuul天生就擁有線程隔離和斷路器的自我保護功能,以及對服務(wù)調(diào)用的客戶端負載均衡功能。
需要注意的事,當使用path與url的映射關(guān)系來配置路由規(guī)則的時候,對于路由轉(zhuǎn)發(fā)的請求不會采用hystrixCommand來包裝,所以這類請求沒有線程隔離和斷路器的保護,并且也不會有負載均衡的能力。因此,我們在使用zuul的時候盡量使用path和serviceId的組合來進行配置,這樣不僅可以保證api網(wǎng)關(guān)的健壯和穩(wěn)定,也能用到ribbon的客戶端負載均衡功能,
我們在使用zuul搭建api網(wǎng)關(guān)的時候,可以通過hystrix和ribbon的參數(shù)來調(diào)整路由請求的各種超時時間等配置,比如下面這些參數(shù)的設(shè)置。
-
hystrix.command.default.execution.isolation.thread.timeoutInMillseconds:該參數(shù)可以用來設(shè)置api網(wǎng)關(guān)中路由轉(zhuǎn)發(fā)請求hystrixCommand執(zhí)行超時時間,單位為毫秒。當路由轉(zhuǎn)發(fā)請求的命令執(zhí)行時間超過該配置值之后,hystrix會將該執(zhí)行命令標記為timeout并拋出異常,zuul會對該異常進行處理并返回如下的json信息給外部調(diào)用方。
{
"timestamp":14454545234324,
"status":500,
"error":"Internal Server Error",
"exception":"com.netflix.zuul.exception.ZuulException",
"message":"TIMEOUT"
}
-
ribbon.ConnectTimeout:該參數(shù)用來設(shè)置路由轉(zhuǎn)發(fā)請求的時候,創(chuàng)建請求連接的超時時間。當ribbon.ConnectTimeout的配置值小于hystrix.command.default.execttion.isolation.thread.timeoutInMilliseconds配置值的時候,若出現(xiàn)路由請求連接超時時,會自動進行重試路由請求,如果請求依然失敗,zuul會返回如下json信息給外部調(diào)用方。
{
"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配置值的時候,當出現(xiàn)路由請求連接超時時,由于此時對于路由轉(zhuǎn)發(fā)的請求命令已經(jīng)超時,所以不會進行重試路由請求,而是直接按請求命令超時處理,返回TIMEOUT的錯誤信息。
-
ribbon.ReadTimeout:該參數(shù)用來設(shè)置路由轉(zhuǎn)發(fā)請求的超時時間,它的處理與ribbon.ConnectTimeout類似,只是它的超時是對請求連接建立之后的處理時間。當ribbon.ReadTimeout的配置值小于hystrix.command.default.execttion.isolation.thread.timeoutInMilliseconds配置值的時候,若路由請求的處理時間超過該配置值并且依賴服務(wù)還沒有響應(yīng)的時候,會自動進行重試路由請求。如果重試后依然沒有獲得請求響應(yīng),zuul會返回NUMBEROF_RETRIES_NEXTSERVER_EXCEEDED錯誤。如果ribbon.ReadTimeout的配置值大于hystrix.command.default.execttion.isolation.thread.timeoutInMilliseconds配置值,若路由請求的處理時間超過該配置值并且依賴服務(wù)還沒有響應(yīng)時,不會進行重試路由請求,而是直接按請求命令超時處理,返回timeout的錯誤信息。
根據(jù)上面的介紹我們知道,在使用zuul的服務(wù)路由時,如果路由轉(zhuǎn)發(fā)請求發(fā)生超時(連接超時或處理超時),只要超時時間的設(shè)置小于hystrix的命令超時時間,那么它就會自動發(fā)起重試。有些背景下,我們需要關(guān)閉重試機制,那么可以通過下面的二個參數(shù)進行設(shè)置。
zuul.retryable=false
zuul.routes.<route>.retryable=false
其中,zuul.retryable用來關(guān)閉全局的重試機制,而zuul.routes.<route>.retryable=false指定路由關(guān)閉重試機制。
本博客代碼
代碼地址