2017-10-05 8 views
4

J'ai un service Web ASP.NET Core 2.0 s'exécutant sur IIS. L'une des méthodes de contrôleur ressemble plus ou moins comme ceci:Erreur liée à la connexion entre HttpClient et le service Web ASP.NET Core 2.0

[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"); 
} 

Il fait quelques mises à jour de bases de données, les enregistrements de la requête à partir d'une table, sérialiser les données et les envoyer en réponse. Les données sont envoyées au format binaire. J'utilise MessagePack-CSharp comme sérialiseur.

Puis j'ai une application cliente qui communique avec ce webservice. C'est la bibliothèque .NET Standard 2.0, qui est référencée depuis l'application de console .NET 4.6.1. J'utilise HttpClient pour demander et HttpResponseMessage.Content.ReadAsByteArrayAsync() pour lire la réponse (code exact voir ci-dessous).

Je voulais faire quelques tests. Ma table a cca. 80 colonnes et contient cca. 140000 enregistrements. Tous sont censés être envoyés au client. Obtenir des données de db prend quelques secondes, alors tout est sérialisé et résultat de cca. 34 Mo est envoyé au client.

J'ai 10 clients. Quand ils appellent en série le webservice, tout fonctionne. Quand j'insiste sur le webservice et tire les clients en parallèle, j'ai presque toujours une erreur sur certains d'entre eux (généralement un ou deux échoue, parfois même 4-5).

Exception suit et il est passé de ReadAsByteArrayAsync appel:

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) 
... 

J'ai trouvé plusieurs SO sujets liés à cette exception (par exemple here), donc je pensais d'abord c'est un problème lié à la clientèle. Les réponses proposées:

  • passage à HTTP 1.0
  • réglage Connection: close au lieu de Connection: keep-alive
  • point de vice-versa ci-dessus

Rien n'a fonctionné pour moi. Je pense que j'ai lu quelque part qu'il y avait un bug dans HttpClient (impossible de trouver la source maintenant). J'ai essayé d'utiliser le plus nouveau paquet System.Net.Http de Nuget. Même problème. J'ai créé l'application console .NET Core et utiliser la version Core de HttpClient. Même problème. J'ai utilisé HttpWebRequest au lieu de HttpClient. Même problème sous-jacent J'exécutais webservice et clients sur la même machine VM. Juste pour exclure certains problèmes locaux, je cours des clients simultanément d'autres ordinateurs. Même problème.

donc j'ai fini avec le code suivant simplifié (juste une application avec 10 fils):

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; 
     } 
    } 
} 

j'étais curieux de savoir le trafic réel, donc je configuration Fiddler. Lorsque l'erreur se produit, Fiddler montre que la réponse est en effet corrompue et qu'une partie de la quantité supposée de données a été réellement envoyée (6MB, 20MB, ... au lieu de 34MB). On dirait qu'il est interrompu au hasard. J'ai joué un certain temps avec Wireshark et j'ai vu que le paquet RST/ACK était envoyé depuis le serveur, mais je ne suis pas assez bon pour analyser une telle communication de bas niveau. Donc, je me suis concentré sur le côté serveur. Bien sûr, j'ai revérifié s'il y a une exception dans la méthode du contrôleur. Tout fonctionne bien.Je mets le niveau de journalisation de trace et trouvé suivante dans le journal:

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 

Je ne trouve pas quelque chose d'intéressant et ASP.NET de base spécifique lié à cette erreur. Selon this documentation, IIS a une option pour spécifier débit minimal, quand il envoie une réponse au client, avec réglage suivant:

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

Je l'utilise dans mon Web.config, mais il n'a pas d'effet (est-elle appliquée aux applications ASP.NET Core ou est-ce le réglage complet du framework uniquement?).

J'ai essayé de retourner FileStreamResult au lieu de FileContentResult, mais encore une fois - cela n'a pas aidé.

De même pour le client, j'ai essayé de trouver un code reproductible minimal pour le côté serveur aussi. La méthode a juste eu Thread.Sleep(8000) (au lieu de l'appel de DB), alors a produit le tableau aléatoire de 50 Mo d'octets et l'a renvoyé. Cela a fonctionné sans aucun problème, donc je suppose que je continuerai à enquêter dans cette direction. Je suis conscient que DB pourrait être un goulot d'étranglement ici, mais je ne sais pas comment il pourrait causer cela (pas d'exception de délai, pas de blocage, ...).

Des conseils? Je voudrais au moins savoir si c'est un problème de serveur ou vraiment client.

Répondre

2

Il semble que votre débit soit inférieur au débit minimum. Ce comportement est décrit dans Kestrel Fundamentals:

Kestrel vérifie toutes les secondes si les données arrivent au débit spécifié en octets/seconde. Si le débit est inférieur au minimum, la connexion est expirée. La période de grâce est le temps que Kestrel donne au client pour augmenter son taux d'envoi au minimum; le taux n'est pas vérifié pendant cette période. La période de grâce permet d'éviter la perte de connexions qui envoient initialement des données à un rythme lent en raison du démarrage lent de TCP.

Le taux minimum par défaut est de 240 octets/seconde, avec une période de grâce de 5 secondes.

Un taux minimum s'applique également à la réponse. Le code pour définir la limite de demande et la limite de réponse est la même sauf pour avoir RequestBody ou Response dans les noms de propriété et d'interface.

Vous pouvez configurer cela dans Program.cs comme ceci:

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

La définition de cette option null indique pas le débit de données minimum doit être appliquée.

+0

Merci beaucoup, cela semble fonctionner. Mais ce qui me dérange, c'est pourquoi cela arrive-t-il? Je suppose que la quantité de données transférées n'est pas énorme pour que les clients aient du mal à traiter la réponse. En outre, ignorer cette limite rend le service vulnérable à Slow Client Attack, je suppose. – Stalker

+0

Même problème que le mien. J'ai un client Java et NetCore 2 Server et depuis que j'ai mis à jour à NetCore 2 nous avons expérimenté des problèmes aléatoires dans certaines connexions entre le client et le serveur, y compris les téléchargements de fichiers. Merci beaucoup – daniherculano

+0

Salut à nouveau @Knelis, après votre solution je pourrais télécharger des fichiers à mes clients à chaque fois mais j'ai déjà un message différent sur mon journal et mon paramètre d'action est nul, malgré le réglage de MinResponseDataRate à null. Il dit: _request expiré car il n'a pas été envoyé par le client à un minimum de 240 octets/seconde_ – daniherculano