콘텐츠로 이동

클러스터 내부 이벤트 브로커

continuum-event-broker는 이벤트 버스 터널의 클러스터 내부 엔드포인트입니다. GPU 클러스터 외부에서 실행되는 라우터는 워커 Pod IP에 직접 연결할 수 없으므로, kv_metrics 부하 신호와 kv-events를 발행하는 Dynamo ZMQ 발행자(publisher)를 직접 구독할 수 없습니다. 브로커는 클러스터 내부에서 실행되며 라우터의 TLS 이벤트 버스로 아웃바운드 연결을 맺고 발행자의 바이트 스트림을 라우터로 중계합니다.

  router (bus server)                     cluster
  ┌─────────────────────┐   HTTP/2+TLS   ┌──────────────────────┐   ZMQ/TCP   ┌───────────┐
  │ discovery, ZMTP     │◀──────────────│ continuum-event-      │────────────▶│ publisher │
  │ client, decoding    │ session +      │ broker (bus client)   │             │ (worker)  │
  └─────────────────────┘ data streams   └──────────────────────┘             └───────────┘

워커 탐색과 모든 구독 결정은 라우터가 담당합니다. 브로커는 메시지를 재인코딩하는 프록시가 아니라 순수 바이트 터널로 동작합니다. subscribe 명령을 받으면 발행자와 TCP 연결을 열고, 원시 ZMTP 바이트를 파싱하지 않은 채 단일 HTTP/2 스트림을 통해 양방향으로 중계합니다. 핸드셰이크, 토픽 구독, 프레임 디코딩은 모두 라우터 내부의 ZMTP 클라이언트가 터널 너머에서 수행하므로 Dynamo가 업그레이드되어도 브로커를 수정할 필요가 없습니다.

와이어 프로토콜 규약은 독립 크레이트인 crates/continuum-event-bus에 정의되어 있으며, 라우터 측 리스너와 브로커 구독기는 src/event_bus/에, 배포 매니페스트는 deploy/ 디렉터리에 있습니다(배포 참고).

동일한 이벤트 버스는 두 번째 클라이언트 역할인 kv_listener도 지원합니다. continuum-kv-listener 역시 라우터에 아웃바운드 연결을 맺고 백엔드별 KV 이벤트 스트림을 푸시하므로, 라우터가 직접 접근할 수 없는 환경에서도 KV 캐시 인덱스를 갱신할 수 있습니다.

자세한 내용은 KV 캐시 최적화와 Dynamo 워커 탐색을 참고하세요. 브로커 바이너리는 소스를 직접 광고하지 않으며 announced_source 대상 요청은 unsupported_target으로 거부합니다.

브로커 동작 방식

  1. 메타데이터 동기화: 감시 대상 Kubernetes 네임스페이스의 DynamoWorkerMetadata 리소스를 조회(list) 및 감시(watch)하여 광고된 발행자 엔드포인트 목록을 구성합니다. 모든 감시 대상 네임스페이스의 초기 조회가 완료되기 전에는 버스에 연결하지 않습니다.
  2. 세션 핸드셰이크: Bearer 자격 증명과 함께 HTTP/2 및 TLS를 통해 POST /bus/v1/session을 보낸 뒤, 역할(broker), 클라이언트 ID, 실행 중인 Kubernetes 네임스페이스, 담당 Dynamo 네임스페이스를 담은 hello 프레임을 전송합니다. 라우터는 협상된 버전, 세션 ID, 적용 한도를 담은 welcome 프레임으로 응답합니다.
  3. 하트비트 전송: 협상된 주기마다 heartbeat를 전송하며, 라우터가 유휴 타임아웃 동안 응답하지 않으면 세션을 종료합니다.
  4. 스트림 연결: 각 subscribe 명령에 대해 보안 모델 검사를 수행한 뒤 발행자에 연결하고 POST /bus/v1/streams/{id}를 호출해 stream_opened를 보고합니다. 검증이나 연결에 실패하면 스트림을 열지 않고 stream_refused를 보고합니다.
  5. 바이트 중계: 양쪽 중 한쪽이 닫힐 때까지 바이트를 중계합니다. 자체적으로 종료된 스트림은 stream_closed(upstream_closed 또는 buffer_overflow)를 보고하며, unsubscribe 명령으로 닫힌 스트림은 별도 보고 없이 종료합니다.
  6. 재연결 및 드레인 처리: 세션이 끊어지면 지터가 적용된 지수 백오프(reconnect_backoff, 기본 1~60초)로 재연결하며 열려 있던 스트림은 세션과 함께 닫힙니다. 라우터가 drain을 보내면 새 스트림 수락을 중단하고 라우터 마감 시각까지 기존 스트림을 정리한 뒤 재연결합니다.
  7. 리밸런스 백오프 제어: rebalance 사유로 드레인된 경우 짧은 지터 대기(250~500ms) 후 즉시 재연결합니다. 단, 1분 이내에 리밸런스가 반복되면 일반 지수 백오프로 전환하여 잦은 리밸런스로 인한 재연결 폭주를 방지합니다.
  8. 정상 종료(Graceful shutdown): SIGTERM 또는 SIGINT 수신 시 draining을 전송하고 새 구독을 draining으로 거부하며, drain_deadline까지 열린 스트림을 정리한 뒤 종료 코드 0으로 종료합니다. 신호를 한 번 더 받으면 즉시 종료합니다.

하나의 세션과 그에 속한 모든 데이터 스트림은 단일 TLS 연결을 공유하므로 연결이 끊어지면 세션과 모든 스트림이 함께 종료됩니다. 각 데이터 스트림은 수명 내내 스트리밍 요청 본문과 스트리밍 응답 본문을 동시에 유지해야 하므로, HTTP/2 클라이언트는 tokio-rustls(aws-lc-rs 프로바이더) 기반의 hyper 클라이언트를 사용합니다.

보안 모델

브로커는 아래의 모든 검증을 통과한 대상에만 연결을 시도합니다. 따라서 라우터가 침해되더라도 브로커를 경유해 클러스터 내부의 임의 주소에 접근할 수 없습니다.

검사 항목 거부 코드
대상 종류가 zmq_tcp인지 확인 (announced_source는 continuum-kv-listener 전용이므로 알 수 없는 종류와 함께 거부) unsupported_target
대상의 Dynamo 네임스페이스가 브로커의 dynamo_namespaces 설정에 포함되어 있는지 확인 target_not_advertised
브로커의 메타데이터 뷰가 초기 동기화를 완료했는지 확인 target_not_advertised
(namespace, host, port)가 해당 네임스페이스 DynamoWorkerMetadata의 event_channels에 등록된 tcp://host:port와 정확히 일치하는지 확인 target_not_advertised
호스트 이름이 유효한 IP 주소로 해석되는지 확인 (IP 리터럴은 자기 자신으로 해석) upstream_connect_failed
해석된 IP 주소가 주소 필터를 통과하는지 확인 target_not_allowed

주소 필터는 DNS 이름 해석 이후 실제로 연결할 IP 주소에 적용되므로 DNS 레코드 조작으로 우회할 수 없습니다. 루프백, 링크 로컬(클라우드 메타데이터 주소 169.254.169.254 포함), 미지정(unspecified), 멀티캐스트, 브로드캐스트 주소는 해당 특수 대역 내부에 완전히 포함되는 allowed_cidrs 항목이 명시적으로 허용하지 않는 한 거부됩니다(0.0.0.0/0으로는 허용되지 않음). allowed_cidrs가 설정된 경우 허용 목록 밖의 모든 주소도 거부됩니다.

모든 거부는 stream_refused로 보고되며 continuum_event_broker_refusals_total{code} 메트릭에 집계됩니다. 로그 폭주를 막기 위해 경고 로그는 오류 코드별로 제한됩니다. 60초 윈도마다 첫 번째 거부만 로그로 남기고 이후 거부는 메트릭으로만 집계하며, 다음 윈도의 첫 거부 로그에 그동안 생략된 횟수를 suppressed 필드로 함께 기록합니다.

초기 목록 조회가 완료되기 전에는 광고된 엔드포인트가 없으며 브로커도 버스에 연결하지 않습니다. 설령 이 상태에서 구독 명령이 평가되더라도 정책에 따라 target_not_advertised로 거부되므로 라우터는 이후에 다시 시도할 수 있습니다.

브로커는 DynamoWorkerMetadata에서 event_channels 맵만 읽습니다. <ip>:<port>/<id>/<endpoint> 형태의 요청 플레인 주소를 담는 endpoints 맵은 발행자가 아니므로 무시합니다. 감시 대상 네임스페이스에 DynamoWorkerMetadata를 생성할 수 있는 주체는 엔드포인트를 광고할 수 있으므로 해당 Kubernetes RBAC이 1차 신뢰 경계가 되며, allowed_cidrs가 위조된 리소스의 영향 범위를 한 번 더 제한합니다.

버스 URL은 반드시 https://여야 하며 평문 URL은 설정 오류로 거부됩니다. 자격 증명은 파일(마운트된 Secret)이나 환경 변수에서 읽고 민감 정보로 마스킹된 Authorization 헤더로만 전송하며 로그에 기록하지 않습니다.

allowed_cidrs를 Pod CIDR로 설정 권장

allowed_cidrs를 클러스터의 Pod CIDR(예: 10.244.0.0/16)로 설정하는 것을 강력히 권장합니다. DynamoWorkerMetadata 검사는 감시 대상 네임스페이스 내 리소스 생성 권한을 신뢰하는 1차 방어선이며, allowed_cidrs는 위조되거나 잘못 작성된 광고로 인해 브로커가 연결할 수 있는 대상을 제한하는 독립적인 2차 방어선입니다.

allowed_cidrs를 비워 두면 브로커는 특수 목적 주소를 제외한 모든 광고된 주소를 허용하므로, 잘못된 광고를 통해 Service IP, 노드, 다른 테넌트의 Pod, 내부 데이터베이스 등으로 연결이 열릴 수 있습니다. 루프백, 링크 로컬(169.254.169.254 포함), 미지정, 멀티캐스트, 브로드캐스트 주소는 설정 여부와 관계없이 항상 차단됩니다.

EndpointSlice 준비 상태

브로커는 EndpointSlice 준비 상태(readiness)로 연결 대상을 필터링하지 않습니다. 브로커 검사의 목적은 대상이 실제 Dynamo 발행자인지 확인하는 것이며, 아직 준비되지 않은 워커의 발행자 역시 해당 워커가 광고한 정상 발행자이기 때문입니다.

해당 발행자를 언제 구독할지는 라우터가 자체 준비 상태 기반 탐색을 통해 결정합니다. 이 검사를 생략함으로써 브로커의 RBAC 권한을 단일 커스텀 리소스로 최소화할 수 있습니다. 각 워커의 DynamoWorkerMetadata는 해당 Pod를 소유자(owner)로 가지므로 Pod가 삭제되면 함께 삭제되어 브로커 뷰에서도 자동으로 제거됩니다.

Watch 재시작 중의 메타데이터 뷰

초기 목록 조회가 완료된 이후 watch 연결이 재시작되는 동안에는 마지막으로 조회한 상태를 유지하고, 다음 목록 조회가 완료될 때 전체 스냅샷을 원자적으로 교체합니다. API 서버 일시 장애 시 뷰를 비우면 모든 구독이 거부되기 때문입니다. 대신 장애 기간 중 삭제된 엔드포인트는 재조회가 완료될 때까지 연결 가능 상태로 남지만, 이 경우에도 allowed_cidrs 필터는 그대로 적용됩니다.

버퍼링 한도

각 데이터 스트림은 라우터 방향으로 세션의 max_stream_buffer_bytes(최대 1 MiB, 라우터의 welcome 응답에 따라 더 작을 수 있음)까지만 버퍼링하며, 한 세션의 모든 데이터 스트림을 합산해 최대 64 MiB까지만 버퍼링합니다. 따라서 라우터가 수많은 스트림을 열고 HTTP/2 수신 윈도를 주지 않더라도 브로커 메모리가 max_streams * max_stream_buffer_bytes만큼 증가하지 않습니다.

큐에 적재되는 각 청크는 실제 바이트 길이에 고정 큐 항목 오버헤드 64바이트를 더해 두 한도에 모두 반영하며, 최소 스트림 예산(16 KiB)에서도 동일하게 오버헤드를 예약합니다. 따라서 아주 작은 세그먼트를 반복 전송하는 발행자도 실제 메모리 사용량에 비례해 한도를 소진합니다.

브로커는 발행자의 ZMQ high-water mark에서 메시지가 조용히 유실되는 현상을 막기 위해 발행자 소켓을 멈추지 않고 계속 읽습니다. 대신 두 버퍼 한도 중 하나라도 초과하면 즉시 해당 HTTP/2 스트림을 리셋하고 소켓을 닫은 뒤 buffer_overflow 코드로 stream_closed를 보고하여 라우터가 재구독하도록 합니다. 반대 방향(라우터에서 발행자 방향)으로는 64 KiB HTTP/2 스트림 윈도가 라우터의 선행 전송량을 제한합니다.

스트림 연결 기한

라우터는 구독 명령을 보낸 뒤 30초(STREAM_OPEN_TIMEOUT)가 지나면 해당 요청을 폐기하고, 뒤늦게 도착하는 데이터 스트림에는 unknown_stream으로 응답합니다. 따라서 브로커는 이 시간 안에 연결을 마칠 수 있도록 단계별 타임아웃을 적용합니다.

  • DNS 이름 해석: 최대 5초
  • 업스트림 TCP 연결: 해석된 모든 주소에 걸쳐 합산 최대 10초 (개별 시도는 upstream_connect_timeout으로도 제한)
  • 버스 HTTP/2 스트림 슬롯 확보: 버스 연결에서 가용 스트림 슬롯을 얻기까지 최대 5초
  • 라우터 응답 헤더 수신: 데이터 스트림 요청 전송 후 라우터의 응답 헤더가 도착하기까지 최대 10초

어느 단계에서든 기한이 만료되면 브로커는 업스트림 소켓을 닫고 스트림 슬롯을 반환한 뒤 다음과 같이 보고합니다.

기한 만료 단계 보고 프레임 코드
업스트림 연결 (모든 해석된 주소) stream_refused upstream_connect_failed
버스 연결에 가용 HTTP/2 스트림 슬롯 없음 stream_refused too_many_streams
라우터 응답 헤더 대기 (슬롯 부족으로 큐에 대기한 경우도 포함) stream_closed idle_timeout

설정

전체 예시는 config.event-broker.yaml.example을 참고하세요.

필드 기본값 의미
bus_url 필수 라우터 이벤트 버스 주소 (https://host[:port])
ca_file 시스템 공인 루트 CA 라우터 인증서 검증에 신뢰할 CA PEM 번들
tls_server_name URL 호스트 라우터 인증서와 대조할 TLS 서버 이름
client_id 필수 라우터에서 자격 증명이 바인딩된 클라이언트 ID
credential_file / credential_env $CONTINUUM_EVENT_BROKER_CREDENTIAL Bearer 자격 증명(32~512바이트)을 읽을 파일 경로 또는 환경 변수
kubernetes_namespace $POD_NAMESPACE, 없으면 ServiceAccount 네임스페이스 hello 프레임에 선언할 네임스페이스
dynamo_namespaces 필수, 최대 64개 hello에 선언하고 대상 검증에 적용할 Dynamo 네임스페이스 목록
watch_namespaces kubernetes_namespace DynamoWorkerMetadata를 감시할 네임스페이스 목록 (최대 32개)
allowed_cidrs 없음 선택적 연결 허용 CIDR 목록 (최대 256개)
metrics_listen 0.0.0.0:9464 /metrics, /healthz, /readyz 수신 주소
reconnect_backoff.initial / .max 1s / 60s 지터가 적용된 지수 재연결 백오프 범위
drain_deadline 25s SIGTERM 수신 시 스트림 정리 대기 시간 (최대 10m)
upstream_connect_timeout 5s 발행자 단일 연결 시도 타임아웃 (전체 시도 합산 상한은 10초)
bus_connect_timeout 10s 버스에 대한 TCP, TLS, HTTP/2 핸드셰이크 타임아웃

정의되지 않은 필드가 있으면 시작 시 거부됩니다. continuum-event-broker --check-config 명령으로 설정 파일, 자격 증명, CA 번들의 유효성을 검사할 수 있습니다.

RBAC

감시 대상 네임스페이스마다 dynamoworkermetadatas.nvidia.com 리소스에 대한 get, list, watch 권한만 필요합니다.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: continuum-event-broker
  namespace: <watched namespace>
rules:
  - apiGroups: ["nvidia.com"]
    resources: ["dynamoworkermetadatas"]
    verbs: ["get", "list", "watch"]

브로커는 어떠한 리소스도 생성, 수정, 패치, 삭제하지 않습니다. Helm 차트와 Kustomize 번들은 감시 대상 네임스페이스마다 이 Role을 자동으로 생성합니다(배포 참고).

메타데이터 뷰의 한도

브로커는 라우터 크레이트에 의존하지 않고 필요한 파서만 자체 구현하며, 라우터와 동일한 메모리 보호 한도를 적용합니다.

  • 리소스 및 발행자 수 상한: 감시 네임스페이스당 리소스 최대 16,384개, 리소스당 검사 채널 항목 최대 1,024개 및 보관 발행자 최대 256개, 중복 광고를 포함한 전체 발행자 참조 최대 65,536개로 제한합니다.
  • 필드 크기 상한: 채널 키 최대 1 KiB, 채널 값 최대 64 KiB, 엔드포인트 문자열 최대 256바이트로 제한합니다.
  • 페이지 단위 조회: list 요청은 32개 단위 페이지로 조회하며 다음 페이지를 가져오기 전에 파싱을 완료하므로, 메모리에는 원본 리소스를 최대 한 페이지만 유지합니다. watch 응답 스트림을 여는 시간은 30초로 제한됩니다.
  • 갱신 시 상한 적용: 목록 페이지를 처리할 때부터 65,536개 발행자 참조 상한을 적용합니다. watch 갱신이 상한을 초과하면 해당 리소스의 이전 광고를 즉시 제거하여 만료된 주소에 대한 연결 허용을 철회합니다.
  • 선택적 역직렬화 및 오류 격리: 리소스에서 spec.data.event_channels만 추출하고 모델 카드나 요청 플레인 엔드포인트는 역직렬화 단계에서 건너뜁니다. 손상된 항목이나 리소스가 있더라도 전체 목록 페이지를 실패시키지 않고 해당 항목만 건너뛰며 continuum_event_broker_metadata_skipped_total에 집계합니다.

메트릭

메트릭 레이블 의미
continuum_event_broker_session_connected 세션이 welcome 상태인 동안 1
continuum_event_broker_session_attempts_total result (welcomed, refused, failed) 세션 연결 시도 횟수
continuum_event_broker_session_ends_total reason 종료된 세션 수 (shutdown, drained, refused, idle_timeout, terminal_error, router_closed, connect_failed 등)
continuum_event_broker_reconnects_total 재연결 횟수
continuum_event_broker_subscribes_total result (opened, refused) 수신한 subscribe 명령 수
continuum_event_broker_refusals_total code 오류 코드별 구독 거부 횟수
continuum_event_broker_stream_closes_total code unsubscribe 없이 종료된 스트림 수
continuum_event_broker_overflow_closes_total 버퍼 오버플로로 닫힌 스트림 수
continuum_event_broker_open_streams 현재 열려 있는 데이터 스트림 수
continuum_event_broker_relayed_bytes_total direction (upstream_to_router, router_to_upstream) 양방향 중계 바이트 수
continuum_event_broker_metadata_synced 모든 감시 대상 네임스페이스의 초기 조회 완료 시 1
continuum_event_broker_metadata_resources 현재 보관 중인 리소스 수
continuum_event_broker_metadata_publishers 현재 보관 중인 고유 광고 발행자 수
continuum_event_broker_metadata_watch_restarts_total reason 종료된 watch 패스 수
continuum_event_broker_metadata_skipped_total reason 건너뛴 항목 또는 리소스 수

모든 레이블 집합은 카디널리티가 고정되어 있습니다. 오류 및 종료 코드는 프로토콜에 정의된 목록만 사용하며(알 수 없는 코드는 unknown), 스트림 ID, 호스트, 네임스페이스는 레이블로 사용하지 않습니다. /readyz 엔드포인트는 초기 메타데이터 뷰 동기화가 완료되면 200 OK를 반환합니다.

패키징

릴리스 워크플로는 4개 Linux 타깃용 브로커 바이너리를 빌드하고 매뉴얼 페이지(continuum-event-broker(1))와 함께 Linux 아카이브 및 Debian 패키지에 포함합니다. docker-event-broker 작업은 crates/continuum-event-broker/Dockerfile을 사용해 distroless 정적 이미지 ghcr.io/lablup/continuum-event-broker를 게시합니다.

이 크레이트는 자체 Cargo.lock을 사용하는 독립 크레이트(워크스페이스 멤버 아님)이므로 라우터의 기능 그래프에 포함되지 않으며, CI는 모든 Rust 코드 변경 시 포맷, 린트, 테스트를 수행합니다.

배포

GPU 클러스터 내부에는 독립 Helm 차트(deploy/helm/continuum-event-broker) 또는 Kustomize 번들(deploy/kubernetes/event-broker/)로 브로커를 배포하고, 라우터 차트에서는 values-dynamo-event-bus.yaml로 라우터 리스너를 노출합니다.

두 배포 방식 모두 브로커 식별자(client_id)당 단일 Pod로 구성된 Deployment를 실행합니다. 라우터는 브로커 ID당 하나의 세션만 유지하며 동일한 ID로 새 세션이 연결되면 기존 세션을 대체하므로, 브로커 재시작 중에도 세션을 끊김 없이 유지하려면 단일 Deployment의 레플리카를 2개로 늘리는 대신 서로 다른 브로커 ID 2개를 배포해야 합니다.

두 브로커 ID가 동일한 Dynamo 네임스페이스를 담당하면 라우터는 구독을 두 브로커에 분산하고, 한쪽 브로커 연결이 끊어지면 재연결 주기 한 번 안에 다른 브로커로 구독을 인계합니다(네임스페이스당 여러 브로커 참고). 라우터 자격 증명 설정과 클러스터-라우터 간 네트워크 연결을 포함한 상세 운영 절차는 배포 가이드의 클러스터 내부 이벤트 브로커를 참고하세요.

테스트

크레이트 자체 테스트는 Kubernetes 클러스터 없이 실행됩니다. 임시 인증서를 사용하는 인프로세스 TLS HTTP/2 버스와 로컬 TCP 발행자를 띄워 양방향 중계, 모든 거부 코드, 요청 플레인 주소 차단, 논블로킹 버퍼 오버플로 처리, 스트림 열기 기한, 리밸런스 재연결 대기, 연결 끊김 후 재연결, 라우터 드레인, 종료 오류, 유휴 타임아웃, SIGTERM 정상 종료를 검증합니다.

메타데이터 파서는 라우터의 합성 DynamoWorkerMetadata 픽스처 사본으로 검증하며, 실제 라우터 리스너와 연동하는 종단 간 터널 테스트는 tests/event_bus/에 포함되어 있습니다.