我正在为外部API设计一个.NET客户端应用程序。它将承担两项主要职责:
服务文档在给定时间内可以发出的最大请求数指定以下规则:
白天:
晚上:
超过这些限制将不会立即导致锁定-不会引发异常。但是提供商可能会感到烦恼,请与我们联系,然后禁止我们使用他的服务。因此,我需要有一些请求延迟机制来防止这种情况。这是我的看法:
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个请求,我们将看到:
如果我的应用程序仅涉及“同步”,这是可以接受的,但是“客户端”请求不能等待那么长时间。
我已经搜索过Web,并且已经阅读了有关令牌/泄漏存储桶和滑动窗口算法的信息,但是我无法将它们转换为我的case和.NET,因为它们主要包含拒绝超过限制。我找到了this repo和that repo,但是它们都是服务端解决方案。
类似于QoS的速率划分,因此“同步”的速率较慢,而“客户端”的速率较快,这不是一种选择。
假设将测量当前请求速率,那么如何计算下一个请求的延迟,以使其能够适应当前情况,遵守所有最大速率,并且不会超过5秒?接近极限时逐渐变慢。
答案 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 {...} }
中返回相应的字段