上文已经讲到了如何设计一个高可用的稳定的网关,那么这里就实现其中两种比较常用的方法。
重试
这里的重试,我将会在IO异常以及请求超时的时候进行一个请求重试。 首先,我们在路由过滤器中添加一个重试的函数,用于在请求出现如上两个异常的时候进行重试。 当然,我们需要增加一些额外的配置参数来设定重试的次数等信息。
而重试的代码,其实就是再一次调用doFilter方法去执行路由过滤器中的逻辑
css
private void doRetry(GatewayContext gatewayContext,int retryTimes){
System.out.println("当前重试次数为"+retryTimes);
gatewayContext.setCurrentRetryTimes(retryTimes+1);
try {
doFilter(gatewayContext);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
最后,我们让我们的服务去请求后端服务,并且在后端服务哪里设定一个长时间的阻塞sleep。 重试的简单实现是比较简单的,当然前提是你理解了之前的所有代码。或者说至少理解了请求转发这一块的代码
限流
常见的限流算法有令牌桶算法和漏桶算法。 这里两种算法我们都可以考虑使用一下。 同时,对于限流这一块,我们需要在配置中心配置限流规则。 例如限流的路径或者限流的服务。同时,根据你的服务是分布式服务还是单体服务,也需要考虑使用不同的方式来存储信息。 比如如果是分布式服务,就需要使用Redis,而如果是单体,那么考虑使用本地缓存即可,比如Guava或者Caffeine。 老样子,我们先编写一个接口,这个接口用于获取对应的限流过滤器。
css
public interface GatewayFlowControlRule {
/**
* 执行流控规则过滤器
* @param flowControlConfig
* @param serviceId
*/
void doFlowControlFilter(Rule.FlowControlConfig flowControlConfig, String serviceId);
}
之后,我们开始编写限流过滤器,根据请求获取对应的限流规则。
css
@Slf4j
@FilterAspect(id=FLOW_CTL_FILTER_ID,
name = FLOW_CTL_FILTER_NAME,
order = FLOW_CTL_FILTER_ORDER)
public class FlowControlFilter implements Filter {
@Override
public void doFilter(GatewayContext ctx) throws Exception {
Rule rule = ctx.getRule();
if(rule != null){
//获取流控规则
Set<Rule.FlowControlConfig> flowControlConfigs = rule.getFlowControlConfigs();
Iterator iterator = flowControlConfigs.iterator();
Rule.FlowControlConfig flowControlConfig;
while (iterator.hasNext()){
GatewayFlowControlRule flowControlRule = null;
flowControlConfig = (Rule.FlowControlConfig)iterator.next();
if(flowControlConfig == null){
continue;
}
String path = ctx.getRequest().getPath();
if(flowControlConfig.getType().equalsIgnoreCase(FLOW_CTL_TYPE_PATH)
&& path.equals(flowControlConfig.getValue())){
flowControlRule = FlowControlByPathRule.getInstance(rule.getServiceId(),path);
}else if(flowControlConfig.getType().equalsIgnoreCase(FLOW_CTL_TYPE_SERVICE)){
//TODO 可以自己实现基于服务的流控
}
if(flowControlRule != null){
//执行流量控制
flowControlRule.doFlowControlFilter(flowControlConfig,rule.getServiceId());
}
}
}
}
}
获取到指定的限流规则之后,就可以开始考虑着手编写如何进行具体的限流了。 比如我们要根据路径进行限流,那么我们首先需要的信息就是服务,以及请求路径。 并且我们需要保存这样子的规则, 每当对应的请求到来时,我们就从缓存中获取对应的限流规则。
java
/**
* 存放路径-流控规则的map
*/
private static ConcurrentHashMap<String, FlowControlByPathRule> servicePathMap = new ConcurrentHashMap<>();
/**
* 通过服务id和路径获取具体的流控规则过滤器
*
* @param serviceId
* @param path
* @return
*/
public static FlowControlByPathRule getInstance(String serviceId, String path) {
StringBuffer buffer = new StringBuffer();
String key = buffer.append(serviceId).append(".").append(path).toString();
FlowControlByPathRule flowControlByPathRule = servicePathMap.get(key);
//当前服务不存在限流规则 则保存之
if (flowControlByPathRule == null) {
flowControlByPathRule = new FlowControlByPathRule(serviceId, path, new RedisCountLimiter(new JedisUtil()));
servicePathMap.put(key, flowControlByPathRule);
}
return flowControlByPathRule;
}
获取完毕限流规则之后,我们就可以着手分析,如何进行具体的限流方法了。 我们获取到指定的限流配置之后,比如是否是分布式服务,是否限流时间和限流限制次数等等信息之后,就可以开始编写具体的限流代码了。 比如如果配置中发现服务是分布式的,那么就使用Redis,然后保存当前的请求路径以及限制次数等信息。
java
/**
* 执行限流
* @param key 限流key 服务+路径
* @param limit 限流次数
* @param expire 超时时间
* @return
*/
public boolean doFlowControl(String key,int limit,int expire){
try {
//执行脚本判断是否触发限流
Object object = jedisUtil.executeScript(key,limit,expire);
if(object == null){
return true;
}
Long result = Long.valueOf(object.toString());
if(FAILED_RESULT == result){
return false;
}
}catch (Exception e){
throw new RuntimeException("分布式限流发生错误");
}
return true;
}
public Object executeScript(String key, int limit, int expire){
Jedis jedis = jedisPool.getJedis();
String lua = buildLuaScript();
String scriptLoad =jedis.scriptLoad(lua);
try {
Object result = jedis.evalsha(scriptLoad, Arrays.asList(key), Arrays.asList(String.valueOf(expire), String.valueOf(limit)));
System.out.println(result);
return result;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedis != null) {
try {
jedis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return null;
}
// 构造lua脚本
private static String buildLuaScript() {
String lua = "local num = redis.call('incr', KEYS[1])\n" +
"if tonumber(num) == 1 then\n" +
"\tredis.call('expire', KEYS[1], ARGV[1])\n" +
"\treturn 1\n" +
"elseif tonumber(num) > tonumber(ARGV[2]) then\n" +
"\treturn 0\n" +
"else \n" +
"\treturn 1\n" +
"end\n";
return lua;
}
而如果不是分布式项目,就可以考虑使用Guava这种本地缓存。 实现方式大差不差,如下
java
public class GuavaCountLimiter {
/**
* guava限流根据
*/
private RateLimiter rateLimiter;
/**
* 最大请求数量
*/
private double maxPermits;
/**
* 没有预热,尽可能按照这个速率来分发许可。速率限制器不会考虑之前的请求,也不会允许短时间内的请求速率超过指定的速率。
* 这可能导致一些请求需要等待,以便速率限制器可以分发足够的许可。
*
* @param maxPermits
*/
public GuavaCountLimiter(double maxPermits) {
this.maxPermits = maxPermits;
rateLimiter = RateLimiter.create(maxPermits);
}
/**
* 创建一个具有预热期的速率限制器。预热期是指在速率限制器刚开始使用时,速率限制器允许请求超过其平均速率。
* 预热结束后按照指定的速度分发许可。
* 提供预热意味着可以在应用启动或负载增加时,允许一些瞬时的高请求速率,然后逐渐调整到稳定的速率。
*
* @param maxPermits 表示每秒的最大许可数量
* @param warmUpPeriodAsSecond 预热时长
*/
public GuavaCountLimiter(double maxPermits, long warmUpPeriodAsSecond) {
this.maxPermits = maxPermits;
rateLimiter = RateLimiter.create(maxPermits, warmUpPeriodAsSecond, TimeUnit.SECONDS);
}
/**
* 路径 - 限流器
*/
public static ConcurrentHashMap<String, GuavaCountLimiter> resourceRateLimiterMap = new ConcurrentHashMap<String,
GuavaCountLimiter>();
public static GuavaCountLimiter getInstance(String serviceId, Rule.FlowControlConfig flowControlConfig) {
if (StringUtils.isEmpty(serviceId) || flowControlConfig == null || StringUtils.isEmpty(flowControlConfig.getValue()) || StringUtils.isEmpty(flowControlConfig.getConfig()) || StringUtils.isEmpty(flowControlConfig.getType())) {
return null;
}
StringBuffer buffer = new StringBuffer();
String key = buffer.append(serviceId).append(".").append(flowControlConfig.getValue()).toString();
GuavaCountLimiter countLimiter = resourceRateLimiterMap.get(key);
if (countLimiter == null) {
//获得当前路径对应的流控次数
Map<String, Integer> configMap = JSON.parseObject(flowControlConfig.getConfig(), Map.class);
//判断是否包含流控规则
if (!configMap.containsKey(FLOW_CTL_LIMIT_DURATION) || !configMap.containsKey(FLOW_CTL_LIMIT_PERMITS)) {
return null;
}
//得到流控时间和时间内限制次数
double permits = configMap.get(FLOW_CTL_LIMIT_PERMITS);
countLimiter = new GuavaCountLimiter(permits);
resourceRateLimiterMap.putIfAbsent(key, countLimiter);
}
return countLimiter;
}
/**
* 获取令牌
* @param permits 需要获取的令牌数量
* @return 是否获取成功
*/
public boolean acquire(int permits) {
boolean success = rateLimiter.tryAcquire(permits);
if (success) {
return true;
}
return false;
}
}
所以到目前位置,就已经大概的实现了如何进行限流的代码了。 之后,我们配置完毕配置中心的信息之后,就可以开始测试我们的限流代码了。
javascript
{
"rules": [
{
"id":"1",
"name":"test-1",
"protocol":"http",
"serviceId":"backend-http-server",
"prefix":"/user",
"paths":[
"/http-server/ping","/user/update"
],
"filterConfigs":[{
"id":"load_balance_filter",
"config":{
"load_balance":"Random"
}
},{
"id":"flow_ctl_filter"
}],
"flowControlConfigs":[{
"type":"path",
"model":"distributed",
"value":"/http-server/ping",
"config":{
"duration":20,
"permits":2
}
}],
"retryConfig":{
"times":5
},
"hystixConfigs":[{
"path":"/http-server/ping",
"timeoutInMilliseconds":5000,
"threadCoreSize":2,
"fallbackResponse":"熔断超时"
}]
}
]
}
我们使用apifox发送过量的请求,就会发现,报错如下 到目前位置我们已经实现了重试和限流,那么下一篇文章我们就需要实现熔断与降级。 因为其实限流和熔断降级是一起的。