首页 > 技术文章 > Spring Cloud(5):服务路由(Zuul)

storml 2019-04-23 17:49 原文

Zuul简介

所有微服务之间的调用,都应该通过服务网关进行路由,服务网关充当服务与服务之间的中介。服务网关像交通警察一样指挥交通,将用户引导到目标微服务实例。服务网关还充当着应用程序内所有微服务调用的入站流量的守门人。有了服务网关,服务客户端永远不会直接调用单个服务的URL,而是将所有调用都放到服务网关上。

 

构建一个Zuul Spring boot项目

首先,在pom.xml中添加依赖spring-cloud-starter-netflix-zuul。

<!-- Spring cloud starter: netflix-zuul -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

其次,在启动类Application中加入@EnableZuulProxy注解。

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

此外,它还是一个Eureka Client和Config Client,如何配置Eureka Client和Config Client请看前面章节。

 

在Zuul中配置路由

Zuul的核心是一个反向代理,即一个中间服务器,它位于客户端服务器与资源服务器之间,客户端服务器只需访问反向代理服务器,而反向代理服务器负责捕获客户端请求,然后代表客户端调用远程资源。配置Zuul有3种方式:

(1)通过服务发现自动映射路由,此时不需要任何配置。

比如我们正常访问一个在Eureka Server注册的服务(Eureka的服务ID为app-sql):

http://localhost:10200/app-sql/sql-sp-search/list(格式为http://[host]:[port]/[context-path]/[path])

如果使用Zuul访问,则为:

http://localhost:10030/server-zuul/app-sql/app-sql/sql-sp-search/list(格式为http://[host]:[port]/[context-path]/[app service-id]/[app context-path]/[path])

 

(2)通过服务发现手动映射路由,Zuul使用了Hystrix和Ribbon库,来帮助方式长时间运行服务调用而影响服务网关的性能。

zuul:
  # 排除所有的基于Eureka的服务ID注册的路由
  ignored-services: '*'
  # 添加前缀
  prefix: /api
  # Eureka的服务ID
  routes:
    app-sql: /s1/**
    app-one: /s2/**
    app-anther-one: /s3/**
# 设置Hystrix超时(default可以替换成具体的某个服务ID)
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 18000
# 设置Ribbon超时(如果是具体的某个服务ID,可以用[service-id].ribbon)
ribbon:
  ConnectTimeout: 1000
  ReadTimeout: 8000
# Zuul不会将敏感HTTP首部(如Cookie,Set-Cookie,Authorization)转发到下游服务。这里排除了Authorization为后面的OAuth2服务
sensitiveHeaders: Cookie,Set-Cookie

此时url为:http://localhost:10030/server-zuul/api/s1/app-sql/sql-sp-search/list(格式为http://[host]:[port]/[context-path]/[prefix]/[app routes.app-sql]/[app context-path]/[path])

[注1] 一般来说,hystrixTimeout >= ribbonTimeout(ReadTimeout + ConnectTimeout)。如果小于,则会出现警告(参考AbstractRibbonCommand.getHystrixTimeout())。其中ribbonTimeout的计算公式可以参考AbstractRibbonCommand.getRibbonTimeout()。

这里计算公式是ribbonTimeout = (ReadTimeout + ConnectTimeout)*(MaxAutoRetries+ 1)*(MaxAutoRetriesNextServer + 1) = (8000 + 1000)* 1 * 2 = 18000ms,所以hystrixTimeout要设置>=18000。

[注2]  这里配置的sensitiveHeaders会在Spring Cloud Security OAuth2中用到。

 

(3)使用静态URL手动映射路由

有些服务没有向Eureka Server注册,并没有受到Eureka Server的管理,比如一个用python写的服务,这时仍然可以建立Zuul直接路由到静态URL,并且可以手动配置Hystrix和Ribbon做到熔断和负载均衡。

zuul:
  routes:
    python-service:
      path: /ps1/**
      # 定义一个服务ID
      serviceId: python-service
hystrix:
  command:
    python-service:
execution: isolation: thread: timeoutInMilliseconds: 18000 python-service: ribbon: NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList # 如果python-service服务有多个实例,则可以负载均衡映射到多个路由 listOfServers: http://localhost:9221,http://localhost:9222 # 设置ribbon的timeout ConnectTimeout: 1000 ReadTimeout: 8000 MaxTotalHttpConnections: 500 MaxConnectionsPerHost: 100

[注1] 参考https://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/2.1.2.RELEASE/single/spring-cloud-netflix.html#netflix-zuul-reverse-proxy

 

过滤器

当我们通过网关自定义逻辑时(如安全性,日志,服务跟踪等),我们可以使用Zuul过滤器

(1)前置过滤器(PRE Filters):在Zuul将请求发送到目的地前调用,可以检查request header,验证用户信息,log记录等。

(2)路由过滤器(ROUTING Filters):调用目标服务前调用。比如它可以将服务调用重定向到另一个地方,这里的重定向并不是HTTP重定向,而是会终止传入的HTTP请求,然后再代表原始调用者发送新的请求。

(3)后置过滤器(POST Filters):在目标服务被调用并返回响应后调用。比如在response header中添加一些信息。

(4)Error过滤器(ERROR Filters):发生error时调用。

它们之间的关系如下图:

[注] 参考https://github.com/Netflix/zuul/wiki/How-it-Works

 

下面是3个过滤器的代码示例:

package com.mytools.filter;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

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

/**
 * 前置过滤器<br>
 */
@Component
public class PreFilter extends ZuulFilter {

    private static final Logger logger = LoggerFactory.getLogger(PreFilter.class);

    private static final String PRE_FILTER_TYPE = "pre";
    private static final int FILTER_ORDER = 1;
    private static final boolean SHOULD_FILTER = true;

    /* Filter type: PRE Filter
     * @see com.netflix.zuul.ZuulFilter#filterType()
     */
    @Override
    public String filterType() {
        return PRE_FILTER_TYPE;
    }

    /* 过滤器的执行顺序
     * @see com.netflix.zuul.ZuulFilter#filterOrder()
     */
    @Override
    public int filterOrder() {
        return FILTER_ORDER;
    }

    /* 是否执行过滤器
     * @see com.netflix.zuul.IZuulFilter#shouldFilter()
     */
    @Override
    public boolean shouldFilter() {
        return SHOULD_FILTER;
    }

    /* run()是每次服务通过过滤器时执行的代码
     * @see com.netflix.zuul.IZuulFilter#run()
     */
    @Override
    public Object run() {

        logger.debug("<<<<< PreFilter start >>>>>");
        RequestContext ctx = RequestContext.getCurrentContext();
        printReqHeader(ctx);
        printZuulReqHeader(ctx);
        logger.debug("<<<<< PreFilter end >>>>>");

        return null;
    }

    private void printReqHeader(RequestContext ctx) {

        HttpServletRequest req = ctx.getRequest();
        List<String> headerNameList = new ArrayList<>();

        if (ctx.getRequest() != null) {
            Enumeration<String> headerNames = req.getHeaderNames();
            while (headerNames.hasMoreElements()) {
                headerNameList.add(headerNames.nextElement());
            }
        }

        if (headerNameList.isEmpty()) {
            logger.info("----- Original Request Header is NULL. -----");
        } else {
            logger.info("----- Original Request Header: -----");
            for (String headerName : headerNameList) {
                logger.info(String.format("%s: %s", headerName, req.getHeader(headerName)));
            }
        }
    }

    private void printZuulReqHeader(RequestContext ctx) {
        Map<String, String> reqMap = ctx.getZuulRequestHeaders();
        if (reqMap == null || reqMap.isEmpty()) {
            logger.info("----- Zuul Request Header is NULL. -----");
        } else {
            logger.info("----- Zuul Request Header: -----");
            reqMap.forEach((p, q) -> {
                logger.info(String.format("%s: %s", p, q));
            });
        }
    }
}
PreFilter
package com.mytools.filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.netflix.zuul.ZuulFilter;

/**
 * 路由过滤器<br>
 */
@Component
public class RoutingFilter extends ZuulFilter {

    private static final Logger logger = LoggerFactory.getLogger(PostFilter.class);

    public static final String ROUTE_FILTER_TYPE = "route";
    private static final int FILTER_ORDER = 1;
    private static final boolean SHOULD_FILTER = true;

    /* Filter type: ROUTING Filter
     * @see com.netflix.zuul.ZuulFilter#filterType()
     */
    @Override
    public String filterType() {
        return ROUTE_FILTER_TYPE;
    }

    /* 过滤器的执行顺序
     * @see com.netflix.zuul.ZuulFilter#filterOrder()
     */
    @Override
    public int filterOrder() {
        return FILTER_ORDER;
    }

    /* 是否执行过滤器
     * @see com.netflix.zuul.IZuulFilter#shouldFilter()
     */
    @Override
    public boolean shouldFilter() {
        return SHOULD_FILTER;
    }

    /* run()是每次服务通过过滤器时执行的代码
     * @see com.netflix.zuul.IZuulFilter#run()
     */
    @Override
    public Object run() {

        logger.debug("<<<<< RoutingFilter start >>>>>");
        logger.info("This is Routing Filter.");
        logger.debug("<<<<< RoutingFilter end >>>>>");

        return null;
    }
}
RoutingFilter
package com.mytools.filter;

import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.netflix.util.Pair;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;

/**
 * 后置过滤器<br>
 */
@Component
public class PostFilter extends ZuulFilter {

    private static final Logger logger = LoggerFactory.getLogger(PostFilter.class);

    private static final String POST_FILTER_TYPE = "post";
    private static final int FILTER_ORDER = 1;
    private static final boolean SHOULD_FILTER = true;

    /* Filter type: POST Filter
     * @see com.netflix.zuul.ZuulFilter#filterType()
     */
    @Override
    public String filterType() {
        return POST_FILTER_TYPE;
    }

    /* 过滤器的执行顺序
     * @see com.netflix.zuul.ZuulFilter#filterOrder()
     */
    @Override
    public int filterOrder() {
        return FILTER_ORDER;
    }

    /* 是否执行过滤器
     * @see com.netflix.zuul.IZuulFilter#shouldFilter()
     */
    @Override
    public boolean shouldFilter() {
        return SHOULD_FILTER;
    }

    /* run()是每次服务通过过滤器时执行的代码
     * @see com.netflix.zuul.IZuulFilter#run()
     */
    @Override
    public Object run() {

        logger.debug("<<<<< PostFilter start >>>>>");
        RequestContext ctx = RequestContext.getCurrentContext();
        printResHeader(ctx);
        printZuulResHeader(ctx);
        logger.debug("<<<<< PostFilter end >>>>>");

        return null;
    }

    private void printResHeader(RequestContext ctx) {

        HttpServletResponse res = ctx.getResponse();
        List<String> headerNameList = new ArrayList<>();

        if (ctx.getRequest() != null) {
            headerNameList.addAll(res.getHeaderNames());
        }

        if (headerNameList.isEmpty()) {
            logger.info("----- Original Response Header is NULL. -----");
        } else {
            logger.info("----- Original Response Header: -----");
            for (String headerName : headerNameList) {
                logger.info(String.format("%s: %s", headerName, res.getHeader(headerName)));
            }
        }
    }

    private void printZuulResHeader(RequestContext ctx) {
        List<Pair<String, String>> resList = ctx.getZuulResponseHeaders();
        if (resList == null || resList.isEmpty()) {
            logger.info("----- Zuul Response Header is NULL. -----");
        } else {
            logger.info("----- Zuul Response Header: -----");
            resList.forEach(elem -> {
                logger.info(String.format("%s: %s", elem.first(), elem.second()));
            });
        }
    }
}
PostFilter

 

使用Actuator查询路由和过滤器信息

Zuul新添加了两个Endpoints用于查看路由和过滤器信息,只需作以下配置即可。

## Actuator info (need add '/actuator' prefix)
management:
  endpoints:
    web:
      exposure:
        # routes: 查看所有路由 | filters: 查看所有过滤器
        include: routes,filters,info,health

 

推荐阅读