[Kafka] 9. Kafka Streams
3. Developer Guide
항목명 | 간단 설명 |
Writing a Streams Application | 스트림 처리 로직을 작성하고 실행하는 기본 구조 제공 (토폴로지 정의) |
Configuring a Streams Application | 설정 지정 (브로커 주소, 앱 ID, 직렬화 방식 등) |
Streams DSL | 고수준 연산을 위한 간편 API (map, filter 등) |
Processor API | 사용자 정의 처리 로직을 위한 저수준 API |
Naming Operators | DSL 연산자에 이름 붙이기 (디버깅/모니터링 용이) |
Data Types & Serialization | 메시지 직렬화/역직렬화 방식 정의 |
Testing | TopologyTestDriver로 로컬 테스트 가능 |
Interactive Queries | 상태 저장소 데이터를 외부에서 조회 가능 |
Memory Management | 메모리 관련 설정 (RocksDB, 캐시 등) |
Running Applications | 다양한 환경에서 실행 가능 |
Managing Topics | 입력/출력/체인지로그 토픽 자동 생성 및 관리 |
Streams Security | 보안 설정 적용 가능 (TLS, SASL 등) |
Application Reset Tool | CLI로 상태 초기화 가능 |
4. Core Concepts
- Kafka Streams는 Kafka에 저장된 데이터를 처리하고 분석하기 위한 Java 기반 클라이언트 라이브러리
주요 특징
- Java 애플리케이션에 쉽게 통합 가능
- Kafka 외의 외부 시스템 의존 없음
- 상태 저장 기능을 통한 빠른 집계 및 조인 가능
- exactly-once processing 보장
- 낮은 지연 시간 (밀리초 단위의)
- Streams DSL / Processor API 제공
Stream Processing Topology
Kafka Streams 애플리케이션은 topology라는 그래프 구조로 처리 로직을 구성한다.
- Stream: 시간순으로 정렬된 변경 불가능한(key-value) 데이터 기록들의 연속
- Processor Topology: 프로세서(노드)와 스트림(엣지)으로 구성된 처리 그래프
- Stream Processor: 한 레코드씩 받아 처리 후, 새로운 결과를 하위 프로세서로 전달
특수 프로세서
- Source Processor: Kafka 토픽에서 데이터를 읽어오는 입구
- Sink Processor: 결과를 Kafka 토픽에 기록하는 출구
Streams DSL 또는 Processor API를 통해 토폴로지를 정의할 수 있다.
시간 개념 (Time Semantics)
스트림 처리에서 시간은 매우 중요하다. Kafka Streams는 다음 3가지 시간 개념을 구분한다.
- 이벤트 시간(Event Time): 데이터가 실제 발생한 시각
- 처리 시간(Processing Time): 애플리케이션이 데이터를 처리한 시각
- 수집 시간(Ingestion Time): Kafka 브로커가 데이터를 토픽에 저장한 시각
Kafka의 설정에 따라 메시지에 자동으로 부여되는 타임스탬프가 어떤 의미를 갖는지 결정된다.
Kafka Streams의 TimestampExtractor를 통해
- 레코드에 내장된 시간, 현재 시각 등을 기반으로 타임스탬프를 정의할 수 있다.
스트림과 테이블의 이중성 (Stream-Table Duality)
Kafka Streams는 스트림과 테이블 간의 이중성(duality) 개념을 지원한다.
- 스트림 → 테이블: 스트림은 테이블의 변경 로그. 전체를 재생하면 현재 상태를 복원 가능
- 테이블 → 스트림: 테이블은 특정 시점의 스트림 스냅샷. 키-값 쌍을 반복(iterate)하면 스트림으로 표현 가능
이 개념을 통해 CDC(Change Data Capture), 상태 복제, 인터랙티브 쿼리 등이 가능하다.
Kafka Streams는 이를 위해 KStream, KTable, GlobalKTable을 제공한다.
집계 (Aggregations)
여러 입력 레코드를 하나로 통합하는 연산이다 (예: count, sum).
- 입력: KStream 또는 KTable
- 출력: 항상 KTable
- out-of-order 데이터가 도착하면, 기존 값을 새 값으로 덮어쓴다 (same key)
윈도우(Windowing)
윈도잉은 특정 키에 대해 레코드를 시간 창으로 그룹화하여 상태 기반 연산을 수행할 수 있게 해준다.
- 윈도우는 키 단위로 관리됨
- grace period를 설정하면 지연 도착한 데이터도 허용 가능
- Kafka Streams는 out-of-order 데이터를 적절히 처리 가능 (이벤트 시간 기반일 때만 적용)
상태 저장 (State)
Kafka Streams는 stateful 연산을 위해 로컬 상태 저장소를 제공한다.
- Join, Aggregation 등은 이전 상태를 기억해야 하므로 상태가 필요
- 상태 저장소는 디스크 기반, 인메모리 등으로 설정 가능
- Interactive Query를 통해 외부에서 읽기 전용 접근 가능
처리 보장 (Processing Guarantees)
Kafka 0.11부터는 exactly-once 처리를 위해 Kafka와 Streams가 긴밀히 통합됨.
- Kafka의 트랜잭션, idempotent producer 기능 활용
- 입력 offset commit, 상태 저장, 출력 기록을 원자적으로 처리
- processing.guarantee를 EXACTLY_ONCE_V2로 설정하면 적용 (broker 2.5 이상 필요)
Out-of-Order 데이터 처리
Out-of-order 데이터는 다음 경우 발생:
- 한 파티션에서 타임스탬프가 오름차순이 아님
- 여러 파티션에서 레코드를 처리할 때, 타임스탬프 기준으로 처리 순서를 강제하지 않음
처리 방법
- Stateless 연산: 영향 없음
- Stateful 연산 (join, aggregation): 영향 있음 → 윈도우 설정 및 버퍼링으로 해결
- Versioned State Store를 사용하면 Stream-Table join 시에도 out-of-order 처리를 강화할 수 있음
5. Architecture
Kafka Streams는 Kafka의 프로듀서/컨슈머 라이브러리를 기반으로
Kafka의 내재된 기능을 적극 활용하여 데이터 병렬 처리, 분산 조정, 장애 허용, 운영 단순성을 제공
스트림 파티션과 태스크 (Stream Partitions and Tasks)
- Kafka는 데이터를 파티션 단위로 저장/전송하며, Kafka Streams는 처리 단위로 파티션을 사용한다.
- Kafka Streams는 Kafka 토픽의 파티션 수를 기준으로 태스크(task)를 생성하며, 각 태스크는 고정된 파티션 목록을 담당한다.
- 각 태스크는 자신만의 토폴로지를 실행하며, 배정된 파티션의 레코드를 하나씩 처리한다.
- 따라서 애플리케이션을 여러 인스턴스로 실행하면 각 인스턴스가 태스크를 분담하여 병렬 처리하게 된다.
🟨 예: 입력 토픽에 5개 파티션이 있으면, 최대 5개의 인스턴스에서 병렬로 처리 가능
- Kafka Streams는 리소스 관리자가 아니라 라이브러리이기 때문에, 인스턴스가 죽으면 다른 인스턴스로 자동 재할당된다.
- Kafka Streams는 StreamsPartitionAssignor 클래스를 사용하여 파티션을 태스크에 고정적으로 배정한다.
스레드 모델 (Threading Model)
- Kafka Streams는 애플리케이션 인스턴스 내에서 사용할 스레드 수를 설정할 수 있다 (num.stream.threads).
- 각 스레드는 하나 이상의 태스크를 실행하며, 서로 상태를 공유하지 않는다.
- 인스턴스를 추가하거나 스레드를 늘리는 것은 토폴로지를 복제하여 병렬 처리를 확장하는 것과 같다.
🟩 Kafka 2.8부터는 실행 중에도 스레드를 추가/제거 가능하며, Kafka Streams가 자동으로 파티션을 재분배함
로컬 상태 저장소 (Local State Stores)
- 상태 기반 연산 (join, aggregation 등)을 수행하려면 **상태 저장소(state store)**가 필요하다.
- Kafka Streams는 태스크별 로컬 상태 저장소를 생성하며, 해당 저장소는 애플리케이션 내 API로 접근 가능하다.
- DSL 연산자(join(), aggregate(), window())를 사용할 경우 Kafka Streams가 자동으로 저장소를 관리한다.
장애 허용 (Fault Tolerance)
Kafka Streams는 Kafka의 내재적 장애 허용 기능을 활용하여 애플리케이션 실패 시에도 안정적으로 복구한다.
- Kafka 파티션은 복제되므로, 데이터는 안전하게 저장되어 있고 필요 시 재처리 가능하다.
- 태스크가 죽으면 다른 인스턴스에서 자동으로 재시작된다.
🟦 상태 저장소 복구
- Kafka Streams는 각 상태 저장소에 대한 changelog 토픽을 Kafka에 만들어, 변경사항을 지속적으로 기록한다.
- 이 토픽은 log compaction이 활성화되어 있어 불필요한 데이터는 정리된다.
- 장애 발생 시, changelog 토픽을 재생하여 상태를 복구한 후 처리 재개한다.
📌 복구 속도 최적화:
- standby replica(예비 복제본)을 설정하면 복구 시간이 단축된다.
- Kafka 2.6부터는 동기화된 복제본이 있는 인스턴스에 우선 할당되도록 보장된다 (num.standby.replicas 설정).
랙 인식 태스크 할당 (Rack-Aware Assignment)
- Kafka Streams는 랙(rack) 정보를 활용해 예비 복제본을 다른 랙에 분산 배치할 수 있다.
- 설정 키 예:
- client.rack: 클라이언트(컨슈머)의 랙 정보
- broker.rack: 브로커의 랙 정보
- rack.aware.assignment.tags: 태스크 할당 시 사용하는 랙 태그 키
- rack.aware.assignment.strategy: 랙 기반 태스크 할당 전략