job 표에 정체된 4,449건을 여러 서버에 나눠 재시도하는 스크립트를 설계했다. 단건 실행 명령이 이미 있어서 그것을 반복해 부르는 방식을 검토했다.
Table of contents
Open Table of contents
상황 — 단건 실행 명령의 성격
명령은 이렇게 생겼다.
./artisan <명령> --job-id=<id>
--job-id 로 도는 이 경로가 평소 워커와 같은 처리를 하는지부터 확인해야 했다.
같은 처리를 부르는 것으로 보이지만 들어가는 문이 다를 수 있고 문이 다르면 그 WHERE 도 다르다.
원인 — 거치지 않는 스코프
실행 코드를 열었다.
private function executeOne($jobId)
{
$job = Job::find($jobId); // 직접 조회
...
}
Job::find 로 바로 찾아서 처리에 들어간다.
// 워커 경로 (여기서만 씀)
Job::standby()->where('fail_status', null)->get();
standby 스코프도 fail_status 조건도 executeOne 은 안 탄다.
executeOne 안에서 차단하는 것이 있는지 봤더니 하나뿐이었다.
if ($job->completed_at) {
return; // 이미 완료면 건너뜀
}
completed_at 만 보고 나머지는 안 본다.
결과 — 두 가지가 편해졌다
이것을 알고 나니 계획이 단순해졌다.
fail_status 가 찍힌 건도 그대로 재시도되므로 사전에 되돌리는 갱신이 필요 없다. 조건을 안 거치는 것이 이 작업에서는 이득으로 작용했다.
이미 완료된 것이 섞여도 completed_at 가드에 걸려 무해하다. 완료 처리와 재시도의 순서를 엄격히 맞추지 않아도 된다는 뜻이다.
안전한 부분과 안 안전한 부분이 갈려 있어서 그 경계를 알고 써야 했다.
제약 — 범위로 넘기면
반대로 위험한 쪽도 같은 성질에서 나온다.
for id in $(seq 340000000 345000000); do ... done
seq 로 범위를 넘기면 그 안의 모든 작업이 실행되고 아무것도 안 걸러 준다.
실제로 그런 스크립트가 이미 있었다.
# id 범위로 작성돼 있음
345085389 ~ 345282019
확인해 보니 그 id 범위에 대상이 0건이었다.
스크립트 범위 345,085,389 ~ 345,282,019
실제 대상 범위 340,405,648 ~ 345,043,486
두 id 범위가 겹치지 않아서 아무것도 안 돌고 있었다. 편의를 위한 인자가 그대로 사고 경로가 되어 있었다.
조치 — 목록을 파일로 고정
방식을 바꿨다.
# DB 에서 추출
mysql -e "SELECT id FROM ... WHERE <조건>" > job_ids.txt
# 한 줄씩
while read -r id; do
./artisan <명령> --job-id="$id"
done < job_ids.txt
job_ids.txt 로 목록을 고정하고 그것만 돈다.
while read 대신 메모리에 담아 돌려도 되는데 파일로 한 데는 이유가 셋 있었다. 중단해도 어디까지 했는지 알면 이어갈 수 있고 무엇을 돌렸는지가 남으며 파일을 쪼개 여러 서버에 나눌 수 있다.
실행 시점에 SELECT 을 다시 하면 그 사이에 대상이 바뀔 수 있다. 파일과 그것을 뽑을 때 쓴 조건이 나중에 범위의 근거가 된다.
검증 — 0건이 조용했다
executeOne 의 성공과 실패에 무엇이 바뀌는지도 봤다.
성공 → 완료 시각 설정
실패 → 로그 갱신, 시도 횟수 증가, 실패 상태 재매핑
실패해도 fail_status 가 남으므로 돌린 뒤에 결과를 집계할 수 있다.
SELECT fail_status, COUNT(*) FROM jobs WHERE id IN (<목록>) GROUP BY 1;
이 건에서 가장 걸린 것은 다른 쪽이었다.
스크립트 실행 → 정상 종료
처리 건수 → 0
잘못된 범위로 돌리면 0건이 처리되는데 종료 코드가 0이라 성공처럼 보인다.
앞서 여러 번 만난 것과 같은 모양이다. 종료 코드가 아니라 건수를 봐야 실제로 무엇을 했는지 알 수 있어서 돌린 뒤에 예상 건수와 대조하는 것을 절차에 넣었다.
정리
- 단건 실행이 워커의 조건을 안 거칠 수 있다
- 같은 처리를 부르는 두 경로가 서로 다른 문을 지난다
- 안쪽 가드는 완료 여부만 보고 나머지는 안 본다
- 그래서 실패 상태 건도 사전 복원 없이 재시도된다
- 완료된 건이 섞여도 가드에 걸려 무해하다
- 안전한 부분과 아닌 부분의 경계를 알고 쓴다
- 범위로 넘기면 의도 밖까지 실행되고 아무것도 안 걸러 준다
- 조건이 바뀌면 예전에 쓴 범위가 빗나간다
- 추출한 목록을 파일로 고정하고 한 줄씩 돈다
- 파일이면 이어가기와 기록과 분배가 된다
- 잘못된 범위는 0건인데 오류가 없다
- 종료 코드가 아니라 건수를 확인한다