某种形式的(web/RESTful)API

可能您正在开发某种形式的(web/RESTful)API,如果它是面向公众的(或者即使它是内部的),您通常希望对它进行某种程度的分级限制。也就是说,限制在一段时间内执行的请求数量,以节省资源和防止滥用。

这可能可以通过一些巧妙的配置在web服务器/负载均衡器级别上实现,但通常您希望速率限制器是特定于客户端的(即您的api sohuld的每个客户端都有一个单独的速率限制),并且客户端的识别方式也各不相同。在负载均衡器上可能仍然可以这样做,但我认为在应用程序级别上这样做是有意义的。

我将使用SpringMVC作为示例,但是任何Web框架都有一个很好的方法来插入拦截器。

下面是一个SpringMVC拦截器的例子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

@Component

public class RateLimitingInterceptor extends HandlerInterceptorAdapter {

    private static final Logger logger = LoggerFactory.getLogger(RateLimitingInterceptor.class);

     

    @Value("${rate.limit.enabled}")

    private boolean enabled;

     

    @Value("${rate.limit.hourly.limit}")

    private int hourlyLimit;

    private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);

    private Map<String, Optional<SimpleRateLimiter>> limiters = new ConcurrentHashMap<>();

     

    @Override

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)

            throws Exception {

        if (!enabled) {

            return true;

        }

        String clientId = request.getHeader("Client-Id");

        // let non-API requests pass

        if (clientId == null) {

            return true;

        }

        SimpleRateLimiter rateLimiter = getRateLimiter(clientId);

        boolean allowRequest = limiter.tryAcquire();

     

        if (!allowRequest) {

            response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());

        }

        response.addHeader("X-RateLimit-Limit", String.valueOf(hourlyLimit));

        return allowRequest;

    }

     

    private SimpleRateLimiter getRateLimiter(String clientId) {

        return limiters.computeIfAbsent(clientId, clientId -> {

            return Optional.of(createRateLimiter(clientId));

        });

    }

    private SimpleRateLimiter createRateLimiter(String applicationId) {

        logger.info("Creating rate limiter for applicationId={}", applicationId);

        return SimpleRateLimiter.create(hourlyLimit, TimeUnit.HOURS, scheduler, applicationId);

    }

     

    @PreDestroy

    public void destroy() {

        // loop and finalize all limiters

        scheduler.shutdown();

    }

}

这将按需初始化每个客户端的速率限制器。或者,在启动时,您可以循环遍历所有注册的API客户端,并为每个客户端创建一个速率限制器。如果速率限制器不允许更多的请求(tryAcquire()返回false),那么启动“太多请求”并中止请求的执行(从拦截器返回“false”)。听起来很简单。但是有几个渔获物。你可能想知道SimpleRateLimiter上述定义。我们会到达那里,但首先让我们看看我们有什么选项的速率限制器的实现。

https://www.douban.com/note/786885209/

最受推荐的似乎是番石榴。它有一个简单的工厂方法,为指定的速率(每秒许可)提供一个速率限制器。但是,它不能很好地容纳WebAPI,因为您不能用预先存在的数量的许可证初始化速率限制器。这意味着在限制器允许请求之前应该经过一段时间。还有另一个问题--如果你的许可少于每秒一次(例如,如果你想要的速率限制是“每小时200次请求”),你可以传递一个分数(小时限制/秒),但是它仍然不能像你期望的那样工作,因为在内部有一个“maxPerms”字段,它会将许可证的数量限制在比你想要的少得多的范围内。此外,速率限制器不允许爆发--您有精确的每秒X许可证,但是您不能在很长的一段时间内扩展它们,例如在一秒钟内有5个请求,然后在接下来的几秒钟内没有请求。事实上,所有这些都是可以解决的,但不幸的是,这些都是通过您无法访问的隐藏字段解决的。多个特性请求已经存在多年了,但是Guava只是没有更新速率限制器,这使得它更不适用于API速率限制。

使用反射,您可以调整参数,使限制器工作。然而,它是丑陋的,它不能保证它将如预期的那样工作何初始化一个番石榴速率限制器与X许可证每小时,具有可爆性和完整的初始许可证。当我以为那样可以的时候,我看到了tryAcquire()有一个synchronized(..)封锁。这是否意味着在简单地检查是否允许提出请求时,所有请求都会等待对方?那就太可怕了。

因此,实际上,番石榴速率限制器并不意味着(Web)API速率限制。也许保持它的特色--贫穷是番石榴阻止人们滥用它的方法?

https://movie.douban.com/people/246404409/

这就是为什么我决定自己实现一些简单的东西,基于Java信号量:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

public class SimpleRateLimiter {

    private Semaphore semaphore;

    private int maxPermits;

    private TimeUnit timePeriod;

    private ScheduledExecutorService scheduler;

    public static SimpleRateLimiter create(int permits, TimeUnit timePeriod) {

        SimpleRateLimiter limiter = new SimpleRateLimiter(permits, timePeriod);

        limiter.schedulePermitReplenishment();

        return limiter;

    }

    private SimpleRateLimiter(int permits, TimeUnit timePeriod) {

        this.semaphore = new Semaphore(permits);

        this.maxPermits = permits;

        this.timePeriod = timePeriod;

    }

    public boolean tryAcquire() {

        return semaphore.tryAcquire();

    }

    public void stop() {

        scheduler.shutdownNow();

    }

    public void schedulePermitReplenishment() {

        scheduler = Executors.newScheduledThreadPool(1);

        scheduler.scheduleAtFixedRate(() -> {

            semaphore.release(maxPermits - semaphore.availablePermits());

        }, 1, 1, timePeriod);

    }

}

些许可证(允许的请求数量)和一段时间。时间周期是“1 X”,其中X可以是秒/分钟/小时/日-取决于您希望如何配置您的限制-每秒,每分钟,每天。调度器每1倍就会补充所获得的许可(在上面的示例中,每个客户端都有一个调度程序,这对于大量客户端来说可能是低效的--您可以传递共享调度程序池)。没有控制突发(一个客户可以使用所有的许可与快速接连的请求),没有热身功能,没有逐步补充。取决于你想要什么,这可能不是理想的,但这只是一个基本的速率限制器,是线程安全,没有任何阻塞。我编写了一个单元测试,以确认限制器是否正常工作,并针对本地应用程序运行性能测试,以确保该限制得到遵守。到目前为止,它似乎正在起作用。

有没有其他选择?嗯,是的-有些is来实现速率限制。然而,这意味着您需要设置并运行Redis。这似乎是“简单”限制速率的开销。(注:它似乎也有)

https://book.douban.com/doulist/144578376/

另一方面,如何在应用程序节点集群中正确地限制速率?应用程序节点可能需要一些数据库或八卦协议来共享关于每个客户端许可(请求)剩余的数据?不一定。解决此问题的一个非常简单的方法是假设负载均衡器在节点之间平均分配负载。这样,您只需将每个节点的限制设置为等于总限制除以节点数。它不会准确,但你很少需要它-允许5-10多个请求不会扼杀你的应用程序,允许5-10的少将不会是戏剧性的用户。

然而,这意味着您必须知道应用程序节点的数量。如果采用自动缩放(例如在AWS中),节点数量可能会根据负载而变化。如果是这样的话,补充计划作业可以通过调用AWS(或其他云提供商)api来获取当前自动缩放组中的节点数,而不是配置硬编码的许可证数量,而是动态地计算“maxPermit”。这仍然比仅仅为了这个目的而支持Redis部署要简单得多。

总的来说,我很惊讶没有一种“规范”的方法来实现速率限制(在Java中)。也许对限速的需求并不像看上去那么普遍。或者是手动实现的--临时禁止使用“太多资源”的API客户端。

更新:有很好,值得一看。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值