HttpClient GetAsync ThreadPool голодает

У нас есть серверный стек, ориентированный на микросервисы. Все микросервисы созданы поверх Nancy и зарегистрированы как службы Windows с topshelf.

У одного из сервисов, который обрабатывает большую часть трафика (~ 5000 запросов в секунду), возникла проблема с голоданием пула потоков на 3 из 8 серверов.

Это исключение, которое мы получаем при достижении определенной конечной точки:

System.InvalidOperationException: There were not enough free threads in the ThreadPool to complete the operation.
   at System.Net.HttpWebRequest.BeginGetResponse(AsyncCallback callback, Object state)
   at System.Net.Http.HttpClientHandler.StartGettingResponse(RequestState state)
   at System.Net.Http.HttpClientHandler.StartRequest(Object obj)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at RandomNamedClient.<GetProductBySkuAsync>d__20.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at ProductService.<GetBySkuAsync>d__3.MoveNext() in ...\ProductService.cs:line 34
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at ProductModule.<>c__DisplayClass15.<<.ctor>b__b>d__1d.MoveNext() in ...\ProductModule.cs:line 32

Эта конечная точка вызывает другую службу, которая находится вне домена моей команды, чтобы получить данные о продукте. Реализация его следующая:

Get["/product/sku/{sku}", true] = async (parameters, ctx) =>
{
    string sku = parameters.sku;
    var product = await productService.GetBySkuAsync(sku);
    return Response.AsJson(new ProductRepresentation(product));
};

ProductService.GetBySkuAsync(string sku) реализация:

public async Task<Product> GetBySkuAsync(string sku)
{
    var productDto = await randomNamedClient.GetProductBySkuAsync(sku);
    if (productDto == null)
    {
        throw new ProductDtoNotFoundException("sku", sku);
    }

    var variantDto = productDto.VariantList.FirstOrDefault(v => v.Sku == sku);

    if (variantDto == null)
    {
        throw new ProductVariantDtoNotFoundException("sku", sku);
    }

    return MapVariantDtoToProduct(variantDto, productDto);
}

RandomNamedClient.GetProductBySkuAsync(string sku) реализация (это из внутреннего пакета):

public async Task<ProductDto> GetProductBySkuAsync(string sku)
{
  HttpResponseMessage result = await this._serviceClient.GetAsync("Product?Sku=" + sku);
  return result == null || result.StatusCode != HttpStatusCode.OK ? (ProductDto) null : this.Decompress<ProductDto>(result);
}

RandomNamedClient.Decompress<T>(HttpResponseMessage response) реализация:

private T Decompress<T>(HttpResponseMessage response)
{
  if (!response.Content.Headers.ContentEncoding.Contains("gzip"))
    return HttpContentExtensions.ReadAsAsync<T>(response.Content).Result;
  using (GZipStream gzipStream = new GZipStream((Stream) new MemoryStream(response.Content.ReadAsByteArrayAsync().Result), CompressionMode.Decompress))
  {
    byte[] buffer = new byte[8192];
    using (MemoryStream memoryStream = new MemoryStream())
    {
      int count;
      do
      {
        count = gzipStream.Read(buffer, 0, 8192);
        if (count > 0)
          memoryStream.Write(buffer, 0, count);
      }
      while (count > 0);
      return JsonConvert.DeserializeObject<T>(Encoding.UTF8.GetString(memoryStream.ToArray()));
    }
  }
}

Все наши сервисы построены как Release/32-bit. Мы ничего не меняли в использовании пула потоков.


person onatm    schedule 09.03.2016    source источник


Ответы (1)


Самой большой проблемой, которую я вижу в этом коде, является метод Decompress<T>, который блокирует асинхронные операции с использованием Task.Result. Это потенциально может затормозить получение потока, в данный момент обрабатывающего запрос к пулу потоков, или, что еще хуже, вызвать взаимоблокировки в вашем коде (именно поэтому вы не должен блокировать асинхронный код). Я не уверен, видели ли вы, что эти запросы тщательно обрабатываются, но если NancyFX обрабатывает маршалинг контекста синхронизации для вас (что выглядит как делает), что вполне может быть основной причиной голодания пулов потоков.

Вы можете изменить это, заставив всю обработку ввода-вывода работать внутри этого метода async, и воспользоваться преимуществами естественного асинхронного API, который эти классы уже предоставляют. В качестве альтернативы, и я определенно не рекомендую этого делать, вы можете использовать ConfigureAwait(false) везде.

(Примечание: вы можете упростить свой код, используя Stream.CopyToAsync())

Правильная асинхронная реализация будет выглядеть так:

private async Task<T> DecompressAsync<T>(HttpResponseMessage response)
{
    if (!response.Content.Headers.ContentEncoding.Contains("gzip"))
        return await response.Content.ReadAsAsync<T>();

    const int bufferSize = 8192;        
    using (GZipStream gzipStream = new GZipStream(
                                   new MemoryStream(
                                        await response.Content.ReadAsByteArrayAsync()), 
                                        CompressionMode.Decompress))
    using (MemoryStream memoryStream = new MemoryStream())
    {
        await gzipStream.CopyToAsync(memoryStream, bufferSize);
        return JsonConvert.DeserializeObject<T>(
                    Encoding.UTF8.GetString(memoryStream.ToArray()));
    }
}
person Yuval Itzchakov    schedule 09.03.2016
comment
спасибо за ваш подробный ответ. Я подозревал, что распаковка блокирует нас при выполнении. Я сообщу команде, поддерживающей клиентскую библиотеку, и, вероятно, приму это как ответ. - person onatm; 09.03.2016
comment
@onatm Надеюсь, это поможет! - person Yuval Itzchakov; 09.03.2016
comment
@onatm Приятно слышать. - person Yuval Itzchakov; 14.03.2016