설치 스크립트를 다시 돌렸다. 옵션 값 하나를 바꾸려는 것이었다.
며칠 뒤 수집이 두 배로 들어왔다.
Table of contents
Open Table of contents
예약이 두 개였다
예약 목록을 열어 봤다.
$ crontab -l
*/5 * * * * /opt/collect/run.sh --offset 0
*/5 * * * * /opt/collect/run.sh --offset 15
두 줄이 있었다. 처음 돌렸을 때 하나가 들어가고 다시 돌렸을 때 하나가 더 들어간 것이다.
--offset 값이 달라 run.sh 가 5분마다 두 번 돌았다. 옵션만 바뀔 줄 알았던 것이 항목을 하나 더 만든 셈이다.
원인 — 더하기만 하는 등록
설치 스크립트의 해당 줄을 봤다.
(crontab -l 2>/dev/null; echo "*/5 * * * * /opt/collect/run.sh --offset $OFFSET") | crontab -
crontab -l 로 기존 목록을 뽑고 거기에 한 줄을 덧붙여 다시 넣는다. 지우는 단계가 없다.
한 번 돌리는 것을 전제로 쓴 코드였다. 실제로는 OFFSET 을 바꿀 때마다 돌리게 되고 그때마다 줄이 하나씩 는다.
지우고 넣게 바꿨다
기존 것을 지운 뒤 넣게 고쳤다.
MARK="# managed-by-collect-installer"
# 기존 항목 제거 후 재등록
( crontab -l 2>/dev/null | grep -v "$MARK"
echo "*/5 * * * * /opt/collect/run.sh --offset $OFFSET $MARK"
) | crontab -
MARK 가 붙은 줄만 grep -v 로 걸러 내고 새 줄을 넣는다. 몇 번을 돌려도 그 표시가 붙은 줄은 하나다.
표시를 붙인 것은 남의 항목을 안 건드리기 위해서다. 같은 crontab 에 다른 작업이 함께 있는데 명령 이름만으로 지우면 손으로 넣은 줄까지 날아간다.
검증 — 등록된 개수를 센다
등록한 다음에 개수를 셌다.
CNT=$(crontab -l 2>/dev/null | grep -c "$MARK")
if [ "$CNT" -ne 1 ]; then
echo "예약 등록 결과가 ${CNT}개입니다 (기대: 1)"
crontab -l | grep "$MARK"
exit 1
fi
echo "예약 등록 확인: 1개"
grep -c 로 센 값이 1이 아니면 목록을 보여 주고 exit 1 한다. 등록 명령이 성공했다는 사실이 아니라 지금 몇 개가 있는지를 본다.
이번 일이 딱 그 차이였는데 crontab - 은 두 번 다 성공했고 그래서 아무도 이상을 몰랐다.
같은 문제가 다른 데도 있었다
여러 번 돌리면 늘어나는 자리를 찾아봤다. 설정 파일에 줄을 덧붙이는 것이 먼저 걸렸다.
# 나쁨
echo "PATH=/opt/bin:\$PATH" >> ~/.bashrc
# 나음
grep -q "^PATH=/opt/bin" ~/.bashrc || echo "PATH=/opt/bin:\$PATH" >> ~/.bashrc
>> 로 덧붙이기만 하면 같은 설정이 여러 번 들어간다. 대개 마지막 것이 이기므로 동작은 맞는데 파일이 지저분해지고 읽는 사람이 헷갈린다.
systemd 유닛 등록도 같았고 iptables 규칙도 같았다. 같은 규칙이 여러 벌 들어가 목록만 길어져 있었다.
셋 다 있으면 안 넣거나 지우고 넣는 쪽으로 고쳤다.
판단 기준 — 두 방식의 차이
두 방식이 같은 것이 아니라는 것을 고치면서 알았다.
[있으면 안 넣는다] 값이 바뀌어도 안 바뀐다 — 옛 값이 남는다
[지우고 넣는다] 값이 갱신된다 — 손으로 고친 것도 덮인다
OFFSET 처럼 인자로 들어오는 값은 지우고 넣는 쪽으로 했는데 바꾸려고 다시 돌리는 것이라 안 바뀌면 목적을 못 이룬다.
~/.bashrc 처럼 사람이 고치는 것은 있으면 안 넣는 쪽으로 뒀다. 다시 돌렸다고 손으로 맞춰 둔 값이 날아가면 안 된다.
변경 내용 — 실행 전 상태 출력
돌리기 전에 지금 무엇이 있고 무엇이 들어갈지를 보여 주게 했다.
echo "현재 등록된 항목:"
crontab -l 2>/dev/null | grep "$MARK" || echo " (없음)"
echo ""
echo "새로 등록할 항목:"
echo " */5 * * * * /opt/collect/run.sh --offset $OFFSET"
echo ""
printf "계속하시겠습니까? (yes): "
read ans
[ "$ans" = "yes" ] || exit 1
현재 항목과 새 항목을 나란히 찍고 read 로 yes 를 받아야 진행하므로 잘못된 OFFSET 을 넣은 것도 그 자리에서 보인다.
사전 준비 — 제거 스크립트
설치만 있고 제거가 없었다. 지우려면 사람이 crontab -e 로 손수 찾아 지워야 했다.
#!/bin/sh
crontab -l 2>/dev/null | grep -v "$MARK" | crontab -
echo "예약 제거 완료"
CNT=$(crontab -l 2>/dev/null | grep -c "$MARK")
[ "$CNT" -eq 0 ] && echo "확인: 0개" || { echo "아직 ${CNT}개 남음"; exit 1; }
제거 쪽에도 grep -c 확인을 넣어 0개가 아니면 실패시킨다. 설치와 제거가 짝이면 번갈아 돌려 결과가 같은지 볼 수 있다.
여러 번 돌려서 확인했다
고친 뒤에 세 번 연속으로 돌렸다.
1회차: 예약 등록 확인: 1개
2회차: 예약 등록 확인: 1개
3회차: 예약 등록 확인: 1개
몇 번을 돌려도 하나다. 한 번 돌려서 되는 것과 여러 번 돌려도 되는 것은 다른 조건이다.
설치 스크립트는 뒤쪽이어야 하는데 값을 바꾸거나 중간에 실패해 다시 돌리는 일이 반드시 생기기 때문이다.
정리
- 등록 스크립트가 기존 것을 안 지우면 실행할 때마다 늘어난다
- 한 번 돌리는 것을 전제로 쓴 코드는 두 번째부터 어긋난다
- 줄 끝에
MARK를 붙이고 그 표시가 있는 것만 지운 뒤 넣는다 - 표시가 있으면 손으로 넣은 남의 항목을 안 건드린다
- 등록 명령의 성공이 아니라
grep -c로 센 개수를 확인한다 - 설정 파일과 서비스 등록과 방화벽 규칙에도 같은 문제가 있다
- 값이 바뀌어야 하면 지우고 넣고 사람이 고치는 것은 있으면 안 넣는다
- 실행 전에 현재 항목과 새 항목을 나란히 보여 준다
- 설치와 제거를 짝으로 만들고 제거에도 개수 확인을 넣는다
- 세 번 연속 돌려 결과가 같은지 확인한다