리워드 정산 스크립트를 새벽에 돌리게 등록했는데 아침에 확인하니 결과가 없었다. 터미널에서 같은 명령을 치면 잘 되고 등록한 것만 안 된다.
Table of contents
Open Table of contents
오류를 어디서 보나
먼저 오류를 찾을 자리가 없었다. crontab 줄에 출력 경로가 없어서 실행이 됐는지도 몰랐다.
0 4 * * * /home/deploy/bin/settle.sh >> /home/deploy/log/settle.log 2>&1
출력을 파일로 받게 바꿨다. 뒤의 2>&1 이 중요하다. 이게 없으면 stderr 가 빠져서 stdout 만 쌓인다. 실패해도 로그가 비고, 빈 로그는 안 돌았다는 것과 구분이 안 된다.
다음 날 로그에 자바를 못 찾는다는 줄이 남았다.
환경 변수가 다르다
로그인해서 치면 되는 이유가 여기 있었다. 로그인 셸은 /etc/profile 과 ~/.bash_profile 을 읽고 거기서 PATH 에 JDK 가 붙는다. cron 은 비대화형·비로그인 셸이라 그 파일들을 읽지 않는다.
실제 값을 보려고 env > /tmp/cronenv.txt 를 한 줄 걸어 뽑아 봤다. cron 이 넣어 주는 것은 HOME·LOGNAME·SHELL=/bin/sh·PATH=/usr/bin:/bin 넷뿐이었다. /usr/local/bin 도 없다.
밖에서 넣어 주기를 기대하지 않고 스크립트가 직접 정하게 했다.
JAVA_HOME=/usr/local/jdk1.6.0_25
PATH=$JAVA_HOME/bin:/usr/bin:/bin
export JAVA_HOME PATH
/usr/local/jdk1.6.0_25/bin/java 처럼 절대 경로로 쓰는 방법도 있다. 앞쪽이 읽기 편하고 뒤쪽이 확실해서, 스크립트에 명령이 여럿이면 환경 변수를 정하고 하나뿐이면 절대 경로로 썼다.
디렉터리도 권한도 달랐다
경로를 고치니 다음 오류가 나왔다. 실행할 jar 를 못 찾는다는 것이다. 터미널에서는 그 디렉터리로 들어가 쳤는데 cron 은 HOME 에서 시작한다.
cd /home/deploy/settle || exit 1
뒤의 || exit 1 이 필요했다. 없으면 HOME 에서 나머지 명령이 다 돈다.
그다음은 권한이었다. 로그 파일을 못 만든다고 났다. 로그인해서 칠 때는 sudo 를 붙였고 cron 은 등록한 계정으로 돈다. chown 으로 로그 디렉터리 소유자를 그 계정으로 바꿨다. sudo 가 필요한 명령은 애초에 이 방식에 안 맞는 것 같다.
겹쳐 도는 것과 되돌리기
정산이 오래 걸리는 날 다음 실행이 들어왔다. 같은 데이터를 두 번 정산해 MySQL 에 중복 행이 생겼다. 잠금 파일로 막았다.
LOCK=/tmp/settle.lock
if [ -f "$LOCK" ]; then exit 0; fi
echo "$$ $(date)" > "$LOCK"
trap 'rm -f "$LOCK"' EXIT
trap 으로 걸어 두면 중간에 죽어도 지워진다. 죽는 방식에 따라 안 지워질 수 있어 파일에 PID 와 시각을 적어 뒀다. 오래된 잠금이면 ps 로 그 번호가 살아 있는지 보고 지울지 정한다.
등록만 하고 다음 날을 기다리면 하루가 그냥 간다. 간격을 2분으로 줄여 한 번 돌려 보고 로그를 확인한 뒤 원래 시각으로 되돌렸다. 시험용 줄을 안 지우면 그게 계속 도니 되돌리는 것까지가 한 묶음이다.
정리
- 예약 실행은 프로필을 안 읽어서 실행 경로가 로그인 셸과 다르다
- 출력을 파일로 받되 오류 출력도 같이 받는다
- 오류 출력이 빠지면 실패와 미실행이 구분되지 않는다
- 환경이 궁금하면 환경 변수를 한 번 뽑아서 실제 값을 본다
- 시작 디렉터리는 홈이다. 스크립트 안에서 옮기고 실패하면 멈춘다
- 권한은 등록한 계정 것이다. 관리자 권한이 필요하면 이 방식에 안 맞는다
- 오래 걸리는 작업은 겹쳐 돈다. 잠금 파일로 막고 끝날 때 지우게 건다
- 등록 뒤에 간격을 줄여 확인하고 시험용 줄을 되돌린다