在使用zuul的过程中会出现一些常见问题:
- token 不往后传
- 老项目改造中路由问题
- 动态路由(根据用户不同-->不同服务)
Token 不后传
由于token是存储在header里面,就需要对header进行设置,在zuul默认的配置中,会拦截掉header里面的Cookie,Set-Cookie,Authorization,如下所示:
zuul:
routes:
users:
path: /myusers/**
sensitiveHeaders: Cookie,Set-Cookie,Authorization # 保证token后传,将其去掉。
url: https://downstream
sensitiveHeaders是一个黑名单,会将敏感的header信息过滤条,如果不过滤的话,在页面中会出现信息混乱的情况,从我个人的角度来看,token一般都是存储在
Authorization 里面,将需要认证的服务,去掉Authorization的限制,即可,也可以设置zuul.ignoreHeaders来设置可以忽略的header信息。
老项目中路由问题
@EnableZuulServer Filter
@EnableZuulServer 创建SimpleRouteLocator对象来加载Spring Boot配置文件。
- Pre Filter:
- ServletDetectionFilter: 检测请求是否通过spring调度。
- FormBodyWrapperFilter: 解析表单数据,往下传递时会再次编码
- SendForwardFilter: 通过RequestDispatcher进行转发请求。这个必须要配置FilterConstants.FORWARD_TO_KEY
- Post Filter:
- SendResponseFilter:从代理请求中写回应到当前对应
- Error Filter:
- SendErrorFilter:如果RequestContext.getThrowable()不为null,转发到/error请求。
@EnableZuulProxy
创建DiscoveryClientRouteLocator从Eureka中获取路由定义。
- Pre Filter:
- PreDecorationFilter:从RouteLocator中决定如何路由和往哪儿路由。
- Route Filter:
- RibbonRoutingFilter: 从RequestContext中获取到FilterConstants.SERVICE_ID_KEY,来进行转发。
- SimpleHostRoutingFilter: 发送到已经确定的地址。
从上面的情况看,zuul获取的路由的方式有:
- 从Eureka中获取服务列表
- 从配置文件中获取数据
路由转换
使用配置文件:
# 新老url映射
# demo-service 目标服务地址;endpoint-service 中间服务地址
#zuul:
# routes:
# demo-service: /endpoint-service/** # RibbonRoutingFilter
# demo 起的自定义名字;path:中间服务地址;serviceId:目标服务的编号
#zuul:
# routes:
# demo:
# path: /endpoint-service/**
# serviceId: demo-service
# 上面配置的负载均衡
#hystrix:
# command:
# demo:
# execution:
# isolation:
# thread:
# timeoutInMilliseconds: 10
#demo:
# ribbon:
# NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
# listOfServers: https://example1.com,http://example2.com # 提供服务的地址列表
# ConnectTimeout: 1000
# ReadTimeout: 3000
# MaxTotalHttpConnections: 500
# MaxConnectionsPerHost: 100
# demo 起的自定义名字;path:中间服务地址;url:目标服务的url
#zuul:
# routes:
# demo:
# path: /endpoint-service/**
# url: http://localhost:8085/
# 在进行url转换的时候,不能不是直接进行后面转换,需要进行自定义对转换:例如test31->test的转换,编写自定义filter,filter类型必须为Route。
#zuul:
# routes:
# demo:
# path: /endpoint-service/**
# url: forward:/demo-service/ # SendForwardFilter
编写代码:
@Component
public class ConvertUrlFilter extends ZuulFilter {
private final static String GETWAY_FOWARD_PREFIX="getway_forward_";
private final static String GETWAY_COMPAY_CONFIG_KEY = "getway_company";
@Autowired
private RedisTemplate redisTemplate;
@Override
public String filterType() {
//这里很重要,必须是route
return "route";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
String url = ctx.getRequest().getRequestURI();
Map forwardMap = getForwardMap(url);
if(forwardMap != null){
String forwardUrl = forwardMap.get(url);
String serviceId = getServiceId(forwardUrl);
String requestUrl = getRequestUrl(forwardUrl,serviceId);
//1.设置目标service的Controller的路径
ctx.put(FilterConstants.REQUEST_URI_KEY,requestUrl);
//2.设置目标service的serviceId
ctx.put(FilterConstants.SERVICE_ID_KEY,serviceId);
/**
* 如果使用setRouteHost(url),不用上面SERVICE_ID_KEY,REQUEST_URI_KEY。
* 如果使用的是一些没有规律的url转换,就使用hashmap、redis、db来进行转化。
* try {
* ctx.setRouteHost(new URI("http://localhost:8083/test/demo-service").toURL());
* } catch (MalformedURLException e) {
* e.printStackTrace();
* } catch (URISyntaxException e) {
* e.printStackTrace();
* }
*/
}
return null;
}
private String getServiceId(String url){
if(url.startsWith("/")){
String temp = url.substring(1);
return temp.split("/")[0];
}else{
return null;
}
}
private String getRequestUrl(String url,String serviceId){
return url.substring(serviceId.length() +1);
}
@Override
public boolean shouldFilter() {
return true;
}
private Map getForwardMap(String originalUrl){
//todo:这里是返回一个map,传入一个originUrl,返回一个要转发的url
return null;
}
}
动态路由(根据用户不同-->不同服务)
网关的本质就是过滤器,在使用过滤器的过程,要注意顺序,保证鉴权过滤器在最后的执行,这样可以减少资源消耗。
例如:ip黑名单、设备黑名单。
可以在Filter中获取header的Authorization,然后解析出来用户名和密码,根据用户的userId来决定注册到某个具体的服务(类似于灰度发布)。
服务返回异常处理
如果需要处理某一个具体的服务的异常,直接指定这个服务,如果是全部的话,设置为*。
@Component
public class CustomFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
return "*";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
if (cause instanceof HystrixTimeoutException) {
return response(HttpStatus.GATEWAY_TIMEOUT);
} else {
return response(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
private ClientHttpResponse response(final HttpStatus status) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return status;
}
@Override
public int getRawStatusCode() throws IOException {
return status.value();
}
@Override
public String getStatusText() throws IOException {
return status.getReasonPhrase();
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("fallback".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
在编写zuul filter的时候,将是否开启过滤器、过滤器的顺序、验证的方式等可以动态变化数据写入到数据库、redis、配置中心,这样可以动态调整。
在使用zuul时,需要开启actuator,可以看到现有的路由信息、filter信息。
sendZuulResponse(false):只控制不向后面route过滤器执行。
限流
容器限流
Tomcat
使用maxThreads设置线程最大数。
Nginx
控制速率(limit_req_zone 用来限制单位时间内的请求数,即速率限制):
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
server {
location / {
limit_req zone=mylimit;
}
}
升级版本():
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
server {
location / {
limit_req zone=mylimit burst=4;
}
}
控制并发数(利用 limit_conn_zone 和 limit_conn 两个指令即可控制并发数):
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
server {
...
limit_conn perip 10;
limit_conn perserver 100;
}
服务端限流
滑动窗口限流
public abstract class CounterLimit {
/**
* 单位时间限制数
*/
protected int limitCount;
/**
* 限制时间
*/
protected long limitTime;
/**
* 时间单位
*/
protected TimeUnit timeUnit;
/**
* 当前是否为受限限制
*/
protected volatile boolean limited;
/**
* 尝试将计数器加1,返回true表示能够访问接口,false表示访问受限
* @return
*/
protected abstract boolean tryCount();
}
public class SlidingWindowCounterLimit extends CounterLimit {
/**
* 格子分布
*/
private AtomicInteger[] gridDistribution;
/**
* 当前时间在计数分布的索引
*/
private volatile int currentIndex;
/**
* 当前时间之前的滑动窗口计数
*/
private int preTotalCount;
/**
* 格子数
*/
private int gridNumber;
/**
* 是否正在执行状态重置
*/
private volatile boolean resetting;
public SlidingWindowCounterLimit(int gridNumber,int limitCount, long limitTime) {
this(gridNumber,limitCount,limitTime, TimeUnit.SECONDS);
}
public SlidingWindowCounterLimit(int gridNumber,int limitCount,long limitTime,TimeUnit timeUnit) {
if (gridNumber <= limitTime){
throw new RuntimeException("gridNumber <= limitTime");
}
this.gridNumber = gridNumber;
this.limitCount = limitCount;
this.limitTime = limitTime;
this.timeUnit = timeUnit;
gridDistribution = new AtomicInteger[gridNumber];
for (int i = 0 ;i < gridNumber;i++){
gridDistribution[i] = new AtomicInteger(0);
}
new Thread(new CounterResetThread()).start();
}
@Override
protected boolean tryCount() {
while (true){
if (limited){
return false;
}else {
int currentGridCount = gridDistribution[currentIndex].get();
if (preTotalCount + currentGridCount == limitCount){
limited = true;
return false;
}
if (!resetting && gridDistribution[currentIndex].compareAndSet(currentGridCount,currentGridCount+1)){
return true;
}
}
}
}
class CounterResetThread implements Runnable{
@Override
public void run() {
while (true){
try{
timeUnit.sleep(1);
int indexToReset = currentIndex - limitCount - 1;
if (indexToReset < 0){
indexToReset += gridNumber;
}
resetting = true;
preTotalCount = preTotalCount - gridDistribution[indexToReset].get() + gridDistribution[currentIndex++].get();
if(currentIndex == gridNumber){
currentIndex = 0;
}
if (preTotalCount + gridDistribution[currentIndex].get() < limitCount){
limited = false;
}
resetting = false;
gridDistribution[indexToReset].set(0);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
}
令牌桶限流
public class TokenBucketLimit {
/**
* 给定时间生成令牌数
*/
private int genNumber;
/**
* 生成令牌话费的时间
*/
private int genTime;
/**
* 时间单位
*/
private TimeUnit timeUnit;
/**
* 最大令牌数
*/
private int maxNumber;
/**
* 已存储的令牌数
*/
private AtomicInteger storedNumber;
public TokenBucketLimit(int genNumber, int genTime, int maxNumber) {
this.genNumber = genNumber;
this.genTime = genTime;
this.maxNumber = maxNumber;
}
public TokenBucketLimit(int genNumber, int genTime, TimeUnit timeUnit, int maxNumber, AtomicInteger storedNumber) {
this.genNumber = genNumber;
this.genTime = genTime;
this.timeUnit = timeUnit;
this.maxNumber = maxNumber;
this.storedNumber = storedNumber;
new Thread(new TokenGenerateThread()).start();
}
public boolean tryAcquire(){
while (true){
int currentStoredNumber = storedNumber.get();
if (currentStoredNumber == 0){
return false;
}
if (storedNumber.compareAndSet(currentStoredNumber,currentStoredNumber-1)){
return true;
}
}
}
class TokenGenerateThread implements Runnable{
@Override
public void run() {
while (true){
if (storedNumber.get() == maxNumber){
try{
timeUnit.sleep(genTime);
}catch (InterruptedException e){
e.printStackTrace();
}
}else {
int old = storedNumber.get();
int newValue = old + genNumber;
if (newValue > maxNumber){
newValue = maxNumber;
}
storedNumber.compareAndSet(old,newValue);
try{
timeUnit.sleep(genTime);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
}
}
漏桶对比
public class LeakyBucketLimit {
/**
* 桶最大容量
*/
private int maxNumber;
/**
* 时间单位
*/
private TimeUnit timeUnit;
/**
* 漏的数量
*/
private int leakNumber;
/**
* 漏的时间
*/
private int leakTime;
/**
* 桶中剩余数量
*/
private AtomicInteger remainingNumber;
public LeakyBucketLimit(int leakNumber, int leakTime, int maxNumber) {
this.maxNumber = maxNumber;
this.leakNumber = leakNumber;
this.leakTime = leakTime;
}
public LeakyBucketLimit(int maxNumber, TimeUnit timeUnit, int leakNumber, int leakTime, AtomicInteger remainingNumber) {
this.maxNumber = maxNumber;
this.timeUnit = timeUnit;
this.leakNumber = leakNumber;
this.leakTime = leakTime;
this.remainingNumber = new AtomicInteger(0);
new Thread(new LeakThread()).start();
}
public boolean tryAcquire(){
while (true){
int currentStoredNumber = remainingNumber.get();
if (currentStoredNumber == maxNumber){
return false;
}
if (remainingNumber.compareAndSet(currentStoredNumber,currentStoredNumber + 1)){
return true;
}
}
}
class LeakThread implements Runnable{
@Override
public void run() {
while (true){
if (remainingNumber.get() == 0){
try{
timeUnit.sleep(leakTime);
}catch (InterruptedException e){
e.printStackTrace();
}
}else {
int old = remainingNumber.get();
int newValue = old - leakNumber;
if (newValue < 0){
newValue = 0;
}
remainingNumber.compareAndSet(old,newValue);
try{
timeUnit.sleep(leakTime);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
}
}
如果不自己编写相应的限流方法,就直接使用guava的RateLimiter。
令牌桶和漏桶对比:
- 令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;
- 漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;
- 令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌),并允许一定程度突发流量;
- 漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),从而平滑突发流入速率;
- 令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率; 两个算法实现可以一样,但是方向是相反的,对于相同的参数得到的限流效果是一样的。
参考文献
8. Router and Filter: Zuul
使用ZuulFilter转发路由
人人都能看懂的 6 种限流实现方案!(纯干货)
架构之高并发:限流
Java限流代码实现