使用 Zuul 构建微服务网关

使用 Zuul 构建微服务网关

一、功能简介

zuul 的核心是一系列的过滤器,这些过滤器可以完成以下功能。

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

二、简单使用

1、步骤

1)、添加依赖

<!-- 服务网关 zuul -->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- end -->
<!-- 服务发现 eureka client -->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- end -->

2)、在启动类上添加 @EnableZuulProxy 注解,表示以代理模式启动 Zuul ,该代理默认使用 Ribbon 来定位注册在 Eureka Server 中的微服务;同时,该代理还整合了 Hystrix,从而实现了容错。

3)、编写配置文件

server:
  port: 8790
spring:
  application:
    name: domo-zuul
eureka:
  client:
    service-url:
      defaultZone: http://zolmk:zolmk@${client.desktop}:8761/eureka/
    register-with-eureka: true 
    fetch-registry: true 
  instance:
    prefer-ip-address: true
    hostname: localhost
management:
  endpoints:
    web:
      exposure:
        include: '*'
client:
  desktop: 192.168.1.7
  notebook: eureka-node-a

4)、启动 application 即可使用 zuul 。

2、路由规则

1)访问 http://zuul-ip:zuul-port/miscro-service-name/**,请求将被转发到 http://miscro-service-name-ip-port/**

默认情况下 zuul 会代理 Eureka Server 上的所有微服务。

3、Zuul 的路由端点

当 @EnableZuulProxy 与 Spring Boot Actuator 配合使用的时候,Zuul 会暴露一个路由管理端点 /actuator/routes,可以查看已暴露的路由端点。

使用 GET ip:port/actuator/routes 返回 Zuul 当前映射的路由列表,使用 POST 可以强制刷新 Zuul 当前映射的路由列表。

4、路由配置详解

这部分主要会讲如何让 zuul 代理部分微服务,或者需要对 URL 进行更精确的控制。

1)自定义微服务访问地址

配置 zuul.routes.指定微服务 serviceId = 指定路径 即可。

zuul:
 routes:
 	microservice: /service/**

这样 http://zuul_ip:port/microservice/** 就会被映射到普通配置下的 http://zuul_ip:port/service/** 地址

2)忽略指定地址

zuul:
	ignored-services: microservice1,microservice2

3) 忽略所有微服务,仅路由指定微服务

zuul:
	ignored-services:'*'
	routes:
		microservice1: /microservice1

这样 zuul 只会路由 microservice1 微服务。

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

zuul:
	routes:
		user-route:
			service-id: microservice-user
			path: /user

user-route 只是给路由一个名称,可随意起名。

此配置会将普通配置下的 http://zuul_ip:port/microservice-user 映射到 http://zuul_ip:port/user

5)同时指定 path 和 URL,例如

zuul:
	routes:
		user-route:
			url: http://localhost:8080/
			path: /user/**

这样就可以将 http://localhost:8080/ 映射到 http://zuul_id:port/user/**

需要注意的是,使用这种方式配置的路由不会作为 HystrixCommand 执行,同时不能使用 Ribbon 来负载均衡多个 URL。

6)同时指定 path 和 URL,并且不破坏 Zuul 的 Hystrix、Ribbon 的特性。

zuul:
	routes:
		user-routes:
			path: /user/**
			service-id: microservice1
ribbon:
	eureka:
		enabled: false
microservice1:
	ribbon:
		listOfServers: localhost:8000,localhost:8001

这样就可以既指定 path 与 URL,又不破坏 Zuul 的 Hystrix 与 Ribbon 特性了。

7)使用正则表达式指定 Zuul 的路由匹配规则

借助 PatternServiceRouteMapper,实现从微服务到映射路由的正则配置。

@Bean
public PatternServiceRouteMapper serviceRouteMapper()
{
    /**
     * 调用构造函数 PatternServiceRouteMapper(String servicePattern,String routePattern
     */
    return new PatternServiceRouteMapper("(?<name>^.+)-(?<version>v.+$)","${version}/${name}");
}

8)路由前缀

zuul:
	prefix: /api
	strip-prefix: false # 当 strip-prefix = false 时,TT 处的配置不生效
	routes:
		microservice-provider-user: /user/** # TT

这样,访问 http://zuul_ip:port/api/microservice-provider-user/1 路径,请求将会被转发到 microservice-provider-user/api/1

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

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

即:http://zuul_ip:port/user/1 ===>> http://microservice-provider-user-ip-port/user/1 == 普通模式下的 http://zuul_ip:port/microservice-provider-user/user/1

9)忽略某些路径

如果想让 zuul 代理某个微服务,同时又想保护该微服务的某些敏感路径,此时,可使用 ignored-Patterns,指定忽略的正则。例如

zuul:
	ignoredPatterns: /**/admin/** #忽略所有包含 /admin/ 的路径
	routes:
		microservice-provider-user: /user/**

5、Zuul 的安全与 Header

1)敏感 Header 的设置

为了防止敏感 Header 外泄,需要为路由指定一系列敏感 Header 列表。

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

也可用用 zuul.sensitive-headers 全局指定 Header,例如

zuul:
	sensitive-headers: Cookie,Set-Cookie,Authorization # 默认时 Cookie,Set-Cookie

2)忽略 Header

也可使用 zuul.ignoreHeaders 丢弃一些 Header

zuul:
	ignored-headers: Header1,Header2

设置后,Header1,Header2就不会传播到其他的微服务中。

默认情况下,zuul.ignored-headers 是空值,但如果 Spring Security 在项目的 classpath 中,那么 zuul.ignored-headers 的默认值就是 Pragma,Cache-Control,X-Frame-Options,X-Content-Type-Options,X-XSS-Protection,Expires。所以,当 Spring Security 在 classpath 中时,如果需要下游微服务使用 Header 时,可以将 zuul.ignoreSecurity-Headers 设置为 false。

6、使用 Zuul 上传文件

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

如果使用了 Ribbon 做负载均衡,那么对于超大文件,还需提升超时设置:

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

使用文件上传功能时,最好设置:

spring:
	http:
		multipart:
			max-file-size: 200MB # max file size 默认 1MB
			max-request-size: 200MB # 默认10MB

7、Zuul 的过滤器

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

Zuul 的大部分功能都是通过过滤器来实现的。Zuul 中定义了四种标准过滤器类型。

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

Zuul 请求的生命周期图:

2)编写 Zuul 过滤器

编写 zuul 过滤器非常简单,仅需实现 import com.netflix.zuul.ZuulFilter 接口接口

@Component
public class PreRequestLogFilter extends ZuulFilter
{
    private static final Logger LOGGER = LoggerFactory.getLogger(PreRequestLogFilter.class);

    @Override
    public String filterType ()
    {
        return "pre"; // 这里更改过滤器类型
    }

    @Override
    public int filterOrder ()
    {
        return 1;
    }

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

    @Override
    public Object run () // 过滤器执行函数
    throws ZuulException
    {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        PreRequestLogFilter.LOGGER.info(String.format("send %s request to %s",request.getMethod(),request.getRequestURL().toString()));
        return new HashMap<>();
    }
}

上面的过滤器仅实现了打印日志的功能。

3)禁用 Zuul 过滤器

Spring Cloud 默认为 Zuul 编写并启用了一些过滤器,例如 DebugFilter、 FormBodyWrapperFilter 等。

如果想要禁用过滤器,仅需配置zuul.<SimpleClassName/>.<filterType>.disable=true 即可

8、Zuul 的容错与回退

zuul 默认已经开启了 Hystrix ,并且 zuul 的容错的粒度是微服务,要实现回退,我们仅需实现 org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider 接口并将其作为 SpringBoot 组件,如下所示:

@Component
public class UserRegisterFallbackProvider implements FallbackProvider,ClientHttpResponse
{

    @Override
    public String getRoute ()
    {
        return "user-register-server"; // 这里设置回退器所作用的微服务
    }


    @Override
    public ClientHttpResponse fallbackResponse (String route, Throwable cause)
    {

        return this;
    }

    @Override
    public HttpStatus getStatusCode ()
    throws IOException
    {
        return HttpStatus.OK;
    }

    @Override
    public int getRawStatusCode ()
    throws IOException
    {
        return getStatusCode().value();
    }

    @Override
    public String getStatusText ()
    throws IOException
    {
        return this.getStatusCode().getReasonPhrase();
    }

    @Override
    public void close ()
    {

    }

    @Override
    public InputStream getBody ()
    throws IOException
    {
        return new ByteArrayInputStream((this.getRoute()+" 不可用").getBytes());
    }

    @Override
    public HttpHeaders getHeaders ()
    {
        HttpHeaders h = new HttpHeaders();
        MediaType mt = new MediaType("application","json", StandardCharsets.UTF_8);
        h.setContentType(mt);
        return h;
    }
}

9、Zuul 的高可用

1)第一种情况,Zuul 客户端也注册在微服务上

这种情况不用多说,必然高可用。

2)第二种情况,zuul 客户端未注册到微服务上

在现实中,基本都是这种情况,因为不可能把所有客户端都注册到 Eureka 上。

这种情况下,可以借助一个额外的负载均衡器来实现 zuul 的高可用,例如 Nginx、HAProxy、F5 等。

如下图,Zuul 客户端将请求发送到负载均衡器,负载均衡器将请求转发到其代理的其中一个 Zuul 节点。这样,就可以实现 Zuul 的高可用。

10、使用 Zuul 聚合微服务

这部分主要使用了 Rxjava 来进行异步请求微服务,当请求完成时,zip 微服务的返回结果,将其发送给客户端。

1、首先在微服务中创建 RestController ,该 controller 为用户提供聚合服务

@RestController
@RequestMapping("/aggregate")
public class AggregationController
{
    private final AggregationService aggregationService;	// 自动注入 aggregationService 该服务提供两个接口 getUserById getMovieUserByUserId

    public AggregationController (AggregationService aggregationService)
    {
        this.aggregationService = aggregationService;
    }

	// 这部分 DeferredResult 类在 org.springframework.web.context.request.async 包中,从 async 可以看出,它是一个异步请求类, deferred 是延期 推迟的意思
    // 这里必须使用该类设置返回结果,因为在 toDeferredResult.observable.subscribe 中 不允许直接赋值,也就是 resultMap = stringObjectHashMap
    @GetMapping("/test/{id}")
    public DeferredResult<HashMap<String,Object>> test(@PathVariable Long id)
    {
        Observable<HashMap<String,Object>> result = this.aggregate(id);
        return this.toDeferredResult(result);
    }

    public Observable<HashMap<String,Object>> aggregate(Long id)
    {
        // 使用 zip 方法将两个请求约定一起发射
        return Observable.zip(
                this.aggregationService.getUserById(),
                this.aggregationService.getMovieUserByUserId(),
                (o1,o2) -> {
                    HashMap<String,Object> map = Maps.newHashMap();
                    map.put("user",o1);
                    map.put("userMovie",o2);
                    return map;
                }
        );
    }

    public DeferredResult<HashMap<String,Object>> toDeferredResult(Observable<HashMap<String,Object>> observable)
    {
        DeferredResult<HashMap<String,Object>> result = new DeferredResult<>();

        observable.subscribe( // 订阅,也就是开始运行
                new Observer<HashMap<String, Object>>()
                {
                    @Override
                    public void onCompleted ()
                    {
                        System.out.println("完成");
                    }

                    @Override
                    public void onError (Throwable throwable)
                    {
                        System.out.println("出错");
                    }

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

AggregationService 接口

public interface AggregationService
{
    public Observable<Object> getUserById();
    public Observable<Object> getMovieUserByUserId();
}

AggregationServiceImpl 类

@Service
public class AggregationServiceImpl implements AggregationService
{

    @Override
    public Observable<Object> getUserById ()
    {
        return Observable.create(subscriber -> // 创建被观察者
        {
            try
            {
                Thread.sleep(2000);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            subscriber.onNext("zolmk");
            subscriber.onCompleted();
        });
    }

    @Override
    public Observable<Object> getMovieUserByUserId ()
    {
        return Observable.create(subscriber -> // 创建被观察者
        {
            try
            {
                Thread.sleep(1000);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            subscriber.onNext("zolmk");
            subscriber.onCompleted();
        });
    }
}

使用浏览器请求 localhost:port/aggregate/test/id 即可观察到结果。

posted @ 2020-12-02 15:11  zolmk  阅读(138)  评论(0编辑  收藏  举报