[英]Simple way to rate limit HttpClient requests
我正在使用 System.Net.Http 中的 HTTPClient 向 API 發出請求。 API 限制為每秒 10 個請求。
我的代碼大致是這樣的:
List<Task> tasks = new List<Task>();
items..Select(i => tasks.Add(ProcessItem(i));
try
{
await Task.WhenAll(taskList.ToArray());
}
catch (Exception ex)
{
}
ProcessItem 方法做了一些事情,但總是使用以下方法調用 API: await SendRequestAsync(..blah)
。 看起來像:
private async Task<Response> SendRequestAsync(HttpRequestMessage request, CancellationToken token)
{
token.ThrowIfCancellationRequested();
var response = await HttpClient
.SendAsync(request: request, cancellationToken: token).ConfigureAwait(continueOnCapturedContext: false);
token.ThrowIfCancellationRequested();
return await Response.BuildResponse(response);
}
最初代碼運行良好,但當我開始使用 Task.WhenAll 時,我開始從 API 收到“超出速率限制”消息。 如何限制發出請求的速率?
值得注意的是,ProcessItem 可以根據項目進行 1-4 次 API 調用。
API 限制為每秒 10 個請求。
然后讓您的代碼執行一批 10 個請求,確保它們至少需要一秒鍾:
Items[] items = ...;
int index = 0;
while (index < items.Length)
{
var timer = Task.Delay(TimeSpan.FromSeconds(1.2)); // ".2" to make sure
var tasks = items.Skip(index).Take(10).Select(i => ProcessItemsAsync(i));
var tasksAndTimer = tasks.Concat(new[] { timer });
await Task.WhenAll(tasksAndTimer);
index += 10;
}
更新
我的 ProcessItems 方法根據項目進行 1-4 次 API 調用。
在這種情況下,批處理不是一個合適的解決方案。 您需要將異步方法限制為一定數量,這意味着SemaphoreSlim
。 棘手的部分是您希望隨着時間的推移允許更多調用。
我沒有嘗試過這段代碼,但我會采用的一般想法是有一個周期函數,最多釋放信號量 10 次。 所以,像這樣:
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(10);
private async Task<Response> ThrottledSendRequestAsync(HttpRequestMessage request, CancellationToken token)
{
await _semaphore.WaitAsync(token);
return await SendRequestAsync(request, token);
}
private async Task PeriodicallyReleaseAsync(Task stop)
{
while (true)
{
var timer = Task.Delay(TimeSpan.FromSeconds(1.2));
if (await Task.WhenAny(timer, stop) == stop)
return;
// Release the semaphore at most 10 times.
for (int i = 0; i != 10; ++i)
{
try
{
_semaphore.Release();
}
catch (SemaphoreFullException)
{
break;
}
}
}
}
用法:
// Start the periodic task, with a signal that we can use to stop it.
var stop = new TaskCompletionSource<object>();
var periodicTask = PeriodicallyReleaseAsync(stop.Task);
// Wait for all item processing.
await Task.WhenAll(taskList);
// Stop the periodic task.
stop.SetResult(null);
await periodicTask;
答案與此類似。
不使用任務列表和WhenAll
,而是使用Parallel.ForEach並使用ParallelOptions將並發任務的數量限制為 10,並確保每個任務至少需要 1 秒:
Parallel.ForEach(
items,
new ParallelOptions { MaxDegreeOfParallelism = 10 },
async item => {
ProcessItems(item);
await Task.Delay(1000);
}
);
或者,如果您想確保每個項目都花費盡可能接近 1 秒的時間:
Parallel.ForEach(
searches,
new ParallelOptions { MaxDegreeOfParallelism = 10 },
async item => {
var watch = new Stopwatch();
watch.Start();
ProcessItems(item);
watch.Stop();
if (watch.ElapsedMilliseconds < 1000) await Task.Delay((int)(1000 - watch.ElapsedMilliseconds));
}
);
或者:
Parallel.ForEach(
searches,
new ParallelOptions { MaxDegreeOfParallelism = 10 },
async item => {
await Task.WhenAll(
Task.Delay(1000),
Task.Run(() => { ProcessItems(item); })
);
}
);
更新的答案
我的 ProcessItems 方法根據項目進行 1-4 次 API 調用。 因此,批量大小為 10 時,我仍然超出了速率限制。
您需要在 SendRequestAsync 中實現滾動窗口。 包含每個請求的時間戳的隊列是一種合適的數據結構。 您將時間戳超過 10 秒的條目出列。 碰巧的是,有一個實現作為對 SO 上類似問題的答案。
原答案
可能對其他人仍然有用
處理此問題的一種直接方法是將您的請求以 10 為一組進行批處理,同時運行這些請求,然后等待總共 10 秒過去(如果還沒有)。 如果這批請求可以在 10 秒內完成,這將使您正確地達到速率限制,但如果這批請求需要更長的時間,則效果不佳。 看一看在.Batch()擴展方法MoreLinq 。 代碼看起來大概像
foreach (var taskList in tasks.Batch(10))
{
Stopwatch sw = Stopwatch.StartNew(); // From System.Diagnostics
await Task.WhenAll(taskList.ToArray());
if (sw.Elapsed.TotalSeconds < 10.0)
{
// Calculate how long you still have to wait and sleep that long
// You might want to wait 10.5 or 11 seconds just in case the rate
// limiting on the other side isn't perfectly implemented
}
}
https://github.com/thomhurst/EnumerableAsyncProcessor
我編寫了一個庫來幫助處理這種邏輯。
用法是:
var responses = await AsyncProcessorBuilder.WithItems(items) // Or Extension Method: items.ToAsyncProcessorBuilder()
.SelectAsync(item => ProcessItem(item), CancellationToken.None)
.ProcessInParallel(levelOfParallelism: 10, TimeSpan.FromSeconds(1));
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.