Search

카프카 브로커 이야기 - 브로커와 토픽

아파치 카프카 애플리케이션 프로그래밍 with 자바 2-4장을 읽고 브로커에 대하여 정리한 내용
 아래 강의를 듣고 보강

브로커

브로커는 카프카 데이터(=레코드, 이벤트)가 모이는 곳이다. 책에선 이런 데이터의 저장소라고 하지만 단순히 들여다 보면 그 이상의 역할을 한다.
그리고 브로커는 분산 시스템으로써 카프카의 인스턴스이다:
즉, 카프카 클러스터의 노드라고 할 수 있다. 책에선 standalone, 브로커 하나로 구성된 클러스터를 실행할 뿐이다. 그리고 주키퍼는 오래된 버전(2.5)를 사용했기 때문에 썼다.
카프카 클러스터는 크게 컨트롤 플레인과 데이터 플레인으로 나눈다:
컨트롤 플레인: 컨트롤러들
데이터 플레인: 브로커들
컨트롤러는 일부 인스턴스에 위치하여 브로커 상태 체크, 리더 선출, 파티션 재분배 등의 일을 한다. 컨트롤러에 장애가 발생하면 다른 인스턴스에서 역할을 하게 된다.
다시, 정확하게, 브로커는 카프카 클러스터 모든 인스턴스에서 실행되며, 데이터 플레인을 담당한다. 브로커는 실질적으로 데이터를 프로듀서로부터 받고 저장해두었가 컨슈머에 보낸다.
카프카의 데이터는 파일 시스템에 저장한다. 토픽-파티션 단위로 디렉토리를 생성한다. 파일은 세그먼트라는 특정 오프셋 범위의 데이터가 모여 있다. 파일 이름은 그 중 가장 작은(oldest) 오프셋이 된다. 그림과 같다면 첫번째 세그먼트의 시작은 47826734 오프셋이다. 파일 구조는 크게 다음과 같다:
.log : 메시지, 메타데이터
.index : 메시지 오프셋의 인덱싱 정보
.timeindex : timestamp 기준 인덱싱. 메시지 삭제나 압축에 사용
디스크에 데이터를 저장하기 전에, 브로커는 페이지 캐시를 사용하여 입출력 속도를 높인다. 프로듀서에서 보낸 데이터가 요청 큐(request queue)까지 오면 입출력 스레드를 통해 페이지 캐시에 쓰고 사용한다. 따라서 카프카의 자바 힙 메모리 영역은 크게 설정할 필요가 없다고 한다.
브로커 사이의(리더 파티션에서 팔로워 파티션으로) 복제(replication)을 하기 위해 purgatory(연옥?)에 저장된다. 이는 입출력 스레드가 복제하는동안 다른 일을 할 수 있게 하기 위함이다(어나더 페이지 캐시?)

데이터(이벤트) 삭제와 로그 압축

브로커는 세그먼트 즉, 파일 단위로 오래된 데이터를 삭제할 수 있다. 우선 현재 데이터의 오프셋이 위치할 세그먼트를 “액티브 세그먼트”라고 한다. 이 액티브 세그먼트는 크기 또는 retention 기반으로 결정된다:
log.segment.bytes (default 1G)
log.segment.ms
값들이 너무 작다면 파일을 열고 닫는데에 입출력 부하가 발생할 것이다.
cleanup 정책을 삭제(cleanup.policy=delete)로 구성한다면, 여기서도 크기 또는 retention 기반으로 오래된 세그먼트를 삭제한다.
retention.ms (default 7d)
retention.bytes
cleanup은 삭제 뿐만 아니라 압축이라는 정책으로도 가능하다(cleanup.policy=compact):
결론부터 말하자면, 키 기준으로 가장 최신의 이벤트만 남기는 것이다(clean log segment를 봐야한다):
Ale: (29), (61), 71
Jay: 37
Tim: (50), 57
Ian: 61

로그 압축 과정

Segment to Clean
cleaned log segments는 이미 압축이 된 영역이다. 압축할 대상 세그먼트는 dirty log segments이다. 액티브 세그먼트는 압축하지 않는다. cleaned log segments의 바로 다음인 첫 dirty log segment 부터하게 되는데 한번에 몇 세그먼트를 할지는 클리너 스레드의 메모리에 따라 결정된다.
Build Dirty Segment Map
압축할 세그먼트 내에서 키 - 오프셋의 맵을 만든다. 오프셋은 가장 나중 값(latest)을 쓴다.
Deleting Events
Retaining Events
Replace Old Segments
이벤트를 삭제할 땐 이전 cleaned log segment과 맵을 확인한다. 만약 이전 cleaned log segment에 같은 키 값이 있다면(e.g. Ale 29) 더 과거의 오프셋일테니 삭제한다. 없다면 맵의 엔트리를 그대로 사용한다. 만약 맵에 없지만 cleaned log segment에 있다면(e.g. Jay 37, Iva 42) 이것이 키에 대한 가장 최근의 오프셋일테니 남겨 둔다. 기존 cleaned log segment와 압축 중이던 세그먼트를 남겨둔 세그먼트, 새 cleaned segment 로 대체한다.
Cleaning Tombstone and Transaction Markers
압축 대상인 세그먼트에 컨슈머가 아직 처리 중인 이벤트가 있을 수 있다. 이럴 경우 압축(이벤트 삭제)을 바로 진행하지 않고, 툼스톤 또는 트랜잭션 마커를 표시한다. 마커가 표시된 이벤트는 특정 시간 이후에(to-delete-time=first-clean-time + delete.retention.ms) 삭제 되게 하여 컨슈머가 소비할 때까지삭제되지 않도록 한다.
토픽 압축이 언제 실행될지는 min.cleanable.dirty.ratio 을 통해 구성할 수 있다. clean logs와 dirty logs의 비율인데, 전체 대비 dirty 로그가 얼마나 많은가(min threshold)로 압축 실행 시점이 결정된다.
이 값이 너무 크다면 압축 효율은 좋겠지만, 스토리지 효율은 떨어진다
e.g. 90%라고 한다면 최대 10%만 압축된 상태일테니 한번에 많은게 압축될 수 있지만, 압축되기 전까지 키에 대한 중복 이벤트가 저장소에 남아 있는다.
반대로 너무 작다면 압축 효율은 낮고(자주 일어날 수 있고), 스토리지 효율은 좋아진다.
브로커의 저장소, 파일시스템의 효율적인 사용을 위해, cleanup 정책으로써 로그를 압축하지만, 그 특징 때문에 활용하기 좋다. CDC로 들어오는 데이터셋의 경우가 그렇다. ksqlDB 테이블이나 Kafka Streams KTables나 cleaned log segment, 이 셋 다 모두 구체화 뷰(materialized view)이지 않은가. 따라서 로그 압축은 각 키에 대한 최신 오프셋의 구체화 뷰를 만든다.

습득 교훈

책으로 큰 가닥을 잡아 공부했는데, 막상 정리하려니 Confluent Kafka의 교육 자료에서 더 도움을 받은거 같다(앞으로 표현도 데이터라고 안하고 이벤트라고 할까 싶다ㅎ).
원랜 ISR까지 정리하려 했으나 두 부분으로 나눠 다음 포스트에서 정리할 것
카프카 브로커는 클러스터의 모든 인스턴스에서 실행되는 데이터 플레인 노드이다.
토픽 이벤트는 파일 시스템에 저장된다.
페이지 캐시를 사용하여 파일 입출력 부하를 최소화한다.
파일 시스템을 효율적으로 사용하기 위해 토픽(로그) 삭제와 압축 정책이 있다.
로그 압축은 유일한 키에 대해 최신 데이터만 남겨두는 구체화 뷰를 만드는 작업이다.