Почему у меня плохая производительность с Task.WhenAll, RestSharp и Async-Await в шлюзе API? - PullRequest
2 голосов
/ 02 мая 2019

Я пишу API-интерфейс поиска, который объединяет результаты нескольких API-интерфейсов микросервисов, используя async-await и ожидающий Task.WhenAll.Самое большее, я делаю 11 вызовов API микросервиса за запрос к API поиска.Тем не менее, я наблюдаю непоследовательную производительность - иногда API поиска занимает 400 миллисекунд (приемлемо), а в других - более секунды (неприемлемо), даже когда я запускаю локально / без нагрузки.Кажется, что большая часть замедления происходит вокруг ожидающего Task.WhenAll.

Три наблюдения:

  • API поиска иногда запускает 1/3 или 1/2 вызововк API микросервиса, затем ждет 600-1000 мс, чтобы запустить следующую связку.Такое поведение пакетирования несовместимо.

  • API поиска иногда вызывает каждый API-интерфейс микросервиса, получает результаты и ожидает еще 500-1000 мс.Например, API поиска может занять 1000 мс, все запросы микросервисов начинаются в пределах 100 мс друг от друга и внутренне завершаются через 400 мс или меньше.Тем не менее, API поиска просто продолжает оставаться в состоянии «Когда» все еще в течение 500 мс после того, как все ответы отправлены обратно.

  • Это поведение не соответствует.Если я выполню это 30 раз, возможно, 5 или 10 из них займут сверхдлинные по сравнению с другими 25 или 20.

Все API написаны на ASP.NET Core MVC, работающем в LinuxDocker-контейнеры в одной Docker-сети (должны быть локальными друг для друга) и являются асинхронными.

В каждом из API я записываю продолжительность входящих запросов, поэтому у меня есть полное представление о времени и продолжительности всего запроса в приложениях.

Шаги, которые я пробовал

  • http-клиентом RestSharp, но я попытался заменить его на .NET HttpClient без каких-либо существенных различий.

  • Пробовал с помощью Parallel.ForEach, который на самом деле был медленнее.

  • Попытка изменить отдельные контроллеры API микросервиса, чтобы не выполнять какую-либо обработку - просто немедленно вернуть 200 без тела - но все равно получить то же поведение.

Контроллер

[HttpGet("get-something")]
public async Task<IEnumerable<int>> GetSomethingAsync(int id)
{
    //search engine is a scoped dependency injected via DI
    return await _searchEngine.Search(id);
}

SearchEngine

public async Task<IEnumerable<int>> Search(int id)
{
    //both clients are transient dependencies injected via DI
    var iceTasks = _iceClient.FetchTasksAsync(id).ToList();
    var fireTasks = _fireClient.FetchTasksAsync(id).ToList();

    var results = await Task.WhenAll(iceTasks.Union(fireTasks));

    return results.SelectMany(r => r.Data);
}

Базовый класс IceClient и FireClient

public List<Task<IRestResponse<IEnumerable<int>>>> FetchTasksAsync(int id)  
{
    var taskList = new List<Task<IRestResponse<IEnumerable<int>>>>();
    var restClient = new RestClient()
    {
        Timeout = 20000 //in ms
    };

    foreach (var url in _urlFactory.Create())
    {
        var task = restClient.ExecuteGetTaskAsync<IEnumerable<int>>(new RestRequest(url));

        taskList.Add(task);
    }

    return taskList;
}

Ожидание Каждый из вызовов API должен начинаться в течение 30 мс друг от друга, а общая продолжительность поиска приблизительно равна продолжительности самой длинной продолжительности API микросервиса.

...