Zablokowanie dostępu do StackExchange.Redis

Wpadam w impas kiedy dzwonię StackExchange.Redis .

Nie wiem dokładnie, co się dzieje, co jest bardzo frustrujące i byłbym wdzięczny za wszelkie informacje, które mogłyby pomóc rozwiązać lub obejść ten problem.


W przypadku, gdy ty też masz ten problem i nie chcesz czytać tego wszystkiego; {]} Proponuję spróbować ustawić PreserveAsyncOrder na false.

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

Zrobienie tego prawdopodobnie rozwiąże impas, który te pytania i odpowiedzi dotyczą i mogą również poprawić wydajność.


Nasza konfiguracja

    Kod jest uruchamiany jako aplikacja konsolowa lub jako rola robocza platformy Azure.
  • wyświetla REST api za pomocą HttpMessageHandler, więc punktem wejścia jest asynchroniczne.
  • niektóre części kodu mają powinowactwo do wątku (jest własnością i musi być obsługiwany przez pojedynczy wątek).
  • niektóre części kodu są tylko asynchroniczne.
  • robimy sync-over-async oraz async-over-sync anty-wzorce. (mieszanie await i Wait()/Result).
  • używamy tylko metod asynchronicznych podczas uzyskiwania dostępu do Redis.
  • używamy StackExchange.Redis 1.0.450 dla. NET 4.5.

Impas

Po uruchomieniu aplikacji/usługi działa normalnie przez jakiś czas, a następnie nagle (prawie) wszystkie przychodzące żądania przestają działać, nigdy nie generują odpowiedzi. Wszystkie te prośby są czekając na połączenie z Redis do zakończenia.

Co ciekawe, po wystąpieniu impasu każde wywołanie do Redis zostanie zawieszone, ale tylko wtedy, gdy wywołania te są wykonywane z przychodzącego żądania API, które są uruchamiane w puli wątków.

Wykonujemy również połączenia do Redis z wątków tła o niskim priorytecie i te połączenia nadal działają nawet po wystąpieniu impasu.

wygląda na to, że impas wystąpi tylko podczas wywoływania Redis na puli wątków nić. nie sądzę, że jest to spowodowane faktem, że te połączenia są wykonywane na wątku puli wątku. Wydaje się raczej, że każde wywołanie asynchroniczne Redis bez kontynuacji lub z Sync safe kontynuacją, będzie nadal działać nawet po zaistnieniu impasu. (Zobacz co myślę się dzieje poniżej)

Powiązane

  • StackExchange.Redis

    Impas spowodowany mieszaniem await i Task.Result (sync-over-async, tak jak my). Ale nasz kod jest uruchamiany bez kontekstu synchronizacji, więc to nie ma zastosowania tutaj, prawda?

  • Jak bezpiecznie mieszać synchronizację i Kod asynchroniczny?

    Tak, nie powinniśmy tego robić. Ale tak jest i będziemy musieli to kontynuować przez jakiś czas. Dużo kodu, który wymaga migracji do świata asynchronicznego.

    Ponownie, nie mamy kontekstu synchronizacji, więc nie powinno to powodować impasów, prawda?

    Ustawienie ConfigureAwait(false) przed dowolnym await nie ma na to wpływu.

  • Wyjątek Timeout po poleceniach asynchronicznych i zadaniu.WhenAny czeka w StackExchange.Redis

    To jest problem z porwaniem wątku. Jaka jest obecna sytuacja w tej sprawie? Czy to może być problem?

  • StackExchange.Redis async Call hangs

    Od odpowiedzi Marka:

    ...mieszanie Wait I wait nie jest dobrym pomysłem. Oprócz martwych punktów, jest to "sync over async" - anty-wzorzec.

    Ale on też mówi:

    SE.Redis omija wewnętrznie kontekst synchronizacji (normalny dla kodu biblioteki), więc nie powinien mieć impasu

    Więc, z mojego zrozumienia StackExchange.Redis powinien być niezależny od tego, czy używamy sync-over-async anty-pattern. Po prostu nie jest to zalecane, ponieważ może to być przyczyną blokad w inny kod.

    W ta sprawa, jednak, o ile mogę powiedzieć, Impas jest naprawdę wewnątrz StackExchange.Redis. Popraw mnie, jeśli się mylę.

Wyniki debugowania

Odkryłem, że impas wydaje się mieć swoje źródło w ProcessAsyncCompletionQueue na linii 124 z CompletionManager.cs.

Fragment tego kodu:

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

Odkryłem, że podczas impasu; activeAsyncWorkerThread jest jednym z naszych wątków, który czeka na zakończenie połączenia Redis. (nasz wątek = wątek zbiorczy wątku running our code ). Tak więc powyższa pętla jest uważana za kontynuowaną w nieskończoność.

Nie znając szczegółów, to na pewno czuje się źle; StackExchange.Redis czeka na wątek, który uważa za aktywny asynchroniczny wątek roboczy , podczas gdy w rzeczywistości jest to wątek, który jest zupełnie odwrotnie.

Zastanawiam się, czy jest to spowodowane thread hijacking problem (którego nie do końca rozumiem)?

Co robić?

Główne dwa pytania jestem próba rozgryzienia:

  1. Czy mieszanie await i Wait()/Result być przyczyną blokad nawet podczas uruchamiania bez kontekstu synchronizacji?

  2. Czy w StackExchange pojawia się błąd/ograniczenie.Redis?

Możliwe rozwiązanie?

Z moich ustaleń dotyczących debugowania wynika, że problem polega na tym, że:

next.TryComplete(true);

...na linii 162 W CompletionManager.cs może w pewnych okolicznościach pozwolić aktualnemu wątkowi (który jest aktywny asynchroniczny wątek roboczy ) odchodzą i zaczynają przetwarzać inny kod, prawdopodobnie powodując impas.

Nie znając szczegółów i po prostu myśląc o tym "fakcie", logiczne wydaje się tymczasowe zwolnienie aktywnego asynchronicznego wątku roboczego podczas wywołania TryComplete.

Wydaje mi się, że coś takiego może zadziałać:

// 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
    }
}
Mam nadzieję, że Marc Gravell to przeczyta i przekaże kilka opinii: -)]}

Nie synchronization context = domyślny kontekst synchronizacji

Napisałem powyżej, że nasz kod nie używa kontekstu synchronizacji. Jest to tylko częściowo prawda: kod jest uruchamiany jako aplikacja konsolowa lub jako rola robocza platformy Azure. W tych środowiskach SynchronizationContext.Current jest null, dlatego napisałem, że uruchamiamy bez kontekstu synchronizacji.

Jednak po przeczytaniu It ' s All About the Synchronizationkontekst nauczyłam się, że tak naprawdę nie jest: {]}

Zgodnie z konwencją, Jeśli bieżący wątek SynchronizationContext jest równy null, to domyślnie ma domyślny SynchronizationContext.

Domyślny kontekst synchronizacji nie powinien być jednak przyczyną blokad, ponieważ kontekst synchronizacji oparty na interfejsie użytkownika (WinForms, WPF) może - ponieważ nie implikuje powinowactwa wątków.

What I think happens

Gdy wiadomość jest ukończone źródło zakończenia jest sprawdzane, czy jest uważane za sync safe. Jeśli tak jest, akcja completion jest wykonywana w linii i wszystko jest w porządku.

Jeśli tak nie jest, ideą jest wykonanie akcji zakończenia na nowo przydzielonym wątku puli wątków. To też działa dobrze, gdy ConnectionMultiplexer.PreserveAsyncOrder jest false.

Jednak, gdy ConnectionMultiplexer.PreserveAsyncOrder jest true (wartość domyślna), wtedy te wątki puli wątków będą serializowały swoją pracę za pomocą kolejki zakończeń i przez zapewnienie, że co najwyżej jednym z nich jest aktywny asynchroniczny wątek roboczy w dowolnym momencie.

Gdy wątek stanie się aktywnym wątkiem roboczym asynchronicznym , będzie tak nadal dopóki nie opróżni kolejki zakończenia.

Problem polega na tym, że akcja zakończenia jest nie jest bezpieczna dla synchronizacji (z góry), nadal jest wykonywana w wątku, który nie może być blokowany , ponieważ zapobiega to innym nie jest bezpieczna dla synchronizacji wiadomości zakończone.

Zauważ, że inne wiadomości, które są zakończone akcją zakończenia, która jest bezpieczna dla synchronizacji, będą nadal działać poprawnie, nawet jeśli aktywny wątek roboczy async jest zablokowany.

Mój sugerowany "fix" (powyżej) nie spowoduje impasu w ten sposób, to jednak bałagan z pojęciem zachowanie asynchronicznej kolejności zakończenia .

Więc może wniosek jest taki, że nie jest bezpiecznie mieszać await z Result/Wait() Gdy PreserveAsyncOrder jest true, bez względu na to, czy pracujemy bez kontekstu synchronizacji?

(przynajmniej dopóki nie będziemy mogli korzystać z. NET 4.6 i nowego TaskCreationOptions.RunContinuationsAsynchronously, chyba )

Author: Community, 2015-06-12

2 answers

Oto sposoby obejścia tego problemu:

Obejście # 1

Domyślnie StackExchange.Redis zapewni, że polecenia są wykonywane w tej samej kolejności, w jakiej otrzymywane są komunikaty wynikowe. Może to spowodować impas, jak opisano w tym pytaniu.

Wyłącz to zachowanie, ustawiając PreserveAsyncOrder na false.

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

Pozwoli to uniknąć impasów i może również poprawić wydajność.

[[11]}zachęcam każdego, kto wpadnie na aby zablokować problemy, aby spróbować tego obejścia, ponieważ jest tak czysty i prosty.

Stracisz gwarancję, że kontynuacje asynchroniczne będą wywoływane w tej samej kolejności, co podstawowe operacje Redis. Nie rozumiem jednak, dlaczego można na tym polegać.


Obejście # 2

Impas występuje, gdy aktywny asynchroniczny wątek roboczy W StackExchange.Redis wykonuje polecenie i po wykonaniu zadania completion inline.

Można zapobiec wykonaniu zadania inline za pomocą niestandardowego TaskScheduler i zapewnić, że TryExecuteTaskInline zwraca 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...
}

Implementacja dobrego harmonogramu zadań może być zadaniem złożonym. Istnieją jednak istniejące implementacje w bibliotece ParallelExtensionExtras (pakiet NuGet ), z którego możesz korzystać lub czerpać inspiracje.

Jeśli twój harmonogram zadań będzie używał własnych wątków (nie z puli wątków), to dobrym pomysłem może być Zezwolenie na inlining, chyba że bieżący wątek pochodzi z puli wątków. Będzie to działać, ponieważ aktywny asynchroniczny wątek roboczy {[32] } W StackExchange.Redis to zawsze wątek puli wątków.

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

Innym pomysłem byłoby dołączenie harmonogramu do wszystkich jego wątków, używając thread-local storage.

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

Upewnij się, że to pole jest przypisane, gdy wątek zacznie działać i wyczyszczone po zakończeniu:

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

Wtedy możesz pozwolić w zależności od tego, które zadania są wykonywane na wątku, który jest" własnością " własnego schedulera:

public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
    // Allow inlining on our own threads.
    return __attachedScheduler.Value == this && this.TryExecuteTask(task);
}
 18
Author: Mårten Wikström,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-11-17 22:56:37

Zgaduję wiele na podstawie szczegółowych informacji powyżej i nie znając kodu źródłowego masz na miejscu. Wygląda na to, że możesz uderzać w jakieś wewnętrzne i konfigurowalne limity w .Net. nie powinieneś uderzać w te limity, więc domyślam się, że nie pozbywasz się obiektów, ponieważ są one pływające między wątkami, co nie pozwoli Ci użyć instrukcji using do czystego obsługi ich życia obiektów.

To określa ograniczenia żądań HTTP. Podobny do starego WCF problem, gdy nie pozbyłeś się połączenia i wtedy wszystkie połączenia WCF zawiodłyby.

Maksymalna liczba jednoczesnych Httpwebrequestów

Jest to bardziej pomoc debugowania, ponieważ wątpię, że naprawdę używasz wszystkich portów TCP, ale dobre informacje o tym, jak znaleźć, ile otwartych portów masz i gdzie.

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

 0
Author: Josh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:47:20