Celery 워커가 죽었는데 모니터링에 안 걸릴 때, 헬스체크 이렇게 설계하세요

Celery 워커가 죽었는데

이 글에서는 Celery 워커가 죽었는데 모니터링 대시보드에는 멀쩡하게 떠 있는 상황이 왜 생기는지, 그리고 그런 사각지대를 없애는 헬스체크 구조를 어떻게 짜는지 확인하실 수 있습니다. 답을 먼저 드리면, systemd나 supervisor가 보는 건 프로세스의 생존 여부일 뿐 실제 태스크 소비 여부가 아니라서 이런 틈이 생깁니다. 워커가 큐를 실제로 소비하고 있는지는 celery inspect ping으로 직접 두드려 보거나, 워커별로 Redis에 하트비트를 남기게 만들어야 정확히 걸러낼 수 있습니다.

아래 내용은 Celery 5.x, 브로커로 Redis 또는 RabbitMQ를 쓰고 pool 옵션은 기본값인 prefork를 사용하는 환경을 전제로 합니다. solo나 threads 풀을 쓰는 경우 자식 프로세스 개념 자체가 없어서 일부 증상은 다르게 나타납니다.

Celery 워커가 죽었는데 왜 모니터링에 안 걸릴까요

대부분의 인프라 모니터링은 “프로세스가 떠 있는가”만 확인합니다. systemd의 systemctl status, supervisor의 supervisorctl status, Datadog의 프로세스 체크 모두 PID가 살아 있으면 정상으로 판단합니다.

문제는 Celery MainProcess는 살아 있는데 자식 워커 프로세스만 죽거나, 브로커와의 연결이 끊긴 채로 재연결에 실패해 큐를 더 이상 소비하지 못하는 경우입니다. 이런 상태에서도 OS 입장에서는 프로세스가 정상 실행 중이라 헬스체크가 통과해버립니다. 큐에 태스크가 쌓이는 걸로 뒤늦게 알아채는 팀이 많은 이유가 여기에 있습니다.

좀비 워커의 정체 – 부모는 살아있고 자식만 죽는 경우

prefork 풀은 MainProcess가 여러 개의 자식 프로세스를 fork해서 태스크를 나눠 처리하는 구조입니다. 태스크 안에서 메모리를 과도하게 써서 리눅스 OOM 킬러가 자식 프로세스만 골라 죽이는 경우, 혹은 C 확장 라이브러리가 세그폴트를 내는 경우가 대표적인 원인입니다.

Celery는 이런 상황을 어느 정도 자체 복구하도록 설계돼 있어서 죽은 자식을 감지하면 새로 fork해서 채워 넣습니다. 그런데 이 복구 루프 자체가 계속 반복되면서 실제 처리량은 떨어지는데 겉보기 프로세스 개수는 유지되는 경우, 혹은 재시작 자체가 브로커 재연결 실패로 멈춰버리는 경우에는 외부에서 알아챌 방법이 없습니다. 이런 상태를 흔히 하트비트 없는 좀비 상태라고 부르는데, 이 개념은 분산 시스템에서 널리 쓰이는 하트비트 방식과 원리가 같습니다.

worker_proc_alive_timeout 설정만으로는 부족한 이유

서버 모니터링 대시보드에 경고 알림이 표시된 화면

Celery 5.3부터 worker_proc_alive_timeout 설정이 추가됐습니다. 기본값은 4.0초이며, 새로 fork한 자식 프로세스가 이 시간 안에 준비 완료 신호를 보내지 않으면 죽은 것으로 간주하고 다시 살립니다.

다만 이 설정은 자식 프로세스가 시작될 때의 기동 실패만 감지합니다. 이미 정상 기동된 워커가 오래 걸리는 태스크에서 멈추거나, 브로커 연결이 조용히 끊어져 큐를 못 읽는 상황까지는 잡아주지 않습니다. 즉 이 옵션 하나로 “Celery 워커가 죽었는데 모니터링에 안 걸리는” 문제 전체를 해결할 수는 없고, 기동 단계의 부분적인 안전장치로만 봐야 합니다.

Redis에 하트비트를 남겨 죽은 워커를 잡는 방법

가장 확실한 방법은 워커가 실제로 태스크를 실행하고 있다는 증거를 직접 남기게 만드는 것입니다. Celery Beat로 짧은 주기의 태스크를 등록하고, 이 태스크가 실행될 때마다 워커 호스트명으로 Redis에 타임스탬프를 씁니다.

# tasks.py
import time
import socket
from myproject.celery import app
from myproject.redis_client import redis_client

@app.task(name="heartbeat.ping")
def heartbeat_ping():
    key = f"celery:heartbeat:{socket.gethostname()}"
    redis_client.set(key, time.time(), ex=180)
# celery.py
app.conf.beat_schedule = {
    "worker-heartbeat-every-minute": {
        "task": "heartbeat.ping",
        "schedule": 60.0,  # 60초마다 실행
    },
}

외부 감시 스크립트나 Prometheus blackbox exporter에서는 이 키의 최근 갱신 시각만 확인하면 됩니다.

import time
import redis

r = redis.Redis(host="localhost", port=6379, db=0)
last = r.get("celery:heartbeat:worker-01")

if last is None or time.time() - float(last) > 180:
    print("ALERT: worker-01 하트비트가 3분 넘게 갱신되지 않았습니다")

여러 워커를 운영한다면 하트비트 태스크를 각 워커의 전용 큐로 라우팅해야 개별 워커의 사망을 따로 잡아낼 수 있습니다. 하나의 공용 큐에만 태스크를 던지면 워커 중 하나만 살아 있어도 하트비트가 계속 갱신돼 나머지 워커의 죽음을 놓치게 됩니다.

분산 시스템 노드 간 하트비트 연결 구조도

celery inspect ping과 Flower를 함께 걸어 보세요

celery inspect ping은 브로커를 통해 컨트롤 명령을 보내고, 살아 있는 워커가 pong으로 응답하는지 확인하는 공식 CLI 명령입니다. 워커가 죽었거나 컨트롤 큐를 소비하지 못하는 상태라면 타임아웃으로 바로 드러납니다.

$ celery -A myproject inspect ping --timeout=5
-> worker-01@myhost: OK
    pong

# 응답이 없으면 이렇게 타임아웃만 출력됩니다
$ celery -A myproject inspect ping --timeout=5
Error: No nodes replied within time constraint

이 명령은 크론으로 1~2분마다 돌려서 exit code를 체크하는 방식으로 스케줄러나 Nagios류 도구에 붙이기 좋습니다. 좀 더 시각적인 확인이 필요하면 Flower를 함께 쓰는 방법도 있는데, Flower는 워커가 보내는 이벤트를 구독해서 대시보드로 보여주는 도구이고 이런 진단 방식은 Celery 모니터링 공식 가이드에도 표준으로 소개돼 있습니다.

각 방식이 무엇을 감지하는지 정리하면 다음과 같습니다.

점검 방식 확인 대상 장점 한계
systemd/supervisor 프로세스 체크 PID 생존 여부 설정이 간단함 좀비 워커(태스크 소비 중단)를 못 잡음
celery inspect ping 컨트롤 큐 응답 여부 실제 처리 경로에 가까운 점검 브로커 자체 장애 시 오탐, 응답까지 수 초 소요
Redis 하트비트 태스크 Beat 스케줄 실행 여부 워커별 개별 감시 가능 Beat 자체도 별도 감시 필요, 큐 라우팅 설계 필요
Flower 이벤트 구독 worker-heartbeat 이벤트 대시보드로 상태 한눈에 확인 Flower 프로세스 자체가 단일 장애점

이 방법이 안 통하는 상황과 트레이드오프

Redis 하트비트 방식은 Beat 프로세스 자체가 죽으면 모든 워커가 정상이어도 하트비트가 갱신되지 않아 전체 오탐이 발생합니다. Beat의 생존은 별도로, 예를 들어 celerybeat 스케줄 파일의 수정 시각을 확인하는 식으로 따로 점검해야 합니다.

celery inspect ping은 브로커 커넥션 풀이 고갈되는 등 브로커 쪽 문제가 있으면 살아 있는 워커도 타임아웃으로 응답하지 않을 수 있어서, 워커 문제인지 브로커 문제인지 구분하는 절차가 추가로 필요합니다. 또한 이 명령을 너무 짧은 주기로 돌리면 브로커에 불필요한 부하를 줄 수 있어서, 보통 1~2분 간격 정도가 실무적으로 무난합니다.

가장 안전한 구성은 단일 방식에 의존하지 않고 프로세스 체크, inspect ping, 큐별 하트비트를 함께 두는 것입니다. 어느 하나가 오탐을 내도 나머지 신호로 실제 장애인지 교차 확인할 수 있기 때문입니다. 지금 운영 중인 워커가 프로세스 체크만으로 감시되고 있다면, 위의 하트비트 태스크부터 워커 하나에 먼저 붙여 보고 실제로 갱신되는지 확인하는 것이 다음 단계로 적당합니다.

헤드리스 브라우저 탐지에 걸리는 진짜 이유와 정상 사용의 경계

Leave a Comment