Spring Cloud(5):服务路由(Zuul)
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
过滤器
当我们通过网关自定义逻辑时(如安全性,日志,服务跟踪等),我们可以使用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)); }); } } }
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; } }
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())); }); } } }
使用Actuator查询路由和过滤器信息
Zuul新添加了两个Endpoints用于查看路由和过滤器信息,只需作以下配置即可。
## Actuator info (need add '/actuator' prefix) management: endpoints: web: exposure: # routes: 查看所有路由 | filters: 查看所有过滤器 include: routes,filters,info,health