web-dev-qa-db-fra.com

Blocage lors de l'accès à StackExchange.Redis

Je suis dans une situation de blocage lors de l'appel StackExchange.Redis .

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


Au cas où vous auriez aussi ce problème et que vous ne voudriez pas lire tout cela; je vous suggère d'essayer de régler PreserveAsyncOrder sur false.

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

Cela résoudra probablement le type de blocage sur lequel porte cette Q&R 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 de travailleur Azure.
  • Il expose un REST api en utilisant HttpMessageHandler donc le point d'entrée est asynchrone.
  • Certaines parties du code ont une affinité de thread (appartient à, et doit être exécuté par, un seul thread).
  • Certaines parties du code sont uniquement asynchrones.
  • Nous faisons sync-over-async et async-over-sync anti-patterns . (en mélangeant await et Wait()/Result).
  • Nous utilisons uniquement des méthodes asynchrones lors de l'accès à 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 tout à coup (presque) toutes les demandes entrantes cessent de fonctionner, elles ne produisent jamais de réponse. Toutes ces demandes sont bloquées en attendant la fin d'un appel à Redis.

Fait intéressant, une fois que le blocage se produit, tout appel à Redis se bloque, mais uniquement si ces appels sont effectués à partir d'une demande d'API entrante, qui est exécutée sur le pool de threads.

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

Il semble qu'un blocage se produira uniquement lors de l'appel à Redis sur un thread de pool de threads. Je ne pense plus que cela soit dû au fait que ces appels sont effectués sur un thread de pool de threads. Il semble plutôt que tout appel Redis asynchrone sans suite, ou avec une suite sync safe, continuera à fonctionner même après le blocage. (Voir Ce que je pense se passe ci-dessous)

En relation

  • StackExchange.Redis Deadlocking

    Blocage provoqué par le mélange de await et Task.Result (Synchronisation sur async, comme nous le faisons). Mais notre code est exécuté sans contexte de synchronisation, ce qui ne s'applique pas ici, non?

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

    Oui, nous ne devrions pas faire ça. Mais nous le faisons, et nous devrons continuer à le faire pendant un certain temps. Beaucoup de code qui doit être migré dans le monde asynchrone.

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

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

  • Exception de délai d'attente après les commandes asynchrones et Task.WhenAny attend dans StackExchange.Redis

    Il s'agit du problème de détournement de thread. Quelle est la situation actuelle à ce sujet? Serait-ce le problème ici?

  • l'appel asynchrone StackExchange.Redis se bloque

    D'après la réponse de Marc:

    ... mélanger Attendre et attendre n'est pas une bonne idée. En plus des blocages, il s'agit de "synchronisation sur async" - un anti-modèle.

    Mais il dit aussi:

    SE.Redis contourne le contexte de synchronisation en interne (normal pour le code de bibliothèque), il ne devrait donc pas avoir le blocage

    Donc, d'après ma compréhension, StackExchange.Redis devrait être indépendant de savoir si nous utilisons l'anti-pattern sync-over-async. Ce n'est tout simplement pas recommandé car cela pourrait être la cause de blocages dans un autre code .

    Dans ce cas, cependant, pour autant que je sache, le blocage est vraiment à l'intérieur de StackExchange.Redis. Corrigez-moi si j'ai tort, s'il-vous plait.

Résultats de débogage

J'ai trouvé que le blocage semble avoir sa source dans ProcessAsyncCompletionQueue sur 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);
}

J'ai trouvé cela pendant l'impasse; activeAsyncWorkerThread est l'un de nos threads qui attend la fin d'un appel Redis. ( notre thread = un thread de pool de threads exécutant notre code). Ainsi, la boucle ci-dessus est réputée se poursuivre pour toujours.

Sans connaître les détails, cela se sent vraiment mal; StackExchange.Redis attend un thread qu'il pense être le thread de travail asynchrone actif alors qu'il s'agit en fait d'un thread qui est tout à fait le contraire.

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

Que faire?

Les deux principales questions que j'essaie de comprendre:

  1. Le mélange de await et Wait()/Result peut-il être la cause de blocages même lors de l'exécution sans contexte de synchronisation?

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

Une solution possible?

D'après mes résultats de débogage, il semble que le problème soit que:

next.TryComplete(true);

... on la ligne 162 dans CompletionManager.cs pourrait dans certaines circonstances laisser le thread courant (qui est le thread de travail asynchrone actif) s'éloigner et démarrer le traitement d'un autre code, pouvant entraîner un blocage.

Sans connaître les détails et penser à ce "fait", il semblerait logique de libérer temporairement le thread de travail asynchrone actif pendant l'invocation de 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 lirait ceci et fournirait 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 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 de travailleur Azure. Dans ces environnements SynchronizationContext.Current est null, c'est pourquoi j'ai écrit que nous exécutons sans contexte de synchronisation.

Cependant, après avoir lu Il s'agit du SynchronizationContext J'ai appris que ce n'est pas vraiment le cas:

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

Cependant, le contexte de synchronisation par défaut ne devrait pas être la cause de blocages, comme le pourrait le contexte de synchronisation basé sur l'interface utilisateur (WinForms, WPF), 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 s'il est considéré sync safe. 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 Est true (la valeur par défaut), ces threads de pool de threads sérialiseront leur travail à l'aide d'une file d'attente de fin et en veillant à ce qu'au plus l'un d'eux est le thread de travail asynchrone actif à tout moment.

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

Le problème est que l'action de complétion est pas de synchronisation sûre (par dessus), elle est toujours exécutée sur un thread qui ne doit pas être bloqué car cela empêchera les autres messages non synchronisés de se terminer.

Notez que les autres messages en cours de réalisation avec une action de fin qui est sync safe continueront de fonctionner correctement, même si le thread de travail asynchrone actif est bloqué .

Ma "correction" suggérée (ci-dessus) ne provoquerait pas un blocage de cette manière, mais elle gâcherait cependant la notion de en préservant l'ordre d'achèvement asynchrone.

Alors peut-être que la conclusion à tirer ici est que il n'est pas sûr de mélanger await avec Result/Wait() lorsque PreserveAsyncOrder est true, que nous exécutions sans contexte de synchronisation?

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

72
Mårten Wikström

Voici les solutions de contournement 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ésultat sont reçus. Cela pourrait provoquer un blocage comme 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 tous ceux qui rencontrent des problèmes de blocage à essayer cette solution de contournement, car c'est tellement propre et simple.

Vous perdrez la garantie que les continuations asynchrones sont appelées dans le même ordre que les opérations Redis sous-jacentes sont terminées. Cependant, je ne vois pas vraiment pourquoi vous pourriez vous fier à cela.


Solution de contournement n ° 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 l'exécution d'une tâche 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...
}

La mise en œuvre d'un bon planificateur de tâches peut être une tâche complexe. Il existe cependant des implémentations existantes dans la bibliothèque ParallelExtensionExtras ( package NuGet ) que vous pouvez utiliser ou vous inspirer.

Si votre planificateur de tâches utilise ses propres threads (pas à partir du pool de threads), il peut être judicieux d'autoriser l'inline sauf si le thread actuel provient du pool de threads. Cela fonctionnera car le thread de travail asynchrone 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 ordonnanceur à tous ses threads, en utilisant stockage local des threads .

private static ThreadLocal<TaskScheduler> __attachedScheduler 
                   = new ThreadLocal<TaskScheduler>();

Assurez-vous que ce champ est attribué lorsque le thread commence à s'exécuter et effacé à 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'incrustation des tâches tant qu'elle est effectuée sur un thread qui appartient au planificateur personnalisé:

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

Je devine beaucoup sur la base des informations détaillées ci-dessus et je ne connais pas le code source que vous avez en place. Il semble que vous atteigniez certaines limites internes et configurables dans .Net. Vous ne devriez pas les toucher, donc je suppose que vous ne disposez pas d'objets 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 objets.

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

Nombre maximum de requêtes HttpWeb simultanées

C'est plus une aide au débogage, car je doute que vous utilisiez vraiment tous les ports TCP, mais de bonnes informations sur la façon de trouver le nombre de ports ouverts et où.

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

0
Josh