1. Zuul原理和配置
1.1 基本原理
网关(Zuul) —— 本质核心就是过滤器 ( 就这样一句话其实就够了)
这些过滤器可以完成以下功能:
- 身份认证与安全:识别每个资源的验证要求,并拒绝那些与要求不符的请求。
- 审查与监控:在边缘位置追踪有意义的数据和统计结果,从而带来精确的生成视图。
- 动态路由:动态地将请求路由到不同的后端集群。
- 压力测试:逐渐增加执行集群的流量,以了解性能。
- 负载分配:为每一种负载类型分配对应容量,并弃用超出限定值得请求。
- 静态响应处理:在边缘位置直接建立部分响应,从而避免其转发到内部集群。
- 多区域弹性:跨越AWS Region进行请求路由,旨在实现ELB(Elastic Load Balancing)使用的多样化,以及让系统的边缘更贴近系统的使用者。
- 在实现了请求路由功能后,我们的微服务应用提供的接口就可以通过统一的API网关入口被客户端访问到了。但是,每个客户端用户请求为服务器应用提供的接口时,它们的访问权限往往都有一定的限制,系统并不会将所有的微服务接口都对它们开放。
在完成了服务路由之后,我们对外开放服务还需要一些安全措施来保护客户端只能访问它应该访问到的资源。所以我们需要利用Zuul的过滤器来实现我们对外服务的安全控制。
在服务网关中定义过滤器只需要继承ZuulFilter抽象类实现其定义的四个抽象函数就可对请求进行拦截与过滤。
1.2 4种不同的过滤器的说明
Zuul中的过滤器根据不同的生命周期阶段分为4种类型的过滤器,分别为: pre、route、error、post。从一个请求通过网关情况可以进行做出他的顺序图如下:
- PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
- ROUTING: 这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务。
- POST: 这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
- ERROR: 在其他阶段发生错误时执行该过滤器。
除了默认的过滤器类型,Zuul还允许我们创建自定义的过滤器类型。例如,我们可以定制一种STATIC类型的过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务。
在完成了pre类型的过滤器处理之后,请求进入第二个阶段routing,也就是之前说的路由请求转发阶段,请求将会被routing类型过滤器处理,这里的具体处理内容就是将外部请求转发到具体服务实例上去的过程,当服务实例将请求结果都返回之后,routing阶段完成,请求进入第三个阶段post,此时请求将会被post类型的过滤器进行处理,这些过滤器在处理的时候不仅可以获取到请求信息,还能获取到服务实例的返回信息,所以在post类型的过滤器中,我们可以对处理结果进行一些加工或转换等内容。
另外,还有一个特殊的阶段error,该阶段只有在上述三个阶段中发生异常的时候才会触发,但是它的最后流向还是post类型的过滤器,因为它需要通过post过滤器将最终结果返回给请求客户端
1.3 核心默认过滤器
在Spring Cloud Zuul中,为了让API网关组件可以更方便的上手使用,它在HTTP请求生命周期的各个阶段默认地实现了一批核心过滤器,它们会在API网关服务启动的时候被自动地加载和启用。我们可以在源码中查看和了解它们,它们定义于spring-cloud-netflix-core模块的org.springframework.cloud.netflix.zuul.filters包下。
类型 | 顺序 | 过滤器 | 功能 |
---|---|---|---|
pre | -3 | ServletDetectionFilter | 标记处理Servlet的类型 |
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 | 处理正常的请求响应 |
禁用指定的Filter
可以在application.yml中配置需要禁用的filter,格式:
zuul:
FormBodyWrapperFilter:
pre:
disable: true
1.4 过滤器的基本代码结构
我们使用的过滤器都必须继承com.netflix.zuul.ZuulFilter。然后分别实现以下四个方法:
- filterType : 设定过滤器的类型(4种选择)
- filterOrder : 该过滤器在这种类型过滤器中的执行排序
- shouldFilter : 是否执行该过滤器的业务逻辑
- run : 该过滤器的业务逻辑
以下代码是1个过滤器的基本结构:
package com.haokeed.cloudzuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
/**
* 该拦截器是一个demo
*/
@Component
public class DemoFilter extends ZuulFilter {
/**
* 拦截类型,4种类型 pre route error post
*
* @return
*/
@Override
public String filterType() {
// FilterConstants.PRE_TYPE;
// FilterConstants.ROUTE_TYPE;
// FilterConstants.ERROR_TYPE;
// FilterConstants.POST_TYPE;
return FilterConstants.PRE_TYPE;
}
/**
* 该过滤器在所有过滤器的执行顺序值
* 值越小,越前面执行
*
* @return
*/
@Override
public int filterOrder() {
return 0;
}
/**
* 该过滤器是否生效
* 返回true执行该过滤器,返回false不执行该过滤器
*
* @return
*/
@Override
public boolean shouldFilter() {
return true;// 返回true 表示执行该拦截器
}
/**
* 过滤器具体的业务逻辑
*
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
// 该过滤器执行的业务逻辑
return null;
}
}
1.5 相关路由配置
1.5.1 基础
默认啥都不配的情况下,默认的网关采用的规则:
http://网关IP:端口号/目标服务名(如api-driver)/目标服务中对应的URI
application.yml
# 基础
# 演示功能:
# 1、默认。
# 2、网关负载均衡。
# 3、路由端点
#api-driver:
# ribbon:
# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
server:
# 本服务的端口号
port: 9100
spring:
application:
# 服务名或服务ID
name: online-taxi-zuul
#注册中心
eureka:
client:
#设置服务注册中心的URL
service-url:
defaultZone: http://root:root@eureka-7900:7900/eureka/
instance:
# 对应调用本服务的主机名或主机地址
hostname: localhost
# 向eureka注册的服务名或服务ID
instance-id: online-taxi-zuul
# 管理(监控)
# 说明文章:https://www.jianshu.com/p/9284f6e3985c
management:
endpoints:
web:
# 开启暴露所有的端点信息
exposure:
# 加载所有的端点,默认只加载了info、health
include: "*"
endpoint:
health:
# 显示健康具体信息,默认(never)不会显示详细信息。
enabled: true
routes:
enabled: true # 开启路由监控
#server:
#prot: 8088 # 更改监控的端口号
logging:
level:
com.netflix: debug # com.netflix包下面的,日志等级配置
org.springframework: DEBUG # com.netflix包下面的日志等级配置
1.5.2 更改服务名/自定义服务名
application.yml
# 更改服务名
#api-driver:
# ribbon:
# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
server:
# 本服务的端口号
port: 9100
spring:
application:
# 服务名或服务ID
name: online-taxi-zuul
#注册中心
eureka:
client:
#设置服务注册中心的URL
service-url:
defaultZone: http://root:root@eureka-7900:7900/eureka/
instance:
# 对应调用本服务的主机名或主机地址
hostname: localhost
# 向eureka注册的服务名或服务ID
instance-id: online-taxi-zuul
zuul:
routes:
# 此处名字随便取,只是作为多组的区分
# 当前样例配置情况为.访问zuul_host/zuul-custom-name/test/sms-test2相当于访问service-sms服务下的/test/sms-test2
# http://zuul_host/zuul-custom-name/test/sms-test2 等同于默认情况下的 http://zuul_host/service-sms/test/sms-test2
custom-zuul-name:
path: /zuul-custom-name/**
service-id: service-sms
custom-zuul-name2:
path: /zuul-custom-name2/**
url: http://localhost:9002/ # 这样的情况下,自定义的负载均衡就失效了
custom-zuul-name3: # 负载均衡有效写法
path: /zuul-custom-name3/**
service-id: service-sms-abc # 这里就不是eureka中的server-id了,可以随便取名字
custom-zuul-name4: # 自己转发自己的
path: /a-forward/**
url: forward:/myController # 访问zuul_host/a-forward 相当于访问了 zuul_host/myController
service-sms: /zuul-api-driver/** # zuul_host/zuul-api-driver/test/sms-test 相当于访问了 zuul_host/service-sms/test/sms-test 注意:如果后面是一个* 表只访问1层,所以zuul_host/zuul-api-driver/或zuul_host/zuul-api-driver/test是能有效访问到servie-sms的,zuul_host/zuul-api-driver/test/sms-test 是访问不过去的
service-sms-abc:
ribbon:
listOfServers: localhost:9002,localhost:9003
ribbon:
eureka:
enabled: false
# 管理(监控)
# 说明文章:https://www.jianshu.com/p/9284f6e3985c
management:
endpoints:
web:
# 开启暴露所有的端点信息
exposure:
# 加载所有的端点,默认只加载了info、health
include: "*"
endpoint:
health:
# 显示健康具体信息,默认(never)不会显示详细信息。
enabled: true
routes:
enabled: true # 开启路由监控
#server:
#prot: 8088 # 更改监控的端口号
logging:
level:
com.netflix: debug # com.netflix包下面的,日志等级配置
org.springframework: DEBUG # com.netflix包下面的日志等级配置
1.5.3 忽略/服务名配置
application.yml
# 更改服务名
#api-driver:
# ribbon:
# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
server:
# 本服务的端口号
port: 9100
spring:
application:
# 服务名或服务ID
name: online-taxi-zuul
#注册中心
eureka:
client:
#设置服务注册中心的URL
service-url:
defaultZone: http://root:root@eureka-7900:7900/eureka/
instance:
# 对应调用本服务的主机名或主机地址
hostname: localhost
# 向eureka注册的服务名或服务ID
instance-id: online-taxi-zuul
zuul:
routes:
api-passenger: /zuul-api-passenger/** # 此处采用了服务名配置,直接前面一个服务名,后面就是更改后的映射。
api-driver: /zuul-api-driver/**
ignored-services:
- api-driver # 加上后,原本http://zuul_host/zuul-api-driver/**和http://zuul_host/api-driver/**都能访问的,现在只支持http://zuul_host/zuul-api-driver/**的方式访问
# ignored-patterns:
# - /*-driver/**
zuul:
routes:
# 请求 http://zuul_host/api/zuul-custom-name/test/sms-test2 相当于默认的 http://zuul_host/service-sms/test/sms-test2
prefix: /api # 前缀
# 是否移除前缀
strip-prefix: true # 实际请求时是否移除前缀,相当于前缀就是看看用的
custom-zuul-name: # 看前面更改服务名说明
path: /zuul-custom-name/**
service-id: service-sms
# 管理(监控)
# 说明文章:https://www.jianshu.com/p/9284f6e3985c
management:
endpoints:
web:
# 开启暴露所有的端点信息
exposure:
# 加载所有的端点,默认只加载了info、health
include: "*"
endpoint:
health:
# 显示健康具体信息,默认(never)不会显示详细信息。
enabled: true
routes:
enabled: true # 开启路由监控
#server:
#prot: 8088 # 更改监控的端口号
logging:
level:
com.netflix: debug # com.netflix包下面的,日志等级配置
org.springframework: DEBUG # com.netflix包下面的日志等级配置
2. 不同过滤器的样例代码
2.1 登录鉴权过滤器
package com.haokeed.cloudzuul.filter;
import com.haokeed.internalcommon.constant.RedisKeyPrefixConstant;
import com.haokeed.internalcommon.util.JwtInfo;
import com.haokeed.internalcommon.util.JwtUtil;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* 该拦截器是一个demo
* 主要展示了鉴权在拦截器种的功能
*/
//@Component
public class DemoFilter extends ZuulFilter {
/**
* redis连接
*/
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 拦截类型,4种类型 pre route error post
*
* @return
*/
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
/**
* 该过滤器在所有过滤器的执行顺序值
* 值越小,越前面执行
*
* @return
*/
@Override
public int filterOrder() {
return 0;
}
/**
* 该过滤器是否生效
* 返回true执行该过滤器,返回false不执行该过滤器
*
* @return
*/
@Override
public boolean shouldFilter() {
//获取上下文
RequestContext requestContext = RequestContext.getCurrentContext();
if (!requestContext.sendZuulResponse()) {// 建议用这种方式
return false;// 返回false 表示不执行该拦截器
}
// 另外一种方式,不让执行该拦截器
// boolean ifContinue = (boolean) requestContext.get("ifContinue");
// if (ifContinue){
// return true;
// }else {
// return false;
// }
return true;// 返回true 表示执行该拦截器
}
/**
* 过滤器具体的业务逻辑
*
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
// 这里样例是一个鉴权拦截器
//获取上下文(重要,贯穿 所有filter,包含所有参数)
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
String token = request.getHeader("Authorization");
if (StringUtils.isNotBlank(token)) {
JwtInfo tokenJwtInfo = JwtUtil.parseToken(token);
if (null != tokenJwtInfo) {
String tokenUserId = tokenJwtInfo.getSubject();
Long tokenIssueDate = tokenJwtInfo.getIssueDate();
System.out.println("UID:" + tokenUserId);
System.out.println("tokenIssueDate:" + tokenIssueDate);
BoundValueOperations<String, String> stringStringBoundValueOperations = redisTemplate.boundValueOps(RedisKeyPrefixConstant.PASSENGER_LOGIN_TOKEN_APP_KEY_PRE + tokenUserId);
String redisToken = stringStringBoundValueOperations.get();
if (redisToken.equals(token)) {
System.out.println("鉴权成功,进入下一个阶段");
return null;
}
}
}
// 不往下走,还走剩下的过滤器,但是不向后面的服务转发。
requestContext.setSendZuulResponse(false);// 这里不让他过 里面设置false就可以
requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
requestContext.setResponseBody("auth fail");
// 设置全局 其他过滤器should中出现该值是false就后面不执行了
// requestContext.set("ifContinue",false);
return null;
}
}
2.2 灰度发布过滤器
这里不再多说:请看我写过的 >> 查看
2.2.1 快速方式(ribbon)——方式
需要引入ribbon
<!-- 实现通过 metadata 进行灰度路由 -->
<dependency>
<groupId>io.jmnarloch</groupId>
<artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
<version>2.1.0</version>
</dependency>
业务代码
package com.haokeed.cloudzuul.filter;
import com.haokeed.cloudzuul.dao.CommonGrayRuleDaoCustom;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import io.jmnarloch.spring.cloud.ribbon.support.RibbonFilterContextHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* 灰度filter
* 这个要结合eureka的处理,请看文章 https://blog.csdn.net/as_hswa/article/details/120135861?spm=1001.2014.3001.5502
* @date 2020/06/29
*/
@Component
public class GrayFilter extends ZuulFilter {
@Override
public String filterType() {
// 使用路由规则
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
// 打开路由
return false;
}
@Autowired
private CommonGrayRuleDaoCustom commonGrayRuleDaoCustom;
@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
int userId = Integer.parseInt(request.getHeader("userId"));
// 根据用户id 查 规则 查库 v1,meata
String userVersion="v1";
// 根据userId查询出来的version字段的值
// if("v1".equals(userVersion)){
//
// }
// 伪代码用userId直接来判断版本好了
// 金丝雀
if (userId == 1){
RibbonFilterContextHolder.getCurrentContext().add("version","v1");
// 普通用户
}else if (userId == 2){
RibbonFilterContextHolder.getCurrentContext().add("version","v2");
}
return null;
}
}
2.3 微服务路由劫持过滤器
该过滤器一般用于自定义的路由规则,相当适合老项目改造时,保持url不变的情况。
2.3.1 传统host请求方式
这种方式如果全部自定义进行路由改造,那配置文件中得添加:
zuul:
routes:
# 全部自定义路由得加这个,要不然实测过程中发现对于当层URI他是不走过滤器的,比如:http://host_zuul/test。知不道为啥,可能需要哪里配置的吧,可我最后没找到原因,也没时间去翻源码。先记录
all:
path: /**
package com.haokeed.cloudzuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
@Component
public class HostFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
System.out.println("HostFilter-shouldFilter");
return true;
}
@Override
public Object run() throws ZuulException {
System.out.println("HostFilter-run");
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// 目标URI
String remoteURI = request.getRequestURI();
if ("/sms-test31".equals(remoteURI)) {
try {
ctx.setRouteHost(new URI("http://service-sms:8083/test/sms-test3").toURL());
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
return null;
}
}
2.3.2 走注册中心的方式
package com.haokeed.cloudzuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class RibbonFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
// 获取请求URI
String remoteAddr = request.getRequestURI();
System.out.println(remoteAddr);
// 和老地址做匹配
if (remoteAddr.contains("/sms-test31")) {
// remoteAddr = newAddr
// 这个要设置,很多地址都没去写这个信息
currentContext.set(FilterConstants.SERVICE_ID_KEY, "service-sms");// 对应的这个服务在注册中心(eureka)的服务ID
currentContext.set(FilterConstants.RETRYABLE_KEY, "/test/sms-test3");// 对应的提供的服务请求的RUI
}
return null;
}
}
2.4 IP请求限制
package com.haokeed.cloudzuul.filter;
import com.haokeed.internalcommon.constant.RedisKeyPrefixConstant;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.bouncycastle.cert.ocsp.Req;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* IP限制
*/
@Component
public class IpLimitFilter extends ZuulFilter {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
// 实际生产中有个小技巧: db中存储 过滤器开关管
// String s=redisTemplate.opsForValue().get("test-filter");
// if(s.trim().equals("1")){
// return true;
// }
// URI地址过滤
// 可以获取里面的userId之类的 都用户请求URL的限制
// String url= RequestContext.getCurrentContext().getRequest().getRequestURI();
// if(url.equals("数据库中的url")){
// return true;
// }
return RequestContext.getCurrentContext().sendZuulResponse();
}
@Override
public Object run() throws ZuulException {
// IP 的限制
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
String ipAddr = getIpAddr(request);
// redis的过期时间来处理 ip:次数
BoundValueOperations<String, String> stringStringBoundValueOperations = redisTemplate.boundValueOps(ipAddr);
String sNum = stringStringBoundValueOperations.get();
Integer iNum = Integer.parseInt(sNum);
if (iNum >= 10) {
currentContext.setResponseStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
currentContext.setResponseBody("请求次错误");
// sendZuulResponse 这个设置只是控制不向下一个某些默认过滤器传递,
// 具体看默认过滤器中是否有shouldFilter->sendZuulResponse()的判断在里面
currentContext.setSendZuulResponse(false);// 不向后面的rout过滤器转发
}
return null;
}
private String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (null == ip || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (null == ip || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (null == ip || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_Client_IP");
}
if (null == ip || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (null == ip || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
2.5 限流
限流实现的方式就采用令牌桶的方式。原理就是一个定时器,定时生成一定量的令牌数据,然后往令牌桶里面丢,这边拿取令牌桶中的令牌一次,就允许请求一次,如果令牌桶中拿不到数据了,那就超过最大负荷了,就限流。原理图如下:
2.5.1 自己实现
package com.haokeed.cloudzuul.filter;
import com.google.common.util.concurrent.RateLimiter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
/**
* 限流过滤器
*/
@Component
public class LimitFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
// 限流应该是所有过滤器的最前面,最为第一道工具,所以该值越小越好
return -10;
}
@Override
public boolean shouldFilter() {
return true;
}
// gova的东西 相当于限流中的令牌生成器
// create(50) 表示每秒生成50个令牌
// create(0.1) 表示10秒生成1个(1秒生成0.1个)
// 2 qps(1秒 2个 请求 Query Per Second 每秒查询量)
private static final RateLimiter RATE_LIMITER = RateLimiter.create(50);
@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
if (RATE_LIMITER.tryAcquire()) {
// RATE_LIMITER.tryAcquire(3); // 1下拿几个令牌
// RATE_LIMITER.acquire();// acquire()属于阻塞,就是一直在这里等待着,tryAcquire()非阻塞,但可以设置等待时间,请自行尝试。
System.out.println("通过");
return null;
} else {
// 被流控的逻辑
System.out.println("被限流了");
// 可以设置limit值,后面所有的过滤器的shouldFilter中都监控该值,如过为false,就都不执行
currentContext.set("limit", false);
/**
* 所有其他过滤器中shouldFilter中添加的Limit的判断
* Object limit = RequestContext.getCurrentContext().get("limit");
* if (null != limit && false == (Boolean) limit) {
* return false;
* }
*/
currentContext.setSendZuulResponse(false);
currentContext.setResponseStatusCode(HttpStatus.TOO_MANY_REQUESTS.value());
}
return null;
}
}
2.5.2 服务限流
这里的服务限流,就是非网关服务的限流。因为实际情况中,每个微服务都是有请求上限的;所以,服务自己也要进行限流,反正别人过度调用自己的服务,被搞崩溃。
这里是一个短信服务的限流:
package com.haokeed.servicesms.filter;
import com.google.common.util.concurrent.RateLimiter;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 服务限流
* @date
*/
@Component
public class LimitFilter implements Filter {
// 2=每秒2个;0.1 = 10秒1个
private static final RateLimiter RATE_LIMITER = RateLimiter.create(1);
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 限流的业务逻辑
if (RATE_LIMITER.tryAcquire()){
filterChain.doFilter(servletRequest,servletResponse);
}else {
servletResponse.setCharacterEncoding("utf-8");
servletResponse.setContentType("text/html; charset=utf-8");
PrintWriter pw = null;
pw = servletResponse.getWriter();
pw.write("限流了");// 这里返回json格式的字符串
pw.close();
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}
2.5.3 Sentinel方式实现
package com.haokeed.cloudzuul.filter;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.alibaba.druid.stat.JdbcConnectionStat;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* Sentinel 是由alibaba出品的,针对于系统负载保护的组件,其有丰富的流量防护手段和多样化的流量整型策略而被广大使用。
* 以下是转自Sentinel官方的介绍: 随着微服务的流行,服务和服务之间的稳定性变得越来越重要。
* Sentinel 是面向分布式服务架构的轻量级流量控制组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护等多个维度来帮助您保障微服务的稳定性。
* Sentinel-wiki Sentinel GitHub wiki 有兴趣的童鞋可以去了解。
* <p>
* 他其实的原理就是令牌桶原理,和前面写的令牌桶实现原理一样:
* 1.生成令牌桶
* 2.使用令牌
* <p>
* <p>
* 注意:不走后面的过滤器的代码请参考前面的LimitFilter
* <p>
*
* @link https://github.com/alibaba/Sentinel
* <!-- sentinel-core -->
* <dependency>
* <groupId>com.alibaba.csp</groupId>
* <artifactId>sentinel-core</artifactId>
* <version>1.6.3</version>
* </dependency>
*/
@Component
public class SentinelFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return -10;
}
@Override
public boolean shouldFilter() {
return false;
}
@Override
public Object run() throws ZuulException {
// 限流的业务逻辑 (使用令牌)
Entry entry = null;
try {
// 获取整个令牌集合中对应的主题令牌
entry = SphU.entry("HelloWorld");
// 正常的业务逻辑
System.out.println("正常请求");
} catch (BlockException e) {
System.out.println("阻塞住了");
// 自己写不走后面过滤器的代码,上面的Limit过滤器中有
//e.printStackTrace();
} finally {
if (null != entry) {
entry.exit();// 退出令牌集合的主题
}
}
return null;
}
/**
* 生成令牌桶,
* 可将该功能直接写到main中去
* <p>
* SentinelFilter.init();//这个放到main中
*/
public static void init() {
// 所有限流规则的合集
List<FlowRule> rules = new ArrayList<>();
// 创建一个规则
FlowRule rule = new FlowRule();
// 限流规则要的资源的名称
rule.setResource("HelloWorld");
// 限流的类型 此处是qps类型
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// 设置令牌桶量是2
rule.setCount(2);
rules.add(rule);
// 将所有规则加载进去
FlowRuleManager.loadRules(rules);
}
}
限流逻辑
package com.haokeed.cloudzuul.service;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.stereotype.Service;
/**
* 对提供的服务的方法上添加对应的注解,就可以对该服务的这个方法进行限流。
* 前面controller之类的调用我们的服务和服务方法都是不变的方式
*
* 希望注解能够启用的话,在config.AopConfiguration中添加关于SentinelResourceAspect的切面配置
*
* 更多详情请查看 https://github.com/alibaba/Sentinel/wiki/%E6%B3%A8%E8%A7%A3%E6%94%AF%E6%8C%81
*/
@Service
public class SentinelService {
/**
* 正常的方法
* value代表着对对应的令牌集合中的主题集合成功的情况,
* blockHandler表示阻塞的处理方法
* blockHandlerClass 阻塞处理异常类 比如:{ExceptionUtil.class} 如果是自己的类,就别写了
* @return
*/
@SentinelResource(value = "SentinelService.success",blockHandler = "fail")
public String success(){
System.out.println("success 正常请求");
return "success";
}
/**
* 阻塞住的方法
* @return
*/
public String fail(BlockException e){
System.out.println("fail 阻塞");
return "fail";
}
}
AOP注入
package com.haokeed.cloudzuul.config;
import com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author 切面配置
*/
@Configuration
public class AopConfiguration {
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
}
3. 遇到的问题记录
3.1 网关header和cookie透传
正常情况下: 请求方请求被调用方,调用方能直接获取Authorization和Cookie,但是使用网关之后,这些数据就不透传了。
理由: 正常情况下微服务的Authorization和Cookie都是用来作为鉴权之类的用的,这些操作基本上都在网关做掉了,所以这些数据没必要透传到后方的范围。但是,传统的微服务改造刚刚开始的时候,这些数据还是要透传的,所以要单独开启。
这个就是遵循设计模式中的
迪米特原则
迪米特原则又叫最小知道原则,只和朋友交流,不和陌生人说话,如boss只和TL交流,TL和组员交流
计算机主板,CPU,显卡,硬盘都是这样的设计原则;相互直接只和主板交流,主板来处理。
解决: 在网关配置中添加配置
zuul:
# 以下配置,表示忽略下面的值向微服务传播,以下配置为空表示:所有请求头都透传到后面的服务。
sensitive-headers:
3.2 老项目改造,之前老url全部都不变情况处理
看前面的2.3 微服务路由劫持过滤器
4 参考文章
https://blog.csdn.net/u010963948/article/details/100146656