spring cloud zuul之路由功能和路由服务降级

1.介绍

Zuul是spring cloud中的微服务网关。网关: 是一个网络整体系统中的前置门户入口。请求首先通过网关,进行路径的路由,定位到具体的服务节点上。也减少了客户端与服务端的耦合,服务可以独立发展,通过网关层来做映射

Zuul主要有两大功能:路由转发和过滤。路由转发能够为全部服务提供一个唯一的入口,起到外部和内部隔离的作用,保障了后台服务的安全性。过滤可以用来鉴权校验,识别每个请求的权限,拒绝不符合要求的请求。

当它和config server组件配合时能够实现动态路由,动态的将请求路由到不同的后端集群中。

Zuul默认是实现了hystrix,ribbon和actuator

本项目使用的Spring Cloud 版本是:Hoxton.SR3

本章博客主要讲以下内容:

1.zuul的路由功能

2.zuul怎么使用Hystrix的服务断路和服务降级

2.路由功能

要创建一个Zuul项目,很简单:

2.1 引入pom文件

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

2.2 主类添加@EnableZuulProxy注解开启网关服务功能

2.3 application.yml

spring:
  application:
    name: zuul-demo
server:
  port: 5555

这样zuul就搭建完成了,然后我们可以通过在配置文件中对zuul进行配置来实现路由转发。

2.4 传统路由配置

2.4.1 单实例配置

在传统的路由的单实例配置中,通过配置path和url来进行转发,配置格式为:zuul.routes.<routeName>.path和zuul.routes.<routeName>.url,如以下配置,<routeName>则表示api-a,该名称可随意定义。但是一组path和url映射关系的路由名要相同

zuul:
  routes: 
    api-a: 
      path: /apia/**
      url: http://localhost:8080/

像如上配置,当我们访问http://localhost:5555/apia/hello时,会被转发到http://localhost:8080/hello

2.4.2 多实例配置

 通过zuul.routes.<routeName>.path和zuul.routes.<routeName>.serviceId参数进行配置,然后利用ribbon的服务列表实现负载均衡(Zuul中自带了对Ribbon的依赖)

zuul:
  routes:
    api-a:
      path: /apia/**
      serviceId: api-a
api-a:
  ribbon:
    listOfServers: http://localhost:8090/

传统的路由模式中,无论是单实例还是多实例,我们都需要自己去维护路由名称,且需要自己定义path和服务实例的列表,这样无疑是很麻烦的。那我们可以将Eureka加进来,因为Eureka中存在所有的服务列表。

2.5 服务路由配置(Zuul和Eureka整合)

通过Zuul和Eureka的整合,可以实现对服务实例的自动化维护,即我们不需要再为每个serviceId指定具体的服务实例列表.但是我们依然是需要维护请求路径的匹配表达式和服务名的映射关系,(也可不维护,会根据默认的路由规则生成)

为Zuul项目添加Eureka的依赖,和@EnableDiscoveryClient注解。

现在我们启动Eureka注册中心,服务提供者端口8090,和Zuul项目

当我们将Zuul和Eureka整合后,只需要像如下配置即可:route名可随意定义

zuul:
  routes:
    #route名,可随意定义
    feign-service-provider:
      path: /feign-service-provider/**
      serviceId: feign-service-provider

我们也可以使用更简单的配置方式:zuul.routes.<serviceId>=<path>,如上的配置等价于:

zuul:
  routes:
    feign-service-provider: /feign-service-provider/**

然后访问http://localhost:5555/feign-service-provider/okuserpost,返回成功

2.5.1 通过接口的方式查看路由列表

首先需要在application.yml中添加如下配置,将actuator的/routes端口暴露出来,如果不添加配置,访问该端口会报404

management:
  endpoints:
    web:
      exposure:
        include: "*"
 

然后访问http://localhost:5555/actuator/routes,会出现以下json结果:

{
"/feign-service-provider/**": "feign-service-provider"
}

2.5.2 路由默认规则

由于再实际的运用过程中,大部分的路由配置规则几乎都会采用服务名作为外部请求的前缀,比如以下例子,其中path路径的前缀使用了feign-service-provider,而对应的服务名也是feign-service-provider,对于这样有规则性的配置内容,我们希望自动化的完成

zuul:
  routes:
    #route名,可随意定义
    feign-service-provider:
      path: /feign-service-provider/**
      serviceId: feign-service-provider

当我们将Zuul和Eureka整合后,Zuul默认实现了这样一套规则,该规则会自动的生成path和serviceId的关联,这样我们就不需要再为这些服务维护这些基本的路由规则了。你启动项目后,要稍微等一会刷新才会有自动创建的路由列表(zuul需要向注册中心注册服务,并需要拉取注册中心的 服务列表,这是需要一点时间的)

由于默认情况下所有Eureka上的服务都会被Zuul自动地创建路由,这回使得一些我们不希望对外开放的服务也会被外部访问到,这时,我们可以使用zuul.ignored-services参数来设置一个服务名表达式来定义不自动创建路由的规则,如当我们设置zuul.ignored-services=*时,Zuul对所有的服务都不自动创建规则。在这种情况下,我们要在配置文件中逐个为需要路由的服务添加规则(使用zuul.routes.<serviceId>=<path>的方式)。

2.5.3 自定义路由

当我们在版本迭代时,一般都会通过服务名来判断版本,比如userservice-v1,userservice-v2,那默认情况下Zuul自动创建的路由就为/userservice-v1,/userservice-v2,但是这样的表达不利于通过路径规则进行版本管理,通常的路由表达式应该/v1/userservice,/v2/userversice.

要实现自定义,只需要增加如下Bean即可:

@Bean
public PatternServiceRouteMapper serviceRouteMapper(){
    return new PatternServiceRouteMapper(
            "(?<name>^.+)-(?<version>v.+$)","${version}/${name}"
    );
}

PatternServiceRouteMapper对象可以让开发者通过正则表达式来自定义服务与路由映射的生成关系。构造函数第一个参数是用来匹配服务名称是否符合该自定义规则的正则表达式,第二个参数是定义根据服务名中定义的内容转换出的路径表达式规则。当开发者在api网关中定义了PatternServiceRouteMapper实现之后,只需符合第一个参数定义规则的服务名,都会优先使用该实现构建出的表达式,如果没有匹配上的服务规则则还是会使用默认的路由映射规则,即采用完整服务名作为前缀的路径表达式。

注意:name匹配的是服务名,不是路由名

2.5.4 路径匹配

对path参数进行正则表达式匹配时,采用的时Ant风格,该风格有3中通配符

?:匹配任意单个字符,例:/user/?,可匹配/user/a,/user/b...

*: 匹配任意数量的字符,但是只能匹配一级目录,例:/user/*,可匹配/user/a,/user/aa....

**:匹配任意数量的字符,支持多级目录,例:/user/**可匹配/user/a/b,/user/a/c

2.5.5 路径优先级匹配

假设现有有两个path:/user/**和/user/a/**

当我们请求/user/a/b时,我们肯定是希望能够匹配上/user/a/**的,那路径的优先级是怎样的?

从源码中可以发现,路由规则是通过LinkedHashMap来存储的,即是有顺序,从前往后匹配,当匹配上后,就会立即返回,并不会继续执行,那路由规则的优先级即是在LinkedHashMap中的优先级。

在加载路径规则时,是按照配置文件application.yml你编写的顺序来加载的,所以我们可以通过调整在配置文件中的顺序来实现路径的优先级匹配。那当我们请求/user/a/b时,我们的配置文件应该这样来写:

zuul:
  routes:
    userserviceA: /user/a/**
    userserviceB: /user/**

2.5.6 忽略表达式

当我们想忽略一些固定规则的path时,可使用zuul.ignored-patterns来指定Ant风格的正则表达式,例如:忽略/hello接口

zuul:
  ignored-patterns: /**/hello/**

2.5.7 路由前缀

如果想统一为路由加个前缀,可以使用zuul.prefix属性

zuul:
  prefix: /api

zuul.strip-prefix:

该配置是控制是否移除代理前缀。通俗来讲就是当我们访问请求地址,被zuul路由规则匹配上并转发时,是否需要带上前缀.

true表示移除,false表示不移除。

接下来举个例子说明:我们先创建如下路由规则,前缀为/service ,不需要代理前缀。假设userservice服务的端口是8080

zuul:
  prefix: /service
  strip-prefix: true
  routes: 
    userservice: /user/**

当我们访问http://localhost:5555/service/user/hello时,其实会被代理去访问http://localhost:8080/hello,即在代理时会除去前缀.

当strip-prefix的值设为false时,表示在代理时需要前缀。同样去访问同一个上面的url,其实会被代理去访问http://localhost:8080/service/hello

2.5.8 路由前缀的bug

在之前的版本会存在这样一个bug(具体的版本可以测试以下,至少在Camden.SR3之前的版本存在该bug):

假设我现在设置的路由前缀为/api,然后存在3个path:/api/a/**,/api-b/**,/cc/**

当启动项目,路由创建路由规则时,会发现,/api/a/**,/api-b/**这两个path创建出来的路由规则是不正确的(可通过/routes端口查看)且访问也是错误的,只有/cc/**是正确的

所以这个bug的大致触发情况就是当路由前缀和path的前缀相同是,会导致创建的路由规则错误。

在我使用的Hoxton.SR3版本中并没有此bug,应该是在之前的哪个版本修复了

2.5.9 本地跳转

zuul还支持forward形式的服务端跳转,按如下配置即可:

zuul:
  routes:
    userservice:
      path: /api/**
      url: forward:/local

当我们访问http://localhost:5555/api/hello时,会被转发到本地的http://localhost:5555/local/hello

2.5.10 动态路由配置

动态路由主要是依赖config组件,我们知道,config client可以从config server上动态拉取配置。 那我们只需要在config server上的配置文件中配置路由属性,然后由本地拉取即可,例如:我在config server上的配置文件中配置如下:

zuul:
  routes:
    userservice: /user/**
    orderservice: /order/**

在config client端即zuul项目中添加如下配置来动态刷新

@ConfigurationProperties("zuul")
@RefreshScope
public ZuulProperties zuulProperties () {
	return new ZuulProperties();
}

3 Cookie

默认情况下,spring cloud zuul在请求路由时,会过滤掉http请求头信息中一些敏感信息,防止它们被传递到下游的外部服务器。默认的敏感头信息通过zuul.sensitiveHeaders参数定义,默认包括cookie,set-Cookie,authorization三个属性。所以,我们在开发web项目时常用的cookie在spring cloud zuul网关中默认时不传递的,这就会引发一个常见的问题,如果我们要将使用了spring securityshiro等安全框架构建的web应用通过spring cloud zuul构建的网关来进行路由时,由于cookie信息无法传递,我们的web应用将无法实现登录和鉴权。为了解决这个问题,配置的方法有很多。

  • 通过设置全局参数为空来覆盖默认值,具体如下:
zuul:
  sensitive-headers: 

这种方法不推荐,虽然可以实现cookie的传递,但是破坏了默认设置的用意。在微服务架构的api网关之内,对于无状态的restful api请求肯定时要远多于这些web类应用请求的,甚至还有一些架构设计会将web类应用和app客户端一样归为api网关之外的客户端应用。

  • 通过指定路由的参数来设置,方法有下面二种。
    方法一:对指定路由开启自定义敏感头。
    方法二:将指定路由的敏感头设置为空。
#方式一:
zuul:
  routes:
    userservice:
      custom-sensitive-headers = true
#方式二
zuul:
  routes:
    userservice:
      sensitive-headers:

比较推荐使用这二种方法,仅对指定的web应用开启对敏感信息的传递,影响范围小,不至于引起其他服务的信息泄露问题。

4. 使用Hystrix的功能来调整路由请求的超时时间等配置

由于zuul默认是依赖了Hystrix和Ribbon,所以我们在配置文件中编写映射规则时,最好使用path+serviceId的方式

4.1  设置网关中路由转发请求的HystrixCommand执行超时时间(单位毫秒)

当路由转发请求的命令执行时间超过该配置值后,Hystrix会抛出异常,Zuul会对该异常进行处理并返回如下Json信息

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 1
{
    "timestamp": "2020-05-06T08:18:27.943+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "TIMEOUT"
}

4.2 设置路由转发请求的时候,创建请求连接的超时时间

ribbon:
  SocketTimeout: 1

4.3 设置路由转发请求的超时时间,该超时时间是对请求连接建立之后的处理时间

ribbon:
  ReadTimeout: 1

5. Zuul的服务降级

如果我们想对第4节的异常进行服务降级,需要创建一个如下的类:

public class MyFallbackProvider implements FallbackProvider {
    @Override
    public String getRoute() {
        // 可指定路由,比如userservice,*表示针对所有的路由,如果出现Hystrix异常则进行以下的服务降级处理
        return "*";
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, final Throwable cause) {
        if (cause instanceof HystrixTimeoutException) {
            return response(HttpStatus.GATEWAY_TIMEOUT);
        } else {
            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;
            }
        };
    }
}

然后将这个对象作为Bean

@Bean
public MyFallbackProvider myFallbackProvider() {
	return new MyFallbackProvider();
}

接着我们再次访问,会出现以下结果:

fallback

即直接返回一个字符串

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章