ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Sharding & Routing: 분산 데이터 파티셔닝과 라우팅
    이코에코(Eco²)/Foundations 2025. 12. 27. 14:27

    분산 시스템에서 데이터를 여러 노드에 분산(Sharding)하고 요청을 올바른 노드로 라우팅(Routing)하는 기술.
    Eco²에서는 Redis Streams 샤딩과 Istio Consistent Hash로 분산 라우팅을 시도했습니다. (현재는 Pub/Sub로 전환)


    1차 지식생산자

    핵심 논문

    논문 저자 발표 핵심 내용
    Consistent Hashing and Random Trees Karger et al. (MIT) STOC 1997 Consistent Hashing 원본 논문
    Dynamo: Amazon's Highly Available Key-value Store DeCandia et al. SOSP 2007 Virtual Nodes, Quorum, 실제 적용
    Jump Consistent Hash Lamping, Veach (Google) 2014 O(1) 메모리, 균등 분포
    The Tail at Scale Jeff Dean, Luiz Barroso CACM 2013 Fanout 시스템 지연 시간 문제

    공식 문서

    기술 문서 핵심 내용
    Redis Streams redis.io/docs/data-types/streams Consumer Groups, XREADGROUP
    Redis XREADGROUP redis.io/commands/xreadgroup Consumer Group 읽기 명령어
    Envoy Ring Hash LB envoyproxy.io Ketama 알고리즘
    Istio Destination Rule istio.io consistentHash 설정
    Kafka Consumer Group kafka.apache.org Partition Assignment 전략

    참고 서적

    자료 저자 핵심 내용
    The Log Jay Kreps (LinkedIn) Log-based 메시징의 이론적 기초
    Designing Data-Intensive Applications Martin Kleppmann Ch.6 Partitioning - 샤딩, 리밸런싱, 라우팅

    1. Consistent Hashing (일관된 해싱)

    Karger et al. (MIT, 1997)
    "노드 추가/제거 시 최소한의 데이터만 재배치"

    1.1 기본 개념

    ┌─────────────────────────────────────────────────────────────────┐
    │                    Consistent Hashing의 필요성                    │
    ├─────────────────────────────────────────────────────────────────┤
    │                                                                  │
    │  문제: 단순 해시 (hash(key) % N)                                │
    │  ───────────────────────────────                                │
    │  • N=4 → N=5로 노드 추가 시                                     │
    │  • 대부분의 키가 다른 노드로 재배치됨                            │
    │  • 캐시 미스 폭발, 성능 저하                                    │
    │                                                                  │
    │  해결: Consistent Hashing                                       │
    │  ─────────────────────────                                       │
    │  • 해시 공간을 원(Ring)으로 표현                                │
    │  • 노드 추가/제거 시 인접한 키만 재배치                          │
    │  • 평균 K/N개의 키만 이동 (K=총 키 수, N=노드 수)                │
    │                                                                  │
    └─────────────────────────────────────────────────────────────────┘

    1.2 해시 링 (Hash Ring)

    ┌─────────────────────────────────────────────────────────────────┐
    │                    Hash Ring 구조                                │
    ├─────────────────────────────────────────────────────────────────┤
    │                                                                  │
    │                         0 (= 2^32)                              │
    │                           ●                                      │
    │                      ╱         ╲                                │
    │                   ╱               ╲                             │
    │                ●                     ●                          │
    │             Node A                  Node B                      │
    │            (hash=0.25)             (hash=0.5)                   │
    │               │                       │                         │
    │               │   ← key1 (0.3)        │                         │
    │               │   ← key2 (0.35)       │                         │
    │               │                       │   ← key3 (0.6)          │
    │               │                       │   ← key4 (0.7)          │
    │                ╲                     ╱                          │
    │                   ╲               ╱                             │
    │                      ╲         ╱                                │
    │                           ●                                      │
    │                        Node C                                   │
    │                       (hash=0.75)                               │
    │                           │                                      │
    │                           │   ← key5 (0.8)                      │
    │                           │   ← key6 (0.9)                      │
    │                                                                  │
    │  규칙: 키는 시계방향으로 가장 가까운 노드에 저장                  │
    │                                                                  │
    └─────────────────────────────────────────────────────────────────┘

    1.3 Virtual Nodes (가상 노드)

    Amazon Dynamo (2007)에서 도입

    ┌─────────────────────────────────────────────────────────────────┐
    │                    Virtual Nodes                                 │
    ├─────────────────────────────────────────────────────────────────┤
    │                                                                  │
    │  문제: 물리 노드만 사용 시                                       │
    │  ─────────────────────────                                       │
    │  • 노드 간 부하 불균형 (해시 분포에 따라)                        │
    │  • 노드 장애 시 하나의 노드가 모든 부하를 받음                   │
    │                                                                  │
    │  해결: Virtual Nodes (VNodes)                                   │
    │  ───────────────────────────                                     │
    │  • 각 물리 노드를 여러 가상 노드로 표현                          │
    │  • 링에 더 균등하게 분포                                        │
    │                                                                  │
    │           0                                                      │
    │           ●                                                      │
    │      A1 ●   ● B1                                                │
    │                                                                  │
    │    C2 ●         ● A2                                            │
    │                                                                  │
    │      B2 ●   ● C1                                                │
    │           ●                                                      │
    │          A3                                                      │
    │                                                                  │
    │  Node A → {A1, A2, A3} (3개의 가상 노드)                        │
    │  Node B → {B1, B2}                                              │
    │  Node C → {C1, C2}                                              │
    │                                                                  │
    └─────────────────────────────────────────────────────────────────┘

    1.4 노드 추가/제거

    ┌─────────────────────────────────────────────────────────────────┐
    │                    노드 추가 시 재배치                            │
    ├─────────────────────────────────────────────────────────────────┤
    │                                                                  │
    │  Before: Node A, B, C                                           │
    │                                                                  │
    │       ┌────────┐       ┌────────┐       ┌────────┐             │
    │       │ Node A │       │ Node B │       │ Node C │             │
    │       │ keys:  │       │ keys:  │       │ keys:  │             │
    │       │ 1,2,3  │       │ 4,5,6  │       │ 7,8,9  │             │
    │       └────────┘       └────────┘       └────────┘             │
    │                                                                  │
    │  After: Node D 추가 (A와 B 사이)                                │
    │                                                                  │
    │       ┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐          │
    │       │ Node A │  │ Node D │  │ Node B │  │ Node C │          │
    │       │ keys:  │  │ keys:  │  │ keys:  │  │ keys:  │          │
    │       │ 1,2    │  │ 3      │  │ 4,5,6  │  │ 7,8,9  │          │
    │       └────────┘  └────────┘  └────────┘  └────────┘          │
    │            │           ▲                                        │
    │            └───────────┘                                        │
    │           key 3만 이동 (1개/9개 = 11%)                          │
    │                                                                  │
    │  단순 해시 (hash % N):                                          │
    │  • N=3 → N=4 변경 시 대부분의 키 재배치 (약 75%)                │
    │                                                                  │
    │  Consistent Hashing:                                            │
    │  • 평균 K/N개만 재배치 (K=총 키 수)                             │
    │                                                                  │
    └─────────────────────────────────────────────────────────────────┘

    1.5 구현 예시

    import hashlib
    from bisect import bisect_right
    
    class ConsistentHash:
        def __init__(self, nodes: list[str], virtual_nodes: int = 150):
            self.ring: dict[int, str] = {}
            self.sorted_keys: list[int] = []
            self.virtual_nodes = virtual_nodes
    
            for node in nodes:
                self.add_node(node)
    
        def _hash(self, key: str) -> int:
            """MD5 해시를 정수로 변환"""
            return int(hashlib.md5(key.encode()).hexdigest(), 16)
    
        def add_node(self, node: str) -> None:
            """노드 추가 (가상 노드 포함)"""
            for i in range(self.virtual_nodes):
                virtual_key = f"{node}:{i}"
                hash_val = self._hash(virtual_key)
                self.ring[hash_val] = node
                self.sorted_keys.append(hash_val)
            self.sorted_keys.sort()
    
        def remove_node(self, node: str) -> None:
            """노드 제거"""
            for i in range(self.virtual_nodes):
                virtual_key = f"{node}:{i}"
                hash_val = self._hash(virtual_key)
                del self.ring[hash_val]
                self.sorted_keys.remove(hash_val)
    
        def get_node(self, key: str) -> str:
            """키가 속한 노드 반환"""
            if not self.ring:
                raise ValueError("No nodes available")
    
            hash_val = self._hash(key)
            idx = bisect_right(self.sorted_keys, hash_val)
    
            # 링의 끝을 넘어가면 처음으로
            if idx == len(self.sorted_keys):
                idx = 0
    
            return self.ring[self.sorted_keys[idx]]
    
    
    # 사용 예시
    ch = ConsistentHash(["shard-0", "shard-1", "shard-2", "shard-3"])
    shard = ch.get_node("job_id_abc123")  # → "shard-2"

    2. Redis Streams Consumer Groups

    Redis 5.0+ (2018)
    Kafka Consumer Group의 Redis 버전

    2.1 Consumer Group 개념

    ┌─────────────────────────────────────────────────────────────────┐
    │                    Consumer Group 구조                           │
    ├─────────────────────────────────────────────────────────────────┤
    │                                                                  │
    │  Stream: scan:events                                            │
    │  ┌─────────────────────────────────────────────────────────┐   │
    │  │  1-0  │  2-0  │  3-0  │  4-0  │  5-0  │  6-0  │ ...    │   │
    │  └─────────────────────────────────────────────────────────┘   │
    │                          │                                       │
    │                          ▼                                       │
    │  ┌─────────────────────────────────────────────────────────┐   │
    │  │  Consumer Group: sse-consumers                           │   │
    │  │                                                          │   │
    │  │  last_delivered_id: 4-0                                  │   │
    │  │                                                          │   │
    │  │  ┌────────────┐  ┌────────────┐  ┌────────────┐        │   │
    │  │  │ Consumer A │  │ Consumer B │  │ Consumer C │        │   │
    │  │  │ pending:   │  │ pending:   │  │ pending:   │        │   │
    │  │  │ [1-0, 2-0] │  │ [3-0]      │  │ [4-0]      │        │   │
    │  │  └────────────┘  └────────────┘  └────────────┘        │   │
    │  │                                                          │   │
    │  │  PEL (Pending Entry List):                               │   │
    │  │  • 1-0 → Consumer A (delivered, not ACKed)              │   │
    │  │  • 2-0 → Consumer A                                      │   │
    │  │  • 3-0 → Consumer B                                      │   │
    │  │  • 4-0 → Consumer C                                      │   │
    │  └─────────────────────────────────────────────────────────┘   │
    │                                                                  │
    └─────────────────────────────────────────────────────────────────┘

    2.2 핵심 명령어

    # Consumer Group 생성
    XGROUP CREATE scan:events sse-consumers $ MKSTREAM
    
    # Consumer Group으로 읽기 (미전달 항목만)
    XREADGROUP GROUP sse-consumers consumer-a 
        COUNT 10 BLOCK 5000 
        STREAMS scan:events >
    
    # 특수 ID:
    #   > : 아직 전달되지 않은 새 메시지만
    #   0 : 내 pending 목록의 처음부터
    
    # 처리 완료 확인
    XACK scan:events sse-consumers 1735123456789-0
    
    # Pending 목록 조회
    XPENDING scan:events sse-consumers
    
    # 오래된 pending 항목 다른 consumer에게 재할당
    XCLAIM scan:events sse-consumers consumer-b 60000 1735123456789-0

    2.3 Kafka vs Redis Streams 비교

    ┌─────────────────────────────────────────────────────────────────┐
    │                    Kafka vs Redis Streams                        │
    ├─────────────────────────────────────────────────────────────────┤
    │                                                                  │
    │  특성              │ Kafka                │ Redis Streams        │
    │  ──────────────────┼──────────────────────┼────────────────────  │
    │  파티셔닝          │ Topic → Partitions   │ 단일 Stream          │
    │                    │ (수평 확장)          │ (Cluster로 샤딩)     │
    │                    │                      │                      │
    │  Consumer 할당     │ 자동 리밸런싱        │ 수동 (XREADGROUP)    │
    │                    │ Coordinator가 관리   │ 앱에서 직접 관리     │
    │                    │                      │                      │
    │  Partition:Consumer│ 1:1 (한 파티션은     │ N:M (한 메시지는     │
    │                    │ 한 consumer만)       │ 한 consumer만)       │
    │                    │                      │                      │
    │  리밸런싱          │ CooperativeSticky    │ 없음 (수동 구현)     │
    │                    │ (KIP-429)            │                      │
    │                    │                      │                      │
    │  Offset 관리       │ Consumer Group       │ PEL (Pending Entry   │
    │                    │ Offset               │ List)                │
    │                    │                      │                      │
    │  메시지 순서       │ 파티션 내 보장       │ Stream 내 보장       │
    │                    │                      │                      │
    │  적합 용도         │ 대용량 이벤트        │ 실시간 이벤트        │
    │                    │ 스트리밍             │ 소규모~중규모        │
    │                                                                  │
    └─────────────────────────────────────────────────────────────────┘

    2.4 Redis Streams의 한계: 자동 리밸런싱 없음

    ┌─────────────────────────────────────────────────────────────────┐
    │  Kafka: 자동 리밸런싱                                            │
    ├─────────────────────────────────────────────────────────────────┤
    │                                                                  │
    │  Consumer 추가 시:                                              │
    │  ┌────────┐  ┌────────┐       ┌────────┐  ┌────────┐  ┌────┐ │
    │  │  P0    │  │  P1    │   →   │  P0    │  │  P1    │  │ P2 │ │
    │  │  C1    │  │  C1    │       │  C1    │  │  C2    │  │ C2 │ │
    │  └────────┘  └────────┘       └────────┘  └────────┘  └────┘ │
    │                                                                  │
    │  Coordinator가 자동으로 파티션을 재할당                          │
    │                                                                  │
    └─────────────────────────────────────────────────────────────────┘
    
    ┌─────────────────────────────────────────────────────────────────┐
    │  Redis Streams: 수동 관리 필요                                   │
    ├─────────────────────────────────────────────────────────────────┤
    │                                                                  │
    │  Consumer 추가 시:                                              │
    │  • 기존 Consumer들이 읽는 Stream에 변화 없음                    │
    │  • 새 Consumer는 새 메시지만 받음                               │
    │  • 부하 분산을 위해 앱에서 직접 로직 구현 필요                  │
    │                                                                  │
    │  해결책:                                                         │
    │  1) 여러 Stream으로 수동 샤딩 (scan:events:0, scan:events:1)    │
    │  2) 앱에서 shard ↔ consumer 할당 로직 구현                      │
    │  3) 외부 coordinator (Kubernetes Lease 등) 사용                 │
    │                                                                  │
    └─────────────────────────────────────────────────────────────────┘

    3. Service Mesh Consistent Hash Routing

    Envoy Proxy의 Ring Hash LB를 Istio에서 활용
    특정 키(header, cookie, query param)를 기준으로 동일한 Pod로 라우팅

    3.1 Envoy Ring Hash LB

    ┌─────────────────────────────────────────────────────────────────┐
    │                    Envoy Ring Hash LB                            │
    ├─────────────────────────────────────────────────────────────────┤
    │                                                                  │
    │  알고리즘: Ketama (Consistent Hashing 구현체)                   │
    │                                                                  │
    │  동작 원리:                                                      │
    │  1. 각 upstream host를 해시 링에 배치                           │
    │  2. 요청의 hash key (header, cookie 등)를 해시                  │
    │  3. 링에서 시계방향으로 가장 가까운 host 선택                   │
    │                                                                  │
    │          Hash Ring                                               │
    │              ●                                                   │
    │         ╱         ╲                                             │
    │       ●             ●                                           │
    │    pod-0          pod-1                                         │
    │       │             │                                            │
    │       │  ← job_id=abc (hash=0.3)                                │
    │       │                                                          │
    │       ●             ●                                           │
    │    pod-3          pod-2                                         │
    │         ╲         ╱                                             │
    │              ●                                                   │
    │                                                                  │
    │  → job_id=abc 요청은 항상 pod-0으로 라우팅                      │
    │                                                                  │
    └─────────────────────────────────────────────────────────────────┘

    3.2 Istio DestinationRule 설정

    # Istio DestinationRule: job_id 기반 Consistent Hash
    apiVersion: networking.istio.io/v1beta1
    kind: DestinationRule
    metadata:
      name: sse-gateway-consistent-hash
      namespace: sse-consumer
    spec:
      host: sse-gateway.sse-consumer.svc.cluster.local
      trafficPolicy:
        loadBalancer:
          consistentHash:
            # 옵션 1: Query Parameter 기반
            httpQueryParameterName: "job_id"
    
            # 옵션 2: Header 기반
            # httpHeaderName: "x-job-id"
    
            # 옵션 3: Cookie 기반
            # httpCookie:
            #   name: "session-id"
            #   ttl: 3600s
    
        connectionPool:
          http:
            h2UpgradePolicy: UPGRADE  # SSE를 위한 HTTP/2

    3.3 Ring Hash 설정 튜닝

    # Envoy 직접 설정 시 (Istio EnvoyFilter)
    apiVersion: networking.istio.io/v1alpha3
    kind: EnvoyFilter
    metadata:
      name: sse-gateway-ring-hash-config
      namespace: sse-consumer
    spec:
      workloadSelector:
        labels:
          app: sse-gateway
      configPatches:
      - applyTo: CLUSTER
        match:
          context: SIDECAR_OUTBOUND
          cluster:
            name: "outbound|80||sse-gateway.sse-consumer.svc.cluster.local"
        patch:
          operation: MERGE
          value:
            lb_policy: RING_HASH
            ring_hash_lb_config:
              minimum_ring_size: 1024    # 링 크기 (균등 분포)
              maximum_ring_size: 8388608 # 최대 링 크기
              hash_function: XX_HASH     # 해시 함수 (기본: XX_HASH)

    3.4 Consistent Hash의 한계

    ┌─────────────────────────────────────────────────────────────────┐
    │                    Consistent Hash Routing 한계                  │
    ├─────────────────────────────────────────────────────────────────┤
    │                                                                  │
    │  1. Pod 추가/제거 시 일부 키 재배치                             │
    │     • Consistent Hash라도 약간의 재배치는 발생                  │
    │     • 기존 SSE 연결이 끊길 수 있음                              │
    │                                                                  │
    │  2. Pod가 없을 때 요청 실패                                     │
    │     • 특정 shard 담당 Pod가 모두 죽으면 해당 키 요청 실패       │
    │                                                                  │
    │  3. 불균등 부하                                                  │
    │     • 특정 job_id가 많은 요청을 받으면 해당 Pod에 부하 집중     │
    │     • Virtual Nodes로 완화 가능하지만 완벽하지 않음             │
    │                                                                  │
    │  해결책:                                                         │
    │  • Pod 수를 shard 수보다 많게 유지                              │
    │  • Fallback: 담당 Pod 없으면 다른 Pod로 라우팅                  │
    │  • HPA: 부하에 따라 Pod 자동 확장                               │
    │                                                                  │
    └─────────────────────────────────────────────────────────────────┘

    4. Pub/Sub Fanout 패턴

    하나의 메시지를 여러 수신자에게 전달하는 패턴

    4.1 Fan-in / Fan-out 개념

    ┌─────────────────────────────────────────────────────────────────┐
    │                    Fan-in / Fan-out                              │
    ├─────────────────────────────────────────────────────────────────┤
    │                                                                  │
    │  Fan-in (수집):                                                 │
    │  ─────────────                                                   │
    │  ┌────────┐                                                      │
    │  │ Source │──┐                                                  │
    │  └────────┘  │                                                  │
    │  ┌────────┐  │    ┌───────────┐                                │
    │  │ Source │──┼───▶│ Aggregator│                                │
    │  └────────┘  │    └───────────┘                                │
    │  ┌────────┐  │                                                  │
    │  │ Source │──┘                                                  │
    │  └────────┘                                                      │
    │                                                                  │
    │  Fan-out (분배):                                                │
    │  ──────────────                                                  │
    │                     ┌────────┐                                  │
    │               ┌────▶│  Sink  │                                  │
    │  ┌─────────┐  │     └────────┘                                  │
    │  │ Source  │──┼────▶┌────────┐                                  │
    │  └─────────┘  │     │  Sink  │                                  │
    │               │     └────────┘                                  │
    │               └────▶┌────────┐                                  │
    │                     │  Sink  │                                  │
    │                     └────────┘                                  │
    │                                                                  │
    └─────────────────────────────────────────────────────────────────┘

    4.2 The Tail at Scale (Google, 2013)

    Jeff Dean, Luiz Barroso
    "Fan-out 시스템에서 지연 시간의 롱테일 문제"

    ┌─────────────────────────────────────────────────────────────────┐
    │                    Tail Latency 증폭                             │
    ├─────────────────────────────────────────────────────────────────┤
    │                                                                  │
    │  문제: Fan-out 시 가장 느린 응답이 전체 지연 결정                │
    │  ────────────────────────────────────────────────                │
    │                                                                  │
    │  단일 서버 p99 = 10ms                                           │
    │                                                                  │
    │  Fan-out 100개 서버:                                            │
    │  • 1 - (0.99)^100 = 63% 확률로 최소 1개가 p99 이상              │
    │  • 전체 요청의 p50이 단일 서버의 p99와 비슷해짐                  │
    │                                                                  │
    │  ┌────────────────────────────────────────────────────────────┐│
    │  │            Latency Distribution                             ││
    │  │                                                             ││
    │  │  단일 서버:  ████████████████░░░░░░░░░░░░░░░░░░░░░         ││
    │  │                           p50    p99                        ││
    │  │                                                             ││
    │  │  100x Fan-out: ░░░░░░░░░░░░░░░░████████████████████████    ││
    │  │                                p50              p99         ││
    │  │                                                             ││
    │  └────────────────────────────────────────────────────────────┘│
    │                                                                  │
    │  해결책:                                                         │
    │  1. Hedged Requests: 여러 replica에 동시 요청, 첫 응답 사용     │
    │  2. Tied Requests: 큐에서 대기 시간이 긴 요청 취소             │
    │  3. Micro-partitioning: 더 많은 작은 파티션                     │
    │  4. Selective Replication: 핫 데이터만 추가 복제                │
    │                                                                  │
    └─────────────────────────────────────────────────────────────────┘

    4.3 SSE-Gateway Fanout 구조

    ┌─────────────────────────────────────────────────────────────────┐
    │                    Eco² SSE-Gateway Fanout                       │
    ├─────────────────────────────────────────────────────────────────┤
    │                                                                  │
    │  현재 구조:                                                      │
    │  ───────────                                                     │
    │                                                                  │
    │  ┌─────────────────────────────────────────────────────────┐   │
    │  │  scan-worker (Producer)                                  │   │
    │  │                                                          │   │
    │  │  job_id → hash(job_id) % 4 → shard 0~3                  │   │
    │  │                                                          │   │
    │  │  XADD scan:events:{shard} * stage vision status done    │   │
    │  └──────────────────────────┬──────────────────────────────┘   │
    │                             │                                    │
    │                             ▼                                    │
    │  ┌─────────────────────────────────────────────────────────┐   │
    │  │  Redis Streams                                           │   │
    │  │  ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌──────┐ │   │
    │  │  │ shard:0    │ │ shard:1    │ │ shard:2    │ │ :3   │ │   │
    │  │  └────────────┘ └────────────┘ └────────────┘ └──────┘ │   │
    │  └──────────────────────────┬──────────────────────────────┘   │
    │                             │                                    │
    │                             ▼                                    │
    │  ┌─────────────────────────────────────────────────────────┐   │
    │  │  sse-gateway (Consumer)                                  │   │
    │  │                                                          │   │
    │  │  Pod-0: XREAD shard:0                                   │   │
    │  │  Pod-1: XREAD shard:1  ← 현재 문제: 각 Pod가 1개만 읽음 │   │
    │  │  Pod-2: XREAD shard:2                                   │   │
    │  │  Pod-3: XREAD shard:3                                   │   │
    │  │                                                          │   │
    │  │  문제: job_id가 shard:2에 있는데 클라이언트가 Pod-0에   │   │
    │  │        연결되면 이벤트를 영원히 못 받음                  │   │
    │  └─────────────────────────────────────────────────────────┘   │
    │                                                                  │
    └─────────────────────────────────────────────────────────────────┘

    4.4 해결책: 라우팅 일관성

    ┌─────────────────────────────────────────────────────────────────┐
    │                    해결책: Consistent Hash Routing               │
    ├─────────────────────────────────────────────────────────────────┤
    │                                                                  │
    │  1. Istio Consistent Hash로 SSE 요청 라우팅:                    │
    │     job_id=abc → hash(abc) → Pod-2                             │
    │                                                                  │
    │  2. Pod-2는 shard:2만 구독 (Producer와 일치)                   │
    │                                                                  │
    │  ┌─────────────────────────────────────────────────────────┐   │
    │  │  Client                                                  │   │
    │  │  GET /stream?job_id=abc                                  │   │
    │  └──────────────────────────┬──────────────────────────────┘   │
    │                             │                                    │
    │                             ▼                                    │
    │  ┌─────────────────────────────────────────────────────────┐   │
    │  │  Istio (Envoy) - consistentHash.httpQueryParameterName  │   │
    │  │                                                          │   │
    │  │  job_id=abc → hash(abc) % 4 = 2 → Pod-2                 │   │
    │  └──────────────────────────┬──────────────────────────────┘   │
    │                             │                                    │
    │                             ▼                                    │
    │  ┌─────────────────────────────────────────────────────────┐   │
    │  │  Pod-2                                                   │   │
    │  │  • XREAD scan:events:2 (자신의 shard만)                 │   │
    │  │  • job_id=abc 이벤트 SSE로 전달                         │   │
    │  └─────────────────────────────────────────────────────────┘   │
    │                                                                  │
    │  핵심: Producer의 shard 선택 = Routing의 shard 선택             │
    │                                                                  │
    └─────────────────────────────────────────────────────────────────┘

    5. Eco² 적용: 샤딩 + 라우팅 통합 설계

    5.1 현재 문제

    Producer (scan-worker):  hash(job_id) % 4 → shard 0~3에 발행
    Consumer (sse-gateway):  각 Pod가 자기 shard만 읽음
    Routing (Istio):         Round Robin (랜덤 Pod로 라우팅)
    
    → job_id가 shard:2에 있는데 클라이언트가 Pod-0에 연결되면 누락

    5.2 단기 해결책

    # 옵션 A: 모든 shard 구독 (가장 빠른 해결)
    # sse-gateway가 모든 shard를 XREAD
    # 자신에게 연결된 클라이언트의 job_id만 필터링하여 전달
    
    async def subscribe_all_shards(job_id: str):
        # 모든 shard를 순회하며 해당 job_id 이벤트 찾기
        shard = hash(job_id) % SHARD_COUNT
        stream_key = f"scan:events:{shard}"
    
        # 이 job의 shard만 읽기 (효율적)
        async for event in xread(stream_key, job_id):
            yield event

    5.3 장기 해결책

    # Istio Consistent Hash 설정
    apiVersion: networking.istio.io/v1beta1
    kind: DestinationRule
    metadata:
      name: sse-gateway-job-routing
      namespace: sse-consumer
    spec:
      host: sse-gateway.sse-consumer.svc.cluster.local
      trafficPolicy:
        loadBalancer:
          consistentHash:
            httpQueryParameterName: "job_id"  # job_id로 라우팅
    
    ---
    # sse-gateway ConfigMap: shard 할당
    # Pod index와 shard 매핑 (수동 관리)
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: sse-gateway-shard-config
      namespace: sse-consumer
    data:
      # Pod 수와 shard 수가 같으면 1:1 매핑
      # Pod 수가 적으면 한 Pod가 여러 shard 담당
      shard_mapping: |
        pod-0: [0, 1]
        pod-1: [2, 3]

    5.4 아키텍처 비교

    ┌─────────────────────────────────────────────────────────────────┐
    │  AS - IS (문제 상황)                                              │
    ├─────────────────────────────────────────────────────────────────┤
    │                                                                  │
    │  Client ──▶ Istio (RoundRobin) ──▶ Pod-0 ──XREAD──▶ shard:0     │
    │                                                                  │
    │  scan-worker ──XADD──▶ shard:2 (job_id=abc)                     │
    │                                                                  │
    │  → Client는 Pod-0에 연결, 이벤트는 shard:2에 있음               │
    │  → 영원히 이벤트를 못 받음                                       │
    │                                                                  │
    └─────────────────────────────────────────────────────────────────┘
    
    ┌─────────────────────────────────────────────────────────────────┐
    │  TO - BE (해결)                                                  │
    ├─────────────────────────────────────────────────────────────────┤
    │                                                                  │
    │  Client ──▶ Istio (consistentHash) ──▶ Pod-2 ──XREAD──▶ shard:2 │
    │             hash(job_id=abc) = 2                                │
    │                                                                  │
    │  scan-worker ──XADD──▶ shard:2 (job_id=abc)                     │
    │               hash(job_id=abc) = 2                              │
    │                                                                  │
    │  → Client와 이벤트가 같은 shard로 라우팅됨                      │
    │  → 정상적으로 이벤트 수신                                        │
    │                                                                  │
    └─────────────────────────────────────────────────────────────────┘

    관련 문서

    Workloads (Eco²)

    외부 자료


    버전 정보

    • 작성일: 2025-12-27
    • Redis 버전: 7.0+
    • Istio 버전: 1.20+

    댓글

ABOUT ME

🎓 부산대학교 정보컴퓨터공학과 학사: 2017.03 - 2023.08
☁️ Rakuten Symphony Jr. Cloud Engineer: 2024.12.09 - 2025.08.31
🏆 2025 AI 새싹톤 우수상 수상: 2025.10.30 - 2025.12.02
🌏 이코에코(Eco²) 백엔드/인프라 고도화 중: 2025.12 - Present

Designed by Mango