여러 프로세스를 한 번에 띄우고 내리는 스크립트를 만들었다. 띄우는 것은 됐는데 내릴 때 오류가 쌓였다.
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 가 먼저 내려가는 순간 matcher 와 api 와 ws 는 쓰던 연결이 끊긴 채로 남는다. 아직 쓰이고 있는 것을 먼저 치우니 나머지가 정상 종료할 길이 없었다.
종료 순서는 따로 정할 것이 아니라 시작 순서 하나에서 나오는 것이었다. 같은 의존 순서를 두 곳에 나눠 적어 두면 한쪽만 고쳤을 때 조용히 어긋난다.
조치 — 역순으로 내렸다
의존하는 쪽을 먼저 내리고 의존받는 쪽을 나중에 내렸다.
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_all 은 check_health 결과를 그대로 줄 세워 보여 준다.
redis OK
matcher OK
api DOWN
ws OK
한눈에 보이니 어디까지 됐는지가 그 자리에서 갈렸고 하나씩 확인하며 빠뜨리는 일이 없어졌다.
정리
- 시작 순서를 정했으면 종료 순서는 그 역순이다
- 따로 적지 말고
SERVICES하나를tac으로 뒤집는다 - 띄우자마자 넘어가지 말고
wait_ready로 기다린다 - 기다림에는 상한을 둔다
- 떠 있는 것과 받을 준비가 된 것은 다르다
- 내릴 때도 끝난 것을 확인하고 넘어간다
- 매번
SIGKILL이 필요하면SIGTERM을 안 다루는 것이다 - 종료 신호를 받으면 하던 것을 끝내고 나가게 한다
- 시작 중 실패하면 앞서 띄운 것을 역순으로 내린다
- 반쯤 뜬 상태가 가장 다루기 어렵다
- 전체 상태를 한눈에 보는 수단을 함께 만든다