Interblocage lors de l’access à StackExchange.Redis

Je suis dans une situation de blocage en appelant StackExchange.Redis .

Je ne sais pas exactement ce qui se passe, ce qui est très frustrant, et j’apprécierais toute consortingbution qui pourrait aider à résoudre ou à résoudre ce problème.


Si vous avez ce problème aussi et que vous ne voulez pas lire tout cela, Je suggère que vous essayez de définir PreserveAsyncOrder sur false .

 ConnectionMultiplexer connection = ...; connection.PreserveAsyncOrder = false; 

Cela résoudra probablement le type d’impasse dans laquelle se déroule cette Q & A et pourrait également améliorer les performances.


Notre configuration

  • Le code est exécuté en tant qu’application Console ou en tant que rôle Azure Worker.
  • Il expose une api REST en utilisant HttpMessageHandler afin que le point d’entrée soit asynchrone.
  • Certaines parties du code ont une affinité de thread (qui appartient à et doit être exécutée par un seul thread).
  • Certaines parties du code sont uniquement asynchrones.
  • Nous effectuons les anti-patterns synchrone sur asynchrone et async-over-sync . (mélange en await et Wait() / Result ).
  • Nous utilisons uniquement des méthodes asynchrones pour accéder à Redis.
  • Nous utilisons StackExchange.Redis 1.0.450 pour .NET 4.5.

Impasse

Lorsque l’application / le service est démarré, il s’exécute normalement pendant un certain temps, puis soudainement (presque) toutes les demandes entrantes cessent de fonctionner, elles ne produisent jamais de réponse. Toutes ces demandes sont bloquées dans l’attente d’un appel à Redis.

Il est intéressant de noter qu’une fois que l’impasse se produit, tout appel à Redis est suspendu, mais uniquement si ces appels sont effectués à partir d’une demande d’API entrante, exécutée sur le pool de threads.

Nous faisons également des appels à Redis à partir de threads d’arrière-plan de faible priorité et ces appels continuent à fonctionner même après le blocage.

Il semble qu’un blocage se produise uniquement lorsque vous appelez Redis sur un thread de pool de threads. Je ne pense plus que cela est dû au fait que ces appels sont effectués sur un thread de pool de threads. Plutôt, il semble que tout appel Redis asynchrone sans continuation, ou avec une poursuite synchrone continue, continuera à fonctionner même après la situation de blocage. (Voir ce que je pense se passe ci-dessous)

en relation

  • StackExchange.Redis Deadlocking

    Deadlock causé par le mixage en await et Task.Result (sync-over-async, comme nous le faisons). Mais notre code est exécuté sans contexte de synchronisation, donc cela ne s’applique pas ici, non?

  • Comment mélanger en toute sécurité la synchronisation et le code asynchrone?

    Oui, nous ne devrions pas faire ça. Mais nous le faisons et nous devons continuer à le faire pendant un certain temps. Beaucoup de code à migrer dans le monde asynchrone.

    Encore une fois, nous n’avons pas de contexte de synchronisation, donc cela ne devrait pas causer de blocages, non?

    Définir ConfigureAwait(false) avant toute await n’a aucun effet sur cela.

  • Délai d’expiration après les commandes asynchrones et Task.WhenAny attend dans StackExchange.Redis

    C’est le problème du détournement de thread. Quelle est la situation actuelle à ce sujet? Serait-ce le problème ici?

  • L’appel asynchrone StackExchange.Redis se bloque

    De la réponse de Marc:

    … mix Attendez et attendez n’est pas une bonne idée. En plus des blocages, il s’agit de “synchrone async” – un anti-pattern.

    Mais il dit aussi:

    SE.Redis ignore le contexte de synchronisation en interne (normal pour le code de la bibliothèque), il ne devrait donc pas avoir d’interblocage

    Donc, d’après ce que je comprends, StackExchange.Redis devrait être indifférent à l’utilisation de l’anti-modèle sync-over-async . Ce n’est tout simplement pas recommandé car cela pourrait être la cause de blocages dans d’ autres codes.

    Dans ce cas, cependant, pour autant que je sache, le blocage est vraiment dans StackExchange.Redis. Corrigez-moi si j’ai tort, s’il-vous plait.

Résultats du débogage

J’ai trouvé que le blocage semble avoir sa source dans ProcessAsyncCompletionQueue sur la ligne 124 de CompletionManager.cs .

Extrait de ce code:

 while (Interlocked.CompareExchange(ref activeAsyncWorkerThread, currentThread, 0) != 0) { // if we don't win the lock, check whether there is still work; if there is we // need to retry to prevent a nasty race condition lock(asyncCompletionQueue) { if (asyncCompletionQueue.Count == 0) return; // another thread drained it; can exit } Thread.Sleep(1); } 

Je l’ai trouvé pendant l’impasse; activeAsyncWorkerThread est l’un de nos threads en attente d’un appel Redis. ( notre thread = un thread de pool de threads exécutant notre code ). Donc, la boucle ci-dessus est réputée continuer pour toujours.

Sans connaître les détails, cela ne va pas du tout. StackExchange.Redis attend un thread qu’il pense être le thread de travail asynchrone actif alors qu’il s’agit en réalité d’un thread qui est tout à fait le contraire.

Je me demande si cela est dû au problème du détournement de thread (que je ne comprends pas bien)?

Que faire?

Les deux principales questions que j’essaie de comprendre:

  1. Le mixage peut-il await et Wait() / Result est-il la cause des blocages, même s’il est exécuté sans contexte de synchronisation?

  2. Sommes-nous en train de rencontrer un bogue ou une limitation dans StackExchange.Redis?

Un correctif possible?

D’après mes résultats de débogage, le problème semble être que:

 next.TryComplete(true); 

… dans la ligne 162 de CompletionManager.cs dans certaines circonstances, le thread en cours (qui est le thread de travail asynchrone actif ) peut se déplacer et commencer à traiter d’autres codes, provoquant éventuellement un blocage.

Sans connaître les détails et penser simplement à ce “fait”, il semblerait logique de libérer temporairement le thread de travail asynchrone actif au cours de l’appel TryComplete .

Je suppose que quelque chose comme ça pourrait fonctionner:

 // release the "active thread lock" while invoking the completion action Interlocked.CompareExchange(ref activeAsyncWorkerThread, 0, currentThread); try { next.TryComplete(true); Interlocked.Increment(ref completedAsync); } finally { // try to re-take the "active thread lock" again if (Interlocked.CompareExchange(ref activeAsyncWorkerThread, currentThread, 0) != 0) { break; // someone else took over } } 

Je suppose que mon meilleur espoir est que Marc Gravell lise ceci et fournisse des commentaires 🙂

Pas de contexte de synchronisation = Le contexte de synchronisation par défaut

J’ai écrit ci-dessus que notre code n’utilise pas de contexte de synchronisation . Cela n’est que partiellement vrai: le code est exécuté en tant qu’application Console ou en tant que rôle Azure Worker. Dans ces environnements, SynchronizationContext.Current est null , raison pour laquelle j’ai écrit que nous courions sans contexte de synchronisation.

Cependant, après avoir lu Tout sur le contexte de synchronisation, j’ai appris que ce n’était pas vraiment le cas:

Par convention, si le SynchronizationContext d’un thread est nul, il a implicitement un SynchronizationContext par défaut.

Le contexte de synchronisation par défaut ne doit cependant pas être à l’origine de blocages, car le contexte de synchronisation basé sur l’interface utilisateur (WinForms, WPF) pourrait le faire, car il n’implique pas d’affinité de thread.

Ce que je pense arrive

Lorsqu’un message est terminé, sa source d’achèvement est vérifiée pour savoir si elle est considérée comme synchrone . Si c’est le cas, l’action d’achèvement est exécutée en ligne et tout va bien.

Si ce n’est pas le cas, l’idée est d’exécuter l’action d’achèvement sur un thread de pool de threads nouvellement alloué. Cela fonctionne également très bien lorsque ConnectionMultiplexer.PreserveAsyncOrder est false .

Cependant, lorsque ConnectionMultiplexer.PreserveAsyncOrder la valeur true (valeur par défaut), ces threads de pool de threads sérialiseront leur travail en utilisant une queue d’achèvement et en s’assurant qu’au plus l’un d’entre eux soit le thread de travail asynchrone actif à tout moment.

Lorsqu’un thread devient le thread de travail asynchrone actif, il continuera à l’être jusqu’à ce qu’il ait épuisé la queue d’achèvement .

Le problème est que l’action d’achèvement n’est pas synchrone (vu de dessus), mais qu’elle est toujours exécutée sur un thread qui ne doit pas être bloqué car cela empêchera l’exécution d’autres messages non synchronisés .

Notez que les autres messages en cours d’exécution avec une action d’achèvement qui est synchrone sûre continueront à fonctionner correctement, même si le thread de travail asynchrone actif est bloqué.

Mon “correctif” suggéré (ci-dessus) ne provoquerait pas un blocage de cette manière, mais cela gênerait la notion de préservation de l’ordre d’achèvement asynchrone .

Donc peut-être que la conclusion à faire ici est qu’il n’est pas sûr de mélanger l’ await avec Result / Wait() lorsque PreserveAsyncOrder est true , que nous courions sans contexte de synchronisation?

( Au moins jusqu’à ce que nous puissions utiliser .NET 4.6 et le nouveau TaskCreationOptions.RunContinuationsAsynchronously , je suppose )

Voici les solutions que j’ai trouvées à ce problème de blocage:

Solution de contournement n ° 1

Par défaut, StackExchange.Redis s’assurera que les commandes sont exécutées dans le même ordre que les messages de résultats. Cela pourrait provoquer un blocage tel que décrit dans cette question.

Désactivez ce comportement en définissant PreserveAsyncOrder sur false .

 ConnectionMultiplexer connection = ...; connection.PreserveAsyncOrder = false; 

Cela évitera les blocages et pourrait également améliorer les performances .

J’encourage toute personne confrontée à des problèmes de blocage à essayer cette solution de contournement, car elle est très simple et claire.

Vous perdez la garantie que les continuations asynchrones sont appelées dans le même ordre que les opérations Redis sous-jacentes. Cependant, je ne vois pas vraiment pourquoi cela dépend de vous.


Solution de rechange # 2

Le blocage se produit lorsque le thread de travail asynchrone actif dans StackExchange.Redis termine une commande et lorsque la tâche d’achèvement est exécutée en ligne.

On peut empêcher une tâche d’être exécutée en ligne en utilisant un TaskScheduler personnalisé et s’assurer que TryExecuteTaskInline renvoie false .

 public class MyScheduler : TaskScheduler { public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { return false; // Never allow inlining. } // TODO: Rest of TaskScheduler implementation goes here... } 

Implémenter un bon planificateur de tâches peut être une tâche complexe. Il existe toutefois des implémentations dans la bibliothèque ParallelExtensionExtras ( package NuGet ) à partir desquelles vous pouvez vous inspirer.

Si votre planificateur de tâches utilisait ses propres threads (pas à partir du pool de threads), il serait peut-être judicieux d’autoriser l’inlining à moins que le thread actuel ne soit issu du pool de threads. Cela fonctionnera car le thread de travail async actif dans StackExchange.Redis est toujours un thread de pool de threads.

 public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { // Don't allow inlining on a thread pool thread. return !Thread.CurrentThread.IsThreadPoolThread && this.TryExecuteTask(task); } 

Une autre idée serait d’attacher votre planificateur à tous ses threads, en utilisant un stockage local de thread .

 private static ThreadLocal __attachedScheduler = new ThreadLocal(); 

Assurez-vous que ce champ est atsortingbué lorsque le thread commence à s’exécuter et efface à la fin:

 private void ThreadProc() { // Attach scheduler to thread __attachedScheduler.Value = this; try { // TODO: Actual thread proc goes here... } finally { // Detach scheduler from thread __attachedScheduler.Value = null; } } 

Ensuite, vous pouvez autoriser l’inclusion de tâches tant qu’elle est effectuée sur un thread “appartenant” au planificateur personnalisé:

 public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { // Allow inlining on our own threads. return __attachedScheduler.Value == this && this.TryExecuteTask(task); } 

Je pense beaucoup aux informations détaillées ci-dessus et je ne connais pas le code source que vous avez en place. Il semble que vous frappiez des limites internes et configurables dans .Net. Vous ne devriez pas les bash, donc je suppose que vous ne disposez pas d’objects car ils flottent entre les threads, ce qui ne vous permettra pas d’utiliser une instruction using pour gérer proprement la durée de vie de leurs objects.

Cela détaille les limitations sur les requêtes HTTP. Similaire à l’ancien problème WCF lorsque vous ne disposiez pas de la connexion et que toutes les connexions WCF échouaient.

Nombre maximal de requêtes HttpWebRequ concurrentes

Ceci est plus une aide au débogage, car je doute que vous utilisiez vraiment tous les ports TCP, mais de bonnes informations sur la manière de trouver combien de ports ouverts vous avez et où.

https://msdn.microsoft.com/en-us/library/aa560610(v=bts.20).aspx