客户端请求速率限制

时间:2019-08-16 09:52:13

标签: c# .net rate-limiting

我正在为外部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个请求。但是由于“同步”责任,有必要尽可能多地使用这些限制。

因此下一个选择是根据当前请求数添加延迟。我已经尝试过自己做某事,也曾经使用过RateLimiter by David Desmaisons,这很好,但这是一个问题:

假设每天我的客户端每秒向API发送3个请求,我们将看到:

  • 每120个请求有20秒的延迟
  • 每6000个请求将延迟约15分钟

如果我的应用程序仅涉及“同步”,这是可以接受的,但是“客户端”请求不能等待那么长时间。

我已经搜索过Web,并且已经阅读了有关令牌/泄漏存储桶和滑动窗口算法的信息,但是我无法将它们转换为我的case和.NET,因为它们主要包含拒绝超过限制。我找到了this repothat repo,但是它们都是服务端解决方案。

类似于QoS的速率划分,因此“同步”的速率较慢,而“客户端”的速率较快,这不是一种选择。

假设将测量当前请求速率,那么如何计算下一个请求的延迟,以使其能够适应当前情况,遵守所有最大速率,并且不会超过5秒?接近极限时逐渐变慢。

1 个答案:

答案 0 :(得分:2)

这可以通过使用您在GitHub上链接的库来实现。我们需要使用由3 CountByIntervalAwaitableConstraint组成的组合TimeLimiter,如下所示:

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()扩展方法来轻松地将TimeLimiter与HTTP客户端一起使用,如下所示:

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。这是两个新的:

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);

您可以根据自己的需要玩数字游戏。

现在可以使用它:
我看到您正在使用中央方法来执行您的请求,这使操作变得更容易。我将仅添加一个可选的布尔参数,该参数确定它是自动执行的请求还是来自用户的请求。 (如果您不仅需要自动和手动请求,还可以用Enum替换此参数)

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 {...} }中返回相应的字段