발송이 이틀 밀렸다. 문제를 고치고 밀린 것을 처리하려고 목록을 돌렸다. 3,200건이 한꺼번에 나갔고 중간부터 전부 실패했다.
Table of contents
Open Table of contents
무엇에 걸렸나
HTTP 응답 본문에 요청 한도를 넘었다는 코드가 왔다. 발송 업체 규격서를 보니 초당 요청 수 제한이 있었다. 상태 코드가 아니라 본문에 코드를 담는 방식이었다.
for (Message m : pending) {
smsClient.send(m);
}
반복문으로 돌리니 초당 수백 건이 나갔다. 밀렸다는 것은 그동안 안 나간 양이 쌓였다는 뜻이고 그걸 한 번에 내보내면 평소의 몇십 배가 된다. 평소에 안 걸리던 한도가 밀린 것을 처리할 때 걸리는 이유가 여기 있다.
속도를 맞췄다
간격을 두고 보냈다. 발송에 걸린 시간을 빼고 쉬게 했다.
long interval = 1000 / 8; // 한도 10보다 낮게
for (Message m : pending) {
long start = System.currentTimeMillis();
smsClient.send(m);
long spent = System.currentTimeMillis() - start;
if (spent < interval) Thread.sleep(interval - spent);
}
한도보다 낮게 잡은 것은 다른 기능도 같은 업체를 부르기 때문이다. 우리가 한도를 다 쓰면 그쪽이 막힌다.
Thread.sleep 은 잰 시간만큼 정확히 쉬지는 않는다. 최소 그만큼 쉰다는 뜻이라 실제로는 조금 더 쉰다. 한도보다 낮게 잡은 김에 그 오차는 그냥 뒀다.
다시 보낼 것과 아닌 것을 나눴다
한도에 걸려 실패한 건이 어떻게 됐는지 확인이 안 됐다. 상태를 안 남겼기 때문이다. MySQL 의 그 행에 결과를 적게 했다.
try {
smsClient.send(m);
update(m, "sent", null);
} catch (RateLimitException e) {
update(m, "ready", "한도 초과"); // 다시 대상
} catch (InvalidNumberException e) {
update(m, "failed", "번호 오류"); // 다시 보내도 소용없음
}
번호가 틀린 것을 계속 재시도하면 한도만 먹는다. 시도 횟수에 상한도 걸었다. 세 번 실패하면 대상에서 빠지고 그건 따로 뽑아 사람이 본다. 상한이 없으면 실패하는 건이 매 회차 정상 건의 자리를 먹는다.
한 번에 가져오는 양
3,200건을 메모리에 다 올리는 것도 문제였다. JDBC 는 기본으로 결과를 다 받아 놓는다. 건수가 더 늘면 OutOfMemoryError 가 난다.
Connector/J 에는 한 줄씩 흘려받는 길이 있다. setFetchSize(Integer.MIN_VALUE) 를 주면 된다. 그런데 그 결과를 다 읽기 전에는 같은 연결로 다른 쿼리를 못 던진다. 우리는 한 건마다 UPDATE 를 해야 해서 못 썼다.
LIMIT 500 으로 끊어 가져와 처리하고 다음을 가져오게 했다.
여기서 한 번 무한 반복이 났다. 실패했을 때 상태를 그대로 두는 경로가 있었다. 그래서 같은 500건을 계속 가져왔다. 꺼낸 것은 반드시 상태가 바뀌게 했다. 실패도 상태 변화다.
밀리지 않게 하는 쪽도 봤다. 왜 이틀이 밀렸는지 보니 crontab 에 걸린 발송 작업이 죽어 있었다. 아무도 몰랐다. 마지막 발송 시각을 보고 한 시간 이상 옛날이면 메일이 오게 했다. 밀린 것을 잘 처리하는 것과 안 밀리게 하는 것은 다른 문제다.
정리
- 밀린 것을 한 번에 처리하면 평소의 몇십 배가 순간에 몰린다
- JDBC 는 결과를 다 받아 놓는다. 건수가 크면
LIMIT으로 끊는다 - 평소에 안 걸리던 한도가 그때 걸린다
- 상대의 한도를 확인하고 그보다 낮게 맞춘다
- 다른 기능도 같은 한도를 쓴다
- 발송 결과를 남기지 않으면 무엇이 실패했는지 모른다
- 다시 보낼 것과 보내도 소용없는 것을 나눈다
- 시도 횟수에 상한을 둔다. 없으면 실패 건이 정상 건의 자리를 먹는다
- 꺼낸 것은 반드시 상태가 바뀌게 한다. 안 그러면 무한 반복이다
- 밀린 것을 처리하는 것과 안 밀리게 하는 것은 다른 문제다