스트림 처리 아키텍처는 Apache Storm의 최소 한 번 처리에서 현대의 정확히 한 번 보장으로 발전했습니다. 이는 Apache Flink와 Spark Structured Streaming에서 도입되었습니다. 기업들이 배치 Lambda 아키텍처에서 지속적인 Kappa 스트림으로 이전함에 따라 복잡성은 간단한 변환에서 윈도우 집계 및 세션화를 위한 분산 상태 관리를 관리하는 것으로 이동했습니다. 데이터 주권 요구와 지역 지연 제약의 출현은 공유 NFS 또는 SAN 스토리지에 의존하지 않고 활성-활성 배포를 필요로 하여 지리적 장애 발생 시 상태 일관성을 유지하는 새로운 문제를 생성했습니다.
상태 저장 스트림 처리는 처리 노드에서 기가바이트의 연산자 상태(키 기반 윈도우, 세션 저장소)를 유지하는 동안 매 초 수백만 개의 이벤트를 수집해야 합니다. 정확히 한 번의 의미는 세 가지 구성 요소(소스 오프셋 추적, 상태 백엔드 업데이트, 싱크 쓰기) 간의 원자적 커밋을 요구합니다. 공유 스토리지 없는 교차 지역 활성-활성 복제는 네트워크 파티션이 발생할 때 스플릿 브레인 위험을 도입하며, 자동 스케일링은 비행 중 레코드를 떨어뜨리거나 처리 시간 보증을 위반하지 않도록 실시간 상태 마이그레이션을 필요로 합니다. 여러 언어(Java, Python, Go) 지원은 전통적으로 직렬화 오버헤드 또는 언어 특정 런타임에 종속되게 만듭니다.
아키텍처는 Apache Kafka 또는 Apache Pulsar를 통합 로그로 사용하고, 언어 비종속적 gRPC 사이드카가 있는 Kubernetes에서 실행되는 처리 노드를 사용하는 분리된 설계를 채택합니다. 상태 관리는 비동기 증분 체크포인트를 S3 호환 객체 저장소로 사용하는 내장 RocksDB를 사용하며, 가벼운 분산 조정 서비스(etcd 또는 ZooKeeper)에 의해 조정됩니다. 정확히 한 번의 의미는 상태를 위한 Chandy-Lamport 스냅샷 알고리즘과 트랜잭션 싱크를 위한 2단계 커밋(2PC) 프로토콜을 통해 달성됩니다(Kafka 트랜잭션 또는 멱등 JDBC 쓰기). 교차 지역 복제는 Kafka MirrorMaker 2 또는 Pulsar Geo-Replication을 통한 로그 기반 상태 전송을 이용하며, 집계 및 키 상태에 대한 버전 관리된 주요 소유권을 위한 CRDT 기반의 교환 가능 카운터를 통해 충돌 해결을 수행합니다.
플랫폼은 수집, 처리, 상태 관리, 조정의 네 가지 논리적 계층으로 구성됩니다.
수집 계층
Apache Kafka 클러스터는 여러 지역에서 작동하며, MirrorMaker 2가 양방향 주제 복제를 가능하게 합니다. 생산자 멱등성과 트랜잭션 ID는 지역 간 생산자 장애가 발생할 때조차도 정확히 한 번의 수집을 보장합니다.
처리 계층
Apache Flink 또는 유사한 스트림 프로세서는 Kubernetes 상태 저장 세트로 실행됩니다. 각 TaskManager는 gRPC 사이드카를 노출하여 Protobuf 직렬화된 작업을 수신하며, Python 및 Go 사용자 정의 함수(UDF)가 gRPC 컨테이너 내에서 실행되도록 하며, Java 런타임은 상태 및 체크포인트 관리를 담당합니다. JobManager는 레코드 키에 대한 일관된 해싱을 사용하여 TaskManagers 간의 토폴로지를 샤딩합니다.
상태 관리
연산자 상태 백엔드는 RocksDB를 사용하며 증분 체크포인트가 활성화됩니다. 체크포인트는 지역 S3 버킷에 비동기적으로 15초마다 델타 상태 변경을 기록합니다. 교차 지역 일관성을 위해 활성-활성 배포는 단조 집계(수, 합계)를 위한 LWW-Element-Set CRDT와 비교 합산 연산을 위한 주요 키 친화성을 사용합니다. 지역 장애 발생 시, 대기 중인 TaskManagers는 Savepoints에서 S3의 상태를 채웁니다.
정확히 한 번의 보장
시스템은 다음을 통해 끝에서 끝까지 정확히 한 번을 구현합니다:
글로벌 차량 공유 플랫폼은 AWS us-east-1 및 AWS eu-west-1에서 운전자의 가용성과 승객 수요를 집계하여 실시간 급증 요금 계산을 요구했습니다. 이전 아키텍처는 복제 지연이 있는 단일 주요 Redis 클러스터를 사용했으며, 이로 인해 가격 계산 중 지역 중단 시 2초 장애 발생 기간이 발생하여 오래되거나 중복된 급증 배수를 생성하여 잘못된 요금 계산과 고객 불만으로 이어졌습니다.
해결책 1: 활성-수동 및 공유 스토리지
팀은 상태 저장을 위해 지역 간 EFS(공유 NFS)를 마운트하는 것을 고려했습니다. 장점: 단일 작성기 의미를 가진 간단한 장애 전환, 강한 일관성. 단점: EFS 지연은 지역 간 접근을 위해 100ms를 초과하여 50ms 처리 SLA를 위반했습니다; 추가로, NFS 쓰기 일관성 문제는 네트워크 파티션 중 체크포인트 손상을 초래했습니다.
해결책 2: Lambda 아키텍처
수정사항을 위해 Kafka Streams와 배치 층으로 Spark를 구현합니다. 장점: 불변 로그를 통한 내결함성, 간단한 복구. 단점: 두 개의 코드 경로 유지 관리로 인한 운영 복잡성; 배치 수정은 급증 요금에 필요했던 서브 초 정확도를 위해 너무 느리게 도착했습니다.
해결책 3: CRDT를 사용하는 활성-활성 스트림 처리
두 지역 모두에서 Apache Flink를 배포하고 RocksDB 상태, 증분 S3 체크포인트 및 차량 수 카운터를 위한 CRDT 기반 카운터를 설치했습니다. 장점: 지역 처리 지연이 20ms 이하로 유지되며, 동시 지역 업데이트에 대한 자동 충돌 해결과 무중단 장애 전환을 가능하게 합니다. 단점: 집계를 교환 가능하게 리팩토링해야 하며(G-Counters 및 PN-Counters 사용), 두 지역 체크포인트에 대한 저장소 비용이 증가했습니다.
팀은 비즈니스 요구 사항인 99.99% 가용성 및 서브 초 장애 전환이 해결책 1의 2초 괴리 시간이나 공유 스토리지 지연을 감당할 수 없기 때문에 해결책 3을 선택했습니다. 그들은 운전사 수를 위한 G-Counters와 최신 가격 배수를 위한 LWW-Registers를 구현했습니다.
결과
시스템은 두 지역 모두에서 15ms p99 지연으로 정확히 한 번의 급증 가격 계산을 달성했습니다. 시뮬레이션된 us-east-1 장애 중에 eu-west-1은 중복 요금 계산 없이 로컬 복제 상태를 사용하여 처리를 계속했습니다. 체크포인트 복구 시간은 평균 800ms로, 서브 초 요구 사항 범위 내에 있었습니다.
체크포인트 간격 조정이 상태 저장 스트림 프로세서의 압력 역전 메커니즘과 어떻게 상호작용합니까?
많은 지원자는 복구 시간을 고려하지 않고 체크포인트 간격을 최적화합니다. 체크포인트 장벽이 압력 역전으로 인해 느리게 정렬될 때 Chandy-Lamport 알고리즘은 파이프라인 실행을 일시 중지하여 연쇄 시간 초과를 발생시킬 수 있습니다. 올바른 접근 방식은 체크포인트 시간 초과를 압력 역전 임계값과 정렬하는 것이며, 높은 부하 시에는 정렬되지 않은 체크포인트(장벽이 버퍼를 초과하여 통과하는) 사용하고, 동기화된 체크포인트 단계와 비동기화된 체크포인트 단계를 분리하는 것입니다. RocksDB의 증분 체크포인트는 SST 압축이 디스크 I/O를 압도하고 압력 역전을 악화시키지 않도록 RateLimiter 구성을 사용하여 조절해야 합니다.
멱등 싱크와 진정한 정확히 한 번 처리 의미 사이의 근본적인 차이는 무엇입니까?
멱등 싱크는 중복 처리가 동일한 출력 상태를 보장합니다(예: PostgreSQL 또는 HBase의 UPSERT 작업), 그러나 재시도 중에 중간 상태를 노출합니다. 만약 싱크가 레코드 A, B를 쓰고, 크래시가 발생하며 A, B, C를 쓰려고 재시도하면, 하류 관찰자는 A, B, A, B, C를 일시적으로 볼 수 있으며, 이는 중복 제거됩니다. 진정한 정확히 한 번(효과적으로 한 번)은 사전 커밋 데이터를 체크포인트 완료 전까지 보이지 않도록 하는 트랜잭션 격리를 필요로 합니다. 이는 싱크가 트랜잭션을 지원해야 함을 의미합니다(예: Kafka 트랜잭션 시 isolation.level=read_committed) 또는 2단계 커밋 프로토콜을 요구합니다. 후보자들은 종종 멱등성이 정확성 문제를 해결하지만 복구 중에 일관성/가시성 문제를 해결하지 못한다는 것을 간과합니다.
이벤트 시간 윈도우링은 교차 지역 장애 발생 시 지연 도착 데이터를 어떻게 처리해야 합니까?
장애가 지역 A에서 지역 B로 발생할 때, 지역 A의 네트워크 버퍼에서 비행 중 레코드는 손실되거나 워터마크 지평선을 초과하여 지연될 수 있습니다. 지원자들은 종종 워터마크를 무기한 연장할 것을 제안하며, 이는 윈도우 완전성 보장을 깨뜨립니다. 올바른 아키텍처는 지연 데이터 캡처를 위한 Side Outputs (Flink 용어)와 Allowed Lateness 사양을 결합합니다. 장애 발생 시, 시스템은 타임스탬프가 있는 S3 Savepoints에서 윈도우를 하이드레이트한 다음, 실패한 지역의 데드 레터 큐에 있는 지연 도착 레코드를 후속 윈도우에 병합하거나 특정 지연 데이터 핸들러를 트리거해야 합니다. 또한, 워터마크 생성을 두 지역 간에서 멱등해야 하며; 벽시계 시간을 워터마크 생성에 사용하면 장애 발생 시 분산을 초래하므로, 워터마크는 두 활성 지역 간 모노톤 이벤트 시간 추출에서 파생되어야 합니다.