상품 정보를 고치면 연동된 채널에 반영하는 구조로 수정 시점에 메시지를 큐에 넣고 워커가 꺼내 채널 API를 부르고 있었다. 가격을 올렸는데 채널에는 그 전 가격이 올라가 있었다.
Table of contents
Open Table of contents
값을 통째로 담은 메시지
메시지가 발행 시점의 상품 상태를 통째로 담고 있었고 워커는 그 값을 그대로 채널에 보냈다. 큐가 밀리지 않을 때는 순서대로 처리되므로 이 구조의 문제가 안 드러난다.
문제는 워커가 여러 대일 때였다. 큐 자체는 순서를 보존하지만 꺼내는 쪽이 병렬이라 먼저 꺼낸 것의 채널 응답이 느리면 나중 것이 먼저 도착하고 꺼내는 순서와 도착하는 순서가 달라지므로 큐를 순서 보장 방식으로 잡아도 해결되지 않는다.
가리키는 것만 담기
그래서 메시지에서 상태 전문을 빼고 식별자와 버전과 바뀐 필드만 남겼다. 워커는 메시지를 꺼낸 뒤에 데이터베이스를 다시 읽어 현재 값을 가져온다.
늦게 처리하더라도 데이터베이스에는 최신 값이 들어 있으므로 최신 값을 보내게 된다. 같은 값을 두 번 보내게 되지만 결과가 같으므로 낡은 값이 남지 않는다.
다시 읽은 행으로 하는 비교
같은 것을 두 번 보내는 것도 줄이려고 채널별로 마지막에 보낸 버전을 기록하고 그보다 낮거나 같으면 건너뛰게 했다. 버전 값으로는 상품 행이 갱신될 때마다 올라가는 카운터를 썼다.
여기서 중요한 것이 비교 대상이다. 메시지에 실린 버전이 아니라 다시 읽은 행의 버전으로 비교해야 하는데 메시지 쪽 값은 발행 시점 것이라 같은 문제를 그대로 안고 있기 때문이다.
채널이 순서를 안 지킬 때
이렇게 해도 채널 호출 두 건이 뒤집히면 낡은 값이 남을 수 있으므로 채널이 조건부 갱신을 지원하는지도 확인했다. 한 곳은 요청에 시각을 받아 더 오래된 것을 무시했고 다른 한 곳에는 그런 장치가 없었다.
그런 장치가 없는 쪽은 같은 상품에 대해 동시에 두 건이 안 나가도록 잠갔다. 같은 상품은 한 번에 하나만 나가고 다른 상품은 그대로 병렬로 처리된다.
다시 읽을 것과 스냅숏이 맞는 것
전부 다시 읽으면 안전한 대신 메시지 수만큼 조회가 따라붙는다. 그래서 대상을 두 종류로 갈라서 다르게 다뤘다.
가격과 재고와 상태처럼 덮어써도 되는 것은 다시 읽게 했는데 마지막 값만 맞으면 되기 때문이다. 주문 접수나 정산 확정처럼 한 번만 일어나는 것은 메시지에 담긴 값을 쓰는데 다시 읽으면 그 사이에 취소된 것이 접수되지 않은 것으로 보여 사실 자체가 바뀌기 때문이다. 이 사고는 큐가 밀릴 때만 나므로 발행 시각과 처리 시각의 차이를 재서 지연이 커지는 구간을 볼 수 있게 했다.
정리
- 큐에 값을 통째로 담으면 꺼낼 때 이미 다른 값일 수 있다
- 큐가 순서를 보존해도 꺼내는 순서와 도착 순서가 다르다
- 덮어써도 되는 것은 식별자만 담고 꺼낸 뒤 다시 읽는다
- 버전 비교는 다시 읽은 행의 값으로 한다
- 채널이 순서를 안 지키면 같은 대상에 하나만 나가게 잠근다
- 한 번만 일어나는 일은 발행 시점 스냅숏이 맞다
- 그것을 다시 읽으면 사실이 바뀐다
- 큐 지연을 재 두면 이 사고가 나는 구간이 보인다