簡體   English   中英

限制 HttpClient 請求的簡單方法

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM