일별 집계에서 어느 하루만 값이 두 배로 나왔다. 다른 날은 전부 평소 수준이었다. 그 하루에만 무엇이 달랐는지부터 확인하기로 했다.
Table of contents
Open Table of contents
원인 — 원본은 정상이었다
먼저 MySQL 에서 그날 원본을 세어 봤더니 평소와 비슷했다. 원본이 정상인데 집계만 두 배로 나오면 집계 과정에 문제가 있는 것이다. 확인할 범위가 배치 쪽으로 좁혀졌다.
배치 코드는 CodeIgniter 의 db->insert() 한 줄이었다.
$sum = 계산();
$this->db->insert('daily_stats', ['date' => $date, 'value' => $sum]);
daily_stats 에 넣기만 한다. 그 테이블을 열어 봤다.
SELECT * FROM daily_stats WHERE date = '...';
그날 날짜의 행이 둘이었다. 하나가 아니라 둘이 있으니 합계가 두 배가 된 것이다.
사람이 한 번 더 돌렸다
실행 로그를 보니 cron 이 정해진 시각에 돌린 것과 몇 분 뒤 것이 각각 있었다. 확인해 보니 첫 실행이 실패한 줄 알고 사람이 다시 돌린 것이었다. 그런데 첫 실행은 정상으로 끝나 있었다.
로그의 어느 줄을 실패로 잘못 읽은 것이 시작이었다. 사람이 로그를 잘못 읽는 일은 앞으로도 생긴다. 사람이 조심하는 쪽은 또 실수하지만 두 번 돌아도 같은 결과가 나오게 만들면 실수해도 안전하다.
두 번 돌아도 같아지게 했다
넣기만 하는 대신 있으면 갱신하게 바꿨다.
INSERT INTO daily_stats (date, value) VALUES (?, ?)
ON DUPLICATE KEY UPDATE value = VALUES(value);
ON DUPLICATE KEY UPDATE 가 걸리려면 date 에 고유 키가 있어야 한다. 키가 없으면 VALUES(value) 까지 가지 않고 그냥 또 들어간다.
ALTER TABLE daily_stats ADD UNIQUE (date);
키를 걸려면 이미 들어간 중복을 먼저 지워야 했다.
DELETE t1 FROM daily_stats t1
JOIN daily_stats t2 ON t1.date = t2.date AND t1.id > t2.id;
id 가 큰 쪽을 지우는 식이다. 여기서 잠깐 걸렸다. 먼저 것은 첫 실행 결과이고 나중 것은 재실행 결과인데 어느 쪽을 남길지가 문제였다. 확인해 보니 값이 같아서 어느 쪽이든 됐다. 달랐으면 나중 것이 최신 자료를 본 것이라 맞았겠지만 왜 다른지를 먼저 봐야 했을 것이다.
REPLACE 도 결과는 비슷한데 그쪽은 옛 행을 지우고 새로 넣는다. 그 행을 가리키는 것이 있으면 이야기가 달라져서 갱신하는 쪽을 골랐다.
조치 — 누적되는 것
이 일이 있고 다른 배치들도 봤더니 넣기만 하는 것이 여럿이었다. 집계와 정산과 통계를 같은 방식으로 전부 바꿨다.
다만 못 바꾸는 것이 있었다. 집계는 다시 계산하면 같은 값이 나오니 덮어써도 되는데 로그 적재처럼 누적되는 것은 덮으면 안 된다. 성격이 다르다.
누적되는 것은 daily_stats 처럼 덮을 수가 없어서 어디까지 처리했는지를 남기게 했다.
last_processed_id
그다음부터 처리하면 두 번 돌아도 같은 것을 다시 처리하지 않는다. 다시 계산할 수 있는 것은 덮어쓰고 누적되는 것은 처리 지점을 남기는 두 방법으로 갈렸고 둘 다 두 번 돌아도 안전해진다.
처음 목적은 사람이 두 번 돌린 실수를 막는 것이었는데 하고 보니 그것만이 아니었다. 배치가 중간에 죽어 자동으로 다시 돌 때도 같은 문제가 난다. 지난 며칠치를 한꺼번에 다시 계산하는 것도 같은 이유로 가능해졌다.
정리
- 원본이 정상인데 집계만 틀리면 집계 과정을 본다
- 같은 날짜의 행이 둘이면 합계가 두 배가 된다
- 로그를 잘못 읽는 일은 앞으로도 생긴다
- 사람이 조심하는 것보다 두 번 돌아도 되게 만드는 편이 안전하다
INSERT만 하는 배치는 한 번 도는 것을 전제한다- 그 전제는 재실행과 장애 복구에서 깨진다
UNIQUE를 걸고ON DUPLICATE KEY UPDATE로 바꾼다- 키를 걸려면 기존 중복을 먼저 지운다
- 값이 다르면 왜 다른지 먼저 본다
- 다시 계산 가능한 것은 덮어쓰고 누적되는 것은 처리 지점을 남긴다