微服务网关背景及简介
不同的微服务一般有不同的网络地址,而外部的客户端可能需要调用多个服务的接口才能完
成一个业务需求。比如一个电影购票的收集APP,可能回调用电影分类微服务,用户微服
务,支付微服务等。如果客户端直接和微服务进行通信,会存在一下问题:
- 客户端会多次请求不同微服务,增加客户端的复杂性
- 存在跨域请求,在一定场景下处理相对复杂
- 认证复杂,每一个服务都需要独立认证
- 难以重构,随着项目的迭代,可能需要重新划分微服务,如果客户端直接和微服务通信,那么重构会难以实施
- 某些微服务可能使用了其他协议,直接访问有一定困难
上述问题,都可以借助微服务网管解决。微服务网管是介于客户端和服务器端之间的中间
层,所有的外部请求都会先经过微服务网关,架构演变成:
这样客户端只需要和网关交互,而无需直接调用特定微服务的接口,而且方便监控,易于认
证,减少客户端和各个微服务之间的交互次数
zuul微服务网关
Zuul是Netflix开源的微服务网关,他可以和Eureka,Ribbon,Hystrix等组件配合使用。Zuul
组件的核心是一系列的过滤器,这些过滤器可以完成以下功能:
- 动态路由:动态将请求路由到不同后端集群
- 压力测试:逐渐增加指向集群的流量,以了解性能
- 负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求
- 静态响应处理:边缘位置进行响应,避免转发到内部集群
- 身份认证和安全: 识别每一个资源的验证要求,并拒绝那些不符的请求
Spring Cloud对Zuul进行了整合和增强。目前,Zuul使用的默认是Apache的HTTPClient,也可以使用Rest Client,可以设置ribbon.restclient.enabled=true.
编写一个简单微服务网关
添加依赖并在启动类上增加注解@EnableZuulProxy,声明一个zuul代理。该代理使用Ribbon来定位注册在 Eureka server中的微服务;同时,该代理还整合了Hystrix,从而实现了容错,所有经过 zuul的请求都会在 Hystrix命令中执行。
在启动类上增加注解@EnableZuulProxy,声明一个zuul代理
配置文件application.yml如下:
启动项目(启动Eureka服务,两个用户微服务,一个订单微服务,和一个网关zull的微服务)
通过网关来调用用户的微服务
发现zuul会代理所有注册到eureka server的微服务,并使用ribbon做负载均衡,zuul的路由规则如下,可以访问地址:
http://localhost:8040/routes
http://ZUUL_HOST:ZUUL_PORT/微服务在Eureka上的serviceId/**会被转发到serviceId对应的微服务如:
http://192.168.56.1:8040/microservice-provider-user/person/getIpAndPort 就会自动转发到用户微服务的getIpAndPort方法上
zuul还自动整合了hystrix,访问地址:http://localhost:8040/hystrix.stream
zuul聚合微服务
许多场景下,外部请求需要查询 zuul后端的多个微服务。举个例子,一个电影售票手机APP,在购票订单页上,既需要查询“电影微服务"获得电影相关信息,又需要查询“用户微服务"获得当前用户的信息。如果让手机端直接请求各个微服务(即使使用 zuul进行转发),那么网络开销、流量耗费、耗费时长可能都无法令我们满意。那么对于这种场景,可使用 zuul聚合微服务请求一一一手机 APP只需发送一个请求给 zuul,由 zuul请求用户微服务以及电影微服务,并组织好数据给手机 APP,使用这种方式,手机端只须发送一次请求即可,简化了客户端侧的开发;不仅如此,由于 zuul、用户微服务、电影微服务一般都在同一个局域网中,因此速度会非常快,效率会非常高。
看实例,其实就是在网关层加一个逻辑,同时请求多个微服务
package com.cym.springcloud.controller;
import com.cym.springcloud.model.Person;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
@RestController
public class AggregationController {
@Autowired
RestTemplate restTemplate;
@GetMapping("/getPersonById/{id}")
public Map<String, Person> getPersonById(@PathVariable Long id) throws Exception {
Map<String, Person> dataMap = new HashMap<>();
Person person = restTemplate.getForObject("http://microservice-provider-user/person/getPersonById/" + id, Person.class);
Person orderPerson = restTemplate.getForObject("http://microservice-order/order/getPersonById/" + id, Person.class);
dataMap.put("person", person);
dataMap.put("orderPerson", orderPerson);
return dataMap;
}
}
然后在启动类中配置一个(RestTemplate的通信)
测试:
zuul的路由配置
前文已经编写了一个简单的 zuul网关,并让该网关代理了所有注册到 Eureka server的微服务。但在现实中可能只想让 zuul代理部分微服务,又或者需要对 URL进行更加精确的控制。
看这样子就实现指令url
- 配置忽略指定微服务,只需在application.yml里加上如下配置(如果多个用逗号隔开)
禁用之后zuul并不会代理用户微服务了
- 忽略所有的微服务,只路由指定的微服务
- 同时指定微服务的serviceId和对应路径
- 同时指定path和url
- 同时指定path和URL,并且不破坏Zuul的Hystrix、Ribbon特性。
- 为Zuul添加映射前缀1
- 为Zuul添加映射前缀2
- 忽略某些路径
- 本地转发
zuul过滤器使用及详解
过滤器(filter)是zuul的核心组件zuul大部分功能都是通过过滤器来实现的。 zuul中定义了4种标准过滤器类型,这些过滤器类型对应于请求的典型生命周期。
- PRE:这种过滤器在请求被路由之前调用。可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
- ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用 Apache HttpCIient或 Netfilx Ribbon请求微服务
- POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
- ERROR:在其他阶段发生错误时执行该过滤器。
zuul请求的生命周期如下图
编写一个过滤器
增加过滤器类<PreRequestLogFilter>,需继承抽象类<ZuulFilter>,然后实现几个抽象方法
public class PreRequestLogFilter extends ZuulFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(PreRequestLogFilter.class);
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
LOGGER.info(String.format("send %s request to %s", request.getMethod(), request.getRequestURL().toString()));
return null;
}
}
由代码可知,自定义的 zuul Filter需实现以下几个方法。
- filterType:返回过滤器的类型。有 pre、 route、 post、 error等几种取值,分别对应上文的几种过滤器。详细可以参考 com.netflix.zuul.ZuulFilter.filterType()中的注释。
- filter0rder:返回一个 int值来指定过滤器的执行顺序,不同的过滤器允许返回相同的数字。
- shouldFilter:返回一个 boolean值来判断该过滤器是否要执行, true表示执行, false表示不执行。
- run:过滤器的具体逻辑。本例中让它打印了请求的 HTTP方法以及请求的地址
运行项目访问地址:http://localhost:8040/microservice-provider-user/getIpAndPort
可以看到zuul服务的后台正常打印了run方法里的日志
禁用zuul过滤器
Spring Cloud默认为Zuul编写并启用了一些过滤器,例如DebugFilter、FormBodyWrapperFilter等,这些过滤器都存放在spring-cloud-netflix-core这个jar包里,一些场景下,想要禁用掉部分过滤器,该怎么办呢?
只需在application.yml里设置zuul.<SimpleClassName>.<filterType>.disable=true
例如,要禁用上面我们写的过滤器,这样配置就行了:
zuul.PreRequestLogFilter.pre.disable=true
zuul的容错与回退
大家可以想一下如果zuul代理的后端微服务挂了会出现什么情况?
zuul默认已经整合了hystrix,也就是zuul也是可以利用hystrix做降级容错处理的,但是zuul监控的粒度是微服务级别,而不是某个API
编写zuul的降级回退类<MyFallbackProvider>如下:
package com.cym.springcloud.fallback;
import com.netflix.hystrix.exception.HystrixTimeoutException;
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;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
@Component
public class MyFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
// 表明是为哪个微服务提供回退,*表示为所有微服务提供回退
return "*";
}
@Override
public ClientHttpResponse fallbackResponse(Throwable cause) {
if (cause instanceof HystrixTimeoutException) {
return response(HttpStatus.GATEWAY_TIMEOUT);
} else {
return this.fallbackResponse();
}
}
@Override
public ClientHttpResponse fallbackResponse() {
return this.response(HttpStatus.INTERNAL_SERVER_ERROR);
}
private ClientHttpResponse response(final HttpStatus status) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
//fallback时的状态码
return status;
}
@Override
public int getRawStatusCode() throws IOException {
//数字类型的状态码,本例返回的其实就是200,详见HttpStatus
return status.value();
}
@Override
public String getStatusText() throws IOException {
//状态文本,本例返回的其实就是OK,详见HttpStatus
return status.getReasonPhrase();
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
// 响应体
return new ByteArrayInputStream("服务不可用,请稍后再试。".getBytes());
}
@Override
public HttpHeaders getHeaders() {
// headers设定
HttpHeaders headers = new HttpHeaders();
MediaType mt = new MediaType("application", "json", Charset.forName("UTF-8"));
headers.setContentType(mt);
return headers;
}
};
}
}
关闭zuul代理的用户微服务,再运行本项目,访问地址:http://localhost:8040/microservice-provider-user/getIpAndPort,将会返回如下内容:
zuul的高可用
分两种场景讨论Zuul的高可用
1、Zuul客户端也注册到了Eureka Server上这种情况下,Zuul的高可用非常简单,只需将多个Zuul节点注册到Eureka Server上,就可实现Zuul的高可用。此时,Zuul的高可用与其他微服务的高可用没什么区别。见下图
当Zuul客户端也注册到Eureka Server上时,只需部署多个Zuul节点即可实现其高可用。Zuul客户端会自动从Eureka Server中查询Zuul Server的列表,并使用Ribbon负载均衡地请求Zuul集群。
2、Zuul客户端未注册到Eureka Server上
现实中,这种场景往往更常见,例如,Zuul客户端是一个手机APP——我们不可能让所有的手机终端都注册到Eureka Server上。这种情况下,我们可借助一个额外的负载均衡器来实现Zuul的高可用,例如Nginx、HAProxy、F5等。
Zuul客户端将请求发送到负载均衡器,负载均衡器将请求转发到其代理的其中一个Zuul节点。这样,就可以实现Zuul的高可用。