目录
- Zuul服务网关☣
- 一、什么是网关?
- 二、Zuul实现API网关
- ① 添加子模块zuul-server
- ② 配置路由
- ③ 服务名称路由
- ④ 简化路由
- 三、网关过滤器
- ① 入门Demo
- ② 统一鉴权
- ③ 网关过滤器异常统一处理
- ④ Zuul请求的声明周期
- 四、Zuul 和 Hystrix 无缝结合
- ① 准备环境
- ② 网关熔断
- ③ 网关限流
前提知识点总结(不属于正文)
一、什么是网关?
API Gateway(APIGW / API 网关),顾名思义,是出现在系统边界上的一个面向 API 的、串行集中式的强管控服务,这里的边界是企业 IT 系统的边界,可以理解为企业级应用防火墙,主要起到隔离外部访问与内部系统的作用。在微服务概念的流行之前,API 网关就已经诞生了,例如银行、证券等领域常见的前置机系统,它也是解决访问认证、报文转换、访问统计等问题的。(网关是用户访问系统的第一关,网关里记录者注册中心中的所有的服务地址,通过网关进行认证,请求分发)
- Nginx 适合做门户网关,是作为整个全局的网关,对外的处于最外层的那种;而 Zuul 属于业务网关,主要用来对应不同的客户端提供服务,用于聚合业务。各个微服务独立部署,职责单一,对外提供服务的时候需要有一个东西把业务聚合起来。
- Zuul 可以实现熔断、重试等功能,这是 Nginx 不具备的。
二、Zuul实现API网关
官网文档:
准备一个聚合项目,一个父项目内两个注册中心,一个消费者,一个服务提供者,这里不惜介绍前面文章有提,启动项目测试看一下服务是否注册到注册中心:
① 添加子模块zuul-server
创建一个java项目,引入依赖和配置即可
4.0.0 com.yjxxt zuul-server 1.0-SNAPSHOT com.yjxxt zuul-demo 1.0-SNAPSHOT org.springframework.cloud spring-cloud-starter-netflix-zuul
application.yml配置
server:
port: 9000 # 端口
spring:
application:
name: zuul-server # 应用名称
启动类
@SpringBootApplication
// 开启 Zuul 注解
@EnableZuulProxy
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
}
② 配置路由这里zuul模块建立完毕
配置路由后平时的服务请求假设:http://localhost:7070/order/10,配置路由后:http://localhost:7070/product-service/order/10,这样指定某服务下的接口
application.yml配置
# 路由规则
zuul:
routes:
product-service: # 路由 id 自定义
path: /product-service/** # 配置请求 url 的映射路径
url: http://localhost:7070/ # 映射路径对应的微服务地址
启动测试:
弊端就是每次更换服务都需要手动修改
③ 服务名称路由
- 添加 Eureka Client 依赖
org.springframework.cloud spring-cloud-starter-netflix-eureka-client
- 配置注册中心和路由规则
# 路由规则
zuul:
routes:
product-service: # 路由 id 自定义
path: /product-service/** # 配置请求 url 的映射路径
serviceId: product-service # 根据 serviceId 自动从注册中心获取服务地址并转发请求
# 配置 Eureka Server 注册中心
eureka:
instance:
prefer-ip-address: true # 是否使用 ip 地址注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
client:
service-url: # 设置服务注册中心地址
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
- 启动类
package com.yjxxt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
// 开启 Zuul 注解
@EnableZuulProxy
// 开启 EurekaClient 注解,目前版本如果配置了 Eureka 注册中心,默认会开启该注解
//@EnableEurekaClient
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
}
④ 简化路由
Zuul 为了方便大家使用,提供了默认路由配置:路由 id 和 微服务名称 一致,path 默认对应 /微服务名称 @Component public class CustomFilter extends ZuulFilter { private static final Logger logger = LoggerFactory.getLogger(CustomFilter.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 rc = RequestContext.getCurrentContext(); HttpServletRequest request = rc.getRequest(); logger.warn("CustomFilter...method={}, url={}", request.getMethod(), request.getRequestURL().toString()); return null; } }
测试http://localhost:9000/product-service/product/521
请求日志的打印
② 统一鉴权类似于认证,即token,否则无权限访问服务
修改过滤器:
@Override public Object run() throws ZuulException { // 获取请求上下文 RequestContext rc = RequestContext.getCurrentContext(); HttpServletRequest request = rc.getRequest(); // 获取表单中的 token String token = request.getParameter("token"); // 业务逻辑处理 if (null == token) { logger.warn("token is null..."); // 请求结束,不在继续向下请求。 rc.setSendZuulResponse(false); // 响应状态码,HTTP 401 错误代表用户没有访问权限 rc.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value()); // 响应类型 rc.getResponse().setContentType("application/json; charset=utf-8"); PrintWriter writer = null; try { writer = rc.getResponse().getWriter(); // 响应内容 writer.print("{"message":"" + HttpStatus.UNAUTHORIZED.getReasonPhrase() + ""}"); } catch (IOException e) { e.printStackTrace(); } finally { if (null != writer) writer.close(); } } else { // 使用 token 进行身份验证 logger.info("token is OK!"); } return null; }测试http://localhost:9000/product-service/product/521
③ 网关过滤器异常统一处理修改过滤器:
@Override public Object run() throws ZuulException { RequestContext rc = RequestContext.getCurrentContext(); Throwable throwable = rc.getThrowable(); logger.error("ErrorFilter..." + throwable.getCause().getMessage(), throwable); // 响应状态码,HTTP 500 服务器错误 rc.setResponseStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value()); // 响应类型 rc.getResponse().setContentType("application/json; charset=utf-8"); PrintWriter writer = null; try { writer = rc.getResponse().getWriter(); // 响应内容 writer.print("{"message":"" + HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase() + ""}"); } catch (IOException e) { e.printStackTrace(); } finally { if (null != writer) writer.close(); } return null; }制造一个异常,添加一个类AccessFilter.java(把认证过滤器提出一个类出来)
@Component public class AccessFilter extends ZuulFilter { private static final Logger logger = LoggerFactory.getLogger(AccessFilter.class); @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 1; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { // 模拟异常 Integer.parseInt("zuul"); // 获取请求上下文 RequestContext rc = RequestContext.getCurrentContext(); HttpServletRequest request = rc.getRequest(); // 获取表单中的 token String token = request.getParameter("token"); // 业务逻辑处理 if (null == token) { logger.warn("token is null..."); // 请求结束,不在继续向下请求。 rc.setSendZuulResponse(false); // 响应状态码,HTTP 401 错误代表用户没有访问权限 rc.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value()); // 响应类型 rc.getResponse().setContentType("application/json; charset=utf-8"); PrintWriter writer = null; try { writer = rc.getResponse().getWriter(); // 响应内容 writer.print("{"message":"" + HttpStatus.UNAUTHORIZED.getReasonPhrase() + ""}"); } catch (IOException e) { e.printStackTrace(); } finally { if (null != writer) writer.close(); } } else { // 使用 token 进行身份验证 logger.info("token is OK!"); } return null; } }添加异常处理过滤器
@Component public class ErrorFilter extends ZuulFilter { private static final Logger logger = LoggerFactory.getLogger(ErrorFilter.class); @Override public String filterType() { return "error"; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { RequestContext rc = RequestContext.getCurrentContext(); ZuulException exception = this.findZuulException(rc.getThrowable()); logger.error("ErrorFilter..." + exception.errorCause, exception); HttpStatus httpStatus = null; if (429 == exception.nStatusCode) httpStatus = HttpStatus.TOO_MANY_REQUESTS; if (500 == exception.nStatusCode) httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; // 响应状态码 rc.setResponseStatusCode(httpStatus.value()); // 响应类型 rc.getResponse().setContentType("application/json; charset=utf-8"); PrintWriter writer = null; try { writer = rc.getResponse().getWriter(); // 响应内容 writer.print("{"message":"" + httpStatus.getReasonPhrase() + ""}"); } catch (IOException e) { e.printStackTrace(); } finally { if (null != writer) writer.close(); } return null; } private ZuulException findZuulException(Throwable throwable) { if (throwable.getCause() instanceof ZuulRuntimeException) return (ZuulException) throwable.getCause().getCause(); if (throwable.getCause() instanceof ZuulException) return (ZuulException) throwable.getCause(); if (throwable instanceof ZuulException) return (ZuulException) throwable; return new ZuulException(throwable, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null); } }关闭自带的异常处理
zuul: # 禁用 Zuul 默认的异常处理 filter SendErrorFilter: error: disable: true结果测试:
④ Zuul请求的声明周期
- HTTP 发送请求到 Zuul 网关
- Zuul 网关首先经过 pre filter
- 验证通过后进入 routing filter,接着将请求转发给远程服务,远程服务执行完返回结果,如果出错,则执行 error filter
- 继续往下执行 post filter
- 最后返回响应给 HTTP 客户端
四、Zuul 和 Hystrix 无缝结合① 准备环境想要实现网络监控,就要配置hystrix,Zuul 的依赖中包含了 Hystrix 的相关 jar 包,所以我们不需要在项目中额外添加 Hystrix 的依赖。但是需要开启数据监控的项目中要添加 dashboard 依赖。
# 引入依赖org.springframework.cloud spring-cloud-starter-netflix-hystrix-dashboard # 配置 # 度量指标监控与健康检查 management: endpoints: web: exposure: include: hystrix.stream启动类// 开启 Zuul 注解@EnableZuulProxy
启动项目测试访问:http://localhost:9000/hystrix
监听服务地址http://localhost:9000/actuator/hystrix.stream
② 网关熔断在 Edgware 版本之前,Zuul 提供了接口 ZuulFallbackProvider 用于实现 fallback 处理。从 Edgware 版本开始,Zuul 提供了接口 FallbackProvider 来提供 fallback 处理。编写实现类:ProductProviderFallback(熔断只有在服务宕机的时候没有任何反馈才会熔断,所有测试时挂掉服务即可)
@Component public class ProductProviderFallback implements FallbackProvider { @Override public String getRoute() { return "product-service"; } @Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) { return new ClientHttpResponse() { @Override public HttpHeaders getHeaders() { HttpHeaders header = new HttpHeaders(); header.setContentType(new MediaType("application", "json", Charset.forName("utf-8"))); return header; } @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream("{"message":"商品服务不可用,请稍后再试。"}".getBytes()); } @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.INTERNAL_SERVER_ERROR; } @Override public int getRawStatusCode() throws IOException { return this.getStatusCode().value(); } @Override public String getStatusText() throws IOException { return this.getStatusCode().getReasonPhrase(); } @Override public void close() { } }; } }一定是服务宕机后刷新才会出现:
③ 网关限流就是在指定的时间内规定多少请求,一旦超标,则不接受请求,多于请求直接返回,限流算法:三种
- 计数器算法
- 漏桶(Leaky Bucket)算法
- 令牌桶(Token Bucket)算法
计数器
设置一个计数器,一分钟比如允许100个请求,所以计数器一分钟之内没满一百则重新计数。缺点就是恶意在59s100个请求,1分钟又100个导致压垮服务,这是一个漏洞,而且容易闲置
漏桶算法
就是用一个水桶下面一个洞,水桶去存放大量请求,再通过小洞一个一个处理请求,一旦请求大于水桶,则直接fallback,类似消息队列
令牌桶算法
一个桶里存放令牌,请求会去拿令牌,令牌会一直生成,多余令牌直接舍弃,请求拿到令牌才能去访问资源,拿到令牌的请求会被执行,否则fallback或者缓存
代码实现:
// Zuul 的限流保护需要额外依赖 spring-cloud-zuul-ratelimit 组件,限流数据采用 Redis 存储所以还要添加 Redis 组件。 //[RateLimit 官网文档]:https://github.com/marcosbarbero/spring-cloud-zuul-ratelimitcom.marcosbarbero.cloud spring-cloud-zuul-ratelimit 2.3.0.RELEASE org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2
全局限流配置
使用全局限流配置,Zuul 会对代理的所有服务提供限流保护(记得开启redis缓存数据库)。server: port: 9000 # 端口 spring: application: name: zuul-server # 应用名称 # redis 缓存 redis: host: 192.168.10.106 # Redis服务器地址 # 配置 Eureka Server 注册中心 eureka: instance: prefer-ip-address: true # 是否使用 ip 地址注册 instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port client: service-url: # 设置服务注册中心地址 defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/ zuul: # 服务限流 ratelimit: # 开启限流保护 enabled: true # 限流数据存储方式 repository: REDIS # default-policy-list 默认配置,全局生效 default-policy-list: - limit: 3 refresh-interval: 60 # 60s 内请求超过 3 次,服务端就抛出异常,60s 后可以恢复正常请求 type: - origin - url - user
启动测试
请求数超过我们设定的阈值,所以返回请求太多,后面还有局部限流,自定义限流,网关调优,Zuul 和 Sentinel 整合等,内容很多这里就先到这。



