Skip to content
isdnetworks
Go back

꺼낸 순간 낡아 있던 값

(수정됨)

상품 정보를 고치면 연동된 채널에 반영하는 구조로 수정 시점에 ProductUpdated 메시지를 SQS 표준 큐에 넣고 워커가 꺼내 채널 API 를 부르고 있었다. 가격을 올렸는데 채널에는 그 전 가격이 올라가 있었다.

Table of contents

Open Table of contents

원인 — 값을 통째로 담은 메시지

메시지가 발행 시점의 상품 상태를 통째로 담고 있었고 워커는 그 값을 그대로 채널에 보냈다. 큐가 밀리지 않을 때는 순서대로 처리되므로 이 구조의 문제가 안 드러난다.

문제는 워커가 여러 대일 때였다. 꺼내는 쪽이 병렬이라 먼저 꺼낸 것의 채널 응답이 느리면 나중 것이 먼저 도착하고 꺼내는 순서와 도착하는 순서가 달라진다. AWS 공식 문서대로 FIFO 큐에서 같은 MessageGroupId 의 메시지는 한 번에 하나씩 엄격한 순서로 처리되므로 상품별로 그룹을 주면 이것 자체는 막을 수 있다.

판단 기준 — 가리키는 것만 담기

두 가지 방법이 있었는데 큐를 FIFO 로 바꾸고 MessageGroupId 에 product_id 를 주는 것과 표준 큐를 두고 메시지에서 상태 전문을 빼는 것이다. 뒤쪽을 골랐다. 이유는 AWS 공식 문서대로 FIFO 큐는 파티션당 API 동작마다 초당 300 트랜잭션 상한이 있고 표준 큐를 FIFO 로 바꾸려면 큐를 새로 만들고 발행자와 소비자를 다 고쳐야 해서다. 대신 메시지 수만큼 조회가 하나씩 따라붙는 것을 감수했다. 메시지에는 product_id 와 version 과 changed_fields 만 남겼고 워커는 메시지를 꺼낸 뒤에 products 를 다시 읽어 현재 값을 가져온다.

늦게 처리하더라도 데이터베이스에는 최신 값이 들어 있으므로 최신 값을 보내게 된다. 같은 값을 두 번 보내게 되지만 결과가 같으므로 낡은 값이 남지 않는다.

다시 읽은 행으로 하는 비교

같은 것을 두 번 보내는 것도 줄이려고 channel_sync_versions.last_sent_version 을 기록하고 그보다 낮거나 같으면 건너뛰게 했다. 버전 값으로는 products.version 이 갱신될 때마다 올라가는 카운터를 썼다.

여기서 중요한 것이 비교 대상이다. 메시지에 실린 버전이 아니라 다시 읽은 행의 버전으로 비교해야 하는데 메시지 쪽 값은 발행 시점 것이라 같은 문제를 그대로 안고 있기 때문이다.

주의 — 채널이 순서를 안 지킬 때

이렇게 해도 채널 호출 두 건이 뒤집히면 낡은 값이 남을 수 있으므로 채널이 조건부 갱신을 지원하는지도 확인했다. 한 곳은 요청에 updated_at 을 받아 더 오래된 것을 무시했고 다른 한 곳에는 그런 장치가 없었다.

그런 장치가 없는 쪽은 같은 상품에 대해 동시에 두 건이 안 나가도록 Redis SETNX 로 잠갔다. 같은 상품은 한 번에 하나만 나가고 다른 상품은 그대로 병렬로 처리된다.

다시 읽을 것과 스냅숏이 맞는 것

전부 다시 읽으면 안전한 대신 메시지 수만큼 조회가 따라붙는다. 그래서 대상을 두 종류로 갈라서 다르게 다뤘다.

가격과 재고와 상태처럼 덮어써도 되는 것은 다시 읽게 했는데 마지막 값만 맞으면 되기 때문이다. 주문 접수나 정산 확정처럼 한 번만 일어나는 것은 메시지에 담긴 값을 쓰는데 다시 읽으면 그 사이에 취소된 것이 접수되지 않은 것으로 보여 사실 자체가 바뀌기 때문이다. 이 사고는 큐가 밀릴 때만 나므로 published_at 과 processed_at 의 차이를 재서 지연이 커지는 구간을 볼 수 있게 했다.

정리


Share this post on:

Previous Post
청구서를 나누는 유일한 수단
Next Post
설계를 바꾼 과금 구조