Соединение было закрыто, ошибка между HttpClient и веб-службой ASP.NET Core 2.0

У меня есть веб-служба ASP.NET Core 2.0, работающая в IIS. Один из методов контроллера выглядит примерно так:

[HttpGet()]
public IActionResult Test()
{
    // do some db updates and get data
    var result = DoSomeStuff();
    // serialize data to byte array
    var output = Serialize(result);

    return File(output, "application/octet-stream");
}

Он выполняет некоторые обновления базы данных, запрашивает записи из таблицы, сериализует данные и отправляет их в качестве ответа. Данные отправляются в двоичном формате. Я использую MessagePack-CSharp в качестве сериализатора.

Затем у меня есть клиентское приложение, которое взаимодействует с этим веб-сервисом. Это библиотека .NET Standard 2.0, на которую ссылается консольное приложение .NET 4.6.1. Я использую HttpClient для запроса и HttpResponseMessage.Content.ReadAsByteArrayAsync() для чтения ответа (точный код см. ниже).

Я хотел сделать несколько тестов. Моя таблица имеет cca. 80 столбцов и содержит прибл. 140000 записей. Все они должны быть отправлены клиенту. Получение данных из БД занимает несколько секунд, затем все сериализуется и результат cca. Клиенту отправляется 34 МБ.

У меня 10 клиентов. Когда они вызывают веб-сервис последовательно, все работает. Когда я параллельно нагружаю веб-сервис и fire-клиенты, я почти всегда получаю ошибку на некоторых из них (обычно один или два выходят из строя, иногда даже 4-5).

Исключение следующее, и оно возникает из вызова ReadAsByteArrayAsync:

System.AggregateException: One or more errors occurred. ---> System.Net.Http.HttpRequestException: Error while copying content to a stream. ---> System.IO.IOException: Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host. ---> System.Net.Sockets.SocketException: An existing connection was forcibly closed by the remote host
   at System.Net.Sockets.Socket.EndReceive(IAsyncResult asyncResult)
   at System.Net.Sockets.NetworkStream.EndRead(IAsyncResult asyncResult)
   --- End of inner exception stack trace ---
   at System.Net.ConnectStream.EndRead(IAsyncResult asyncResult)
   at System.IO.Stream.<>c.<BeginEndReadAsync>b__43_1(Stream stream, IAsyncResult asyncResult)
   at System.Threading.Tasks.TaskFactory`1.FromAsyncTrimPromise`1.Complete(TInstance thisRef, Func`3 endMethod, IAsyncResult asyncResult, Boolean requiresSynchronization)
...
---> (Inner Exception #0) System.Net.Http.HttpRequestException: Error while copying content to a stream. ---> System.IO.IOException: Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host. ---> System.Net.Sockets.SocketException: An existing connection was forcibly closed by the remote host
   at System.Net.Sockets.Socket.EndReceive(IAsyncResult asyncResult)
   at System.Net.Sockets.NetworkStream.EndRead(IAsyncResult asyncResult)
   --- End of inner exception stack trace ---
   at System.Net.ConnectStream.EndRead(IAsyncResult asyncResult)
   at System.IO.Stream.<>c.<BeginEndReadAsync>b__43_1(Stream stream, IAsyncResult asyncResult)
   at System.Threading.Tasks.TaskFactory`1.FromAsyncTrimPromise`1.Complete(TInstance thisRef, Func`3 endMethod, IAsyncResult asyncResult, Boolean requiresSynchronization)
...

Я нашел несколько потоков SO, связанных с таким исключением (например, здесь), поэтому я сначала подумал, что это проблема, связанная с клиентом. Предлагаемые ответы:

  • переход на HTTP 1.0
  • установка Connection: close вместо Connection: keep-alive
  • наоборот пункт выше

У меня ничего не получалось. Кажется, я где-то читал, что в HttpClient была какая-то ошибка (сейчас не могу найти источник). Я попытался использовать новейший пакет System.Net.Http от Nuget. Та же проблема. Я создал консольное приложение .NET Core и использую базовую версию HttpClient. Та же проблема. Я использовал HttpWebRequest вместо HttpClient. Та же основная проблема.

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

Итак, я получил следующий упрощенный код (всего одно приложение с 10 потоками):

private async void Test_Click(object sender, RoutedEventArgs e)
{
    try
    {
        var tasks = Enumerable.Range(1, 10).Select(async i => await Task.Run(async () => await GetContent(i))).ToList();

        await Task.WhenAll(tasks);

        MessageBox.Show(String.Join(Environment.NewLine, tasks.Select(t => t.Result.ToString())));
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.ToString());
    }
}

private async Task<Int32> GetContent(Int32 id)
{
    using (var httpClient = new HttpClient())
    {
        var url = "http://localhost/TestService/api/test";

        using (var responseMessage = await httpClient.GetAsync(url).ConfigureAwait(false))
        {
            // just read everything and return length
            // ReadAsByteArrayAsync throws sometimes an exception
            var content = await responseMessage.Content.ReadAsByteArrayAsync();
            return content.Length;
        }
    }
}

Меня интересовал фактический трафик, поэтому я установил Fiddler. Когда возникает ошибка, Fiddler показывает, что ответ действительно поврежден и фактически отправлена ​​только часть предполагаемого объема данных (6 МБ, 20 МБ, ... вместо 34 МБ). Кажется, что он прерывается случайно. Я немного поиграл с Wireshark и увидел, что пакет RST/ACK отправляется с сервера, но я недостаточно хорош для анализа связи такого низкого уровня.

Итак, я сосредоточился на серверной части. Конечно, я дважды проверил, есть ли какие-либо исключения в методе контроллера. Все работает нормально. Я установил уровень журнала для трассировки и обнаружил в журнале следующее:

info: Microsoft.AspNetCore.Server.Kestrel[28]
      Connection id "0HL89D9NUNEOQ", Request id "0HL89D9NUNEOQ:00000001": the connection was closed becuase the response was not read by the client at the specified minimum data rate.
dbug: Microsoft.AspNetCore.Server.Kestrel[10]
      Connection id "0HL89D9NUNEOQ" disconnecting.
...
info: Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv[14]
      Connection id "0HL89D9NUNEOQ" communication error.
Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networking.UvException: Error -4081 ECANCELED operation canceled

Я не нашел ничего интересного и специфичного для ASP.NET Core, связанного с этой ошибкой. Согласно этой документации, IIS имеет возможность указать минимальная пропускная способность, когда он отправляет ответ клиенту, со следующей настройкой:

<system.applicationHost>
  <webLimits minBytesPerSecond="0"/>
</system.applicationHost>

Я использую его в своем Web.config, но он не имеет никакого эффекта (применяется ли он к приложениям ASP.NET Core или это только полная настройка фреймворка?).

Пробовал вернуть FileStreamResult вместо FileContentResult, но опять же - не помогло.

Как и в случае с клиентом, я пытался найти минимальный воспроизводимый код и для серверной части. Метод просто имел Thread.Sleep(8000) (вместо вызова db), затем сгенерировал случайный 50-мегабайтный массив байтов и вернул его. Это сработало без каких-либо проблем, поэтому, думаю, я продолжу расследование в этом направлении. Я знаю, что db может быть узким местом здесь, но не уверен, как это могло вызвать это (без исключения тайм-аута, без взаимоблокировки,...).

Любые советы? Я хотел бы, по крайней мере, знать, действительно ли это проблема, связанная с сервером или клиентом.


person Stalker    schedule 05.10.2017    source источник


Ответы (1)


Похоже, ваша пропускная способность падает ниже минимальной скорости передачи данных. Это поведение описано в Основы Kestrel:

Kestrel проверяет каждую секунду, поступают ли данные с указанной скоростью в байтах в секунду. Если скорость падает ниже минимальной, время соединения истекает. Льготный период — это количество времени, которое Kestrel дает клиенту, чтобы увеличить скорость отправки до минимума; курс в это время не проверяется. Льготный период помогает избежать разрыва соединений, которые изначально отправляют данные с низкой скоростью из-за медленного старта TCP.

Минимальная скорость по умолчанию составляет 240 байт/с с 5-секундным льготным периодом.

Минимальная ставка также применяется к ответу. Код для установки лимита запросов и лимита ответов одинаков, за исключением наличия RequestBody или Response в именах свойств и интерфейсов.

Вы можете настроить это в Program.cs следующим образом:

var host = new WebHostBuilder() 
    .UseKestrel(options => 
    { 
        options.Limits.MinResponseDataRate = null;
    })

Установка для этого параметра значения null означает, что минимальная скорость передачи данных не должна применяться.

person Knelis    schedule 05.10.2017
comment
Большое спасибо, кажется, это работает. Но что меня постоянно беспокоит, так это то, почему это происходит. Я полагаю, что объем передаваемых данных не настолько велик, чтобы клиенты с трудом обрабатывали ответ. Кроме того, я думаю, что игнорирование этого ограничения делает сервис уязвимым для Slow Client Attack. - person Stalker; 05.10.2017
comment
Такая же проблема у меня. У меня есть клиент Java и сервер NetCore 2, и с тех пор, как я обновился до NetCore 2, мы экспериментировали со случайными проблемами в некоторых соединениях между клиентом и сервером, включая загрузку файлов. Большое спасибо - person daniherculano; 10.11.2017
comment
Еще раз привет @Knelis, после вашего решения я мог каждый раз загружать файлы своим клиентам, но иногда у меня уже есть другое сообщение в моем журнале, и мой параметр действия равен нулю, несмотря на то, что для MinResponseDataRate задано значение null. Пишет: время ожидания запроса истекло, так как клиент не отправил его со скоростью не менее 240 байт/с - person daniherculano; 13.11.2017
comment
@daniherculano Похоже, вы нажимаете MinRequestBodyDataRate вместо MinResponseDataRate - person halter73; 17.11.2017