Spring Cloud 入门系列七 -- 服务网关 Zuul

1 场景导入

我们都知道,在微服务架构中,一般有着多个服务提供者,而且服务可能经常改变。 那我们外部的服务应该如何访问内部各种微服务呢?事实上,后端的服务往往不会直接开放给调用端,而是通过服务网关根据 url 路由到相应的服务。网关就通过对外暴露 API ,屏蔽内部微服务的微小变动,从而保持系统的稳定性。

2 什么是 Zuul?

Zuul 是 Spring Cloud 全家桶中的微服务 API 网关,所有请求都会先经过 Zuul 到达后端的应用程序。Zuul 提供了认证和安全,性能监测,动态路由,压力测试,负载卸载,静态资源处理等功能。

3 Zuul 的基础使用

我们先新建一个项目,命名为 zuul,其 pom.xml 如下,我们往里面添加了 zuul 和 eureka 的依赖。

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.6.RELEASE</version>
		<relativePath/> 
	</parent>
	<groupId>com.example</groupId>
	<artifactId>Zuul</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>Zuul</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		
		<dependency> 
  			<groupId>org.springframework.cloud</groupId>  
  			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> 
		</dependency>
		
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
		</dependency>
       
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	
	<dependencyManagement> 
  		<dependencies> 
    		<dependency> 
      			<groupId>org.springframework.cloud</groupId>  
      			<artifactId>spring-cloud-dependencies</artifactId>  
      			<version>Finchley.SR2</version>  
      			<type>pom</type>  
      			<scope>import</scope> 
   			 </dependency> 
  		</dependencies> 
	</dependencyManagement>

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

</project>

然后记得添加配置文件

spring.application.name=zuul
server.port=8086
# 设置与Eureka Server交互的地址,查询服务和注册服务都需要依赖这个地址
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

然后在启动类上添加注解 @EnableDiscoveryClient 和 @EnableZuulProxy,其中 @EnableZuulProxy 用于添加对 Zuul 的支持。

package com.example.Zuul;

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

@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
public class ZuulApplication {

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

}

我们分别运行 Eureka,EurekaProducer,Zuul 三个项目,在浏览器输入 http://localhost:8761/
在这里插入图片描述
我们发现 Zuul ,EurekaProducer 均已注册至 Eureka。

然后在浏览器输入 http://localhost:8086/producer/helloWorld?id=3&saying=%E7%89%9B%E9%80%BC

在这里插入图片描述
事实上,我们输入的 http://localhost:8086/producer/helloWorld?id=3&saying=%E7%89%9B%E9%80%BC 可以等效为 http://localhost:8080/helloWorld?id=3&saying=牛逼,其中8080端口是 EurekaProducer 使用的端口。在实现微服务架构时,服务名与服务实例地址的关系在 Eureka 服务器中就存在了。在我们将 Zuul 注册到 Eureka 服务器中时,可以去发现其他服务并完成对 serviceId 的映射。Zuul 的默认路由规则如下:http://ZUUL_HOST:ZUUL_PORT/微服务在Eureka上的serviceId/** 将会被转发至 serviceId 对应的微服务。

4 Zuul 路由功能的实现

我们仔细观察之前使用 Zuul 进行访问的路径

http://localhost:8086/producer/helloWorld?id=3&saying=%E7%89%9B%E9%80%BC

我们发现,必须要知道项目的名称才能访问!!不知道的话访问不了!!那我们再认真地想一想,如果让用户知道项目的名称,使用 Zuul 就意义不大了,要知道使用原来的路径也是可以访问的啊。我们知道,Zuul 有代理的功能,不想让用户看到真实的实现,既然如此,我们可以为 Zuul 设置一些路由规则来隐藏真实的实现。

我们修改一下配置文件

# 通过/proxy/**来访问producer项目
zuul.routes.producer=/proxy/**

在这里插入图片描述
但是,仅仅这样好像不太行啊,我们用原来的方法照样可以访问啊!那么有没有一种方法可以忽略掉应用名称访问的呢?自然是有的,添加一行配置即可。

# 忽略producer的应用名称访问
zuul.ignored-services=producer

话说如果一个系统微服务很多的话,使用这种配置方式岂不是非常麻烦?其实最简单的是我们可以选择使用通配符模式来完成。

# 忽略全部应用名称访问
zuul.ignored-services="*"

所有访问都需要配置一个映射路径来完成,使用服务名称的信息访问将被禁止。

5 Zuul 中的 Filter

Filter 是 Zuul 的核心,其实现了 Zuul 的大部分功能,主要用来实现对外服务的控制。Filter 一共有 “PRE”,“ROUTING”,“POST”,“ERROR” 四个生命周期,对应四种过滤器类型。

  1. PRE:在请求被路由之前调用,我们可以使用其进行权限验证
  2. ROUTING:该过滤器将请求路由到微服务,我们可以用来构建发送给微服务的请求并请求微服务
  3. POST:在路由到微服务之后被调用,我们可以用来收集统计信息
  4. ERROR:在其他阶段发生错误时执行该过滤器。

下面我们演示如何自定义一个过滤器,实现一个功能:要求所有访问申请必须带有用户名信息,且我这里限定用户名必须为 Ling,只有用户名为 Ling 才可以继续访问下去,否则会直接打印 username error。下面我们就来实现这个过滤器。

我们实现一个继承 ZuulFilter 的类。需要覆盖其中四个方法。

package com.example.Zuul.filter;

import javax.servlet.http.HttpServletRequest;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;

public class FirstFilter extends ZuulFilter{

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

	//过滤器执行的具体操作
	@Override
	public Object run() throws ZuulException {
		RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        
        //获取参数
        String username = request.getParameter("username");
        
        //符合要求
        if(username.equals("Ling")) {
        	ctx.setSendZuulResponse(true); //进行路由
            ctx.setResponseStatusCode(200);
            ctx.set("isSuccess", true);
        }else {
        	ctx.setSendZuulResponse(false); //不进行路由
            ctx.setResponseStatusCode(400);
            ctx.setResponseBody("username error");
            ctx.set("isSuccess", false);
        }
        
        return null;
	}

	//定义filter的类型,可选择pre、route、post、error四种
	@Override
	public String filterType() {
		return "pre";
	}

	//定义filter的顺序,数字越小越先执行
	@Override
	public int filterOrder() {
		return 0;
	}

}

然后我们需要把我们自己实现的过滤器加入请求拦截队列中,在启动类添加如下代码:

	@Bean
	public FirstFilter firstFilter() {
		return new FirstFilter();
	}

我们测试一下过滤器的作用,先后运行 Eureka,EurekaProducer,Zuul 三个项目,在浏览器输入 http://localhost:8086/proxy/helloWorld?id=3&saying=%E7%89%9B%E9%80%BC&username=Ling1,显示如下:
在这里插入图片描述
我们发现这个请求由于用户名不为 Ling 而被拦截,说明这个过滤器是起到作用的。接下来我们访问 http://localhost:8086/proxy/helloWorld?id=3&saying=%E7%89%9B%E9%80%BC&username=Ling,由于访问请求可以到达微服务,说明过滤器已通过,请求正常响应了。
在这里插入图片描述

6 路由熔断

当后端服务出现异常时,我们不希望将异常抛出给最外层,期望服务可以自动进行降级。Zuul 给我们提供了类似的支持,当某个服务出现异常时,直接返回我们预设的信息。

我们通过自定义的方法,并且将其指定给某个 route 来实现该 route 访问出问题的熔断处理,主要通过实现 FallbackProvider 接口。我们先来看一下这个接口:

/**
 * Provides fallback when a failure occurs on a route.
 *
 * @author Ryan Baxter
 * @author Dominik Mostek
 */
public interface FallbackProvider {

	/**
	 * The route this fallback will be used for.
	 * @return The route the fallback will be used for.
	 */
	public String getRoute();

	/**
	 * Provides a fallback response based on the cause of the failed execution.
	 *
	 * @param route The route the fallback is for
	 * @param cause cause of the main method failure, may be <code>null</code>
	 * @return the fallback response
	 */
	ClientHttpResponse fallbackResponse(String route, Throwable cause);
}

getRoute 表示要拦截哪个服务,fallbackResponse 则表示 Zuul 断路时应该提供一个什么样的返回值。

我们创建一个类来实现 FallbackProvider 接口。

package com.example.Zuul.fallback;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

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;

/**
 * zuul路由熔断的实现
 * @author 30309
 *
 */
@Component
public class MyFallback implements FallbackProvider{

	//熔断拦截哪个服务
	@Override
	public String getRoute() {
		//*表示为所有服务提供回退服务,当然也可以指定具体的服务
		//return "*";
		return "producer";
	}

	//定制返回内容
	@Override
	public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
		if (cause != null && cause.getCause() != null) {
            System.out.println("出错了!" + cause);
        }

        return response(HttpStatus.INTERNAL_SERVER_ERROR);
	}
	
	private ClientHttpResponse response(final HttpStatus status) {
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() {
                return status;
            }

            @Override
            public int getRawStatusCode() {
                return status.value();
            }

            @Override
            public String getStatusText() {
                return status.getReasonPhrase();
            }

            @Override
            public void close() {
            }

            @Override
            public InputStream getBody() {
                return new ByteArrayInputStream("The service is unavailable.".getBytes(StandardCharsets.UTF_8)); //返回前端的内容
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders httpHeaders = new HttpHeaders();
                httpHeaders.setContentType(MediaType.APPLICATION_JSON_UTF8); //设置头
                return httpHeaders;
            }
        };
    }
}

现在我们修改一下 producer 项目的接口,让该服务在执行过程中休眠一段时间并确保其超时。

package com.example.EurekaProducer.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HelloWorldController {

	@RequestMapping("/helloWorld")
	@ResponseBody
	public String helloWorld(int id,String saying) {
		
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		return "来自8080端口:序号为" + id + "的用户发布了一条新消息:" + saying;
	}
}

然后我们运行 Eureka,EurekaProducer,Zuul 三个项目,输入 http://localhost:8086/proxy/helloWorld?id=3&saying=aa,结果发现 producer 服务已被熔断。
在这里插入图片描述
观察控制台,其打印如下,说明我们已经实现了 Zuul 的路由熔断功能。

出错了!com.netflix.client.ClientException

从上面的测试可见,Zuul 只支持服务级别的熔断,不支持具体 URL 级别的熔断。

7 路由重试

有时可能因为种种原因,服务暂时不可用,我们希望在这个时候对服务进行重试,Zuul 事实上也帮助我们实现了这个功能。

我们继续修改 Zuul 项目,增加 Spring Retry 依赖。

		<dependency>
    		<groupId>org.springframework.retry</groupId>
    		<artifactId>spring-retry</artifactId>
		</dependency>

修改配置文件如下

spring.application.name=zuul
server.port=8086
# 设置与Eureka Server交互的地址,查询服务和注册服务都需要依赖这个地址
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

# 忽略全部应用名称访问
zuul.ignored-services="*"
# 通过/proxy/**来访问producer项目
zuul.routes.producer=/proxy/**
# 是否开启重试功能,默认为false
zuul.retryable=true

# 开启eureka负载均衡策略
ribbon.eureka.enabled=true
# 当前实例的重试次数,默认为一次
ribbon.MaxAutoRetries=3
# 切换实例的重试次数
ribbon.MaxAutoRetriesNextServer=0
# 是否所有操作都重试,为false时只对get请求重试,为true会对所有请求重试,慎用
ribbon.OkToRetryOnAllOperations=false

修改一下 producer 项目的接口

package com.example.EurekaProducer.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HelloWorldController {

	@RequestMapping("/helloWorld")
	@ResponseBody
	public String helloWorld(int id,String saying) {
		
		System.out.println("Hello World!");
		
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		return "来自8080端口:序号为" + id + "的用户发布了一条新消息:" + saying;
	}
}

结果我们发现 Hello World 打印了四次,说明我们已经重试过三次了。我们的路由重试功能也实现成功。
在这里插入图片描述

8 超时配置

配置如下

# 使用serviceId进行路由,请求处理的超时时间
ribbon.ReadTimeout=60000
# 使用serviceId进行路由,请求连接的超时时间
ribbon.ConnectTimeout=60000
# 使用url进行路由,请求连接的超时时间
zuul.host.connect-timeout-millis=60000
# 使用url进行路由,请求处理的超时时间
zuul.host.socket-timeout-millis=60000

9 总结

在实际开发中,不同客户端会使用不同的负载将请求分发到 Zuul,Zuul 会通过 Eureka 调用后端服务,最后对外输出。

参考:【Spring Cloud 基础设施搭建系列】Spring Cloud Demo项目 Zuul的路由重试和路由熔断
springcloud(十一):服务网关Zuul高级篇
springcloud(十):服务网关zuul初级篇

发布了113 篇原创文章 · 获赞 206 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Geffin/article/details/102813846