장비 열세 대에서 수집 로그를 한꺼번에 받아 오는 스크립트를 만들었다. 한 대씩 돌리면 반나절이 걸린다. 열세 개를 백그라운드로 띄우고 wait 으로 전부 기다리게 짰다.
끝났다는 출력이 나왔다. 그런데 같은 시각에 세 대는 아직 파일을 쓰고 있었다.
Table of contents
Open Table of contents
wait이 기다리는 것은 자기 자식뿐이다
wait 은 현재 셸이 직접 띄운 자식 프로세스만 기다리고 매뉴얼에도 그렇게 적혀 있다.
for host in $HOSTS; do
collect "$host" &
done
wait
echo "done"
이 형태 자체는 흔한 것이라 틀릴 데가 없어 보였는데 문제는 collect 안의 재시작 로직이었다.
collect 는 실패하면 자기를 다시 띄웠다. nohup 으로 새 프로세스를 만들고 자신은 종료한다.
새로 뜬 쪽은 그 셸의 자식이 아니라서 wait 이 세는 집합 밖으로 나간 것이다.
죽은 자식이 밖에서 되살아나면
그러면 이런 순서가 된다.
열세 개 중 세 개가 중간에 실패한다
그 셋이 각각 새 프로세스를 만들고 원래 프로세스는 끝난다
wait 은 자기 자식 열세 개가 전부 끝난 것을 확인한다
새로 뜬 셋은 아직 돌고 있는데 echo "done" 이 나온다
wait 은 거짓말을 하지 않았고 자기가 아는 자식 열세 개는 정말로 다 끝났다.
다만 내가 아는 작업과 셸이 아는 자식이 서로 다른 집합이 됐고 같은 말이 다른 것을 가리켰다.
종료 코드도 같은 이유로 못 믿는다
$? 가 0이라는 것도 마찬가지인데 실패한 셋의 원래 프로세스는 재시작을 띄운 뒤 정상 종료했다.
실패는 재시작 쪽으로 옮겨 갔는데 종료 코드는 성공으로 남았다. 로그에는 이렇게 찍혀 있었다.
[10:02] collect started: 13
[10:41] wrapper exit 0
[10:58] host-04 writing chunk 118
wrapper exit 0 뒤에 17분이나 더 파일이 쓰이고 있었다. 종료 코드 하나만 보고 다음 단계로 넘어갔으면 반쯤 받은 파일을 쓸 뻔했다.
조치 — 완료 판정의 두 근거
래퍼의 종료를 완료 판정에 쓰는 것을 그만두고 대신 두 가지를 따로 확인하게 했다.
첫째는 살아 있는 프로세스가 있는지다. pgrep -f collect 로 세면 셸의 자식이든 아니든 이름으로 잡히므로 재시작분도 포함된다.
둘째는 각 대상이 어디까지 갔는지이고 진행률을 파일에 남기게 하고 그것을 읽는다.
프로세스 있음 → 도는 중
프로세스 없음 + 진행률 미달 → 죽음
프로세스 없음 + 진행률 끝 → 정상 종료
둘을 같이 봐야 도는 중과 끝남과 죽음이 갈리고 하나만으로는 셋 중 둘이 붙어 버린다.
pgrep 만 보면 죽은 것과 끝난 것이 같아 보인다. 진행률만 보면 도는 중인 것과 죽은 것이 붙는다.
재시작을 쓰면 갈라지는 것이 정상이다
처음에는 재시작 로직을 빼려고 했는데 그러면 wait 이 정확해지기 때문이다.
빼지 않았다. 열세 대 중 몇은 원래 응답이 불안정해서 재시작 없이 돌리면 매번 사람이 붙어 다시 실행해야 했다.
재시작은 그 문제를 실제로 줄이고 있어서 정확한 wait 을 얻으려고 버리면 손해가 더 컸다.
대신 래퍼가 아는 집합과 실제로 도는 집합이 갈라지는 것을 정상 동작으로 받아들였다. 완료 판정 쪽을 그 전제에 맞춰 고친 것이다.
재시작을 쓰는 구조에서는 두 집합이 갈라지는 것이 예외가 아니라 기본이다. 그 전제를 스크립트 주석에 적어 다음 사람이 같은 가정을 안 하게 했다.
정리
wait은 셸이 직접 띄운 자식만 기다린다nohup으로 새로 뜬 프로세스는 그 집합 밖이다- 래퍼의 종료는 작업 완료의 근거가 되지 못한다
- 재시작을 띄우고 정상 종료하면 실패가 성공 코드 뒤로 숨는다
- 완료 판정은
pgrep -f생존 확인과 대상별 진행률 두 가지로 한다 - 프로세스만 세면 죽은 것과 끝난 것이 안 갈린다
- 진행률만 보면 도는 중인 것과 죽은 것이 안 갈린다
- 재시작이 실제로 값을 하면 그것을 빼는 대신 판정을 고친다
- 두 집합이 갈라지는 것은 예외가 아니라 기본 동작이다