SpringCloud技术整理(十)微服务网关Zuul
SpringCloud技术整理(十)微服务网关Zuul

SpringCloud技术整理(十)微服务网关Zuul

项目地址:

http://gitlab.zack.net.cn/root/springcloudexample

##########################################################

微服务网关

通过eureka注册中心, 微服务之间可以通过fiegn进行服务调用, 通过hystrix实现了服务调用失败处理, 通过ribbon实现了微服务调用负载均衡,通过springboot admin实现了服务状态监控, 至此微服务架构已经初具雏形.

还存在的问题:

每一个微服务的 HOST:PORT 都不相同, 系统内部进行微服务调用时可以通过eureka+feign进行调用,不必关心服务提供者的 HOST:PORT.
而外部客户端(浏览器/APP)可能需要调用多个服务的接口才能完成一个业务流程(假设以看电影功能为例, 可能需要调用: 电影分类微服务,用户微服务,支付微服务,播放微服务)

如果让客户端直接与各个微服务进行通信, 可能存在以下问题:
  • 客户端会多次请求不同的微服务, 增加了客户端的复杂性
  • 存在跨域请求, 增加了部分场景的处理难度
  • 外部调用的高延迟
  • 认证复杂, 每个微服务可能要单独认证
  • 后续迭代可能需要重新划分微服务(合并或者拆分微服务), 如果客户端直接通信, 将很难进行重构
  • 容易暴露微服务所在机器地址, 增加了安全隐患
而这些问题都可以通过网关解决:
  • 客户端与网关进行通信, 由网关统一进行服务调用微服务
  • 易于监控, 可在网关收集监控数据进行分析
  • 易于认证, 可在网关上进行认证,然后再将请求转发到后端的微服务,无需在每个微服务中进行认证
  • 减少客户端与各个微服务之间的交互次数
  • 网关与各个微服务之间一般处于同一个局域网, 延迟极低

##########################################################

Zuul网关

Zuul是netflix开源的微服务网关, 可以和eureka, ribbon, hystrix等组件配合使用.
Zuul核心为一系列过滤器, 通过这些过滤器可以完成以下功能:

  • 身份认证与安全: 识别每个资源的验证要求, 拒绝与要求不符合的请求.
  • 审查与监控: 在边缘位置进行数据追踪,从而产生准确的生产视图.
  • 动态路由: 动态地将请求路由到不同的后端集群.
  • 压力测试: 逐渐增大指向集群的流量, 以了解性能.
  • 负载分配: 为每一种负载类型分配对应容量, 并弃用超出限定的请求.
  • 静态响应处理: 在边缘位置直接建立部分响应,避免其转发到内部集群.

#########################################################

整合Zuul网关

在SpringCloudExample项目的pom文件中的中声明zuul网关的版本
<!--声明zuul网关版本-->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
	<version>2.0.1.RELEASE</version>
</dependency>

根据spring.io给出的springcloud与springboot版本对照:
Greenwich -------Springboot 2.1.x
Finchley --------Springboot 2.0.x
Edgware ---------Springboot 1.5.x
这里引入zuul版本时需要引入2.0以上版本, 否则会扫描不到zuul依赖包

新增Api_Zuul模块

image.png

修改zuul网关的pom文件,引入依赖
<packaging>jar</packaging>
    <dependencies>
        <!--引入zuul网关依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <!--引入eureka client依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
编写zuul网关的application.yml配置文件
server:
  port: 81
spring:
  application:
    name: api-zuul
eureka:
  instance:
    # 注册到eureka server
    prefer-ip-address: true
  client:
    # 从注册中心拉去服务注册信息的间隔(秒)
    registry-fetch-interval-seconds: 5
    service-url:
      defaultZone: http://user:123456@localhost:8761/eureka,http://localhost:8762/eureka
    # 开启健康检查
    healthcheck:
      enabled: true
编写zuul网关的springboot启动类
package cn.zack;

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

/**
 * 使用@EnableZuulProxy注解,声明一个zuul代理
 * 该代理使用ribbon来定位注册在eureka server上的服务
 * 该代理还整合了hystrix,从而实现了容错,所有经过zuul的请求都会在hystrix命令包裹中执行
 */
@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class, args);
    }
}
启动两个eureka server, 启动两个服务提供者, 启动一个服务消费者, 启动zuul网关

image.png

测试网关和负载均衡

调用服务消费者的getFun1方法, 得到返回报文调用example1或者调用example2, 多次调用, 可以看到之前配置的ribbon的负载均衡策略"随机"生效
image.png
http://zuul网关地址/微服务名称/方法名 的格式调用消费者的getFun1方法, 同样可以得到服务消费者的getFun1方法的返回报文, 多次调用依然可以看出ribbon负载均衡策略为"随机"
image.png

测试容错

关闭服务提供者2, 只保留服务提供者1
通过zuul网关调用服务消费者的getFun4方法, 由于服务提供者1的此方法在此之前加入了除零异常, 因此可以看到此次zuul网关的路由转发进入了fallback回退
image.png
再次启用服务提供者2,重新调用,由于负载均衡使用随机策略,只有在调用到服务提供者2的服务时才会成功,如果频繁调用导服务提供者1的调用失败率较高, hystrix会打开服务提供者1的断路器,暂时停止此服务提供
image.png

##########################################################

Zuul的路由配置

自定义路由映射
  • 编辑application.yml配置文件,添加路由映射配置信息
zuul:
  # 自定义路由映射配置
  routes:
    microservice-consumer1: /consumer1/**
  • 重启zuul网关模块, 通过映射的路径调用服务消费者的getFun1方法, 可以看到zuul路由将/consumer1/** 的请求映射到了microservice-consumer1微服务.
    image.png
    而原先的完整路径(http://localhost:81/microservice-consumer1/getFun1 )依然可以正常访问
    通过这种方式,可以为每一个对微服务进行路由映射
忽略指定微服务
  • 修改zuul配置信息
zuul:
  # 自定义路由映射配置
  routes:
    microservice-consumer2: /consumer2/**
  # 忽略指定微服务, 多个微服务以逗号隔开
  ignored-services: microservice-consumer1
  • 重启zuul网关, 重启服务消费者1, 同时启动服务消费者2
    再次通过网关调用服务消费者1的getFun1方法, 由于路由映射忽略了服务消费者1,无法通过zuul网关调用此服务,所以会导致404
    image.png
  • 而未被忽略的服务消费者2可以被正常映射
    image.png
忽略所有微服务, 只路由指定微服务
  • 大多数情况下, 我们只想让zuul代理指定的微服务, 此时可以将zuul.ignored-services设为'*'
zuul:
  # 自定义路由映射配置
  routes:
    microservice-consumer1: /consumer1/**
    microservice-consumer2: /consumer2/**
  # 忽略指定微服务, 多个微服务以逗号隔开, 忽略全部微服务用'*'
  ignored-services: '*'
  • 此时无法直接通过网关调用微服务, 只能通过网关映射的路径进行调用:
    image.png
    image.png
以每个微服务为节点进行路由配置
  • 修改zuul配置信息
zuul:
  # 自定义路由映射配置
  routes:
  #节点路由名字
    consumer1-routes:
      service-id: microservice-consumer1
      path: /consumer1/**
    consumer2-routes:
      service-id: microservice-consumer2
      path: /consumer2/**
  # 忽略指定微服务, 多个微服务以逗号隔开, 忽略全部微服务用'*'
  ignored-services: '*'
  • 这种形式可以以每个微服务为路由节点进行分别配置
配置路由前缀
  • 修改zuul配置文件
zuul:
  # 自定义路由映射配置
  routes:
    consumer1-routes:
      service-id: microservice-consumer1
      path: /consumer1/**
    consumer2-routes:
      service-id: microservice-consumer2
      path: /consumer2/**
  # 忽略指定微服务, 多个微服务以逗号隔开, 忽略全部微服务用'*'
  ignored-services: '*'
  # 配置路由前缀
  prefix: /api
  • 重启zuul网关, 通过路由前缀才可以调用服务
    image.png
细粒度的路由控制
  • 如果想让zuul代理某个微服务, 同时又想保护该微服务的某些敏感路径
zuul:
  # 自定义路由映射配置
  routes:
    consumer1-routes:
      service-id: microservice-consumer1
      path: /consumer1/**
    consumer2-routes:
      service-id: microservice-consumer2
      path: /consumer2/**
  # 忽略指定微服务, 多个微服务以逗号隔开, 忽略全部微服务用'*'
  ignored-services: '*'
  # 更细粒度地忽略部分服务(例如consumer1微服务的四个方法, 不想暴露getFun1)
  ignored-patterns: /**/getFun1/**
  # 配置路由前缀
  prefix: /api
  • 重启zuul网关,再次调用consumer1的getFun1方法出现404, 此时zuul已对consumer1的getFun1服务进行了保护.
    image.png
通过zuul网关进行文件上传
  • 由MultipartFile默认配置可知默认上传单个文件最大1MB, 单次请求最多上传10MB文件
    image.png
    有两种方式修改文件上传大小限制:
    1.直接修改配置文件(此处修改之后重新编译仍无法生效,原因未知,放弃)
    2.以spring注入bean的方式
  • 修改consumer2的启动类,注入文件上传配置类
package cn.zack;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import javax.servlet.MultipartConfigElement;
/**
 * 使用@EnableFeignClients开启SpringCloudFeign的支持
 */
@EnableFeignClients
@SpringBootApplication
public class Microservice_Consumer2Application {
    public static void main(String[] args) {
        SpringApplication.run(Microservice_Consumer2Application.class,args);
    }
    @Bean
    public MultipartConfigElement multipartConfigElement(){
        MultipartConfigFactory multipartConfigFactory = new MultipartConfigFactory();
        // 单个文件上传和单次请求都限制在1GB
	multipartConfigFactory.setMaxFileSize("1024000KB");
        multipartConfigFactory.setMaxRequestSize("1024000KB");
        return multipartConfigFactory.createMultipartConfig();
    }
}
  • 修改consumer2的controller,添加文件上传方法
@PostMapping(path = "uploadFile")
    public String uploadFile(@RequestParam("files")MultipartFile file) throws IOException {
        // 获取上传文件的输入流
        InputStream inputStream = file.getInputStream();
        return "OK";
    }
  • 由于使用了网关,网关也要进行文件上传配置
package cn.zack;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;

import javax.servlet.MultipartConfigElement;

/**
 * 使用@EnableZuulProxy注解,声明一个zuul代理
 * 该代理使用ribbon来定位注册在eureka server上的服务
 * 该代理还整合了hystrix,从而实现了容错,所有经过zuul的请求都会在hystrix命令包裹中执行
 */
@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class, args);
    }
    @Bean
    public MultipartConfigElement multipartConfigElement(){
        MultipartConfigFactory multipartConfigFactory = new MultipartConfigFactory();
        // 单个文件上传和单次请求都限制在1GB
        multipartConfigFactory.setMaxFileSize("1024000KB");
        multipartConfigFactory.setMaxRequestSize("1024000KB");
        return multipartConfigFactory.createMultipartConfig();
    }
}
  • 重启consumer2微服务, 重启zuul网关,使用postman进行文件上传\n添加头信息
    image.png
    添加请求体信息,文件选择小文件(此次约9MB)
    image.png
    此时可以正常上传.
  • 如果选择超过1GB限制的大文件进行上传(例如1.4GB),zuul网关则会抛出异常:
    org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (1485881590) exceeds the configured maximum (1048576000)
    此时只能通过调整文件上传大小限制解决
  • 如果选择不超过1GB限制的大文件进行上传(例如700MB),由于等待时间较长,zuul网关会抛出超时异常
    Caused by: com.netflix.hystrix.exception.HystrixRuntimeException: microservice-consumer2 timed-out and no fallback available.
    此时,需要修改zuul网关的配置文件,提升超时时间
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 30000

######################################################

Zuul的过滤器

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

zuul大部分功能都是通过过滤器实现的, zull中定义了四种标准过滤器类型:
PRE:
这种类型的过滤器在请求被路由之前调用. 可利用这种过滤器实现身份验证,在集群中选择请求的微服务,记录调试信息等等.
ROUTING:
这种过滤器将请求路由到微服务. 这种过滤器用于构建发送给微服务的请求,并使用apache httpclient或者ribbon请求微服务.
POST:
这种过滤器在路由到微服务以后执行. 这种过滤器可用来为响应添加标准的http header,收集统计信息,将响应从微服务发送给客户端等.
ERROR
在其他阶段发生错误时执行此过滤器.
除此之外,zuul还允许创建自定义的过滤器类型.

编写zuul过滤器

编写zuul过滤器非常简单,只需要继承ZuulFilter,实现其抽象方法即可.
以pre类型过滤器为例:

package cn.zack.filter;

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

import javax.servlet.http.HttpServletRequest;

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

    /**
     * 过滤器的执行顺序
     * @return
     */
    public int filterOrder() {
        return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
    }

    /**
     * 过滤器是否执行
     * @return
     */
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 过滤器的具体逻辑
     * @return
     * @throws ZuulException
     */
    public Object run() throws ZuulException {
        // 取当前requestcontext域
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();
        logger.info("===============");
        logger.info(String.format("send %s request to %s",request.getMethod(),request.getRequestURL().toString()));
        logger.info("===============");
        return null;
    }
}
重启zuul网关,通过网关随便调用一个微服务

image.png
与此同时,zuul网关模块的控制台可以看到过滤器打印出的信息:
image.png
此时pre过滤器在此请求路由到微服务之前打印出了这个请求的访问地址以及请求方式, 这些信息可以用于记录日志等.