Program Club

StackExchange.Redis에 액세스 할 때 교착 상태

proclub 2020. 10. 27. 23:42
반응형

StackExchange.Redis에 액세스 할 때 교착 상태


StackExchange.Redis를 호출 할 때 교착 상태에 빠졌습니다 .

무슨 일이 일어나고 있는지 정확히 알지 못합니다. 매우 실망 스럽습니다.이 문제를 해결하거나 해결하는 데 도움이 될 수있는 의견을 보내 주시면 감사하겠습니다.


당신도이 문제가 있고이 모든 것을 읽고 싶지 않다면; 으로 설정 PreserveAsyncOrder하는 것이 좋습니다 false.

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

이렇게하면이 Q & A의 교착 상태가 해결되고 성능도 향상 될 수 있습니다.


우리의 설정

  • 코드는 콘솔 애플리케이션 또는 Azure 작업자 역할로 실행됩니다.
  • HttpMessageHandler를 사용하여 REST API를 노출 하므로 진입 점이 비동기입니다.
  • 코드의 일부 부분에는 스레드 선호도가 있습니다 (단일 스레드가 소유하고 실행해야 함).
  • 코드의 일부는 비동기 전용입니다.
  • 우리는 sync-over-asyncasync-over-sync 안티 패턴을 수행하고 있습니다. (혼합 awaitWait()/ Result).
  • Redis에 액세스 할 때만 비동기 메서드를 사용하고 있습니다.
  • .NET 4.5 용 StackExchange.Redis 1.0.450을 사용하고 있습니다.

이중 자물쇠

애플리케이션 / 서비스가 시작되면 잠시 동안 정상적으로 실행되고 갑자기 (거의) 모든 수신 요청이 작동을 중지하고 응답을 생성하지 않습니다. 이러한 모든 요청은 Redis에 대한 호출이 완료되기를 기다리는 동안 교착 상태가됩니다.

흥미롭게도 교착 상태가 발생하면 Redis에 대한 모든 호출이 중단되지만 해당 호출이 스레드 풀에서 실행되는 수신 API 요청에서 이루어진 경우에만 중단됩니다.

또한 우선 순위가 낮은 백그라운드 스레드에서 Redis를 호출하고 있으며 이러한 호출은 교착 상태가 발생한 후에도 계속 작동합니다.

스레드 풀 스레드에서 Redis를 호출 할 때만 교착 상태가 발생하는 것처럼 보입니다. 더 이상 이러한 호출이 스레드 풀 스레드에서 이루어지기 때문이라고 생각하지 않습니다. 오히려 연속하지 않거나 동기화 안전 연속을 사용하는 모든 비동기 Redis 호출은 교착 상태가 발생한 후에도 계속 작동하는 것처럼 보입니다. (아래에서 내가 생각하는 일 참조)

관련

  • StackExchange.Redis 교착 상태

    혼합 awaitTask.Result(동기화를 통한 비동기 화로 인한 교착 상태 ). 하지만 우리 코드는 동기화 컨텍스트없이 실행되므로 여기에는 적용되지 않습니다.

  • 동기화 및 비동기 코드를 안전하게 혼합하는 방법은 무엇입니까?

    예, 그렇게해서는 안됩니다. 그러나 우리는 그렇게하고 있으며 잠시 동안 계속해야 할 것입니다. 비동기 세계로 마이그레이션해야하는 많은 코드.

    다시 말하지만 동기화 컨텍스트가 없으므로 교착 상태가 발생해서는 안됩니다.

    ConfigureAwait(false)이전에 설정 하는 await것은 이에 영향을 미치지 않습니다.

  • 비동기 명령 및 Task.WhenAny가 StackExchange.Redis에서 대기 한 후 시간 초과 예외

    이것이 스레드 하이재킹 문제입니다. 이것에 대한 현재 상황은 무엇입니까? 이것이 여기서 문제일까요?

  • StackExchange.Redis 비동기 호출이 중단됨

    Marc의 대답에서 :

    ... 기다림과 대기를 혼합하는 것은 좋은 생각이 아닙니다. 교착 상태에 더해, 이것은 안티 패턴 인 "sync over async"입니다.

    그러나 그는 또한 다음과 같이 말합니다.

    SE.Redis는 내부적으로 동기화 컨텍스트를 우회하므로 (라이브러리 코드의 경우 정상) 교착 상태가 없어야합니다.

    그래서, 내 이해에서 StackExchange.Redis는 우리가 sync-over-async 안티 패턴을 사용하는지 여부에 대해 불가지론 적이어야합니다 . 다른 코드 에서 교착 상태의 원인이 될 수 있으므로 권장되지 않습니다 .

    그러나이 경우 내가 알 수있는 한 교착 상태는 실제로 StackExchange.Redis 내부에 있습니다. 내가 틀렸다면 나를 고쳐주세요.

발견 사항 디버그

교착 상태가 124 행ProcessAsyncCompletionQueue있는 것 같습니다 .CompletionManager.cs

해당 코드의 스 니펫 :

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

나는 교착 상태 동안 그것을 발견했습니다. activeAsyncWorkerThreadRedis 호출이 완료되기를 기다리는 스레드 중 하나입니다. ( 우리 스레드 = 코드를 실행하는 스레드 풀 스레드 ). 따라서 위의 루프는 영원히 계속되는 것으로 간주됩니다.

세부 사항을 알지 못한다면 이것은 확실히 잘못된 것 같습니다. StackExchange.Redis는 활성 비동기 작업자 스레드 라고 생각하는 스레드를 기다리고 있지만 실제로는 그와는 정반대 인 스레드입니다.

이것이 스레드 하이재킹 문제 (내가 완전히 이해하지 못함 ) 때문인지 궁금합니다 .

무엇을해야합니까?

내가 알아 내려는 두 가지 주요 질문은 다음과 같습니다.

  1. 동기화 컨텍스트없이 실행하는 경우에도 혼합 awaitWait()/ Result교착 상태의 원인이 될 있습니까?

  2. StackExchange.Redis에서 버그 / 제한 사항이 발생합니까?

가능한 해결책?

내 디버그 결과에서 문제는 다음과 같습니다.

next.TryComplete(true);

... 라인 162에서CompletionManager.cs 어떤 상황에서는 현재 스레드 ( 활성 비동기 작업자 스레드 )가 방황하고 다른 코드 처리를 시작하여 교착 상태를 일으킬 수 있습니다.

세부 사항을 모르고이 "사실"에 대해 생각하지 않고 호출 하는 동안 활성 비동기 작업자 스레드 를 일시적으로 해제하는 것이 논리적으로 보입니다 TryComplete.

나는 이와 같은 것이 효과가 있다고 생각합니다.

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

내 최고의 희망은 Marc Gravell 이 이것을 읽고 피드백을 제공하는 것입니다 :-)

동기화 컨텍스트 없음 = 기본 동기화 컨텍스트

위에서 코드가 동기화 컨텍스트를 사용하지 않는다고 작성했습니다 . 이는 부분적으로 만 해당됩니다. 코드는 콘솔 애플리케이션 또는 Azure 작업자 역할로 실행됩니다. 이러한 환경에서는 SynchronizationContext.Current이므로 동기화 컨텍스트 없이null 실행한다고 썼습니다 .

그러나 SynchronizationContext에 관한 모든 것을 읽은 후 실제로는 그렇지 않다는 것을 알게되었습니다.

규칙에 따라 스레드의 현재 SynchronizationContext가 null 인 경우 암시 적으로 기본 SynchronizationContext가 있습니다.

UI 기반 (WinForms, WPF) 동기화 컨텍스트는 스레드 선호도를 의미하지 않으므로 기본 동기화 컨텍스트가 교착 상태의 원인이되어서는 안됩니다.

내가 생각하는 것

메시지가 완료되면 완료 소스가 동기화 안전 으로 간주되는지 확인 합니다 . 그렇다면 완료 작업이 인라인으로 실행되고 모든 것이 정상입니다.

그렇지 않은 경우 새로 할당 된 스레드 풀 스레드에서 완료 작업을 실행하는 것이 아이디어입니다. 이것은 너무 잘 때 일 ConnectionMultiplexer.PreserveAsyncOrder이다 false.

그러나, ConnectionMultiplexer.PreserveAsyncOrder이다 true(기본값), 다음 해당 스레드 풀 스레드는 사용하여 자신의 작품을 직렬화합니다 완료 큐를 그들 대부분 하나는 것을 보장함으로써 활성 비동기 작업자 스레드 언제든지이.

스레드가 활성 비동기 작업자 스레드되면 완료 대기열 을 비울 때까지 계속 됩니다 .

The problem is that the completion action is not sync safe (from above), still it is executed on a thread that must not be blocked as that will prevent other non sync safe messages from being completed.

Notice that other messages that are being completed with a completion action that is sync safe will continue to work just fine, even though the active async worker thread is blocked.

My suggested "fix" (above) would not cause a deadlock in this way, it would however mess with the notion of preserving async completion order.

So maybe the conclusion to make here is that it is not safe to mix await with Result/Wait() when PreserveAsyncOrder is true, no matter whether we are running without synchronization context?

(At least until we can use .NET 4.6 and the new TaskCreationOptions.RunContinuationsAsynchronously, I suppose)


These are the workarounds I've found to this deadlock problem:

Workaround #1

By default StackExchange.Redis will ensure that commands are completed in the same order that result messages are received. This could cause a deadlock as described in this question.

Disable that behavior by setting PreserveAsyncOrder to false.

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

This will avoid deadlocks and could also improve performance.

I encourage anyone that run into to deadlock problems to try this workaround, since it's so clean and simple.

You'll loose the guarantee that async continuations are invoked in the same order as the underlying Redis operations are completed. However, I don't really see why that is something you would rely on.


Workaround #2

The deadlock occur when the active async worker thread in StackExchange.Redis completes a command and when the completion task is executed inline.

One can prevent a task from being executed inline by using a custom TaskScheduler and ensure that TryExecuteTaskInline returns 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...
}

Implementing a good task scheduler may be a complex task. There are, however, existing implementations in the ParallelExtensionExtras library (NuGet package) that you can use or draw inspiration from.

If your task scheduler would use its own threads (not from the thread pool), then it might be a good idea to allow inlining unless the current thread is from the thread pool. This will work because the active async worker thread in StackExchange.Redis is always a thread pool thread.

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

Another idea would be to attach your scheduler to all of its threads, using thread-local storage.

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

Ensure that this field is assigned when the thread starts running and cleared as it completes:

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

Then you can allow inlining of tasks as long as its done on a thread that is "owned" by the custom scheduler:

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

I am guessing a lot based on the detailed information above and not knowing the source code you have in place. It sounds like you may be hitting some internal, and configurable, limits in .Net. You shouldn't be hitting those, so my guess is that you are not disposing of objects since they are floating between threads which won't allow you to use a using statement to cleanly handle their object lifetimes.

This details the limitations on HTTP requests. Similar to the old WCF issue when you didn't dispose of the connection and then all WCF connections would fail.

Max number of concurrent HttpWebRequests

This is more of a debugging aid, since I doubt you really are using all the TCP ports, but good info on how to find how many open ports you have and to where.

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

참고URL : https://stackoverflow.com/questions/30797716/deadlock-when-accessing-stackexchange-redis

반응형