使用Zuul构建微服务网关

使用Zuul构建微服务网关

为什么要使用微服务网关

经过前文的讲解,微服务架构已经初具雏形,但还有一些问题——不同的微服务一般会有不同的网络地址,而外部客户端(例如手机APP)可能需要调用多个服务的接口才能完成一个业务需求。
如果让客户端直接与各个微服务通信,会有以下的问题:

  • 客户端多次请求不同的微服务,增加了客户端的复杂性
  • 存在跨域请求,在一定场景下处理相对复杂
  • 认证复杂,每个服务都需要独立认证
  • 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将很难实施。
  • 某些微服务可能使用了对防火墙/浏览器不友好的协议,直接访问时会有一定的困难。
    以上问题可借助微服务网关解决。微服务网关是介于客户端和服务器之间的中间层,所有的外部请求都会先经过微服务网关。
    微服务网关封装了应用程序的内部结构,客户端只用跟网关交互,而无须直接调用特定微服务的接口。这样,开发就可以得到简化。不仅如此使用微服务网关还有以下优点。
  • 易于监控。可在微服务网关收集监控数据并将其推送到外部系统进行分析。
  • 易于认证。可在微服务网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
  • 减少了客户端与各个微服务之间的交互次数。

Zuul简介

Zuul是Netflix开源的微服务网关,它可以和Eureka、Ribbon、Hystrix等组件配合使用。Zuul的核心是一系列的过滤器,这些过滤器可以完成以下功能。

  • 身份认证与安全:识别每个资源的验证要求,并拒绝那些与要求不符的请求。
  • 审查与监控:在边缘位置追踪有意义的数据和统计结果,从而带来精确的生产视图。
  • 动态路由:动态地将请求路由到不同地后端集群。
  • 压力测试:逐渐增加指向集群地流量,以了解性能。
  • 负载分配:为每一种负载类型分配对应容量。并弃用超出限定值地请求。
  • 静态响应处理:在边缘位置直接建立部分响应,从而避免其转发到内部集群。
  • 多区域弹性:跨越AWS Region进行请求路由,旨在实现ELB使用地多样化,以及让系统的边缘更贴近系统的使用者。
    Spring Cloud对Zuul进行了整合与增强。目前,Zuul使用的默认HTTP客户端是Apache HTTP Client,也可以使用RestClient或者okhttp3.OkHttpClient。如果想要使用RestClient,可以设置ribbon.restclient.enabled=true;想要使用okhttp3.OkHttpClient,可以设置ribbon.okhttp.enabled=true。

编写Zuul微服务网关

1)创建一个Maven工程,ArtifactId是microservice-gateway-zuul,并为项目添加以下依赖。

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

2)在启动类上添加注解@EnableZuulProxy,声明一个Zuul代理。该代理使用Ribbon来定位注册在Eureka Server中的微服务;同时,该代理还整合了Hystrix,从而实现了容错,所有经过Zuul的请求都会在Hystrix命令中执行。

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {
    public static void main(String[] args){
        SpringApplication.run(ZuulApplication.class,args);
    }
}

3)编写配置文件application.xml

server:
  port: 8040
spring:
  application:
    name: microservice-gateway-zuul
eureka:
  client:
    service-url: 
      defaultZone: http://localhost:8761/eureka/

测试一、路由规则
1)启动项目microservice-discovery-eureka。
2)启动项目microservice-provider-user。
3)启动项目microservice-consumer-movie-ribbon。
4)启动项目microservice-gateway-zuul。
5)访问http://localhost:8040/microservice-consumer-movie/user/1,请求会被转发到http://localhost:8010/user/1(电影微服务)。
在这里插入图片描述
6)访问http://localhost:8040/microservice-provider-user/1,请求会转发到http://localhost:8000/1(用户微服务)
在这里插入图片描述
说明在默认情况下,Zuul会代理所有注册到Eureka Server的微服务,并且Zuul的路由规则如下:
http://ZUUL_HOST:ZUUL_PORT/微服务在Eureka上的serviceId/**会被转发到serviceId对应的微服务。
测试二、负载均衡
1)启动项目microservice-discovery-eureka
2)启动多个microservice-provider-user实例
3)启动项目microservice-gateway-zuul。此时,Eureka Server首页。
在这里插入图片描述

4)多次访问http://localhost:8040/microservice-provider-user/1,会发现两个用户微服务节点都打印了
在这里插入图片描述
说明Zuul可以使用Ribbon达到负载均衡的效果。
测试三、Hystrix容错与监控
1)启动项目microservice-discovery-eureka
2)启动项目microservice-provider-user
3)启动项目microservice-consumer-movie-ribbon
4)启动项目microservice-gateway-zuul
5)启动项目microservice-hystrix-dashboard
6)访问http://localhost:8040/microservice-consumer-movie/user/1,可获得预期结果。
在这里插入图片描述

7)在Hystrix Dashboard中输入http://localhost:8040/hystrix.stream,随意指定某个Title并单击Monitor Stream按钮后
在这里插入图片描述
说明Zuul已经整合了Hystrix

管理端点

当@EnableZuulProxy与Spring Boot Actuator配合使用时,Zuul会暴露两个端点:/routes和filters。借助这些端点,可方便、直观地查看以及管理Zuul。

routes端点

/routes端点的使用非常简单

  1. 使用GET方法访问该端点,即可返回Zuul当前映射的路由列表。
  2. 使用POST方法访问该端点就会强制刷新Zuul当前映射的路由列表。尽管路由会自动刷新,Spring Cloud依然提供了强制立即刷新的方式。
  3. Spring Cloud Edgware对/routes端点进行了进一步的增强,我们可使用/routes?format=details查看更多与路由相关的详情设置。
    由于spring-cloud-starter-netfilx-zuul已经包含了spring-boot-starter-actuator,因此之前编写的microservice-gateway-zuul已具备路由管理的能力。
    测试
    1)启动项目microservice-discovery-eureka
    2)启动项目microservice-provider-user
    3)启动项目microservice-consumer-movie-ribbon
    4)修改项目microservice-gateway-zuul的配置,设置management.security.enabled: false
    5)启动项目microservice-gateway-zuul
    6)使用浏览器访问http://localhost:8040/routes,可获得如下的结果
    在这里插入图片描述
    7)访问http://localhost:8040/routes?format=details
    在这里插入图片描述
    由结果可知,此时可看到更多与路由相关的配置,例如路由id、转发路径、是否重试等,这些对于我们调试很有用
    也可使用类似方式测试Zuul路由的自动刷新与强制刷新。

filters端点

从Spring Cloud Edgware版本开始,Zuul提供了/filters端点。访问该端点即可返回Zuul中当前所有过滤器的详情,并按照类型分类。
如下是/filters端点的展示结果,从中,我们可以了解当前Zuul中,error、post、pre、route四种类型的过滤器分别有哪些,每个过滤器order(执行顺序)是多少,以及是否启用等信息。这对Zuul问题的定位很有用。

在这里插入图片描述

路由配置详情

前文已经编写了一个简单的Zuul网关,并让该网关代理了所有注册到Eureka Server的微服务。但在现实中可能只想让Zuul代理部分微服务,又或者需要对URL进行更加精确的控制。
1)自定义指定微服务的访问路径
配置zuul.routes.指定微服务的serviceId = 指定路径接口

zuul:
  routes: 
    microservice-provider-user: /user/**

完成设置后,microservice-provider-user微服务就会被映射到/user/** 路径。
2)忽略指定微服务
忽略服务非常简单,可以使用zuul.ignored-services配置需要忽略的服务,多个服务间用逗号分隔。

zuul:
  ignored-services: microservice-provider-user,microservice-consumer-movie

这样就可让Zuul忽略microservice-provider-user和microservice-consumer-movie微服务,只代理其他微服务。
3)忽略所有微服务,只路由指定微服务
很多场景下,可能只想要让Zuul代理指定的微服务,此时可以将zuul.ignored-services设为’*’

zuul:
  routes:
    microservice-provider-user: /user/**
  ignored-services: '*'

这样就可以让Zuul只路由microservice-provider-user微服务。
4)同时指定微服务的serviceId和对应路径。

zuul:
  routes:
    user_route:  #该配置方式中,user-routes只是给路由一个名称,可以任意起名。
      service-id: microservice-provider-user
      path: /user/**  #service-id对应的路径

5)同时指定path和URL

zuul:
  routes:
    user_route:
      url: http://localhost:8000/
      path: /user/**

这样可以将/user/**映射到http://localhost:8000/**
需要注意的是,使用这种方式配置的路由不会作为HystrixCommand执行,同时也不能使用RIbbon来负载均衡多个URL。
6)同时指定path和URL,并且不破坏Zuul的Hystrix、Ribbon特性

zuul:
  routes:
    user_route:
      service-id: microservice-provider-user
      path: /user/**
ribbon:
  eureka:
    enable: false
microservice-provider-user:
  ribbon:
    listOfServers: localhost:8000,localhost:8001

7)使用正则表达式指定Zuul的路由匹配规则
借助PatternServiceRouteMapper,实现从微服务到映射路由的正则配置

package com.example.config;

import org.springframework.cloud.netflix.zuul.filters.discovery.PatternServiceRouteMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RouteMapperConfig {
    @Bean
    public PatternServiceRouteMapper serviceRouteMapper(){
        //调用构造函数PatternServiceRouteMapper(String servicePattern, String routePattern)
        //servicePattern指定微服务的正则
        //routePattern指定路由正则
        return new PatternServiceRouteMapper("(?<name>^.+)-(?<version>v.+$)","${version}/${name}");
    }
}

通过这段代码即可实现将诸如microservice-provider-user-v1这个微服务,映射到/v1/microservice-provider-user/**这个路径。
8)路由前缀

zuul:
  prefix: /api
  strip-prefix: false
  routes:
    microservice-provider-user: /user/**

这样,访问Zuul的/api/user/1路径,请求将被发送到microservice-provider-user的/api/1。

zuul:
  routes:
    microservice-provider-user:
      path: /user/**
      strip-prefix: false

这样,访问Zuul的/api/user/1路径,请求将被发送到microservice-provider-user的/user/1。
9)忽略某些路径
上文讲解了如何忽略微服务,但有时还需要更细粒度的路由控制。例如,想让Zuul代理某个微服务,同时又想保护该微服务的某些敏感路径。此时,可使用ignored-Patterns,指定忽略的正则。

zuul:
  ignored-patterns: /**/admin/**
  routes:
    microservice-provider-user: /user/**

这样就可将microservice-provider-user微服务映射到/user/**路径,但会忽略该微服务中所有包含/admin/的路径。
10)本地转发

zuul:
  routes: 
    route-name:
      path: /path-a/**
      url: 
        forward: /path-b

这样,当访问Zuul的/path-a/**路径,将转发到Zuul的/path-b/**
注:若无法掌握Zuul路由的规律,可将com.example包的日志级别设为DEBUG。这样,Zuul就会打印转发的具体细节,从而有助于更好地理解Zuul地路由配置。

logging:
  level:
    com.example: DEBUG

Zuul的安全与Header

敏感Header的设置

一般来说,可在同一个系统中的服务之间共享Header。不过应尽量防止让一些敏感的Header外泄。因此,在很多场景下,需要通过为路由指定一些了敏感Header列表。

zuul:
  routes:
    microservice-consumer-movie:
      path: /users/**
      sensitive-headers: Cookie,Set-Cookie,Authorization
      url: https://downstream

这样就可为microservice-consumer-movie微服务指定敏感Header了。
也可用zuul.sensitive-headers全局指定敏感Header

zuul:
  sensitive-headers: Cookie,Set-Cookie,Authorization

需要注意的是,如果使用zuul.routes.*.sensitive-headers的配置方式,会覆盖全局的配置。

忽略Header

可使用zuul.ignoredHeaders属性丢弃一些Header

zuul:
  ignored-headers: Header1,Header2

这样设置后,Header1和Header2将不会传播到其他的微服务中。
默认情况下,zuul.ignored-headers是空值,但如果Spring Security在项目的classpath中,那么zuul.ignored-headers的默认值就是Pragma,Cache-Contronl,X-Frame-Options,X-Content-Type-Option,X-XSS-Protection,Expires。所以,当Spring Security在项目的classpath中,同时又需要使用下游微服务的Spring Security的Header时,可以将zuul.ignoreSecurity-Headers设置false。

使用Zuul上传文件

对于小文件(1M以内)上传,无须任何处理,即可正常上传。对于大文件(10M以上)上传,需要为上传路径添加/zuul前缀。也可使用zuul.servlet-path自定义前缀。
也就是说,假设zuul.routes.microservice-flie-upload = /microservice-file-upload/**,如果http://{HOST}:{PORT}/upload是微服务microservice-file-upload的上传路径,则可使用Zuul的/zuul/microservice-file-upload/upload路径上传大文件。
如果Zuul使用了Ribbon做负载均衡,那么对于超大的文件,需要提升超时设置。

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMillseconds: 60000
ribbon:
  ConnectTimeout: 3000
  ReadTimeout: 60000

1)创建一个Maven工程,ArtifactId是microservice-file-upload,并为项目添加以下依赖。

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

2)在启动类上添加@SpringBootApplication、@EnableEurekaClient注解。

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class FileUploadApplication {
    public static void main(String[] args){
        SpringApplication.run(FileUploadApplication.class,args);
    }
}

3)编写Controller

package com.example.controller;

import org.springframework.stereotype.Controller;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;

@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();
    }
}

配置文件application.yml,在其中添加如下内容

server:
  port: 8050
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true
spring:
  application:
    name: microservice-file-upload
  http:
    multipart:
      max-file-size: 2000Mb			#Max file size,默认1M
      max-request-size: 2500Mb		#Max request size,默认10M

由配置可知,已经将该服务注册到Eureka Server上,并配置了文件上传大小的限制。这样一个文件上传的微服务就编写完成了。

curl -F "file=@D:/user.csv" localhost:8050/upload

在这里插入图片描述

测试
1)准备1个小文件(1M以下),记为small.file;1个超大文件(1G以上,2G以上),记为large.file。
2)启动microservice-discovery-eureka
3)启动microservice-file-upload
4)启动microservice-gateway-zuul
5)测试直接上传到microservice-file-upload上:使用命令curl -F "file=@D:/user.csv" localhost:8050/upload,上传大文件,发现可以正常上传。同理,小文件也可以上传。
6)测试通过Zuul上传小文件

curl -v -H "Transfer-Encoding: chunked" -F "file=@D:/user.csv" localhost:8040/microservice-file-upload/upload

在这里插入图片描述
7)测试通过Zuul上传大文件,不添加/zuul前缀。

curl -v -H "Transfer-Encoding: chunked" -F "file=@D:/user.csv" localhost:8040/microservice-file-upload/upload

在这里插入图片描述
8)测试通过Zuul上传大文件,添加/zuul前缀

curl -v -H "Transfer-Encoding: chunked" -F "file=@D:/test.exe" localhost:8040/zuul/microservice-file-upload/upload

在这里插入图片描述
9)因此,不妨在application.yml中添加如下内容。

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
  ConnectTimeout: 3000
  ReadTimeout: 60000

10)关闭microservice-gateway-zuul,启动microservice-gateway-zuul-file-upload再次使用如下命令进行测试,此时已可正常上传文件。
在这里插入图片描述

Zuul的过滤器

过滤器类型与请求生命周期

Zuul大部分功能都是通过过滤器来实现的。Zuul中定义了4种标准过滤器类型,这些过滤器类型对应请求的典型生命周期。

  • PRE:这种过滤器在请求被路由之前调用。可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
  • POUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务。
  • POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
  • ERROR:在其他阶段发送错误时执行该过滤器。
    除了默认的过滤器类型,Zuul还允许创建自定义的过滤器类型。例如,可以定制一种STATIC类型的 过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务。

内置过滤器详解

Spring Cloud默认为Zuul编写并启用了一些过滤器,这些过滤器有什么作用呢?我们不妨结合@EnableZuulServer、@EnableZuulProxy两个注解进行讨论。
可将@EnableZuulProxy简单理解为@EnableZuulServer的增强版。事实上,当Zuul与Eureka、Ribbon等组件配合使用时,@EnableZuulProxy是我们最常用的注解,这里使用的也是@EnableZuulServer。
@RequestContext,其用于在过滤器之间传递消息。它的数据保存在每个请求的ThreadLocal中。它用于存储请求路由到哪里、错误、HttpServletRequest、HttpServletResponse等信息。RequestContext扩展了ConcurrentHashMap,所以,理论上任何数据都可以存储在RequestContext中。

@EnableZuulServer所启用的过滤器

pre类型过滤器
1)ServletDetectionFilter:该过滤器用于查看请求是否通过Spring Dispatcher。检查后,通过FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY设置布尔值。
2)FormBodyWrapperFilter:解析表单数据,并为请求重新编码。
3)DebugFilter:顾名思义,调试用的过滤器。当设置zuul.includer-debug-header=true抑或设置zuul.debug.request=true,并在请求时加上了debug=true的参数,例如$ZUUL_HOST:ZUUL_PORT/some-path?debug=true,就会开启该过滤器。该过滤器会把RequestContext.setDebugRouting()以及RequestContext.setDebugRequest()设为true。
route类型过滤器
SendForwardFilter:该过滤器使用Servlet RequestDispatcher转发请求,转发位置存储在RequestContext的属性FilterConstants.FORWARD_TO_KEY中。这对转发到Zuul自身的端点很有用。

zuul:
	routes:
		abc:
			path: /path-a/**
			url: forward:/path-b

post类型过滤器
SendResponseFilter:将代理请求的响应写入当前响应。
error类型过滤器
SendErrorFilter:若RequestContext.getThrowable()不为null,则默认转发到/error,也可以设置error.path属性来修改默认的转发路径。

@EnableZuulProxy所启用的过滤器

如果使用注解@EnableZuulProxy,那么除上述过滤器外,Spring Cloud还会安装以下过滤器。
pre类型过滤器
PreDecorationFilter:该过滤器根据提供的RouteLocator 确定路由到的地址,以及怎样去路由。同时,该过滤器还为下游请求设置各种代理相关的header。
route类型过滤器
1)RibbonRoutingFilter:该过滤器使用Ribbon、Hystrix和可插拔的HTTP客户端发送请求。serviceId在RequestContext的属性FilterConstants.SERVICE_ID_KEY中。该过滤器可使用如下这些不同的HTTP客户端。

  • Apache HttpClient:默认的HTTP客户端
  • Squareup OkHttpClient v3:若需使用该客户端,需保证com.squareup.okhttp3的依赖在classpath中,并设置ribbon.okhttp.enable=true。
  • Netflix RibbonHTTP Client:设置ribbon.restclient.enable=true即可启用该HTTP客户端。该客户端有一定限制,例如不支持PATCH方法,另外,它有内置的重试机制。
    2)SimpleHostRoutingFilter:该过滤器通过Apache HttpClient向指定的URL发送请求。URL在RequestContext.getRouteHost()中。

编写Zuul过滤器

1)复制项目microservice-gateway-zuul,将ArtifactId修改为microservice-gateway-zuul-filter。
2)编写自定义Zuul过滤器

package com.example.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;

import javax.servlet.http.HttpServletRequest;

public class PreRequestLogFilter extends ZuulFilter {
    private static final Logger LOGGER = LoggerFactory.getLogger(PreRequestLogFilter.class);
    @Override
    public String filterType() {
        //pre类型的过滤器
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        //在org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter之前执行
        return FilterConstants.PRE_DECORATION_FILTER_ORDER -1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request=ctx.getRequest();
        PreRequestLogFilter.LOGGER.info(String.format("send %s request to %s",request.getMethod(),request.getRequestURI().toString()));
        return null;
    }
}

由代码可知,自定义的Zuul Filter需实现以下几个方法。

  • filterType:返回过滤器的类型。有pre、route、post、error等几种取值,分别对应上文的几种过滤器。
  • filterOrder:返回一个int值来指定过滤器的执行顺序,不同的过滤器允许返回相同的数字。
  • shouldFilter:返回一个boolean值来判断该过滤器是否要执行,true表示执行,false表示不执行。
  • run:过滤器的具体逻辑。本例中让它打印了请求的HTTP方法以及请求的地址。
    3)创建配置类
package com.example.config;

import com.example.filter.PreRequestLogFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {
    @Bean
    public PreRequestLogFilter preRequestLogFilter(){
        return new PreRequestLogFilter();
    }
}

测试
1)启动microservice-discovery-eureka
2)启动microservice-provider-user
3)启动microservice-gateway-zuul-filter
4)访问http://localhost:8040/microservice-provider-user/1,可获得类似如下的日志
在这里插入图片描述
说明编写的自定义Zuul过滤器被执行了。
注:这里只是演示了一个非常简单的Zuul过滤器。事实上,我们可使用过滤器做很多事,例如安全认证、灰度发布、限流。

禁用Zuul过滤器

Spring Cloud默认为Zuul编写并启用了一些过滤器,例如DebugFilter、FormBodyWrapperFilter、PreDecorationFilter等。这些过滤器都存放在spring-cloud-netflix-core这个Jar包的org.springframework.cloud.netflix.zuul.filters包中。
如果想禁用部分过滤器,只需设置zuul.<SimpleClassName>.<filterType>.disable=true,即可禁用SimpleClassName所对应的过滤器。以过滤器org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter为例,只需设置zuul.SendResponseFilter.post.disable=true,接口禁用该过滤器。

Zuul的容错与回退

在Spring Cloud中,Zuul默认已经整合了Hystrix。首先来做一个简单的实验。
1)启动项目microservice-discovery-eureka。
2)启动项目microservice-provider-user。
3)启动项目microservice-gateway-zuul。
4)启动项目microservice-hystrix-dashboard。
5)访问http://localhost:8040/microservice-provider-user/1,可正常获得结果。
在这里插入图片描述

6)访问http://localhost:8040/hystrix.stream,可获得Hystrix的监控数据。
在这里插入图片描述

7)访问http://localhost:8030/hystrix,并在监控地址一栏填入http://localhost:8040/hystrix.stream
在这里插入图片描述
由图可知,Zuul的Hustrix监控的粒度是微服务,而不是某个API;同时也说明,所有经过Zuul的请求,都会被Hystrix保护起来。
8)关闭项目microservice-provider-user,再次访问http://localhost:8040/microservice-provider-user/1,会看到页面输出类似如下的异常:
在这里插入图片描述
Zuul实现回退
想要为Zuul添加回退,需要实现ZuulFallbackProvider接口。在实现类中,指定为哪个微服务提供回退,并提供一个ClientHttpResponse作为回退响应。
1)复制项目microservice-gateway-zuul,将ArtifactId修改为microservice-gateway-zuul-fallback。
2)编写Zuul的回退类:

package com.example.entity;

import com.netflix.hystrix.exception.HystrixTimeoutException;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;

@Component
public class MyFallbackProvider implements FallbackProvider {

    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("服务不可用,请稍后再试。".getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                MediaType mt = new MediaType("application","json", Charset.forName("UTF-8"));
                headers.setContentType(mt);
                return headers;
            }
        };
    }

    @Override
    public ClientHttpResponse fallbackResponse(Throwable cause) {
        if (cause instanceof HystrixTimeoutException){
            return response(HttpStatus.GATEWAY_TIMEOUT);
        }else{
            return this.fallbackResponse();
        }
    }

    @Override
    public String getRoute() {
        //表示是为哪个微服务提供回退,*表示为所有微服务提供回退
        return "*";
    }

    @Override
    public ClientHttpResponse fallbackResponse() {
        return this.response(HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

添加回退之后,重复之前的实验,当Zuul所代理的任意微服务无法正常响应时,会返回
在这里插入图片描述

饥饿加载

我们知道,Zuul整合了Ribbon实现负载均衡,而Ribbon默认是懒加载的。

zuul:
  ribbon:
    eager-load:
      enable: true

Query String编码

当处理请求时,query param会被编码,因此,可在Zuul过滤器中进行一些适当的修改。这些参数在route过滤器中构建请求时,将被重新编码。如果qurey param使用JavaScript的encodeURLComponent()方法进行编码,那么重新编码后的结果可能与原始值不同——尽管在大多数情况下不会引起问题,但某些Web服务器可能会对复杂的query string进行编码。
要强制让query string与HttpServletRequest.getQueryString()保持一致,可使用如下配置:

zuul:
  force-original-query-string-encoding: true

Hystrix隔离策略与线程池

隔离策略

1)为项目microservice-gateway-zuul设置zuul.ribbon-isolation-strategy=thread。
2)按照之前的操作查看图标。
在这里插入图片描述
可以发现,之前Thread Pools一栏没有数据,这说明了两个问题:
1)默认情况下,Zuul的Hystrix隔离策略是SEMAPHORE。
2)可使用zuul.ribbon-isolation-strategy=thread将隔离策略改为THREAD。

线程池配置

当zuul.ribbon-isolation-strategy=thread时,Hystrix的线程隔离策略将作用于所有路由,HystrixThreadPoolKey默认为RibbonCommand,这意味着,所有路由的HystrixCommand都会在相同的Hystrix线程池中执行。
可使用以下配置,让每个路由使用独立的线程池:

zuul:
  thread-pool:
    use-separate-thread-pools: true

这样,HystrixThreadPoolkey将与路由的服务标识相同。对于本例,则是microservice-provider-user。
如果想为HystrixThreadPoolKey添加前缀,可使用类似如下的配置。

zuul:
  thread-pool:
    use-separate-thread-pools: true
    thread-pool-key-prefix: prefix-

这样,HystrixThreadPoolkey将变成${prefix}-{服务标识}。对于本例,则是prefix-microservice-provider-user。

Zuul的高可用

Zuul的高可用非常关键,因为外部请求到后端微服务的流量都会经过Zuul。故而在生产环境中一般都需要部署高可用的Zuul以避免单点故障。
注:这里的Zuul客户端是指广义上的“客户端”,即向Zuul发送的浏览器、手机等终端。

Zuul客户端也注册到了Eureka Server上

这种情况下,Zuul的高可用非常简单,只需将多个Zuul节点注册到Eureka Server上,就可实现Zuul的高可用。此时,Zuul的高可用与其他微服务的高可用没什么区别。
当Zuul客户端也注册到Eureka Server上时,只需部署多个Zuul节点即可实现其高可用。Zuul客户端会自动从Eureka Server中查询Zuul Server的列表,并使用Ribbon负载均衡地请求Zuul集群。

Zuul客户端未注册到Eureka Server上

实现中,这种场景往往更常见,例如,Zuul客户端是一个手机APP——不可能让所有的手机终端都注册到Eureka Server上。这种情况下,可借助一个额外的负载均衡器来实现Zuul的高可用,例如Nginx、HAProxy、F5等。
Zuul客户端将请求发送到负载均衡器,负载均衡器将请求转发到其代理的其中一个Zuul节点。这样,就可以实现Zuul的高可用。

使用Sidecar整合非JVM微服务

非JVM微服务可操作Eureka的REST端点,从而实现注册与发现。事实上,也可使用Sidecar更加方便地整合非JVM微服务。
Spring Cloud Netflix Sidecar的灵感来自Netflix Prana,它包括了一个简单的HTTP API来获取指定服务所有实例的信息(例如主机和端口)。不仅如此,还可通过内嵌的Zuul来代理服务调用,改代理从Eureka Server中获取信息。非JVM微服务需要实现健康检查,以便Sidecar将它的状态上报给Eureka Server,健康检查的形式如下:

{
	"status":"UP"
}

其中,status用于描述微服务的状态,常见取值有UP、DOWN、OUT_OF_SERVICE以及UNKNOWN等。

编写Node.js微服务

先来编写一个非JVM微服务,以Node.js为例进行演示。

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/,会返回{"index":"欢迎来到首页"}
	if(pathname === '/'){
		res.end(JSON.stringify({"index":"欢迎来到首页"}));
	}
	//访问http://localhost:8060/health,会返回{"status":"UP"}
	else if (pathname === '/health.json'){
		res.end(JSON.stringify({"status":"UP"}));
	}
	else{
		res.end("404");
	}
});
//创建监听,并打印日志
server.listen(8060,function(){
	console.log('listening on localhost:8060');
})

这是一个非常简单的Node.js服务。
测试
1)使用node node-service.js命令即可启动该服务。
2)访问http://localhost:8060/health.json
在这里插入图片描述

3)访问http://localhost:8060/
在这里插入图片描述
4)访问其他端点会返回404
在这里插入图片描述

编写Sidear

1)创建一个Maven工程,ArtifactId是microservice-sidecar,并为项目添加以下依赖。

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

2)在启动类上添加@EnableSidecar注解,声明这是一个Sidecar。

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.sidecar.EnableSidecar;

@SpringBootApplication
@EnableSidecar
public class SidecarApplication {
    public static void main(String[] args){
        SpringApplication.run(SidecarApplication.class);
    }
}

@EnableSidecar是一个组合注解,它整合了@EnableCiruitBreaker和@EnableZuulProxy。
3)在项目的配置文件application.yml

server:
  port: 8070
spring:
  application:
    name: microservice-sidercar-node-service
eureka:
  client:
    service-url: 
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true
sidecar:
  port: 8060				#Node.js微服务的端口
  health-uri: http://localhost:8060/health.json   #Node.js微服务的健康检查URL

由配置可知,现在已经把Sidecar注册到了Eureka Server上,并用sidercar.port属性指定了非JVM微服务所监听的端口,用sidecar.health-uri属性指定了非JVM微服务的健康检查URL。
这样一个Sidecar就编写完成了。
测试一、非JVM微服务访问JVM微服务
1)启动microservice-discovery-eureka。
2)启动microservice-provider-user。
3)启动node-service
4)启动microservice-sidecar。
5)访问http://localhost:8070/microservice-provider-user/1
在这里插入图片描述
测试二、JVM微服务调用非JVM微服务的接口
使用Sidecar微服务的serviceId,即可调用非JVM微服务的接口。

package com.example.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class SidecarController {
    @Autowired
    private RestTemplate restTemplate;
    @GetMapping("/test")
    public String findById(){
        return this.restTemplate.getForObject("http://microservice-sidecar-node-service/",String.class);
    }
}

在这里插入图片描述

Sidecar的端点

Sidecar提供了一些端点,这些端点有助于管理Sidecar。

  • /:该端点返回一个测试页面,该页面展示Sidecar的常用端点。
  • /host/{serviceId}:该端点返回DiscoveryClient.getInstances(serviceId),即指定微服务在Eureka上的是列表。
  • /ping:该端点返回"OK"字符串。
  • /{serviceId}:由于Sidecar整合了Zuul,因此可使用该端点,将请求转发到serviceId所对应的微服务。

Sidecar与Node.js微服务分离部署

前文是将Sidecar与非JVM微服务部署在同一台主机上。现实中,常常会将Sidecar与JVM微服务分离部署,例如部署在不同的主机或者容器中。
方法一

eureka:
  instance:
    hostname: 非JVM微服务的hostname

方法二
对于Spring Cloud Dalston及更高版本,除方法一外,也可通过以下属性实现分离。

sidecar:
  hostname: 非JVM微服务的hostname
  ip-address: 非JVM微服务的IP地址

Sidecar原理分析

1)访问Eureka Server的路径:http://localhost:8761/eureka/apps/microservice-sidecar-node-service
在这里插入图片描述
由结果可知,注册到Eureka Server上的端口是8060,homePageUrl是http://192.168.2.107:8060/,恰恰是node-service的端口和首页。因此,注册到Eureka Server上的微服务可使用microservice-sidecar-node-service这个名称请求node-serviec的接口。
2)由于@EnableSidecar整合了注解@EnableZuulProxy,可尝试访问Sidecar的/routes端点:http://localhost:8070/routes
在这里插入图片描述
因此,非JVM微服务可通过Sidecar请求其他注册在Eureka Server的微服务。
3)可尝试将node-service多次启停,并观察Sidecar的/health端点。Sidercar会获取node-service的健康状态,并将该状态传播到Eureke Server。使用这种方式,Eureka Server就能感知到非JVM微服务的健康状态。
在这里插入图片描述
在这里插入图片描述

使用Zuul聚合微服务

许多场景下,外部请求需要查询Zuul后端的多个微服务。举个例子,一个电影售票手机APP,在购票订单页上,既需要查询"电影微服务"获得电影相关信息,又需要查询"用户微服务"获得当前用户的信息。如果让手机端直接请求各个微服务,那么网络开销、流量耗费、耗费时长可能都无法令我们满意。那么对于这种场景,可使用Zuul聚合微服务请求——手机APP只需发送一个请求给Zuul,由Zuul请求用户微服务以及电影微服务,并组织好数据给手机APP。
使用这种方式,在手机端只需发送一次请求即可,简化了客户端侧的开发;不仅如此,由于Zuul、用户微服务、电影微服务一般都在同一个局域网中,因此速度会非常快,效率会非常高。
1)复制项目microservice-gateway-zuul,将ArtifactId修改为microservice-gateway-zuul-aggregation。
2)修改启动类ZuulApplication:

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableZuulProxy
public class ZuulAggregationApplication {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
    public static void main(String[] args){
        SpringApplication.run(ZuulAggregationApplication.class,args);
    }
}

3)创建实体类:

package com.example.entity;

import java.math.BigDecimal;

public class User {
    private Long id;
    private String username;
    private String name;
    private Integer age;
    private BigDecimal balance;
    //省略getter和setter
}

4)创建Java类,名为AggregationService

package com.example.service;

import com.example.entity.User;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

@Service
public class AggregationService {
    @Autowired
    private RestTemplate restTemplate;

    @HystrixCommand(fallbackMethod = "fallback")
    public Observable<User> getUserById(Long id) {
        //创建一个被观察者
        return Observable.create(observer -> {
            //请求用户微服务的/{id}端点
            User user = restTemplate.getForObject("http://microservice-provider-user/{id}", User.class);
            observer.onNext(user);
            observer.onCompleted();
        });
    }

    @HystrixCommand(fallbackMethod = "fallback")
    public Observable<User> getMovieUserByUserId(Long id) {
        return Observable.create(observer -> {
            // 请求电影微服务的/user/{id}端点
            User movieUser = restTemplate.getForObject("http://microservice-consumer-movie/user/{id}", User.class);
            observer.onNext(movieUser);
            observer.onCompleted();
        });
    }

    public User fallback(Long id) {
        User user = new User();
        user.setId(-1L);
        return user;
    }
}

5)创建Controller,在Controller中聚合多个请求

package com.example.controller;

import com.example.ZuulAggregationApplication;
import com.example.entity.User;
import com.example.service.AggregationService;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import rx.Observable;
import rx.Observer;

import java.util.HashMap;

@RestController
public class AggregationController {
    public static final Logger LOGGER = LoggerFactory.getLogger(ZuulAggregationApplication.class);
    @Autowired
    private AggregationService aggregationService;
    @GetMapping("/aggregate/{id}")
    public DeferredResult<HashMap<String, User>> aggregate(@PathVariable Long id){
        Observable<HashMap<String,User>> result = this.aggregateObservable(id);
        return this.toDeferredResult(result);
    }

    public Observable<HashMap<String,User>> aggregateObservable(Long id){
        //合并两个或者多个Observables发射出的数据项,根据指定的函数变换它们
        return Observable.zip(
                this.aggregationService.getUserById(id),
                this.aggregationService.getMovieUserByUserId(id),
                (user,movieUser)->{
                    HashMap<String,User>map = Maps.newHashMap();
                    map.put("user",user);
                    map.put("movieUser",movieUser);
                    return map;
                }
        )
    }

    public DeferredResult<HashMap<String,User>> toDeferredResult(Observable<HashMap<String,User>> details){
        DeferredResult<HashMap<String,User>> result=new DeferredResult<>();
        //订阅
        details.subscribe(new Observer<HashMap<String, User>>() {
            @Override
            public void onCompleted() {
                LOGGER.info("完成...");
            }

            @Override
            public void onError(Throwable e) {
                LOGGER.error("发生错误...",e);
            }

            @Override
            public void onNext(HashMap<String, User> stringUserHashMap) {
                result.setResult(stringUserHashMap);
            }
        });
        return result;
    }
}

测试一、微服务聚合测试
1)启动项目microservice-discovery-eureka
2)启动项目microservice-provider-user
3)启动项目microservice-consumer-movie
4)启动项目microservice-gateway-zuul-aggregation
5)访问http://localhost:8040/aggregate/1
在这里插入图片描述
6)停掉microservice-provider-user和microservice-consumer-movie
7)访问http://localhost:8040/aggregate/1
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值