크롤링한 자료를 정리하는 배치를 만들어 cron 에 걸었다. 돌릴 때마다 처리 건수가 달랐고, 로그에는 대상이 1,204건이라고 찍혔는데 실제로는 1,251건을 처리했다.
Table of contents
Open Table of contents
세는 것과 가져오는 것이 따로였다
COUNT(*) 를 한 번 돌려 로그에 적고, 그다음 같은 조건으로 LIMIT 100 씩 가져와 처리하는 구조였다. 두 쿼리가 각각 도니 그 사이에 대상이 변할 수 있다.
크롤러가 동시에 돌면서 새 자료를 계속 넣고 있었다. 세고 나서 가져오기 시작하는 사이에 조건에 맞는 행이 늘어난 것이다. 로그의 숫자와 실제 처리한 숫자가 다른 이유가 여기 있었다.
표시부터 했다
처리를 시작할 때 대상에 회차 표시를 남기게 바꿨다.
$batch = date('YmdHis');
$this->db->query(
"UPDATE crawl_data SET batch_id = ? WHERE status = 'raw' AND batch_id IS NULL",
[$batch]
);
$total = $this->db->affected_rows();
affected_rows() 가 곧 표시된 개수다. UPDATE 한 문장이 세는 일과 표시하는 일을 같이 하므로 그 사이가 없다.
가져올 때는 그 표시가 붙은 것만 본다. 표시한 뒤에 들어온 자료는 batch_id 가 비어 있으니 이번 회차 대상이 아니다.
이전 구조에는 다른 문제도 있었다. 유입이 처리보다 빠르면 조건에 맞는 행이 계속 있어서 반복문이 안 끝났다. 표시로 집합을 고정하니 대상이 정해진 개수가 된다. 그것만 처리하면 끝나고 나머지는 다음 회차가 가져간다.
남은 표시를 회수하는 기준
중간에 죽으면 batch_id 만 찍히고 처리는 안 된 행이 남는다. 다음 실행에서 DATE_SUB 로 시간을 재서 회수하게 했다.
$this->db->query(
"UPDATE crawl_data SET batch_id = NULL
WHERE status = 'raw' AND batch_id IS NOT NULL
AND batch_date < DATE_SUB(NOW(), INTERVAL 30 MINUTE)"
);
30분은 실제 처리 시간을 재서 정했다. 가장 오래 걸린 회차가 8분이라 거기에 여유를 뒀다. 너무 짧게 잡으면 아직 도는 중인 것을 회수해서 두 번 처리하고, 너무 길게 잡으면 복구가 늦어진다.
회차별로 볼 수 있게 됐다
표시가 데이터에 남으니 회차 단위로 결과를 조회할 수 있게 됐다.
batch_id total done pending failed
20131223-0300 1204 1198 0 6
20131222-0300 1180 1180 0 0
어느 회차에 실패가 몰렸는지 보인다. 이전에는 전체 실패 수만 알 수 있어서 특정 회차의 문제인지 계속 나는 문제인지 갈리지 않았다.
겹쳐 도는 것도 같이 막았다. 두 개가 동시에 돌면 서로 표시를 나눠 가지니 결과는 맞는데 부하가 두 배가 된다. GET_LOCK 에 대기 시간을 0으로 줘서 못 잡으면 바로 끝내게 했다. 이 잠금은 세션이 끊기면 알아서 풀린다. 표시는 남는데 잠금은 안 남으니 회수는 표시 쪽만 하면 됐다.
정리
- 세는 쿼리와 가져오는 쿼리가 따로면 그 사이에 대상 집합이 변한다
- 처리 전에 대상에 표시를 남겨 집합을 고정한다
UPDATE로 표시하면서 세면affected_rows()가 그 값이다- 대상이 고정되면 유입이 빨라도 반복문이 끝난다
- 죽어서 남은
batch_id는DATE_SUB로 회수하고 기준은 실제 처리 시간을 재서 정한다 - 회수 기준이 짧으면 도는 중인 것을 두 번 처리하고 길면 복구가 늦다
- 회차 식별자가 데이터에 남으면 회차별 결과를 볼 수 있다
- 겹쳐 도는 것은
GET_LOCK대기 0으로 막고 그 잠금은 세션이 끊기면 풀린다