第6章 使用Zuul进行服务路由
在像微服务架构这样的分布式架构中,有时候需要做一些统一的动作,例如日志记录和追踪、记录接口调用的时间等,为了解决这个问题,需要将一些横切关注点抽象成一个独立的服务,这个服务会作为所有微服务的过滤器和路由器,这种横切关注点被成为服务网关。服务客户端不再直接调用微服务,取而代之的是,所有调用都通过服务网关进行路由,然后被路由到最终目的地。Zuul是开源的服务网关实现。
我们通过一个简单的图看下什么是服务网关。
服务网关充当的是客户端与服务端之间的中介,网关从客户端调用中分离出路径,此路径用于确定客户端真正想调用的服务,可以看到,其实网关还是所有微服务调用的入口,相当于守门人的角色,这样横切关注点就可以在网关实现,而不用每个服务都加上切面。服务网关中可以做的事情如下:
- 路由—网关将所有服务调用放置在URL和API路由的后面,这样客户端调用只需要知道一个IP和Port即可。服务网关可以根据传入的url判断客户端真正请求的服务,执行智能路由
- 验证和授权—由于所有服务调用都要经过网关进行路由,所以网关是检查服务调用是否已经进行了验证并被授权调用该服务
- 度量数据收集和日志记录—可以做些基本的统计工作,如服务调用次数,服务响应时间,日志关联Id的记录。
接下来介绍SpringCloud集成Zuul。Zuul提供了较多功能,我们具体介绍:
1、将所有服务的路由映射到一个服务中,也就是提供服务的统一入口
2、构建过滤器。
下面介绍怎样建立一个Zuul项目。和以前一样,修改构建脚本、修改引导类、修改配置文件。
首先构建脚本添加对于zuul依赖项:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
这个依赖告诉SpringCloud框架,该服务将运行Zuul,并适当地初始化Zuul。
接下来修改引导类,为了使服务成为一个Zuul服务,需要添加一个@EnableZuulProxy的注解
@SpringBootApplication
//使服务成为一个Zuul服务
@EnableZuulProxy
public class ZuulServerApplication {
@LoadBalanced
@Bean
public RestTemplate getRestTemplate(){
RestTemplate template = new RestTemplate();
List interceptors = template.getInterceptors();
if (interceptors == null) {
template.setInterceptors(Collections.singletonList(new UserContextInterceptor()));
} else {
interceptors.add(new UserContextInterceptor());
template.setInterceptors(interceptors);
}
return template;
}
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
}
还需要注意的一点是,Zuul也需要注册到Eureka上。其将使用Eureka来通过服务ID查找服务,然后使用Ribbon对来自Zuul的请求进行客户端负载均衡。
我们应该清楚,Zuul的核心是一个反向代理,反向代理负责捕获客户端的请求,然后代表客户端调用远程资源,Zuul必须知道如何将进来的请求映射到不同的服务中,主要有以下几种机制
通过服务发现自动映射路由
理论上将Zuul的所有映射都是在其application.yml中定义路由来完成的。但是Zuul可以根据其服务ID自动路由请求,例如如下的URL。
http://localhost:5555/organizationservice/v1/organizations/442adb6e-fa58-47f3-9ca2-ed1fecdfe86c。其中Zuul服务器通过http://localhost:5555进行访问。路径的第一部分organizationservice尝试调用在Eureka上注册的逻辑名称为organizationservice的服务。使用服务发现手动映射路由
Zuul允许开发人员更细粒度地明确定义路由映射,而不是单纯地依赖服务的Eureka服务ID创建的自动路由,可以通过在Zuul服务上修改application.yml配置文件来手动定义路由映射。如果想要排除掉Eureka服务ID路由的自动映射,只提供自定义的组织服务路由,可以使用ignored-services参数,如果屏蔽掉所有的自动映射,属性值需设为‘*’。另外服务网关的一种常见模式是通过使用/api之类的前缀来为所有服务添加前缀,从而区别API路由和内容路由。如下:
zuul:
prefix: /api
ignored-services: '*'
routes:
producer:
strip-prefix: false
organizationservice: /organization/**
licensingservice: /licensing/**
虽然Zuul作为网关代理确实很灵巧,,但是Zuul的真正威力在于执行一组一致的应用程序,例如:安全性、日志记录、服务跟踪等。因为开发人员想将这些逻辑应用于所有微服务中,而不是在每个微服务中都要使用切面或者注解。那么此时Zuul的过滤器就大杀四方了。通过Zuul的过滤器,我们可以自定义自己的逻辑,Zuul的过滤器有三种:
- 前置过滤器,在请求发送到实际目的地之前被调用,例如常用的判断Http的请求首部
- 后置过滤器,将响应返回给客户端时被调用
- 路由过滤器,简单来讲,就是通过路由过滤器的请求可以一部分被调用到A服务,一部分调用到B服务,服务升级时,可以使用,一小部分客户使用升级后的服务作为测试,大部分客户使用原服务。
下面通过一个图,简单描述下:
下面,我们构建第一个过滤器
前置过滤器
功能需求:检查所有到网关的请求,并判断是否存在名为tmx-correlation-id的HTTP首部。这有点类似于日志追踪中的GUID,用于跨多个服务跟踪用户请求。前置过滤器的代码如下:
@Component
//所有Zuul过滤器必须扩展ZuulFilter,并覆盖如下四个方法
public class TrackingFilter extends ZuulFilter{
private static final int FILTER_ORDER = 1;
private static final boolean SHOULD_FILTER=true;
private static final Logger logger = LoggerFactory.getLogger(TrackingFilter.class);
//封装的过滤器类
@Autowired
FilterUtils filterUtils;
/**
* 告诉Zuul,这是前置过滤器
* @return
*/
@Override
public String filterType() {
return FilterUtils.PRE_FILTER_TYPE;
}
/**
* 不同类型过滤器的执行顺序
* @return
*/
@Override
public int filterOrder() {
return FILTER_ORDER;
}
/**
* 是否执行该过滤器
* @return
*/
public boolean shouldFilter() {
return SHOULD_FILTER;
}
private boolean isCorrelationIdPresent(){
if (filterUtils.getCorrelationId() !=null){
return true;
}
return false;
}
private String generateCorrelationId(){
return java.util.UUID.randomUUID().toString();
}
/**
* 每次服务调用通过过滤器时,都会调用。检查首部是否存在,如果不存在,则设置一个
* @return
*/
public Object run() {
if (isCorrelationIdPresent()) {
logger.debug("tmx-correlation-id found in tracking filter: {}. ", filterUtils.getCorrelationId());
}
else{
filterUtils.setCorrelationId(generateCorrelationId());
logger.debug("tmx-correlation-id generated in tracking filter: {}.", filterUtils.getCorrelationId());
}
RequestContext ctx = RequestContext.getCurrentContext();
logger.debug("Processing incoming request for {}.", ctx.getRequest().getRequestURI());
return null;
}
}
FilterUtils类用于封装所有过滤器使用的常用功能,如下:
@Component
public class FilterUtils {
public static final String CORRELATION_ID = "tmx-correlation-id";
public static final String AUTH_TOKEN = "tmx-auth-token";
public static final String USER_ID = "tmx-user-id";
public static final String ORG_ID = "tmx-org-id";
public static final String PRE_FILTER_TYPE = "pre";
public static final String POST_FILTER_TYPE = "post";
public static final String ROUTE_FILTER_TYPE = "route";
public String getCorrelationId(){
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.getRequest().getHeader(CORRELATION_ID) !=null) {
return ctx.getRequest().getHeader(CORRELATION_ID);
}
else{
return ctx.getZuulRequestHeaders().get(CORRELATION_ID);
}
}
public void setCorrelationId(String correlationId){
RequestContext ctx = RequestContext.getCurrentContext();
ctx.addZuulRequestHeader(CORRELATION_ID, correlationId);
}
public final String getOrgId(){
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.getRequest().getHeader(ORG_ID) !=null) {
return ctx.getRequest().getHeader(ORG_ID);
}
else{
return ctx.getZuulRequestHeaders().get(ORG_ID);
}
}
public void setOrgId(String orgId){
RequestContext ctx = RequestContext.getCurrentContext();
ctx.addZuulRequestHeader(ORG_ID, orgId);
}
public final String getUserId(){
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.getRequest().getHeader(USER_ID) !=null) {
return ctx.getRequest().getHeader(USER_ID);
}
else{
return ctx.getZuulRequestHeaders().get(USER_ID);
}
}
public void setUserId(String userId){
RequestContext ctx = RequestContext.getCurrentContext();
ctx.addZuulRequestHeader(USER_ID, userId);
}
public final String getAuthToken(){
RequestContext ctx = RequestContext.getCurrentContext();
return ctx.getRequest().getHeader(AUTH_TOKEN);
}
public String getServiceId(){
RequestContext ctx = RequestContext.getCurrentContext();
//We might not have a service id if we are using a static, non-eureka route.
if (ctx.get("serviceId")==null) return "";
return ctx.get("serviceId").toString();
}
}
现在我们启动网关服务和组织服务,调用下:
后置过滤器
后置过滤器是收集指标并完成与用户相关联的日志记录的理想场所,下面实现将传递给微服务的关联ID注入回用户。使用后置过滤器将关联ID注入HTTP响应首部中。后置过滤器的代码如下:
@Component
public class ResponseFilter extends ZuulFilter{
private static final int FILTER_ORDER=1;
private static final boolean SHOULD_FILTER=true;
private static final Logger logger = LoggerFactory.getLogger(ResponseFilter.class);
@Autowired
FilterUtils filterUtils;
@Override
public String filterType() {
return FilterUtils.POST_FILTER_TYPE;
}
@Override
public int filterOrder() {
return FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
return SHOULD_FILTER;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
logger.debug("Adding the correlation id to the outbound headers. {}", filterUtils.getCorrelationId());
ctx.getResponse().addHeader(FilterUtils.CORRELATION_ID, filterUtils.getCorrelationId());
logger.debug("Completing outgoing request for {}.", ctx.getRequest().getRequestURI());
return null;
}
}
我们调用下,看下响应中是否存在对应的首部。