У меня есть веб-служба 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 может быть узким местом здесь, но не уверен, как это могло вызвать это (без исключения тайм-аута, без взаимоблокировки,...).
Любые советы? Я хотел бы, по крайней мере, знать, действительно ли это проблема, связанная с сервером или клиентом.