ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Cloudflare Queues][운영] multi-consumer에서 같은 idempotency key collision이 보일 때 어떤 write-before-check 로그부터 줄이나
    기타개발지식/풀스택개발 2026. 8. 8. 20:12

    IT 리서치 노트

    [Cloudflare Queues][운영] multi-consumer에서 같은 idempotency key collision이 보일 때 어떤 write-before-check 로그부터 줄이나

    Cloudflare Queues에서 multi-consumer를 돌리기 시작하면 같은 idempotency key collision이 애플리케이션 버그처럼 보일 때가 많다. 하지만 2026년 8월 8일 기준 Cloudflare 공식 문서를 다시 보면, 배치 기본 재시도는 all-or-nothing으로 다시 묶일 수 있고, ack·retry precedence 규칙은 개별 메시지와 배치 호출이 다른 결과를 만들며, explicit retry는 더 이상 consumer concurrency scaling과 직접 연결되지 않는다. 이 글은 같은 idempotency key collision이 보일 때 어떤 write-before-check 로그부터 줄여야 원인을 빨리 자를 수 있는지 정리한 것이다.

    1. 개요

    결론부터 말하면 multi-consumer collision은 먼저 idempotency key, consumer invocation id, write_before_check stage, storage result, batch disposition 다섯 칸을 한 trace로 묶어야 한다. 충돌 자체보다 먼저 구분해야 할 것은 "누가 먼저 썼는가", "충돌이 check 전에 났는가", "그 뒤 batch가 ack로 끝났는가 retry로 갔는가"다.

    이미 consumer concurrency와 duplicate 로그 글이 scale-up 뒤 기본 duplicate 메모를 다뤘다면, 이번 글은 그중에서도 같은 key collision을 storage write 순서 기준으로 더 잘게 자르는 단계다. poison message와 batch-level retry 판단은 ack·retry·retryAll 글과도 바로 이어진다.

    2. 어디서 실제로 막히는가

    현장에서 가장 흔한 혼동은 세 가지다. 첫째, 같은 key collision을 보자마자 "consumer가 너무 많이 붙었다"로 단정한다. 둘째, 중복 판정 테이블이나 Redis key는 보지만 실제 insert 시점과 conflict 시점을 같은 trace로 남기지 않는다. 셋째, retryAll과 개별 retry를 같은 "재시도"로 한 줄에 적어 배치 재전달과 애플리케이션 충돌을 한 덩어리로 본다.

    Cloudflare의 how-queues-works 문서는 기본 배치 재시도가 all-or-nothing으로 갈 수 있다고 설명하고, batching and retries 문서는 ack와 retry precedence 규칙을 따로 적는다. changelog는 explicit retry가 더 이상 concurrency scaling에 직접 영향을 주지 않는다고 남겼다. 이 셋을 합치면 collision triage는 scale 메트릭 하나로 끝낼 일이 아니라는 점이 분명해진다.

    특히 write-before-check 구조를 쓰는 팀은 같은 key collision이 세 종류로 나뉜다. 이미 다른 consumer가 선행 insert를 끝낸 정상 conflict, storage timeout 뒤 retry가 붙으며 재전달된 conflict, 그리고 실제로 같은 비즈니스 key가 중복 생산된 upstream duplicate다. 이 셋을 같은 로그로 보면 incident 회고 기준이 끝까지 섞인다.

    • 증상: 같은 idempotency key collision이 burst처럼 보인다.
    • 실패: insert 시작·성공·conflict 순서를 로그에 안 남긴다.
    • 막힘: retry()와 retryAll()을 같은 재시도 메모로 본다.
    • 누락: consumer invocation id와 batch disposition을 같이 저장하지 않는다.
    충돌처럼 보이는 상황 먼저 볼 곳 판단 기준
    같은 key가 거의 동시에 두 번 보인다 consumer invocation id와 insert stage 서로 다른 consumer가 같은 순간대에 진입했는지 본다
    conflict 뒤 다시 같은 메시지가 온다 batch disposition retry()인지 retryAll()인지 구분한다
    중복인지 upstream 재생성인지 애매하다 message id와 idempotency key 같은 비즈니스 key에 다른 message id가 왔는지 본다

    3. 실무에서 적용하는 순서

    실무 적용 순서는 다섯 단계가 가장 짧다. 먼저 idempotency key와 message id를 같은 로그로 묶는다. 두 번째로 insert 시작과 storage result를 남긴다. 세 번째로 consumer invocation id를 붙여 선행 write 주체를 구분한다. 네 번째로 batch disposition을 ack, retry, retryAll 수준으로 명시한다. 마지막으로 충돌을 세 가지 버킷으로 나눠 본다. 정상 unique conflict, timeout 뒤 재전달 conflict, upstream duplicate다.

    1. idempotency key와 message id를 같은 trace로 저장한다.
    2. insert_started와 insert_conflict를 분리해 남긴다.
    3. consumer invocation id를 붙여 동시 처리 주체를 자른다.
    4. ack·retry·retryAll을 batch disposition 필드로 남긴다.
    5. 충돌을 normal conflict·redelivery conflict·upstream duplicate로 분류한다.

    이 구조를 잡아 두면 scale tuning과 애플리케이션 수정을 분리하기 쉬워진다. 예를 들어 invocation id 둘이 같은 key에 동시에 insert_started를 남겼고 한쪽만 conflict라면 경쟁 write 문제다. 반대로 한 invocation에서 timeout 뒤 retryAll이 나고 다시 같은 key conflict가 왔다면 batch 재전달 경로를 먼저 손봐야 한다.

    collision 메모 예시
    trace_id=...
    message_id=msg_123
    idempotency_key=order:7781
    consumer_invocation_id=inv_42
    stage=insert_started|insert_succeeded|insert_conflict
    storage_result=ok|conflict_on_unique|timeout
    batch_disposition=ack()|retry()|retryAll()

    write-before-check 팀이 자주 놓치는 부분은 conflict 뒤 행동이다. conflict가 났다고 곧장 retryAll을 걸면 이미 성공한 다른 메시지까지 함께 묶여 재전달될 수 있다. 개별 retry 또는 ack 판단 기준을 따로 적어 두어야 배치 폭발을 막는다.

    4. 공식 문서와 예시 화면으로 확인하기

    첫 공식 화면은 Queues 동작 설명이다. 기본값에서는 배치 안 마지막 메시지 실패가 전체 배치 재전달로 이어질 수 있다는 점을 먼저 잡아야 한다.

    Cloudflare는 기본 배치 재시도가 all-or-nothing으로 동작할 수 있다고 설명한다.
    Cloudflare는 기본 배치 재시도가 all-or-nothing으로 동작할 수 있다고 설명한다.

    즉 multi-consumer 환경에서 같은 idempotency key가 다시 보일 때는 단순 duplicate라기보다 배치 재전달과 개별 재시도 경로가 섞였을 가능성을 먼저 열어 둬야 한다. 이 전제를 놓치면 DB write 충돌만 보고 consumer 구조를 과하게 의심하게 된다.

    두 번째 자료는 batching and retries 문서의 precedence 규칙이다. Cloudflare는 ack와 retry, ackAll과 retryAll이 어떤 우선순위로 적용되는지 명확히 적고 있다.

    개별 메시지와 배치 단위의 ack·retry 호출은 우선순위 규칙을 따른다.
    개별 메시지와 배치 단위의 ack·retry 호출은 우선순위 규칙을 따른다.

    write-before-check 로그를 설계할 때도 이 우선순위를 같이 봐야 한다. 같은 key collision이 보여도 개별 메시지 retry인지, 배치-level retryAll 여파인지 구분하지 않으면 원인이 뒤섞인다.

    세 번째 화면은 changelog다. Cloudflare는 explicit retry가 더 이상 consumer concurrency scaling에 영향을 주지 않는다고 남겨 두었다.

    retry()와 retryAll() 자체는 이제 consumer concurrency 축과 별도로 봐야 한다.
    retry()와 retryAll() 자체는 이제 consumer concurrency 축과 별도로 봐야 한다.

    이 문구 덕분에 운영 메모를 더 잘 자를 수 있다. 충돌이 보인다고 해서 retry 호출만으로 scale down이나 scale up 원인을 곧장 묶지 말고, 애플리케이션 write 순서와 consumer 수를 따로 관찰하는 편이 낫다.

    실무에서는 어떤 필드를 먼저 남길지 고정해 두는 편이 빠르다. 같은 key collision이 보여도 read-before-write 경로인지, write-before-check 경로인지, 이미 다른 consumer가 commit한 것인지 분리하는 데 필요한 최소 로그를 표로 묶었다.

    idempotency key collision triage에 필요한 write-before-check 로그표다.
    idempotency key collision triage에 필요한 write-before-check 로그표다.

    이미 consumer concurrency와 duplicate 로그 글이 scale-up 뒤 기본 로그 축을 다뤘다면, 이번 표는 같은 key collision을 write 순서 기준으로 더 좁히는 후속편이다.

    마지막 자료는 write-before-check 로그 예시다. 핵심은 중복 판정 함수를 예쁘게 만드는 것이 아니라, insert 시점과 unique conflict와 batch disposition을 같은 trace id에 붙여 두는 것이다.

    multi-consumer collision을 좁히기 위한 write-before-check 로그 예시다.
    multi-consumer collision을 좁히기 위한 write-before-check 로그 예시다.

    이 메모 구조를 남겨 두면 같은 key collision이 보였을 때 DB unique index 충돌인지, 상태 조회가 늦어 선행 write를 못 본 것인지, retryAll로 다시 묶인 것인지가 훨씬 빨리 잘린다. 2026년 8월 9일 후속 글인 poison message fingerprint·DLQ 재처리 글을 함께 보면 collision 이후 격리와 재처리 순서까지 같은 운영 메모로 이어 붙일 수 있다. 배치 분기 자체는 ack·retry·retryAll 글과 같이 보면 더 자연스럽다.

    5. 주의사항과 리스크

    첫 번째 리스크는 충돌을 모두 scale 문제로 해석하는 것이다. 두 번째는 write-before-check 구조인데 insert_started 로그가 없어 실제 선행 write를 확인할 수 없는 것이다. 세 번째는 batch-level retryAll이 남긴 재전달을 애플리케이션 duplicate로 오해하는 것이다.

    운영 전에 확인할 때는 최소한 message id, idempotency key, invocation id, storage result, batch disposition 다섯 필드를 빠짐없이 남기는 편이 좋다. 이 다섯 칸이 없으면 같은 collision 그래프를 봐도 저장소 경쟁인지 재전달인지 설명이 분리되지 않는다.

    • collision은 scale 이슈와 write 순서 이슈를 먼저 나눠 본다.
    • insert 시작과 conflict 시점을 같은 trace에 남긴다.
    • retryAll과 retry를 하나의 "재시도"로 뭉뚱그리지 않는다.

    6. 결론

    Cloudflare Queues multi-consumer에서 같은 idempotency key collision이 보일 때는 concurrency 숫자보다 write-before-check 로그 구조를 먼저 고정하는 편이 맞다. 누가 먼저 썼는지, conflict가 언제 났는지, 그 뒤 batch가 어떻게 끝났는지를 같은 trace에 넣으면 정상 conflict와 재전달 conflict와 실제 duplicate를 훨씬 짧게 자를 수 있다.

    • idempotency key와 invocation id를 같이 남긴다.
    • storage result와 batch disposition을 분리해서 본다.
    • collision을 scale 메트릭 하나로 설명하려 하지 않는다.

    7. 참고 링크

    1. https://developers.cloudflare.com/queues/reference/how-queues-works/
    2. https://developers.cloudflare.com/queues/configuration/batching-retries/
    3. https://developers.cloudflare.com/queues/reference/delivery-guarantees/
    4. https://developers.cloudflare.com/queues/platform/changelog/
Designed by Tistory.