关于限流:从0到1构造自定义限流组件-京东云技术团队

1次阅读

共计 5961 个字符,预计需要花费 15 分钟才能阅读完成。

一 背景

在零碎高可用设计中,接口限流是一个十分重要环节,一方面是出于对本身服务器资源的爱护,另一方面也是对依赖资源的一种保护措施。比方对于 Web 利用,我限度单机只能解决每秒 1000 次的申请,超过的局部间接返回谬误给客户端。尽管这种做法侵害了用户的应用体验,然而它是在极其并发下的无奈之举,是短暂的行为,因而是能够承受的。

二 设计思路

常见的限流有 2 种思路

  • 第一种是限度总量, 也就是限度某个指标的累积下限,常见的是限度以后零碎服务的用户总量,例如:某个抢购流动商品数量只有 100 个,限度参加抢购的用户下限为 1 万个,1 万当前的用户间接回绝。
  • 第二种是限度工夫量,也就是限度一段时间内某个指标的下限,例如 1 分钟内只容许 10000 个用户拜访;每秒申请峰值最高为 10 万。

三 限流算法

目前实现限流算法次要分为 3 类,这里不具体开展介绍:

1)工夫窗口

固定工夫窗口算法是最简略的限流算法,它的实现原理就是管制单位工夫内申请的数量,然而这个算法有个毛病就是临界值问题。
为了解决临界值的问题,又推出滑动工夫窗口算法,其实现原理大抵上是将工夫分为一个一个小格子,在统计申请数量的时候,是通过统计滑动工夫周期内的申请数量。

2)漏斗算法

漏斗算法的外围是管制总量,申请流入的速率不确定,超过流量局部益出,该算法比拟实用于针对突发流量,想要尽可能的接管全副申请的场景。其毛病也比拟显著,这个总量怎么评估,大小怎么配置,而且一旦初始化也没法动静调整。

3)令牌桶算法

令牌桶算法的外围是管制速率,令牌产生的速度是要害,一直的申请获取令牌,获取不到就抛弃。该算法比拟实用于针对突发流量,以爱护本身服务资源以及依赖资源为主,反对动静调整速率。毛病的话实现比较复杂,而且会抛弃很多申请。

四 实现步骤

咱们自定义的这套限流组件有是基于 guava RateLimiter 封装的,采纳令牌桶算法以管制速率为主,反对 DUCC 动静配置,同时反对限流后的降级措施。接下来看一下整体实现计划

1、自定义 RateLimiter Annotation 标签

这里次要对限流相干属性的一个定义,包含每秒产生的令牌数、获取令牌超时工夫、降级逻辑实现以及限流开关等内容

@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SysRateLimit {

    /**
     * 每秒产生的令牌数 默认 500
     *
     * @return
     */
    double permitsPerSecond() default 500D;

    /**
     * 获取令牌超时工夫 默认 100
     *
     * @return
     */
    long timeout() default 100;

    /**
     * 获取令牌超时工夫单位 默认毫秒
     *
     * @return
     */
    TimeUnit timeUnit() default TimeUnit.MILLISECONDS;

    /**
     * 服务降级办法名称 Spring bean id
     *
     * @return
     */
    String fallbackBeanId() default "";

    /**
     * 限流 key 惟一
     *
     * @return
     */
    String limitKey() default "";}

2、基于 Spring Aspect 结构切面

首先就是咱们须要结构一个 Aspect 切面用于扫描咱们自定义的 SysRateLimit 标签

@Slf4j
@EnableAspectJAutoProxy
@Aspect
public class SysRateLimitAspect {
    
    /**
     * 自定义切入点
     */
    @Pointcut("@annotation(com.jd.smb.service.ratelimiter.annotation.SysRateLimit)")
    public void pointCut() {}

    /**
     * 办法前执行限流计划
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 如果未获取到对象,间接执行办法
        if (signature == null) {return joinPoint.proceed();
        }

        try {Method method = joinPoint.getTarget().getClass().getDeclaredMethod(signature.getName(), signature.getMethod().getParameterTypes());
            // 获取注解对象
            SysRateLimit sysRateLimit = method.getAnnotation(SysRateLimit.class);
            if (sysRateLimit == null) {return joinPoint.proceed();
            }
            
        } catch (Exception e) {// todo log}
        return joinPoint.proceed();}
}

获取自定义 SysRateLimit 标签的各种属性

 // 限流 key
String limitKey = sysRateLimit.limitKey();
if (StringUtils.isBlank(limitKey)) {return joinPoint.proceed();
}
// 令牌桶数量
double permitsPerSecond = sysRateLimit.permitsPerSecond();
// 获取令牌超时工夫
long timeout = sysRateLimit.timeout();
// 获取令牌超时工夫单位
TimeUnit timeUnit = sysRateLimit.timeUnit();

将咱们自定义的 SysRateLimiter 和 Guava RateLimiter 进行整合

  1. 首先咱们须要结构一个全局 Map,用于存储咱们开启限流的办法,key 就是咱们定义的 limitKey,value 就是咱们转换后的 Guava RateLimiter
 /**
 * 存储 RateLimiter(key: limitKey value:RateLimiter)*/
private static final Map<String, RateLimiter> LIMITER_MAP = new ConcurrentHashMap<>();
  1. 接着就是外围逻辑:这里首先从咱们创立的 Map 中获取 Guava RateLimiter,获取不到就创立 RateLimiter.create(permitsPerSecond);而后调用 RateLimiter.tryAcquire() 尝试获取令牌桶,获取胜利则执行后续的逻辑,这里重点获取失败后,咱们须要执行咱们的降级办法。( 留神:Guava RateLimiter 有很多 API,这里咱们不展开讨论,后续会针对 Guava 限流的源码进行具体的解析)
RateLimiter rateLimiter;
// Map 中是否存在 存在间接获取
if (LIMITER_MAP.containsKey(limitKey)) {rateLimiter = LIMITER_MAP.get(limitKey);
} else {
    // 不存在创立后放到 Map 中
    rateLimiter = RateLimiter.create(permitsPerSecond);
    LIMITER_MAP.put(limitKey, rateLimiter);
}
// 尝试获取令牌
if (!rateLimiter.tryAcquire(timeout, timeUnit)) {
    // todo 限流后降级措施
    return this.fallBack(sysRateLimit, joinPoint, signature);
}

降级计划执行

下面咱们在获取令牌桶超时后,须要执行咱们的降级逻辑,怎么做呢?也很简略,咱们在定义 SysRateLimiter 的时候有个 fallBackBeanId,这个就是咱们执行降级逻辑的 bean 对象 Id,须要咱们提前进行创立。接着咱们看一下是怎么实现的。

    /**
     * 执行降级逻辑
     *
     * @param sysRateLimit
     * @param joinPoint
     * @param signature
     * @return
     */
    private Object fallBack(SysRateLimit sysRateLimit, ProceedingJoinPoint joinPoint, MethodSignature signature) {String fallbackBeanId = sysRateLimit.fallbackBeanId();
        // 当没有配置具体的降级实现计划的时候 能够联合业务世纪状况设置限流错误码
        if (StringUtils.isBlank(fallbackBeanId)) {
            // 自定义的 能够联合本人零碎里的进行设置
            return ApiResult.error(ResultCode.REACH_RATE_LIMIT);
        }

        try {
            // SpringContext 中通过 BeanId 获取对象 SpringUtils 只是获取 bean 对象的工具类 有多种实现形式 可自行百度
            Object bean = SpringUtils.getBean(fallbackBeanId);
            Method method = bean.getClass().getMethod(signature.getName(), signature.getParameterTypes());
            // 执行对应的办法
            return method.invoke(bean, joinPoint.getArgs());
        } catch (Exception e) {// todo error log}
        return ApiResult.error(ResultCode.REACH_RATE_LIMIT);
    }

这样咱们大略的一个架子就弄好了。接下来咱们看看理论该如何应用

3、具体利用

在办法入口引入 SysRateLimiter 标签

@Slf4j
@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
public class UserQueryController extends AbstractController {

    /**
     * 查问用户信息
     *
     * @param request
     * @return
     */
    @GetMapping("/info/{id}")
    @SysRateLimit(permitsPerSecond = 500, limitKey = "UserQueryController.info", fallbackBeanId = "userQueryControllerFallBack",
            timeout = 100, timeUnit = TimeUnit.MILLISECONDS)
    public ApiResult<UserInfo> info(@PathVariable Long id, HttpServletRequest request) {
        // todo 业务逻辑查问 这里不开展
        return ApiResult.success();}
}

设置降级办法

@Service
public class UserQueryControllerFallBack {

    /**
     * 降级后执行的逻辑
     *
     * @param request
     * @return
     */
    public ApiResult<UserInfo> info(Long id, HttpServletRequest request) {
        // todo 编写限流降级后的逻辑 能够是降级码 也能够是默认对象
        return ApiResult.success(null);
    }
}

当申请进来的时候,会联合咱们设置的阈值进行令牌桶的获取,获取失败后会执行限流,这里咱们进行了限流后的降级解决。其实到这里咱们实现限流组件的简略封装和应用,然而仍有一些点须要咱们进行解决,例如如何动静设置令牌的数量,接下来咱们就看一下如何实现令牌的动静设置。

4、动静设置令牌数量

通过 DUCC 配置令牌数量 咱们须要定义一个 DUCC 配置,这外面内容很简略,配置咱们设置 limitKey 的令牌数量

@Data
@Slf4j
@Component
public class RateLimitConfig {

    /**
     * 配置 config key: limitKey value: 数量
     */
    private Map<String, Integer> limitConfig;

    /**
     * 监听 ducc 配置
     *
     * @param json
     */
    @LafValue(key = "rate.limit.conf")
    public void setConfig(String json) {if (StringUtils.isBlank(json)) {return;}
        Map<String, Integer> map = JsonModelUtils.getModel(json, Map.class, null);
        if (map != null) {Wrapper.wrapperBean(map, this, true);
        }
    }
}

通过 DUCC 配置获取指定 limitKey 的令牌数量,获取失败则采纳办法设置默认数量,这样咱们前面设置令牌数量就能够通过 DUCC 动静的配置了

 /**
     * 获取令牌桶数量
     *
     * @param sysRateLimit
     * @return
     */
    private double getPermitsPerSecond(SysRateLimit sysRateLimit) {
        // 办法默认令牌数量
        double defaultValue = sysRateLimit.permitsPerSecond();
        if (rateLimitConfig == null || rateLimitConfig.getLimitConfig() == null) {return defaultValue;}
        // 配置的令牌数量
        Integer value = rateLimitConfig.getLimitConfig().get(sysRateLimit.limitKey());
        if (value == null) {return defaultValue;}
        return value;
    }

5、后续其余配置

其实后续咱们的其余属性都能够通过 DUCC 动态化的来配置,这里呢因为和令牌桶数量相似,就不再开展形容了。感兴趣的小伙伴能够自行设置,依据咱们的应用,应用默认配置即可。

作者:京东批发 王磊

起源:京东云开发者社区

正文完
 0