HttpClient GetAsync ThreadPool ความอดอยาก

เรามีสแต็กแบ็กเอนด์ที่เน้นไมโครเซอร์วิส ไมโครเซอร์วิสทั้งหมดที่สร้างขึ้นบน Nancy และลงทะเบียนเป็นบริการ windows ด้วย topshelf

หนึ่งในบริการที่จัดการการรับส่งข้อมูลส่วนใหญ่ (~ 5,000 ความต้องการ/วินาที) เริ่มมีปัญหาการขาดแคลนเธรดพูลบนเซิร์ฟเวอร์ 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 ซึ่งอาจทำให้การเรียกค้นเธรดที่กำลังประมวลผลคำขอไปยังเธรดพูลหยุดชะงัก หรือที่แย่กว่านั้นคือทำให้เกิด การหยุดชะงักในโค้ดของคุณ (นี่คือสาเหตุที่ทำให้คุณ ไม่ควรบล็อกโค้ด async) ฉันไม่แน่ใจว่าคุณเคยเห็นคำขอเหล่านั้นได้รับการประมวลผลอย่างละเอียดหรือไม่ แต่ถ้า NancyFX กำลังจัดการการจัดบริบทการซิงโครไนซ์ให้คุณ (ซึ่งดูเหมือนว่า มัน) ซึ่งอาจเป็นต้นตอของความอดอยากของเธรดพูลได้เป็นอย่างดี

คุณสามารถแก้ไขสิ่งนี้ได้โดยทำให้การประมวลผล IO ทั้งหมดทำงานภายในเมธอด 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