zuul学习三:zuul路由详解(二)

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 secutityshiro在登录完成之后,通过重定向的方式跳转到登录后的页面,此时登录后的请求结果状态吗为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语言你想利用EurekaRibbon和配置服务器? 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.portsidecar.health-uriapplication.ymlsidecar.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-hystrixspring-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指定路由关闭重试机制。

本博客代码
代码地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容