Skip to content
isdnetworks
Go back

먼저 뜬 것이 나중에 내려가야 했다

여러 프로세스를 한 번에 띄우고 내리는 스크립트를 만들었다. 띄우는 것은 됐는데 내릴 때 오류가 쌓였다.

Table of contents

Open Table of contents

증상 — 시작 순서만 정해 뒀다

띄우는 쪽은 이렇게 적혀 있었다.

start_all() {
    start redis
    start matcher
    start api
    start ws
}

의존하는 것을 먼저 띄운다. 이건 맞았다.

내리는 것은 같은 순서로 썼다.

stop_all() {
    stop redis
    stop matcher
    stop api
    stop ws
}

redis 를 먼저 내리니 그것을 쓰던 셋이 오류를 내며 죽었다.

원인 — 종료 순서를 따로 적은 것

redis 가 먼저 내려가는 순간 matcherapiws 는 쓰던 연결이 끊긴 채로 남는다. 아직 쓰이고 있는 것을 먼저 치우니 나머지가 정상 종료할 길이 없었다.

종료 순서는 따로 정할 것이 아니라 시작 순서 하나에서 나오는 것이었다. 같은 의존 순서를 두 곳에 나눠 적어 두면 한쪽만 고쳤을 때 조용히 어긋난다.

조치 — 역순으로 내렸다

의존하는 쪽을 먼저 내리고 의존받는 쪽을 나중에 내렸다.

stop_all() {
    stop ws
    stop api
    stop matcher
    stop redis
}

시작 순서를 정했으면 종료 순서는 그 역순이다.

적는 자리를 아예 하나로 줄였다.

SERVICES="redis matcher api ws"

start_all() { for s in $SERVICES; do start "$s"; done }
stop_all()  { for s in $(echo $SERVICES | tr ' ' '\n' | tac); do stop "$s"; done }

SERVICES 를 한 번만 적고 내릴 때 tac 으로 뒤집는다.

순서가 어긋날 자리가 없어졌고 목록에 하나를 끼워 넣을 때도 한 줄만 고치면 양쪽이 함께 맞는다.

검증 — 떴는지 확인하고 다음으로

띄우자마자 다음으로 넘어가던 것도 문제였다.

start() {
    "$1" &
    wait_ready "$1" || { echo "준비 실패: $1"; exit 1; }
}

wait_ready() {
    for i in $(seq 1 30); do
        check_health "$1" && return 0
        sleep 1
    done
    return 1
}

wait_ready 로 준비될 때까지 기다리고 상한을 둬서 영영 안 기다리게 했다.

check_health 는 프로세스가 살아 있는지가 아니라 실제로 응답하는지로 봤다. pidof 에 잡히는 것과 요청을 받을 준비가 된 것은 서로 다른 상태다.

내릴 때도 끝난 것을 확인했다.

stop() {
    kill -TERM "$(pidof $1)"
    for i in $(seq 1 20); do
        pidof "$1" > /dev/null || return 0
        sleep 1
    done
    echo "정상 종료 안 됨. 강제 종료: $1"
    kill -KILL "$(pidof $1)"
}

SIGTERM 을 주고 기다렸다가 안 끝나면 SIGKILL 로 끊는다.

SIGKILL 까지 갔다는 것을 그때마다 로그에 남겼다. 매번 강제로 끊고 있으면 그 프로세스가 종료 신호를 안 다루는 것이다.

대응 — 하던 것을 끝내고 나가게

실제로 하나가 매번 SIGKILL 로 끝나고 있었다.

pcntl_signal(SIGTERM, function () use (&$running) {
    $running = false;
    error_log('종료 신호를 받았습니다. 현재 작업을 마치고 종료합니다.');
});

while ($running) {
    $job = $queue->pop();
    if (!$job) { usleep(100000); pcntl_signal_dispatch(); continue; }
    process($job);
    pcntl_signal_dispatch();
}

pcntl_signal 로 받아 $running 을 내리고 돌던 process 를 마친 뒤에 빠져나간다.

받자마자 죽으면 돌던 $job 이 어중간한 상태로 남고 배포할 때마다 그것을 손으로 정리해야 한다.

주의 — 반쯤 뜬 상태

시작 중에 하나가 실패하면 앞에서 띄운 것들이 그대로 남는다.

started=""
for s in $SERVICES; do
    if start "$s"; then
        started="$s $started"
    else
        echo "시작 실패: $s. 앞서 띄운 것을 내립니다."
        for r in $started; do stop "$r"; done
        exit 1
    fi
done

started 에 쌓아 두고 실패하면 역순으로 내린다.

반쯤 뜬 상태가 전부 죽어 있는 상태보다 훨씬 다루기 어려웠다. 무엇이 떠 있는지 모르는 채로 다시 시작하면 겹쳐서 뜬다.

지금 무엇이 떠 있는지 보는 것도 같이 만들었다.

status_all() {
    for s in $SERVICES; do
        printf "%-10s " "$s"
        check_health "$s" && echo "OK" || echo "DOWN"
    done
}

status_allcheck_health 결과를 그대로 줄 세워 보여 준다.

redis      OK
matcher    OK
api        DOWN
ws         OK

한눈에 보이니 어디까지 됐는지가 그 자리에서 갈렸고 하나씩 확인하며 빠뜨리는 일이 없어졌다.

정리


Share this post on:

Previous Post
매칭은 양방향으로 확인한다
Next Post
경로 하나에 근거 넷