【第五章】API服务网关(Zuul) 上

2021-07-16 21:15

阅读:435

微服务场景下,每一个微服务对外暴露了一组细粒度的服务。客户端的请求可能会涉及到一串的服务调用,如果将这些微服务都暴露给客户端,那么客户端需要多次请求不同的微服务才能完成一次业务处理,增加客户端的代码复杂度。另外,对于微服务我们可能还需要服务调用进行统一的认证和校验等等。微服务架构虽然可以将我们的开发单元拆分的更细,降低了开发难度,但是如果不能够有效的处理上面提到的问题,可能会造成微服务架构实施的失败。

Zuul参考GOF设计模式中的Facade模式,将细粒度的服务组合起来提供一个粗粒度的服务,所有请求都导入一个统一的入口,那么整个服务只需要暴露一个api,对外屏蔽了服务端的实现细节,也减少了客户端与服务器的网络调用次数。这就是API服务网关(API Gateway)服务。我们可以把API服务网关理解为介于客户端和服务器端的中间层,所有的外部请求都会先经过API服务网关。因此,API服务网关几乎成为实施微服务架构时必须选择的一环。

Spring Cloud Netflix的Zuul组件可以做反向代理的功能,通过路由寻址将请求转发到后端的粗粒度服务上,并做一些通用的逻辑处理。

通过Zuul我们可以完成以下功能:

  • 动态路由
  • 监控与审查
  • 身份认证与安全
  • 压力测试: 逐渐增加某一个服务集群的流量,以了解服务性能;
  • 金丝雀测试
  • 服务迁移
  • 负载剪裁: 为每一个负载类型分配对应的容量,对超过限定值的请求弃用;
  • 静态应答处理

1. 构建网关

1.1 构建Zuul-Server

编写pom.xml文件

Zuul-Server是一个标准的Spring Boot应用,所以还是继承自我们之前的parent:

 

xml version="1.0" encoding="UTF-8"?>
project xmlns="http://maven.apache.org/POM/4.0.0" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    modelVersion>4.0.0modelVersion>

    parent>
        groupId>twostepsfromjava.cloudgroupId>
        artifactId>twostepsfromjava-cloud-parentartifactId>
        version>1.0.0-SNAPSHOTversion>
        relativePath>../parentrelativePath>
    parent>

    artifactId>zuul-serverartifactId>
    name>Spring Cloud Sample Projects: Zuul Proxy Servername>

    dependencies>
        dependency>
            groupId>org.springframework.cloudgroupId>
            artifactId>spring-cloud-starter-zuulartifactId>
        dependency>
        dependency>
            groupId>org.springframework.cloudgroupId>
            artifactId>spring-cloud-starter-eurekaartifactId>
        dependency>
    dependencies>

    build>
        plugins>
            plugin>
                groupId>org.springframework.bootgroupId>
                artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>
project>

 

这里我们增加了spring-cloud-starter-zuul的依赖。

编写启动类

/**
 * TwoStepsFromJava Cloud -- Zuul Proxy 服务器
 *
 * @author CD826(CD826Dong@gmail.com)
 * @since 1.0.0
 */
@EnableZuulProxy
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

这里我们增加了对主应用类增加了@EnableZuulProxy,用以启动Zuul的路由服务。

编写配置文件application.properties

server.port=8280

spring.application.name=ZUUL-PROXY

eureka.client.service-url.defaultZone=http://localhost:8260/eureka

这里定义服务名称为: ZUUL-PROXY,端口设为: 8280

1.2 构建User-Service

为了后面的则是我们再增加一个微服务: 用户服务。

编写pom.xml文件

同样继承自我们之前的parent:

xml version="1.0" encoding="UTF-8"?>
project xmlns="http://maven.apache.org/POM/4.0.0" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    modelVersion>4.0.0modelVersion>

    parent>
        groupId>twostepsfromjava.cloudgroupId>
        artifactId>twostepsfromjava-cloud-parentartifactId>
        version>1.0.0-SNAPSHOTversion>
        relativePath>../parentrelativePath>
    parent>

    artifactId>user-serviceartifactId>
    name>Spring Cloud Sample Projects: User Service Servername>

    dependencies>
        dependency>
            groupId>org.springframework.bootgroupId>
            artifactId>spring-boot-starter-webartifactId>
        dependency>
        dependency>
            groupId>org.springframework.cloudgroupId>
            artifactId>spring-cloud-starter-eurekaartifactId>
        dependency>

        dependency>
            groupId>${project.groupId}groupId>
            artifactId>service-apiartifactId>
            version>${project.version}version>
        dependency>
    dependencies>

    build>
        plugins>
            plugin>
                groupId>org.springframework.bootgroupId>
                artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>
project>

编写启动类

启动类和之前的Product-Service一样,所以这里不再列出来。

编写服务接口

示例的服务接口非常简单,就是根据给定的登录名称查询一个用户信息。如下:

/**
 * User API服务
 *
 * @author CD826(CD826Dong@gmail.com)
 * @since 1.0.0
 */
@RestController
@RequestMapping("/users")
public class UserEndpoint {
    protected Logger logger = LoggerFactory.getLogger(UserEndpoint.class);

    @Value("${server.port:2200}")
    private int serverPort = 2200;

    @RequestMapping(value = "/{loginName}", method = RequestMethod.GET)
    public User detail(@PathVariable String loginName) {
        String memos = "I come form " + this.serverPort;
        return new User(loginName, loginName, "/avatar/default.png", memos);
    }
}

其中User类定义在之前的service-api项目中,代码如下:

/**
 * 用户信息DTO对象
 *
 * @author CD826(CD826Dong@gamil.com)
 * @since 1.0.0
 */
public class User {
    private static final long serialVersionUID = 1L;

    // ========================================================================
    // fields =================================================================
    private String loginName;                                   // 用户登陆名称
    private String name;                                        // 用户姓名
    private String avatar;                                      // 用户头像
    private String memos;                                       // 信息备注

    // ========================================================================
    // constructor ============================================================
    public User() {
    }

    public User(String loginName, String name, String avatar, String memos) {
        this.loginName = loginName;
        this.name = name;
        this.avatar = avatar;
        this.memos = memos;
    }

    // ==================================================================
    // setter/getter ====================================================
    // ... 省略,请自行补充 ...
}

编写配置文件application.properties

server.port=2200

spring.application.name=USER-SERVICE

eureka.client.service-url.defaultZone=http://localhost:8260/eureka

 

这里定义服务名称为: USER-SERVICE,默认端口设为: 2200

代码修改,就是这么多,下面让我们启动进行测试。

1.3 启动测试

启动各服务

请按照下面的顺序启动各服务器:

  1. Service-discovery
  2. Product-Service
  3. User-Service(2200)
  4. User-Service(2300): java -jar user-service-1.0.0-SNAPSHOT.jar --server.port=2300
  5. Zuul-Server

Ok, 服务启动后我们可以在Eureka服务器看到如下界面:

 
技术分享图片

这里我们启动两个User-Service主要是为了后面进行负载均衡测试使用。

测试路由服务

首先,我们在浏览器中输入以下地址: http://localhost:8280/product-service/products,将会显示以下界面:

技术分享图片

然后,我们在浏览器中输入以下地址: http://localhost:8280/user-service/users/admin,将会显示以下界面:

 
技术分享图片

可见,Zuul-Server已经帮我们路由到相应的微服务。

负载均衡测试

接下来我们测试一下负载均衡是否可以正常工作。前面我们已经启动了两个User-Service微服务,端口分别为:2200和2300。我们多次在浏览器中输入以下地址: http://localhost:8280/user-service/users/admin进行请求,我们将会看到以下信息会在屏幕中交替输出:

{"loginName":"admin","name":"admin","avatar":"/avatar/default.png","memos":"I come form 2200"}
{"loginName":"admin","name":"admin","avatar":"/avatar/default.png","memos":"I come form 2300"}

 

 

可见,负载均衡也是正常工作的。

Hystrix容错与监控测试

之前我们是在Mall-Web项目中集成Hystrix的监控,那么我们启动该服务。然后在Hystrix Dashboard中输入: http://localhost:8280/hystrix.stream,然后进行监控,那么我们将看到如下界面:

 
技术分享图片

这说明,Zuul已经整合了Hystrix。

spring-cloud-starter-zuul本身已经集成了hystrix和ribbon,所以Zuul天生就拥有线程隔离和断路器的自我保护能力,以及对服务调用的客户端负载均衡功能。但是,我们需要注意,当使用path与url的映射关系来配置路由规则时,对于路由转发的请求则不会采用HystrixCommand来包装,所以这类路由请求就没有线程隔离和断路器保护功能,并且也不会有负载均衡的能力。因此,我们在使用Zuul的时候尽量使用path和serviceId的组合进行配置,这样不仅可以保证API网关的健壮和稳定,也能用到Ribbon的客户端负载均衡功能。

2. Zuul配置

2.1 路由配置详解

或许你会觉得神奇,之前我们什么也没有配置,通过http://localhost:8280/product-service/products、http://localhost:8280/user-service/users/admin已经可以正确的访问到我们的微服务了,这就是Zuul的默认路由映射功能在起作用,那么接下来具体来看看Zuul是怎么进行路由配置的。

1) 服务路由默认规则

当我们构建API服务网关时引入Eureka时,那么Zuul会自动为每个服务都创建一个默认路由规则: 访问路径的前缀为serviceId配置的服务名称,也就是之前为什么我们能够所使用:

http://localhost:8280/product-service/products

 

 

来访问Product-Service中所提供的products服务端点的原因。

2) 自定义微服务访问路径

配置格式为: zuul.routes.微服务Id = 指定路径,如:

zuul.routes.user-service = /user/**

 

 

这样,我们后面就可以通过/user/来访问user-service所提供的服务,比如之前的访问可以更改为: http://localhost:8280/user/users/admin。

所要配置的路径可以指定一个正则表达式来匹配路径,因此,/user/*只能匹配一级路径,但是通过/user/**可以匹配所有以/user/开头的路径。

3) 忽略指定微服务

配置格式为: zuul.ignored-services=微服务Id1,微服务Id2...,多个微服务之间使用逗号分隔。如:

zuul.ignored-services=user-service,product-service

4) 同时指定微服务Id和对应路径

zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=service-A

zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=service-B

5) 同时指定微服务Url和对应路径

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

 

如之前所述,通过url配置的路由不会由HystrixCommand来执行,自然,也就得不到Ribbon的负载均衡、降级、断路器等功能。所以在实施尽量使用serviceId进行配置,也可以采用下面的配置方式。

6) 指定多个服务实例及负载均衡

如果需要配置多个服务实例,则配置如下:

zuul.routes.user.path: /user/**
zuul.routes.user.serviceId: user

ribbon.eureka.enabled=false
user.ribbon.listOfServers: http://192.168.1.10:8081, http://192.168.1.11:8081

 

7) forward跳转到本地url

zuul.routes.user.path=/user/**
zuul.routes.user.url=forward:/user

 

8) 路由前缀

可以通过zuul.prefix可为所有的映射增加统一的前缀。如: /api。默认情况下,代理会在转发前自动剥离这个前缀。如果需要转发时带上前缀,可以配置: zuul.stripPrefix=false来关闭这个默认行为。例如:

zuul.routes.users.path=/myusers/**
zuul.routes.users.stripPrefix=false

 

注意: zuul.stripPrefix只会对zuul.prefix的前缀起作用。对于path指定的前缀不会起作用。

 

9) 路由配置顺序

如果想按照配置的顺序进行路由规则控制,则需要使用YAML,如果是使用propeties文件,则会丢失顺序。例如:

zuul:
  routes:
    users:
      path: /myusers/**
    legacy:
      path: /**

上例如果是使用properties文件进行配置,则legacy就可能会先生效,这样users就没效果了。

10) 自定义转换

我们也可以一个转换器,让serviceId和路由之间使用正则表达式来自动匹配。例如:

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

这样,serviceId为“users-v1”的服务,就会被映射到路由为“/v1/users/”的路径上。任何正则表达式都可以,但是所有的命名组必须包括servicePattern和routePattern两部分。如果servicePattern没有匹配一个serviceId,那就会使用默认的。在上例中,一个serviceId为“users”的服务,将会被映射到路由“/users/”中(不带版本信息)。这个特性默认是关闭的,而且只适用于已经发现的服务。

2.2 Zuul的Header设置

敏感Header设置

同一个系统中各个服务之间通过Headers来共享信息是没啥问题的,但是如果不想Headers中的一些敏感信息随着HTTP转发泄露出去话,需要在路由配置中指定一个忽略Header的清单。

默认情况下,Zuul在请求路由时,会过滤HTTP请求头信息中的一些敏感信息,默认的敏感头信息通过zuul.sensitiveHeaders定义,包括CookieSet-CookieAuthorization。配置的sensitiveHeaders可以用逗号分割。

对指定路由的可以用下面进行配置:

# 对指定路由开启自定义敏感头
zuul.routes.[route].customSensitiveHeaders=true 
zuul.routes.[route].sensitiveHeaders=[这里设置要过滤的敏感头]

设置全局:

zuul.sensitiveHeaders=[这里设置要过滤的敏感头]

忽略Header设置

如果每一个路由都需要配置一些额外的敏感Header时,那你可以通过zuul.ignoredHeaders来统一设置需要忽略的Header。如:

zuul.ignoredHeaders=[这里设置要忽略的Header]

在默认情况下是没有这个配置的,如果项目中引入了Spring Security,那么Spring Security会自动加上这个配置,默认值为: Pragma,Cache-Control,X-Frame-Options,X-Content-Type-Options,X-XSS-Protection,Expries

此时,如果还需要使用下游微服务的Spring Security的Header时,可以增加下面的设置:

zuul.ignoreSecurityHeaders=false

2.3 Zuul Http Client

Zuul的Http客户端支持Apache Http、Ribbon的RestClient和OkHttpClient,默认使用Apache HTTP客户端。可以通过下面的方式启用相应的客户端:

# 启用Ribbon的RestClient
ribbon.restclient.enabled=true

# 启用OkHttpClient
ribbon.okhttp.enabled=true

如果需要使用OkHttpClient需要注意在你的项目中已经包含com.squareup.okhttp3相关包。

 

3. Zuul容错与回退

我们再来仔细看一下之前Hystrix的监控界面:

 
技术分享图片

请注意,Zuul的Hystrix监控的粒度是微服务,而不是某个API,也就是所有经过Zuul的请求都会被Hystrix保护起来。假如,我们现在把Product-Service服务关闭,再来访问会出现什么结果呢?结果可能不是我们所想那样,如下:

 
技术分享图片

呃,比较郁闷是么!那么如何为Zuul实现容错与回退呢?

Zuul提供了一个ZuulFallbackProvider接口,通过实现该接口就可以为Zuul实现回退功能。那么让我们改造之前的Zuul-Server

3.1 实现回退方法

代码如下:

/**
 * Product Service服务失败回退处理
 *
 * @author CD826(CD826Dong@gmail.com)
 * @since 1.0.0
 */
@Component
public class ProductServiceFallbackProvider implements ZuulFallbackProvider {
    protected Logger logger = LoggerFactory.getLogger(ProductServiceFallbackProvider.class);

    @Override
    public String getRoute() {
        // 注意: 这里是route的名称,不是服务的名称,
        // 如果这里写成大写PRODUCT-SERVICE将无法起到回退作用
        return "product-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("商品服务暂不可用,请稍后重试!".getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
                return headers;
            }
        };
    }
}

需要说明的是:

  • getRoute方法返回了我们要为那个微服务提供回退。这里需要注意的返回的值是route的名称,不是服务的名称,不能够写为: PRODUCT-SERVICE,否则该回退将不起作用;
  • fallbackResponse方法返回ClientHttpResponse对象,作为我们的回退响应。这里实现非常简单仅仅是返回:商品服务暂不可用,请稍后重试! 的提示。

3.2 重启测试

重启Zuul-Server,再重复上面的实验,将会看到以下界面:

 
技术分享图片

说明,回退方法已经起作用了。如果你的没有起作用,那么仔细检查一下getRoute的返回是否正确。

 

 

 

 

 

 

 

 

 原文地址:http://www.jianshu.com/p/be5b26a9fa42

 


评论


亲,登录后才可以留言!