一、为什么使用服务网关

不使用服务网关的时候,客户端直接与各个微服务通信,如图

 

jeecg微服务网关原理 微服务网关设计_客户端

这样会带来以下问题

  • 客户端会多次请求不同的微服务,增加了客户端的复杂性。
  • 存在跨域请求,在一定场景下处理相对复杂。
  • 认证复杂,每个服务都需要独立认证。
  • 难以重构,随着项目的迭代,可能需要重新划分微服务。
  • 某些微服务可能使用了防火墙/浏览器不友好的协议,直接访问会有一定的困难。

微服务网关是介于客户端与服务器端之间的中间层,所有的外部请求都会先经过微服务网关。使用微服务网关可以解决以上的问题。

jeecg微服务网关原理 微服务网关设计_Zuul_02

如上图,微服务网关封装了应用程序的内部结构,客户端只需跟网关交互,而无需直接调用特定微服务的接口。除此之外,还有以下优点

  • 易于监控。可在微服务网关收集监控数据并将其推送到外部系统进行分析。
  • 易于认证。可在微服务网关上进行认证,然后再将请求转发到后端的微服务,而无需在每个微服务中进行认证。
  • 减少了客户端与每个微服务之间的交互次数。

二、Zuul简介

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

  • 身份认证与安全:识别每个资源的验证要求,并拒绝那些与要求不符的请求。
  • 审查与监控:在边缘位置追踪有意义的数据和统计结果,从而带来精确的生产视图。
  • 动态路由:动态地将请求路由到不同的后端集群。
  • 压力测试:逐渐增加指向集群的流量,以了解性能。
  • 负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求。
  • 静态响应处理:在边缘位置直接建立部分响应,从而避免其转发到内部集群。
  • 多区域弹性:跨越AWS Region进行请求路由,旨在实现ELB(Elastic Load Balancing)使用的多样化,以及让系统的边缘更贴近系统的使用者。

Spring Cloud对Zuul进行了整合与增强。

三、编写Zuul微服务网关

1)新建maven项目microservice-gateway-zuul。pom.xml引入Eureka和Zuul的依赖。

<?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/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.zhh</groupId>
    <artifactId>microservice-gateway-zuul</artifactId>
    <version>1.0</version>
    
    <!-- Spring Boot依赖  -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.1.RELEASE</version>
    </parent>

    <!-- Spring Cloud管理依赖  -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Finchley.M7</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    <dependencies>
    	<!-- spring-boot-test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
		<!-- eureka-client -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- zuul -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
    </dependencies>

    <!-- 注意: 这里必须要添加, 否则各种依赖有问题  -->
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/libs-milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

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

@SpringBootApplication
@EnableZuulProxy
public class MicroserviceGatewayZuulApplication {

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

}

3)编写配置文件application.yml。

server:
  port: 8084
spring:
  application:
    name: gateway-zuul
 
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8080/eureka/

从配置可知,仅是添加了Zuul的依赖,并将Zuul注册到Eureka Server上。

4)首先启动Eureka Server项目和若干微服务项目,之后启动这个项目。

访问http://ZUUL_HOST:ZUUL_PORT/微服务在Eureka上的serviceId/**,就会被转发到serviceId对应的微服务。

除了动态路由功能之外,还有负载均衡、容错监控的功能。

四、Zuul的路由端点

当@EnableZuulProxy与Spring Boot Actuator配合使用时,Zuul会暴露一个路由管理端点(2.0之后暂时不知道是什么)。

借助这个端点,可以方便、直观地查看以及管理Zuul的路由。

GET访问为查询,POST访问为强制立即刷新(尽管路由会自动刷新)。

五、路由配置详解

1、自定义指定微服务的访问路径

zuul:
  routes:
    consumer-order: /o/**

这样的话,consumer-order微服务就会被映射到/o/**路径。

2、忽略指定微服务

多个用逗号分隔。

zuul:
  ignored-services: consumer-order

这样可让zuul忽略consumer-order微服务,只代理其它微服务。

3、忽略所有微服务,只路由指定微服务

zuul:
  ignored-services: '*'    # 使用'*'可忽略所有微服务
  routes:
    consumer-order: /o/**

这样就只路由consumer-order微服务,忽略其它微服务。

4、同时指定微服务的serviceId和对应路径

zuul:
  routes:
    order:    # 只是给路由的一个名称,可以任意起名
      serviceId: consumer-order
      path: /o/**    # serviceId对应的路径

5、同时指定path和URL

zuul:
  routes:
    order:
      url: http://localhost:8082/    # 指定的url
      path: /o/**    # url对应的路径

这样就可将/o/**映射到http://localhost:8082/**。

还有其它的,比如使用正则表达式指定Zuul的路由匹配规则路由前缀忽略某些路径等,这里就不介绍了。有兴趣的小伙伴可以自行去了解。

六、Zuul的安全与Header

1、sensitiveHeaders:会过滤客户端请求中的和该配置项匹配的headers

比如

zuul:  
    sensitiveHeaders: X-ABC

如果客户端在发请求是带了X-ABC,那么X-ABC不会传递给下游服务

2、ignoredHeaders:会过滤服务之间通信附带的headers

比如

zuul:
    ignoredHeaders: X-ABC

如果客户端在发请求是带了X-ABC,那么X-ABC依然会传递给下游服务。但是如果下游服务再转发就会被过滤

还有一种情况就是客户端带了X-ABC,在ZUUL的Filter中又addZuulRequestHeader("X-ABC", "new"),
那么客户端的X-ABC将会被覆盖,此时不需要sensitiveHeaders。如果设置了sensitiveHeaders: X-ABC,那么Filter中设置的X-ABC依然不会被过滤。

七、使用Zuul上传文件

上传文件含有中文名,需要为上传路径添加/zuul前缀(当前版本貌似已经修复,不需要添加/zuul前缀也可以)。

对于小文件(1M以内)上传无需任何处理。对于大文件(10M以上)上传,需要为上传路径添加 /zuul 前缀,也可以使用zuul.servlet-path自定义前缀。

假设 zuul.routes.file-upload = /file-upload/**,如果http://{HOST}:{PORT}/uoload是微服务file-upload上传路径,则可以使用Zuul的/zuul/file-upload/upload路径上传大文件。

如果Zuul使用Ribbon做负载均衡,那么对于超大文件(例如500M)需要提升超时时间

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

添加/zuul前缀是指需要使用ZuulServlet来实现文件上传。

八、Zuul的过滤器

过滤器是Zuul的核心组件。

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

通过看com.netflix.zuul.ZuulFilter类中filterType方法的注释,我们知道Zuul中定义了4种标准过滤器类型

jeecg微服务网关原理 微服务网关设计_微服务_03

这些过滤器类型对应于请求的典型生命周期

  • pre:在请求被路由之前调用。可以实现身份认证、在集群中选择请求的微服务、记录调试信息等。
  • route:将请求路由到微服务。用于构建发送给微服务的请求,并使用Apache HttpClient或Netflix Ribbon请求微服务。
  • post:在路由到微服务以后执行。可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
  • error:在其它阶段发生错误时执行该过滤器。

此外,还有一个static类型的过滤器StaticResponseFilter。

2、编写Zuul过滤器

1)编写自定义Zuul过滤器,让其记录请求日志。

重写四个方法。

/**
 * 记录请求日志过滤器
 * @author z_hh
 * @time 2019年2月19日
 */
public class PreRequestLogFilter extends ZuulFilter {
	private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

	/**
	 * 返回一个boolean值来判断该过滤器是否要执行。
	 */
	@Override
	public boolean shouldFilter() {
		return true;
	}

	/**
	 * 执行的具体逻辑。
	 */
	@Override
	public Object run() throws ZuulException {
		RequestContext ctx = RequestContext.getCurrentContext();
		HttpServletRequest request = ctx.getRequest();
		LOG.info("send {} request to {}", request.getMethod(), request.getRequestURL().toString());
		return null;
	}

	/**
	 * 过滤器类型。有pre、route、post、error等集中取值。
	 */
	@Override
	public String filterType() {
		return "pre";
	}

	/**
	 * 指定执行的顺序,允许不用过滤器返回相同的数字。
	 */
	@Override
	public int filterOrder() {
		return 1;
	}

}

2)使用Java Bean的方式将过滤器注入到Spring IOC容器。

@Configuration
public class BeanConfiguration {

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

3)启动,通过网关访问微服务,看到控制台打印出如下信息

jeecg微服务网关原理 微服务网关设计_jeecg微服务网关原理_04

说明自定义的Zuul过滤器执行了。 

3、禁用过滤器

配置文件设置zuul.<SimpleClassName>.<filterType>.disable=true即可。例如禁用刚才编写的自定义Zuul过滤器,则为zuul.PreRequestLogFilter.pre.disable=true。

九、Zuul的容错与回退

把微服务关掉,然后通过网关访问,会看到如下页面

jeecg微服务网关原理 微服务网关设计_SpringCloud2.x_05

Zuul的Hystrix监控的粒度是微服务,而不是某个API;同时也说明,所有经过Zuul的请求,都会被Hystrix保护起来。

下面为Zuul添加一个回退功能

1)编写Zuul的回退类。

实现FallbackProvider接口,重写getRoute和fallbackResponse两个方法。

@Component
public class OrderFallbackProvider implements FallbackProvider {

	@Override
	public String getRoute() {
		// 表明是为哪个微服务提供回退
		return "consumer-order";
	}

	@Override
	public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
		return new ClientHttpResponse() {
			
			@Override
			public HttpHeaders getHeaders() {
				// headers设定
				HttpHeaders headers = new HttpHeaders();
				MediaType mediaType = new MediaType("application", "json", Charset.forName("UTF-8"));
				headers.setContentType(mediaType);
				return headers;
			}
			
			@Override
			public InputStream getBody() throws IOException {
				// 响应体
				return new ByteArrayInputStream("订单微服务不可用,请稍后再试!".getBytes());
			}
			
			@Override
			public String getStatusText() throws IOException {
				// 状态文本
				return this.getStatusCode().getReasonPhrase();
			}
			
			@Override
			public HttpStatus getStatusCode() throws IOException {
				// fallback时的状态码
				return HttpStatus.OK;
			}
			
			@Override
			public int getRawStatusCode() throws IOException {
				// 数字类型的状态码
				return HttpStatus.OK.value();
			}
			
			@Override
			public void close() {
				// TODO Auto-generated method stub
			}
		};
	}

}

2)重启项目,通过网关访问一个未启动的微服务,将会看到返回如下信息

jeecg微服务网关原理 微服务网关设计_SpringCloud2.x_06

十、Zuul的高可用

两种方式

1、Zuul客户端也注册到Eureka Server上

2、借助额外的负载均衡器(Nginx、HAProxy、F5等)实现

十一、使用Zuul聚合微服务

许多场景下,外部请求需要查询Zuul后端的多个微服务。如果让客户端直接请求各个微服务(即使使用Zuul进行转发),那么网络开销、消耗时长、复杂度等可能都无法令我们满意。对于这种情形,可使用Zuul聚合微服务请求——客户端只需发送一个请求给Zuul,由Zuul请求各个微服务,并组织好数据给客户端。这样不仅可以简化客户端的开发,还可以提高效率。

比如使用响应式编程,RxJava、Webflux等。