客户端请求速率限制

6
我正在为外部API设计一个.NET客户端应用程序,它将有两个主要职责:
同步 - 定期向API发出一批请求并将响应保存到我的数据库中。
客户端 - 从我的客户端用户那里传递请求到API。
服务文档规定了在给定时间内可以发出的最大请求数量的以下规则:
在白天:
每小时最多6000个请求(约为每秒1.67个)
每分钟最多120个请求(每秒2个)
每秒最多3个请求
在晚上:
每小时最多8000个请求(约为每秒2.23个)
每分钟最多150个请求(每秒2.5个)
每秒最多3个请求
超过这些限制不会立即导致封锁 - 不会抛出任何异常。但是提供者可能会感到恼怒,与我们联系,然后禁止我们使用他的服务。因此,我需要有一些请求延迟机制来防止这种情况发生。以下是我的想法:
public async Task MyMethod(Request request)
{
  await _rateLimter.WaitForNextRequest(); // awaitable Task with calculated Delay
  await _api.DoAsync(request);
  _rateLimiter.AppendRequestCounters();
}

最安全和简单的选择是只尊重最低限制,即每2秒最多3个请求。但由于“同步”责任,需要尽可能多地使用这些限制。
因此,下一个选项是根据当前请求计数添加延迟。我已经尝试了自己的一些方法,并且也使用了 David Desmaisons的RateLimiter,这本来没问题,但是有一个问题:
假设我的客户端每天发送3个请求到API,我们将会看到:
- 每120个请求有20秒的延迟 - 每6000个请求有约15分钟的延迟
如果我的应用程序只涉及“同步”,那么这将是可以接受的,但“客户端”请求不能等待那么长时间。
我在网上搜索了有关令牌/漏桶和滑动窗口算法的信息,但由于它们主要涵盖拒绝超过限制的请求,因此我无法将它们翻译成我的情况和.NET。我找到了this repothat repo,但它们都只是服务端解决方案。
类似QoS的速率分割,使“同步”具有较慢的速率,“客户端”具有较快的速率,不是一个选项。
假设当前请求速率将被测量,如何计算下一个请求的延迟时间,以便它能够适应当前情况,尊重所有最大速率,并且不会超过5秒钟?就像在接近极限时逐渐减速一样。

尊重最高速率限制肯定是最明智的选择吧? - Lukas
每秒3个请求意味着每分钟180个,每小时10,800个,因此在这种情况下,我将超过每分钟和每小时的限制。 - AstroPiotr
抱歉,我看错了! - Lukas
你不能使用链接到的GitHub库中的组合速率限制器吗? - MindSwipe
这就是我所做的,最终导致了巨大的延迟。但让我再检查一下,也许我使用它们的方式不对。 编辑:再次检查后,仍然不是我想要的。 - AstroPiotr
1个回答

4
这可以通过使用您在GitHub上链接的库来实现。我们需要使用由3个CountByIntervalAwaitableConstraint组成的时间限制器,如下所示:
var hourConstraint = new CountByIntervalAwaitableConstraint(6000, TimeSpan.FromHours(1));
var minuteConstraint = new CountByIntervalAwaitableConstraint(120, TimeSpan.FromMinutes(1))
var secondConstraint = new CountByIntervalAwaitableConstraint(3, TimeSpan.FromSeconds(1));

var timeLimiter = TimeLimiter.Compose(hourConstraint, minuteConstraint, secondConstraint);

我们可以通过以下方式进行测试,以查看是否有效:

for (int i = 0; i < 1000; i++)
{
    await timeLimiter;
    Console.WriteLine($"Iteration {i} at {DateTime.Now:T}");
}

这将每秒运行3次,直到我们达到120次迭代(第119次迭代),然后等待一分钟结束,继续每秒运行3次。我们也可以(再次使用库)通过使用提供的AsDelegatingHandler()扩展方法,轻松地在HTTP客户端中使用TimeLimiter,如下所示:

var handler = TimeLimiter.Compose(hourConstraint, minuteConstraint, secondConstraint);
var client = new HttpClient(handler);

我们也可以使用 CancellationToken,但据我所知,不能同时将其用作HttpClient的处理程序。以下是您可以如何与 HttpClient 一起使用它:

var timeLimiter = TimeLimiter.Compose(hourConstraint, minuteConstraint, secondConstraint);
var client = new HttpClient();

for (int i = 0; i < 100; i++)
{
    await composed.Enqueue(async () =>
    {
        var client = new HttpClient();
        var response = await client.GetAsync("https://hacker-news.firebaseio.com/v0/item/8863.json?print=pretty");
        if (response.IsSuccessStatusCode)
            Console.WriteLine(await response.Content.ReadAsStringAsync());
        else
            Console.WriteLine($"Error code {response.StatusCode} reason: {response.ReasonPhrase}");
    }, new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token);
}

更好地回答OP的问题:

如果您想确保用户可以发送请求而无需等待限制结束,我们需要每秒/分钟/小时为用户分配一定数量的请求。因此,我们需要一个新的TimeLimiter,并调整我们的API TimeLimiter。以下是两个新的TimeLimiter:

var apiHourConstraint = new CountByIntervalAwaitableConstraint(5500, TimeSpan.FromHours(1));
var apiMinuteConstraint = new CountByIntervalAwaitableConstraint(100, TimeSpan.FromMinutes(1));
var apiSecondConstraint = new CountByIntervalAwaitableConstraint(2, TimeSpan.FromSeconds(1));

// TimeLimiter for calls automatically to the API
var apiTimeLimiter = TimeLimiter.Compose(apiHourConstraint, apiMinuteConstraint, apiSecondConstraint);

var userHourConstraint = new CountByIntervalAwaitableConstraint(500, TimeSpan.FromHours(1));
var userMinuteConstraint = new CountByIntervalAwaitableConstraint(20, TimeSpan.FromMinutes(1));
var userSecondConstraint = new CountByIntervalAwaitableConstraint(1, TimeSpan.FromSeconds(1));

// TimeLimiter for calls made manually by a user to the API
var userTimeLimiter = TimeLimiter.Compose(userHourConstraint, userMinuteConstraint, userSecondConstraint);

您可以根据需要调整数字。

现在来使用它:
我看到你正在使用一个中央方法来执行你的请求,这使得它更容易。我只会添加一个可选的布尔参数来确定它是自动执行的请求还是用户发出的请求。(如果您想要更多不仅仅是自动和手动请求的内容,可以将此参数替换为枚举)

public static async Task DoRequest(Request request, bool manual = false)
{
    TimeLimiter limiter;
    if (manual)
        limiter = TimeLimiterManager.UserLimiter;
    else
        limiter = TimeLimiterManager.ApiLimiter;

    await limiter;
    _api.DoAsync(request);
}

static class TimeLimiterManager
{
    public static TimeLimiter ApiLimiter { get; }

    public static TimeLimiter UserLimiter { get; }

    static TimeLimiterManager()
    {
        var apiHourConstraint = new CountByIntervalAwaitableConstraint(5500, TimeSpan.FromHours(1));
        var apiMinuteConstraint = new CountByIntervalAwaitableConstraint(100, TimeSpan.FromMinutes(1));
        var apiSecondConstraint = new CountByIntervalAwaitableConstraint(2, TimeSpan.FromSeconds(1));

        // TimeLimiter to control access to the API for automatically executed requests
        ApiLimiter = TimeLimiter.Compose(apiHourConstraint, apiMinuteConstraint, apiSecondConstraint);

        var userHourConstraint = new CountByIntervalAwaitableConstraint(500, TimeSpan.FromHours(1));
        var userMinuteConstraint = new CountByIntervalAwaitableConstraint(20, TimeSpan.FromMinutes(1));
        var userSecondConstraint = new CountByIntervalAwaitableConstraint(1, TimeSpan.FromSeconds(1));

        // TimeLimiter to control access to the API for manually executed requests
        UserLimiter = TimeLimiter.Compose(userHourConstraint, userMinuteConstraint, userSecondConstraint);
    }
}

这并不完美,因为当用户每分钟不执行20个API调用,但您的自动化系统需要每分钟执行超过100个API调用时,它将不得不等待。

关于白天/黑夜差异:您可以为Api/UserLimiter使用2个备份字段,并在属性的{ get {...} }中返回适当的字段。


“然后等待直到一分钟结束,然后每秒运行3次” - 这正是我想要避免的,就像我在帖子中所说的那样,这可能意味着等待20秒钟才能过去一分钟,并且当接近每小时6000个请求限制时,需要等待约15分钟才能过去一个小时。” - AstroPiotr
啊,我明白了。让我看看我能做些什么。我想我可以简单地为用户手动保留一些调用(例如,每分钟预留20个调用)。 - MindSwipe
是的,我考虑过了。识别上下文并为同步和用户设置单独的限制器,这不是一个坏主意,如果没有其他解决方案,我可能会选择它。但我要寻求的是更加灵活的方法,不让任何人无论如何都有大量的超时。 - AstroPiotr
1
完全没有任何大的超时时间会很困难。如果你想完全没有超时,你可以把TimeLimiter限制在1.67秒,但是(正如你所说),你就会遇到这样的问题:有时需要快速执行大量的查询。 - MindSwipe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接