微服务网关
在微服务架构中,后端服务往往不直接开放给调用端,而是通过一个API网关根据请求的url,路由到相应的服务。当添加API网关后,在第三方调用端和服务提供方之间就创建了一面墙,这面墙直接与调用方通信进行权限控制,后将请求均衡分发给后台服务端。
为什么需要API Gateway
1. 简化客户端调用复杂度
在微服务架构模式下后端服务的实例数一般是动态的,对于客户端而言很难发现动态改变的服务实例的访问地址信息。因此在基于微服务的项目中为了简化前端的调用逻辑,通常会引入 API Gateway 作为轻量级网关,同时 API Gateway 中也会实现相关的认证逻辑从而简化内部服务之间相互调用的复杂度。
2. 数据裁剪以及聚合
通常而言不同的客户端对于显示时对于数据的需求是不一致的,比如手机端或者 Web 端又或者在低延迟的网络环境或者高延迟的网络环境。
因此为了优化客户端的使用体验,API Gateway 可以对通用性的响应数据进行裁剪以适应不同客户端的使用需求。同时还可以将多个 API 调用逻辑进行聚合,从而减少客户端的请求数,优化客户端用户体验.
3. 多渠道支持
当然我们还可以针对不同的渠道和客户端提供不同的API Gateway,对于该模式的使用由另外一个大家熟知的方式叫Backend for front-end, 在Backend for front-end模式当中,我们可以针对不同的客户端分别创建其BFF,进一步了解BFF可以参考这篇文章:Pattern: Backends For Frontends
4. 遗留系统的微服务化改造
对于系统而言进行微服务改造通常是由于原有的系统存在或多或少的问题,比如技术债务,代码质量,可维护性,可扩展性等等。API Gateway的模式同样适用于这一类遗留系统的改造,通过微服务化的改造逐步实现对原有系统中的问题的修复,从而提升对于原有业务响应力的提升。通过引入抽象层,逐步使用新的实现替换旧的实现。
在Spring Cloud体系中, Spring Cloud Zuul就是提供负载均衡、反向代理、权限认证的一个API gateway。
Spring Cloud Zuul
Spring Cloud Zuul路由是微服务架构的不可或缺的一部分,提供动态路由,监控,弹性,安全等的边缘服务。Zuul是Netflix出品的一个基于JVM路由和服务端的负载均衡器。
使用代码示例进行说明,这里直接将gateway服务化.
- 创建项目cloud-zuul-server-demo
引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
配置文件:
spring.application.name=zuul-server
server.port=8888
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
启动类添加注解:
@SpringBootApplication
@EnableZuulProxy
public class ZuulServerDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerDemoApplication.class, args);
}
}
这样zuul就搭建成功了
和前面的项目进行结合,启动前面的注册中心和eureka的两个提供者,再将zuul-server启动,这样就可以进行测试了.访问localhost:8888/eureka-service-producter/hello?name=wangzhi,这样就是通过zuul来访问提供者,并且多刷新几次,可以看到实现了负载均衡,均匀出现. 感兴趣的可以将消费者也启动一下,然后访问消费者看看会出现什么情况.额外说一下前面路径中的eureka-service-producter其实就是访问项目的spring.applicaiton.name,也就是注册中心可以看到的application,这个看清楚就没问题.
到这里,zuul网关的使用和自动转发机制就完成了,下面说说zuul相对的高级知识.
Zuul的核心
Filter是Zuul的核心,用来实现对外服务的控制。Filter的生命周期有4个,分别是“PRE”、“ROUTING”、“POST”、“ERROR”,整个生命周期可以用下图来表示。
Zuul大部分功能都是通过过滤器来实现的,这些过滤器类型对应于请求的典型生命周期。
- PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
- ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务。
- POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
- ERROR:在其他阶段发生错误时执行该过滤器。 除了默认的过滤器类型,Zuul还允许我们创建自定义的过滤器类型。例如,我们可以定制一种STATIC类型的过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务。
Zuul中默认实现的Filter
类型 | 顺序 | 过滤器 | 功能 |
pre | -3 | ServletDetectionFilter | 标记助理server的类型 |
pre | -2 | Servlet30WrapperFilter | 包装HttpServletRequest请求 |
pre | -1 | FormBodyWrapperFilter | 包装请求体 |
route | 1 | DebugFilter | 标记调试标志 |
route | 5 | PreDecorationFilter | 处理请求上下文供后续使用 |
route | 10 | RibbonRoutingFilter | serviceId请求转发 |
route | 100 | SimpleHostRoutingFilter | url请求转发 |
route | 500 | SendForwardFilter | forward请求转发 |
post | 0 | SendErrorFilter | 处理有错误的请求响应 |
post | 1000 | SendResponseFilter | 处理正常的请求响应 |
对于默认的过滤器,我们可以禁用的,在配置文件中添加(不过一般没有取消的情况):
zuul.FormBodyWrapperFilter.pre.disable=true
自定义filter
其实也很简单,继承ZuulFilter就可以了,重写四个方法,但是要了解个四个方法的返回值分别表示的是什么意思?
public class MyFilter extends ZuulFilter {
/**
* 定义filter的类型,有pre、route、post、error四种
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 定义filter的顺序,数字越小表示顺序越高,越先执行
* @return
*/
@Override
public int filterOrder() {
return 10;
}
/**
* 表示是否需要执行该filter,true表示执行,false表示不执行
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* filter需要执行的具体操作,比如说权限认证,过滤等等
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
return null;
}
}
举个例子:
public class TokenFilter extends ZuulFilter {
private final Logger logger = LoggerFactory.getLogger(TokenFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
logger.info("--->>> TokenFilter {},{}", request.getMethod(), request.getRequestURL().toString());
// 获取请求参数信息
String token = request.getParameter("token");
if (StringUtils.isNotBlank(token)) {
//对请求进行路由
context.setSendZuulResponse(true);
context.setResponseStatusCode(200);
context.set("isSuccess", true);
return null;
} else {
//不对其进行路由
context.setSendZuulResponse(false);
context.setResponseStatusCode(400);
context.setResponseBody("token is empty");
context.set("isSuccess", false);
return null;
}
}
}
要想在过滤器中起作用,还需要额外配置一个,在启动类中添加过滤器的Bean
@Bean
public TokenFilter tokenFilter(){
return new TokenFilter();
}
启动注册中心,提供者和修改后的zuul,这次访问的时候就需要在路径后面加上token=xx,否则请求会被拦截.“PRE”类型的Filter做很多的验证工作,在实际使用中我们可以结合shiro、oauth2.0等技术去做鉴权、验证。
路由熔断
我们的后端服务出现异常的时候,我们不希望将异常抛出给最外层,期望服务可以自动进行一降级。Zuul给我们提供了这样的支持。当某个服务出现异常时,直接返回我们预设的信息。
我们通过自定义的fallback方法,并且将其指定给某个route来实现该route访问出问题的熔断处理。主要继承ZuulFallbackProvider接口来实现,ZuulFallbackProvider默认有两个方法,一个用来指明熔断拦截哪个服务,一个定制返回内容。
public interface ZuulFallbackProvider {
/**
* The route this fallback will be used for.
* @return The route the fallback will be used for.
*/
public String getRoute();
/**
* Provides a fallback response.
* @return The fallback response.
*/
public ClientHttpResponse fallbackResponse();
}
实现类通过实现getRoute方法,告诉Zuul它是负责哪个route定义的熔断。而fallbackResponse方法则是告诉 Zuul 断路出现时,它会提供一个什么返回值来处理请求。后来Spring又扩展了此类,丰富了返回方式,在返回的内容中添加了异常信息,因此最新版本建议直接继承类FallbackProvider。
案例:修改zuul-server-demo
创建HelloFallback
@Component
public class HelloFallback implements FallbackProvider {
private final Logger logger = LoggerFactory.getLogger(FallbackProvider.class);
/**
* 返回指定要处理的 service。
* @return
*/
@Override
public String getRoute() {
return "eureka-service-producter";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
if (cause != null && cause.getCause() != null) {
String reason = cause.getCause().getMessage();
logger.info("Excption {}",reason);
}
return fallbackResponse();
}
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
@Override
public String getStatusText() throws IOException {
return "OK";
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("The service is unavailable.".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
进行测试,启动注册中心,两个提供者,zuul. 输入http://localhost:8888/eureka-service-producter/hello?name=wangzhi&token=wangzhi进行访问,多刷新几次可以看到没有问题,这个时候将一个提供者关闭,再次刷新可以看到问题就出来了,返回The service is unavailable. 这个就是zuul的路由熔断. 但是注意的是: ** Zuul 目前只支持服务级别的熔断,不支持具体到某个URL进行熔断。**
路由重试
有时候因为网络或者其它原因,服务可能会暂时的不可用,这个时候我们希望可以再次对服务进行重试,Zuul也帮我们实现了此功能,需要结合Spring Retry 一起来实现。下面我们以上面的项目为例做演示。
- 改动zuul项目
添加依赖:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
添加配置:
#是否开启重试功能
zuul.retryable=true
#对当前服务的重试次数
ribbon.MaxAutoRetries=2
#切换相同Server的次数
ribbon.MaxAutoRetriesNextServer=0
- 改动一个提供者项目
修改controller方法就好,只修改一个项目的就好
private Logger logger = Logger.getLogger(HelloController.class);
@GetMapping("hello")
public String hello(@RequestParam String name){
logger.info("request two name is "+name);
try{
Thread.sleep(1000000);
}catch ( Exception e){
logger.error(" hello two error",e);
}
return "hello," + name + "222222";
}
这次访问,虽然还会返回The service is unavailable.但是可以在改动的提供者项目的控制台看到日志打印了3遍,说明重试了两次.
注意
开启重试在某些情况下是有问题的,比如当压力过大,一个实例停止响应时,路由将流量转到另一个实例,很有可能导致最终所有的实例全被压垮。说到底,断路器的其中一个作用就是防止故障或者压力扩散。用了retry,断路器就只有在该服务的所有实例都无法运作的情况下才能起作用。这种时候,断路器的形式更像是提供一种友好的错误信息,或者假装服务正常运行的假象给使用者。
不用retry,仅使用负载均衡和熔断,就必须考虑到是否能够接受单个服务实例关闭和eureka刷新服务列表之间带来的短时间的熔断。如果可以接受,就无需使用retry。
对于微服务来说,zuul也是很重要的,所以在必要情况下应该实现高可用,也就是搭建集群,和前面搭建集群一样道理没有什么区别.为了保证Zuul的高可用性,前端可以同时启动多个Zuul实例进行负载,在Zuul的前端使用Nginx或者F5进行负载转发以达到高可用性。
上面就是zuul的全部内容的,以后有需要再进行补充.