Active-Active 데이터베이스의 Streams

Active-Active 데이터베이스의 Streams (Streams in Active-Active databases)

Active-Active 데이터베이스에서 Streams를 사용하는 방법을 설명하는 페이지예요. 핵심만 말하면, 여러 지역(region)에서 같은 논리적 스트림에 동시에 쓸 수 있다는 점이 일반 Redis와 다르죠. 그 과정에서 생기는 ID 중복, 충돌 해결, 소비 그룹 동작까지 이 페이지에서 정리해 드릴게요.

출처: Redis 공식 문서 — Streams in Active-Active databases

Streams 기본 개념

Redis Streamappend-only log(추가 전용 로그) 처럼 동작하는 데이터 구조예요. 각 스트림 항목(entry)은 다음으로 구성돼요.

  • 유일하고 단조 증가하는 ID
  • 일련의 key-value 쌍으로 이루어진 페이로드(payload)

항목을 추가할 때는 XADD 명령을 쓰고, 읽을 때는 XRANGE, XREADGROUP, XREAD 명령을 사용해요 (단, 아래에서 설명할 XREAD 관련 주의점이 있어요).

Streams와 Active-Active

Active-Active 데이터베이스에서는 하나 이상의 지역에서 같은 논리적 스트림에 쓸 수 있어요. 스트림은 Active-Active 데이터베이스의 모든 지역에 걸쳐 동기화돼요.

아래 예시에서 두 지역이 스트림에 동시에 쓰는 상황을 볼게요. 동기화 후 양쪽 지역이 동일한 스트림을 갖는다는 점에 주목하세요.

시점 Region 1 Region 2
t1 XADD messages * text hello XADD messages * text goodbye
t2 XRANGE messages - + → [1589929244828-1] XRANGE messages - + → [1589929246795-2]
t3 — Sync — — Sync —
t4 XRANGE messages - + → [1589929244828-1, 1589929246795-2] XRANGE messages - + → [1589929244828-1, 1589929246795-2]

또 하나 눈여겨볼 점은, 동기화된 스트림에는 중복 ID가 없다는 거예요. 데이터베이스가 ID를 생성하도록 맡겨 두면, 같은 ID를 가진 스트림 항목이 두 개 이상 생길 일은 없어요.

내부 구조 (확인 필요)

Redis Open Source는 각 스트림을 하나의 radix tree(코드 베이스에서는 rax라고 불러요)로 구현해요. 반면 Active-Active 데이터베이스는 지역마다 하나의 rax 로 단일 논리적 스트림을 구현해요. 각 지역은 자기와 연결된 rax에만 항목을 추가하고(rax 전체에서 항목은 삭제 가능), XREADXREADGROUP은 모든 rax 트리를 동시에 순회하면서 각 rax의 항목 ID를 비교해 적절한 항목을 반환해요.

충돌 해결

Active-Active 데이터베이스는 잠재적 충돌을 "observed-remove"(관찰된 것만 제거) 방식으로 자동 해결해요. 이 방식에서는 삭제가 로컬에서 관찰 가능한 데이터에만 영향을 줘요.

아래 예시에서 스트림 x가 t1에 생성되고, t3에는 두 지역에 존재하게 돼요.

시점 Region 1 Region 2
t1 XADD messages * text hello
t2 — Sync — — Sync —
t3 XRANGE messages - + → [1589929244828-1] XRANGE messages - + → [1589929244828-1]
t4 DEL messages XADD messages * text goodbye
t5 — Sync — — Sync —
t6 XRANGE messages - + → [1589929246795-2] XRANGE messages - + → [1589929246795-2]

t4에서 Region 1은 스트림을 삭제하고, 동시에 Region 2는 3700으로 끝나는 ID의 항목을 추가해요. 동기화 후 t6에는 3700으로 끝나는 항목이 양쪽 지역에 모두 존재해요. 그 이유는 t4에서 스트림이 삭제될 때 그 항목이 아직 "보이지 않았기" 때문이에요.

ID 생성 모드

보통은 Redis가 스트림 항목 ID를 스스로 생성하게 두는 게 좋아요. XADD 호출에서 ID 자리에 *를 지정하면 되죠. 하지만 스트림에 항목을 추가할 때 자체 ID를 직접 제공할 수도 있어요.

Active-Active 데이터베이스는 비동기로 복제되기 때문에, 직접 ID를 제공하면 중복 ID가 생길 수 있어요. 여러 지역에서 같은 스트림에 쓸 때 이런 일이 발생할 수 있죠.

시점 Region 1 Region 2
t1 XADD x 100-1 f1 v1 XADD x 100-1 f1 v1
t2 — Sync — — Sync —
t3 XRANGE x - + → [100-1, 100-1] XRANGE x - + → [100-1, 100-1]

이 시나리오에서는 t1에 ID가 100-1인 항목 두 개가 추가돼요. 동기화 후 스트림 x에는 같은 ID를 가진 항목 두 개가 들어 있게 되죠.

참고: Redis Open Source의 Stream ID는 대시('-')로 구분된 두 개의 정수로 이뤄져요. 서버가 ID를 생성할 때 첫 번째 정수는 현재 시간(밀리초), 두 번째 정수는 시퀀스 번호예요. 즉 ID 형식은 MS-SEQ예요.

중복 ID를 막고 원래 Redis Streams 설계를 지키기 위해, Active-Active 데이터베이스는 XADD세 가지 ID 모드를 제공해요.

  1. Strict (엄격)XADD가 서버 생성 ID(* 지정자) 또는 밀리초(MS) 부분만으로 이뤄진 ID만 허용해요. 밀리초 부분만 제공하면, ID의 시퀀스 번호는 지역 ID를 이용해 계산돼요. 이렇게 해서 스트림의 중복 ID를 막죠. Strict 모드는 전체 ID(밀리초와 시퀀스 번호를 모두 포함한 ID)를 거부해요.
  2. Semi-strict (반엄격) – Strict 모드와 거의 같지만, 전체 ID(MS-SEQ) 를 허용해요. 전체 ID를 허용하기 때문에 이 모드에서는 중복 ID가 가능해요.
  3. Liberal (자유)XADD가 단조 증가하는 어떤 ID든 허용해요. 밀리초 부분이 주어지면 시퀀스 번호는 0으로 설정돼요. 이 모드에서도 중복 ID가 생길 수 있어요.

기본값이면서 권장되는 모드는 Strict인데, 중복 ID를 막아 주거든요.

경고: 왜 중복 ID를 막고 싶을까요? 첫째, 스트림에 중복 ID가 있으면 XDEL, XCLAIM 같은 명령이 항목 하나 이상에 영향을 줄 수 있어요. 둘째, 데이터베이스를 내보내거나(export) 이름을 바꿀 때(rename) 중복 항목이 제거될 수 있어요.

XADD의 ID 생성 모드를 바꾸려면 명령줄 유틸리티 rladmin을 사용해요.

Strict 모드 설정:

rladmin tune db crdb crdt_xadd_id_uniqueness_mode strict

Semi-strict 모드 설정:

rladmin tune db crdb crdt_xadd_id_uniqueness_mode semi-strict

Liberal 모드 설정:

rladmin tune db crdb crdt_xadd_id_uniqueness_mode liberal

XREAD 관련 주의

Redis Open Source와 Active-Active가 아닌 데이터베이스에서는 XREAD로 스트림 항목을 순회할 수 있어요. 하지만 Active-Active 데이터베이스에서 XREAD항목을 건너뛸 수 있어요. 여러 지역이 같은 스트림에 쓸 때 이런 일이 생겨요.

아래 예시에서 XREAD는 항목 115-2를 건너뛰어요.

시점 Region 1 Region 2
t1 XADD x 110 f1 v1 XADD x 115 f1 v1
t2 XADD x 120 f1 v1
t3 XADD x 130 f1 v1
t4 XREAD COUNT 2 STREAMS x 0 → [110-1, 120-1]
t5 — Sync — — Sync —
t6 XREAD COUNT 2 STREAMS x 120-1 → [130-1]
t7 XREAD STREAMS x 0 →[110-1, 115-2, 120-1, 130-1] XREAD STREAMS x 0 →[110-1, 115-2, 120-1, 130-1]

XREAD모든 쓰기가 단일 지역에서만 일어날 때만 안정적으로 스트림을 소비할 수 있어요. 그 외에는 항상 안정적인 소비를 보장하는 XREADGROUP을 쓰는 게 좋아요.

Consumer groups (소비 그룹)

Active-Active 데이터베이스는 Redis Streams의 소비 그룹을 완전히 지원해요. 아래는 두 개의 소비 그룹을 동시에 생성하는 예시예요.

시점 Region 1 Region 2
t1 XGROUP CREATE x group1 0 XGROUP CREATE x group2 0
t2 XINFO GROUPS x → [group1] XINFO GROUPS x → [group2]
t3 — Sync — — Sync —
t4 XINFO GROUPS x → [group1, group2] XINFO GROUPS x → [group1, group2]

참고: Redis Open Source는 전역 pending 항목 목록(global PEL)을 담는 radix tree(rax) 하나와, 각 소비자별 PEL을 담는 rax 하나씩을 사용해요. 전역 PEL은 모든 소비자 PEL의 합집합이며, 각 PEL은 서로 겹치지 않아요.

Active-Active 데이터베이스의 스트림은 지역마다 전역 PEL과 소비자별 PEL을 유지해요. > 특수 ID가 아닌 다른 ID가 주어지면 XREADGROUP은 모든 소비자의 모든 PEL을 동시에 순회하면서, 각 PEL의 항목 ID를 비교해 다음 항목을 반환해요.

소비 그룹 충돌 해결

소비 그룹 충돌은 "delete wins"(삭제 승리) 방식으로 자동 해결돼요. 소비 그룹에 대한 동시 작업이 있으면, 삭제가 같은 그룹에 대한 다른 동시 작업을 이겨요.

이 예시에서 t4의 DEL은 관찰된 group1과 관찰되지 않은 group2를 모두 삭제해요.

시점 Region 1 Region 2
t1 XGROUP CREATE x group1 0
t2 — Sync — — Sync —
t3 XINFO GROUPS x → [group1] XINFO GROUPS x → [group1]
t4 DEL x XGROUP CREATE x group2 0
t5 — Sync — — Sync —
t6 EXISTS x → 0 EXISTS x → 0

아래 예시에서는 t4의 XGROUP DESTROY가 Region 1에서 만든(관찰된) group1과 Region 3에서 만든(관찰되지 않은) group1 모두에 영향을 줘요.

시점 Region 1 Region 2 Region 3
t1 XGROUP CREATE x group1 0
t2 — Sync — — Sync —
t3 XINFO GROUPS x → [group1] XINFO GROUPS x → [group1] XINFO GROUPS x → []
t4 XGROUP DESTROY x group1 XGROUP CREATE x group1 0
t5 — Sync — — Sync — — Sync —
t6 EXISTS x → 0 EXISTS x → 0 EXISTS x → 0

그룹 복제 (Group replication)

XREADGROUPXACK 호출은 소비자 그룹이나 소비자의 상태를 바꿔요. 하지만 모든 변경을 복제하는 것은 비효율적이에요.

Active-Active 데이터베이스에서 소비자 그룹을 성능 좋게 유지하기 위한 규칙은 이래요.

  1. 그룹 존재(CREATE/DESTROY)는 복제돼요.
  2. 대부분의 XACK 작업은 복제돼요.
  3. XGROUP SETID, DELCONSUMER 같은 다른 작업은 복제되지 않아요.

예를 들어:

시점 Region 1 Region 2
t1 XADD messages 110 text hello
t2 XGROUP CREATE messages group1 0
t3 XREADGROUP GROUP group1 Alice STREAMS messages > → [110-1]
t4 — Sync — — Sync —
t5 XRANGE messages - + → [110-1] XRANGE messages - + → [110-1]
t6 XINFO GROUPS messages → [group1] XINFO GROUPS messages → [group1]
t7 XINFO CONSUMERS messages group1 → [Alice] XINFO CONSUMERS messages group1 → []
t8 XPENDING messages group1 - + 1 → [110-1] XPENDING messages group1 - + 1 → []

지역 간에 XREADGROUP을 쓰면 같은 항목을 여러 번 읽을 수 있어요. Active-Active Streams가 "적어도 한 번(at-least-once) 읽기" 또는 "단일 소비자"용으로 설계됐기 때문이에요. 위 예시에서 Region 2는 어떤 소비자 그룹 활동도 알지 못하므로, XREADGROUP 트래픽을 Region 1에서 Region 2로 돌리면 이미 읽은 항목을 다시 읽게 돼요.

복제 성능 최적화

소비자는 XACK 명령으로 메시지를 확인(ack)해요. 각 ack은 실질적으로 마지막으로 소비한 메시지를 기록하는 셈이라, 지역 간 트래픽이 많아질 수 있어요. 이 트래픽을 줄이기 위해, 읽은 항목이 모두 확인(ack)된 경우에만 XACK 메시지를 복제해요.

시점 Region 1 Region 2 설명
t1 XADD x 110-0 f1 v1
t2 XADD x 120-0 f1 v1
t3 XADD x 130-0 f1 v1
t4 XGROUP CREATE x group1 0
t5 XREADGROUP GROUP group1 Alice STREAMS x > → [110-0, 120-0, 130-0]
t6 XACK x group1 110-0
t7 — Sync — — Sync — 110-0과 그 이전 항목들(없음)이 확인됨. 110-0에 대한 XACK 효과를 복제함.
t8 XACK x group1 130-0
t9 — Sync — — Sync — 130-0은 확인됐지만 그 이전 항목(120-0)은 아님. 130-0에 대한 XACK 효과는 복제하지 않음
t10 XACK x group1 120-0
t11 — Sync — — Sync — 120-0과 그 이전 항목들(110-0~130-0)이 확인됨. 130-0에 대한 XACK 효과를 복제함.

이 시나리오에서 XREADGROUP 트래픽을 Region 1에서 Region 2로 돌려도 110-0, 120-0, 130-0을 다시 읽지 않아요. 즉 XREADGROUP은 이미 확인(ack)된 항목을 반환하지 않는다는 뜻이에요.

보장 (Guarantees)

  • XREAD와 달리 XREADGROUP은 스트림 항목을 건너뛰지 않아요.
  • 트래픽 리다이렉션 상황에서 XREADGROUP은 읽혔지만 확인되지 않은 항목을 반환할 수 있어요. 이미 확인된 항목을 반환할 수도 있죠.
  • 여러 지역에서 항목이 추가될 때, Redis는 단일 읽기 명령(XREAD, XREADGROUP, XRANGE)의 응답에서 항목 순서를 보장해요. 하지만 일련의 읽기 명령에 대해서는 정렬된 ID를 보장하지 않아요. 예를 들어 두 번 연속 실행한 XREADGROUP GROUP group consumer COUNT 1 STREAMS key >이 내려가는 ID를 응답할 수 있어요.

요약 (Summary)

Active-Active Streams를 쓰면 여러 지역에서 같은 논리적 스트림에 쓸 수 있어요. 그 결과 Active-Active Streams 동작은 Redis Open Source와 조금 달라져요. 정리하면 이래요.

스트림 명령

  1. strict ID 생성 모드를 쓰면 XADD는 전체 스트림 항목 ID(MS와 SEQ를 모두 포함한 ID)를 허용하지 않아요.
  2. XREAD는 둘 이상의 지역에서 동시에 쓰여지는 스트림을 순회할 때 항목을 건너뛸 수 있어요. 안정적인 순회를 원하면 XREADGROUP을 사용하세요.
  3. 새 ID가 현재 ID보다 작으면 XSETID가 실패해요.

소비자 그룹 관련

다음 소비자 그룹 연산은 복제돼요.

  1. 연속된 XACK 연산
  2. 소비자 그룹 생성/삭제 (XGROUP CREATEXGROUP DESTROY)

그 외의 모든 소비자 그룹 메타데이터는 복제되지 않아요.

몇 가지 추가 참고사항:

  1. XGROUP SETIDDELCONSUMER는 복제되지 않아요.
  2. 소비자는 로컬에 존재해요 (XREADGROUP이 소비자를 암시적으로 생성해요).
  3. 스트림 이름을 바꾸면(RENAME) 모든 소비자 그룹 정보가 삭제돼요.

더 알아보기 (Learn more)