busy 프로세스 디버깅

때때로 php start.php status 명령을 실행하면 busy 상태의 프로세스를 볼 수 있습니다. 이는 해당 프로세스가 비즈니스를 처리 중임을 나타내며, 정상적인 경우 비즈니스 처리가 완료되면 해당 프로세스는 idle 상태로 돌아옵니다. 일반적으로는 문제가 없습니다. 그러나 busy 상태가 계속 유지되고 idle 상태로 복귀하지 않는 경우, 프로세스 내 비즈니스가 차단되거나 무한 루프에 빠졌음을 의미합니다. 다음 방법을 통해 문제를 파악할 수 있습니다.

strace + lsof 명령어를 통한 위치 확인

1. status에서 busy 프로세스의 pid 찾기
php start.php status 명령을 실행한 후, 다음과 같이 표시됩니다.

그림에서 busy 프로세스의 pid1172511748입니다.

2. strace로 프로세스 추적
하나의 프로세스 pid를 선택합니다(여기서는 11725 선택). strace -ttp 11725 명령을 실행하면 다음과 같이 표시됩니다.

프로세스가 계속해서 poll([{fd=16, events=.... 시스템 호출을 반복하고 있는 것을 볼 수 있으며, 이는 fd가 16인 파일 설명자로부터 읽기 이벤트를 기다리고 있음을 의미합니다. 즉, 해당 설명자가 데이터를 반환하기를 기다리고 있는 것입니다.

시스템 호출을 아무것도 표시하지 않으면, 현재 터미널을 유지한 채 새로운 터미널을 열고 kill -SIGALRM 11725 (프로세스에 알람 신호를 보냄) 명령을 실행하여 strace 터미널이 응답하는지, 특정 시스템 호출에서 차단되고 있는지 확인합니다. 여전히 아무 것도 표시되지 않으면 프로그램이 비즈니스에서 무한 루프에 빠졌음을 나타낼 수 있습니다. 문제 해결을 위해 아래의 다른 원인을 참고하십시오.

시스템이 epoll_wait 또는 select 시스템 호출에서 차단되는 것은 정상입니다. 이는 프로세스가 이미 idle 상태에 있음을 나타냅니다.

3. lsof로 프로세스 설명자 보기
lsof -nPp 11725 명령을 실행하면 다음과 같이 표시됩니다.

설명자 16은 마지막 줄에 있는 16u 기록과 관련이 있습니다. 여기서 fd=16 설명자가 tcp 연결임을 확인할 수 있으며, 원격 주소는 101.37.136.135:80입니다. 이는 프로세스가 http 자원에 액세스하고 있음을 의미하며, 반복적으로 poll([{fd=16, events=....가 http 서버로부터 데이터 반환을 기다리고 있음을 설명합니다. 이는 프로세스가 busy 상태인 이유를 설명합니다.

해결:
프로세스가 차단된 위치를 알게 되었으니 해결하기가 더 쉬워집니다. 예를 들어, 위의 위치 확인 결과 비즈니스에서 curl을 호출하고 있으며, 해당 url이 오랜 시간 동안 응답하지 않아 프로세스가 계속 기다리고 있습니다. 이 경우 url 제공자에게 연락하여 url 응답 지연의 원인을 찾을 수 있습니다. 또한 curl 호출 시 초시간 매개변수를 추가해야 합니다. 예를 들어, 응답이 없을 경우 2초 후 타임아웃을 설정하여 장시간 차단되는 상황을 피할 수 있습니다 (이렇게 하면 프로세스는 약 2초간 busy 상태가 될 수 있습니다).

프로세스가 장시간 busy 상태에 빠지는 다른 원인

프로세스가 차단되어 busy 상태가 되는 것 외에도 다음과 같은 원인들로 인해 프로세스가 busy 상태가 될 수 있습니다.

1. 비즈니스에 치명적인 오류가 있어 프로세스가 계속 종료됨
현상: 이 경우 시스템의 부하가 상대적으로 높아지며, status에서의 load average가 1 이상입니다. 프로세스의 exit_count 숫자가 매우 높고 계속 증가하는 것을 볼 수 있습니다.
해결: 디버그 모드로 실행( php start.php start에서 -d를 빼고)하여 워커맨을 실행하고 비즈니스 오류를 확인하여 문제를 해결합니다.

2. 코드에서 무한 루프가 발생함
현상: top에서 busy 프로세스가 CPU를 많이 사용하고 있으며, strace -ttp pid 명령이 아무런 시스템 호출 정보를 출력하지 않습니다.
해결: gdb와 php 소스 코드를 통한 위치 파악 참조. 대략적인 단계 요약은 다음과 같습니다:

  1. php -v 명령으로 버전을 확인합니다.
  2. 해당 php 버전의 소스를 다운로드합니다.
  3. gdb --pid=busy 프로세스의 pid 명령을 실행합니다.
  4. source php 소스 경로/.gdbinit 명령을 실행합니다.
  5. zbacktrace 명령으로 호출 스택을 인쇄합니다.
    마지막 단계에서 현재 실행 중인 php 코드의 호출 스택을 볼 수 있으며, php 코드의 무한 루프 위치를 확인할 수 있습니다.
    주의: 만약 zbacktrace가 호출 스택을 출력하지 않는 경우, php 컴파일 시 -g 매개변수가 포함되지 않았을 수 있습니다. 이 경우 php를 재컴파일해야 하며, 이후 워커맨을 재시작하여 위치를 파악해야 합니다.

3. 무한정 타이머 추가
비즈니스 코드가 타이머를 계속 추가하지만 삭제하지 않으면 프로세스 내에서 타이머가 점점 더 많아져 결국 프로세스가 무한 타이머를 실행하게 됩니다. 예를 들어 다음 코드를 참조하십시오:

$worker = new Worker;
$worker->onConnect = function($con){
    Timer::add(10, function(){});
};
Worker::runAll();

위 코드는 클라이언트 연결이 수립되면 타이머 하나가 추가되지만, 전체 비즈니스 코드 내에서 타이머를 삭제하는 로직이 없습니다. 이로 인해 시간이 지남에 따라 프로세스 내에서 타이머가 계속 증가하고 마지막으로 busy 상태로 이어질 수 있습니다.
올바른 코드는 다음과 같습니다:

$worker = new Worker;
$worker->onConnect = function($con){
    $con->timer_id = Timer::add(10, function(){});
};
$worker->onClose = function($con){
    Timer::del($con->timer_id);
};
Worker::runAll();