C # Параллельное несколько вызовов Parallel HttpClient с тайм-аутом

У меня есть API, который должен вызывать параллельно 4 HttpClients с поддержкой параллелизма 500 пользователей в секунду (все они вызывают API одновременно)

Должен быть строгий тайм-аут, позволяющий API вернуть результат, даже если не все вызовы HttpClients вернули значение.
Конечные точки являются внешними сторонними API, и я не могу их контролировать и не знаю кода.
Я провел обширное исследование по этому вопросу, но даже если многие решения работают, мне нужно то, которое потребляет меньше ресурсов ЦП, поскольку у меня небольшой серверный бюджет.

Пока я придумал это:

var conn0 = new HttpClient
{
    Timeout = TimeSpan.FromMilliseconds(1000),
    BaseAddress = new Uri("http://endpoint")
};
var conn1 = new HttpClient
{
    Timeout = TimeSpan.FromMilliseconds(1000),
    BaseAddress = new Uri("http://endpoint")
};
var conn2 = new HttpClient
{
    Timeout = TimeSpan.FromMilliseconds(1000),
    BaseAddress = new Uri("http://endpoint")
};
var conn3 = new HttpClient
{
    Timeout = TimeSpan.FromMilliseconds(1000),
    BaseAddress = new Uri("http://endpoint")
};

var list = new List<HttpClient>() { conn0, conn1, conn2, conn3 };
var timeout = TimeSpan.FromMilliseconds(1000);
var allTasks = new List<Task<Task>>();

//the async DoCall method just call the HttpClient endpoint and return a MyResponse object
foreach (var call in list)
{
    allTasks.Add(Task.WhenAny(DoCall(call), Task.Delay(timeout)));
}
var completedTasks = await Task.WhenAll(allTasks);
var allResults = completedTasks.OfType<Task<MyResponse>>().Select(task => task.Result).ToList();
return allResults;

Я использую WhenAny и две задачи, одну для вызова, одну для тайм-аута. Если задача вызова опаздывает, другая все равно возвращается.

Теперь этот код работает отлично, и все асинхронно, но мне интересно, есть ли лучший способ добиться этого.
Каждый отдельный вызов этого API создает множество потоков, и при 500 одновременных пользователях требуется в среднем 8 (восемь) D3_V2 4-ядерные машины Azure, что приводит к сумасшедшим расходам, и чем выше тайм-аут, тем выше загрузка ЦП.

Есть ли лучший способ сделать это, не используя так много ресурсов ЦП (возможно, Parallel Linq лучше, чем этот)?

Достаточно ли одного тайм-аута HttpClient, чтобы остановить вызов и вернуться, если конечная точка не ответит вовремя, без необходимости использовать вторую задачу в WhenAny?

ОБНОВЛЕНИЕ:

  • Конечные точки являются сторонними API, я не знаю кода и не имею никакого контроля, вызов выполняется в JSON и возвращает JSON или строку.
  • Некоторые из них время от времени отвечают через 10+ секунд или застревают и работают очень медленно, поэтому тайм-аут должен освободить потоки и вернуться, даже если с частичными данными от другого, который вернулся вовремя.
  • Кэширование возможно, но только частично, поскольку данные постоянно меняются, например, акции и торговля валютой в режиме реального времени.

person Francesco Cristallo    schedule 20.02.2017    source источник
comment
Можете ли вы предоставить дополнительную информацию о типе и характере конечных точек, к которым вы звоните, является ли кэширование частью результатов жизнеспособным в вашем случае использования, почему существует жесткий тайм-аут (кажется, что это связано с затратами, но есть ли какие-либо другие причины) , у вас есть доступ к коду и инфраструктуре конечных точек или вы вызываете сторонний API?   -  person univ    schedule 20.02.2017
comment
Я добавил некоторую информацию к вопросу. Я хотел бы знать, есть ли что-то неправильное в том, как сейчас реализовано, и возможно ли сделать лучше, чем это, из-за множества параллельных/параллельных решений, которые есть у .net.   -  person Francesco Cristallo    schedule 20.02.2017


Ответы (1)


Ваш подход с использованием двух задач только для тайм-аута работает, но вы можете сделать лучше: используйте CancellationToken для задачи, а для получения ответов с сервера:

var cts = new CancellationTokenSource();
// set the timeout equal to the 1 second
cts.CancelAfter(1000);
// provide the token for your request
var response = await client.GetAsync(url, cts.Token);  

После этого вы просто можете отфильтровать completed< /а> задачи:

var allResults = completedTasks
    .Where(t => t.IsCompleted)
    .Select(task => task.Result).ToList();

Такой подход уменьшит количество создаваемых вами задач не менее чем в два раза и снизит нагрузку на ваш сервер. Кроме того, он предоставит вам простой способ отменить некоторую часть обработки или даже всю обработку. Если ваши задачи полностью независимы друг от друга, вы можете использовать Parallel.For для вызова http-клиентов, но по-прежнему использовать токен для отмены операции:

ParallelLoopResult result = Parallel.For(list, call => DoCall(call, cts.Token));
// handle the result of the parallel tasks

или, используя PLINQ:

var results = list
    .AsParallel()
    .Select(call => DoCall(call, cts.Token))
    .ToList();
person VMAtm    schedule 20.02.2017
comment
Я попробовал подход с маркером отмены, но токен отмены не всегда соблюдается Httpclient. Я провел некоторые исследования и почти все время сталкивался с этой проблемой: «отмена запроса httpclient, почему taskcanceledException CancellationToke»> stackoverflow.com/questions/29319086/ httpclient создает новый CancelToken и пытается выполнить действие, которое в случае занимает, скажем, 10 секунд, блокирует все API на эту сумму времени. Вот почему я выбрал строгое решение с двумя задачами, но оно требует слишком больших вычислительных ресурсов. - person Francesco Cristallo; 21.02.2017
comment
Я изучу и попробую подход PLINQ и опубликую обновление, спасибо за вашу помощь. - person Francesco Cristallo; 21.02.2017