잔고 변경 이벤트를 큐로 보내는데 순서가 뒤집혀 잔고가 틀리는 일이 있었다. 넣은 순서와 큐가 준 순서는 같았으므로 큐 자체는 순서를 지키고 있었다.
Table of contents
Open Table of contents
큐가 준 순서와 끝난 순서
소비하는 쪽이 여럿이라 각자 하나씩 받아 동시에 처리하고 있었다. 앞의 이벤트가 외부 호출 때문에 몇 초 걸리는 사이에 뒤의 두 이벤트가 먼저 끝났다.
입금이 반영되기 전에 출금이 먼저 반영되니 중간 잔고가 음수로 찍혔다. 큐가 순서를 줘도 처리가 끝나는 순서는 별개라는 것이 이 문제의 전부였다.
같은 대상을 한 소비자에게
계좌 번호로 파티션을 나눠서 같은 계좌의 이벤트는 항상 같은 소비자에게 가게 했다. 그 소비자 안에서는 순서대로 처리되므로 같은 계좌의 순서가 지켜진다.
다른 계좌끼리는 여전히 병렬로 돌기 때문에 전부를 하나로 처리하는 것보다 훨씬 빠르다. 순서를 지켜야 하는 범위를 계좌 단위로 좁힌 것이 이 방식의 핵심이었다.
배정이 바뀌는 순간
파티션을 소비자 수로 나눠 쓰면 소비자가 늘거나 줄 때 어떤 계좌의 담당이 바뀐다. 바뀌는 순간에 이전 담당이 처리 중이던 것과 새 담당이 받은 것이 겹칠 수 있었다.
그래서 배정이 회수될 때 처리 중인 것을 끝내고 진행 위치를 커밋한 뒤에 넘기게 했다. 커밋 전에 넘기면 다음 소비자가 같은 이벤트를 다시 처리하게 된다.
순서가 필요한 것만 고른다
이벤트 종류를 훑어보니 순서가 결과를 바꾸는 것은 잔고 변경뿐이었고 알림과 통계는 순서와 무관했다. 전부 순서를 지키게 만들면 병렬성이 사라져 처리량이 크게 준다.
순서를 지키는 것은 공짜가 아니므로 그것이 결과를 바꾸는 종류만 골라서 적용했다. 나머지는 그대로 병렬로 두었다.
순번과 쏠림
이벤트에 순번을 붙여서 이미 반영된 것보다 낮은 순번이 오면 버리게 했고 파티션이 순서를 지키는 위에 한 겹을 더 둔 셈이다. 순번이 안 이어지는 횟수를 지표로 세니 파티션을 나눈 뒤에는 0이 됐고 소비자를 늘린 날에만 잠깐 올라갔다.
파티션별 대기 건수를 보니 한쪽에 몰려 있었는데 활동이 많은 계좌가 그 파티션에 걸려 있었다. 나누는 기준이 고르게 나뉘는지도 함께 봐야 했고 파티션 수를 늘려 흩으니 나아졌다.
정리
- 큐가 순서를 줘도 처리가 끝나는 순서는 별개다
- 순서가 필요하면 같은 대상을 한 소비자가 맡게 한다
- 순서를 지킬 범위를 대상 단위로 좁히면 병렬성이 남는다
- 소비자가 늘거나 줄 때 배정이 바뀐다
- 처리 중인 것을 끝내고 진행 위치를 커밋한 뒤 넘긴다
- 순서를 지키는 것은 공짜가 아니므로 필요한 종류만 고른다
- 순번을 붙여 늦게 온 것을 버리고 뒤집힌 횟수를 센다
- 나누는 기준이 고르게 나뉘는지도 함께 본다