Today
-
Yesterday
-
Total
-
  • 교착상태(deadlock)를 프로세스 상태와 디버거를 사용해서 찾아내기
    Programmer/Programming 2017. 1. 6. 15:37

    멀티미디어(multimedia) 소스를 수신 받아서 가공하고 송출하는 프로그램을 개선하고 있었다.
    에이징 테스트(aging test)에서 얼마간 프로그램이 동작하다가 미디어를 송출하지 못하는 버그를 발견했다.
    로그를 살펴보니 송출이 멈추기 전에 수신부터 진행이 되지 않는 것을 발견했다.
    UDP로 입력 데이터는 들어오는데 읽지 못하고 있었다.

     

    기능이 멈춰있으면 있으면 두가지를 의심할 수 있다.
    하나는 무한루프, 나머지 하나는 교착상태(deadlock)이다.
    무한루프에 빠지거나 교착 상태에 진입하면 다음 단계로 나아가지 못한다.

     

    무한 루프인지 교착 상태인지 간단하게 판단하는 방법은 프로세스의 CPU 점유율과 상태를 확인하는 것이다.
    `ps -u`나 top 유틸리티로 확인한다.

     

    무한 루프는 CPU를 과도하게 점유하는지 여부로 알 수 있다.[각주:1]
    이 상황에서는 프로세스의 상태는 계속 'R'[각주:2]로 나타난다.

    아래는 무한 루프에 빠진 프로세스의 사례이다.
    infloop 프로세스는 과도하게 CPU를 사용하고 있고 'R' 상태이다.

    $ ps -u
    USER   PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
    user 23840  0.1  0.1  21716  5408 pts/0    Ss   12:33   0:00 -bash
    user 23862  0.0  0.0  36072  3388 pts/0    R+   12:33   0:00 ps -u
    ...
    user 24029  101  0.0   4376   820 pts/1    R+   12:40   9:10 infloop

     

    반면, 교착상태는 CPU 점유율이 정상보다 더 낮고,
    프로세스의 상태가 장시간 'S'[각주:3]임을 보여준다.

    아래 deadlock 프로세스는 CPU를 거의 사용하지 않으며 'S' 상태이다.

     

    $ ps -u
    USER   PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
    user 23840  0.1  0.1  21716  5408 pts/0    Ss   12:33   0:00 -bash
    user 23862  0.0  0.0  36072  3388 pts/0    R+   12:33   0:00 ps -u
    ...
    user 24359  0.0  0.0  88620   876 pts/2    Sl+  12:40   9:10 deadlock

    문제의 미디어 처리 프로세스는 교착상태의 프로세스 정보를 보여주고 있었다.

     

     

     

    문제의 상태에 진입한 프로세스를 gdb에 붙여서 상태를 확인한다.
    아래 명령으로 gdb에 붙이고, 'Ctrl+C'로 gdb 프롬프트를 얻는다.

    $ gdb -p <PID>

     

    교착상태에 빠진 쓰레드(thread)를 찾아야 한다.
    그러기 위해서는 먼저 쓰레드의 목록을 얻는다.

    - 쓰레드 목록 얻기 :

    (gdb) info thread

     

    각 쓰레드를 이동하며 콜스택(call stack)을 살펴본다.

    - 쓰레드 이동 하기 :

    (gdb) thread <THREAD-ID>

    콜스택을 보면, 잠그려고 대기 중인지 아닌지 확인이 가능하다.

    - 콜스택 보기 :

    (gdb) bt

    콜스택에서 xxx_lock_wait_xxx, XxxWaitForSingleObjectXxx 함수를 찾는다.
    이런 함수에서 멈춰있는 쓰레드는 데드락이 걸린 것으로 의심할 수 있다.
    위 함수를 호출하는 소스 코드를 찾아서 잠금 핸들러를 확인한다.

     

    어딘가에서 잠금하고 풀지 않았기 때문에 무한 대기중에 걸린 것이다.
    잠긴 곳을 찾은 방법을 살펴보자.

     

    로그를 살펴보면서 잠금 대기 함수 핸들러를 사용하는 다른 부분을 찾아본다.
    즉, 멈춤 시점부터 이전으로 로그를 거슬러 올라가며 연관된 코드를 살펴본다.
    로그를 남긴 남긴 코드 부근에서 잠금 핸들러를 사용한 코드가 있는지 조사한다.
    분명히 이 핸들러를 잠그기만하고 풀지 않은 부분이 있을 것이다.

     

    이제는 왜 잠그기만 하고 풀지 못했는지 원인을 찾는다.

    잠금을 푸는 것을 잊어버린 버그처럼 간단한 것일 수도 있고,

    여러 잠금 핸들러, 소켓 대기 등이 서로 꼬여있는 상태일 수도 있다.

    디버거에서 찾은 잠금 핸들러에서 시작해서, 근본 원인을 찾을 때까지 원인의 원인을 찾는 과정을 반복해야 한다.

     

    예시에 언급된 미디어 프로그램은 직접적인 원인은 쓰레드 중 하나에서 핸들러를 잠그고 Redis 명령을 내리고 대기 상태에 빠졌다.

    이 트랜잭션이 멈춘 이유는 다른 쓰레드에서 Redis 연결 소켓을 오염시켰기 때문이다.

    근본 원인은 Redis 연결 소켓 오염인 셈이다.

    즉, 핸들러 잠금이 근본 원인이 아님을 주목해야 한다.

     

    이와 같이 근본 원인이 상당히 멀리 떨어져 있는 경우가 허다하다.
    이것을 찾는 것은 각자의 몫으로 하겠다.
    대신에 근본 원인을 찾아서 해결하는 원칙을 링크로 소개한다.
    - 대기 상태의 원인 파악 없이 타임아웃 같은 핵을 쓰면 안된다.
    - 원인의 이유 찾기를 근본 원인을 찾을 때까지 반복한다.

     

    이와같이 교착상태의 이유를 프로세스 상태와 디버거를 사용해서 찾아낸다.
    원인의 이유를 파고들어 근본 원인을 찾아내서 문제를 해결한다.
    코드 변경 전과 후를 같은 재현 조건에서 반복 테스트 해서 문제를 정확히 해결했음을 검증한다.

    1. 루프 내에 sleep이나 timed I/O가 사용된 경우는 이 방법으로는 쉽게 확인할 수 없다. [본문으로]
    2. running or runnable [본문으로]
    3. waiting for an event to complete [본문으로]

    댓글

Designed by Tistory.