Post

Chapter 06. 카프카 내부 메커니즘

6.1 클러스터 멤버십 📡

브로커 목록 관리

  • 카프카 클러스터의 브로커 목록은 ZooKeeper에서 관리하며 /brokers/ids 경로에 저장됨

브로커 ID

  • 브로커는 생성 시 고유한 ID를 가짐
  • ID는 사용자가 직접 설정하거나, 자동으로 생성 가능

Ephemeral 노드

  • ZooKeeper는 브로커 ID를 Ephemeral 노드로 저장
  • 브로커가 삭제되면 해당 Ephemeral 노드도 자동으로 제거됨

브로커 ID 재활용

  • 운영 중인 카프카에서는 모든 브로커 ID가 완전히 제거되지 않음
  • 동일한 ID를 새 브로커에 할당하면, 기존 브로커를 대체하여 정상적으로 동작 가능

6.2 컨트롤러 🎛️

컨트롤러의 역할

  • 파티션 리더 선출과 같은 핵심 역할 수행
  • 클러스터 동작을 조율

컨트롤러 선출 과정

  1. 첫 번째 브로커가 ZooKeeper의 /controller 경로에 Ephemeral 노드를 생성하며 컨트롤러로 지정
  2. 이후에 시작된 다른 브로커들은 노드 생성에 실패하고, /controller의 변경 사항을 감지하기 위해 watch를 설정함
  3. 이를 통해 클러스터 내에서 항상 하나의 컨트롤러만 유지됨

컨트롤러 장애 처리

기존 컨트롤러 장애 시,

  • 다른 브로커들이 /controller의 변경을 감지하고 노드 생성을 시도
  • 가장 먼저 생성에 성공한 브로커가 새로운 컨트롤러로 선출
  • epoch 값으로 컨트롤러 세대를 관리하며, 이전 세대 요청은 무시

브로커 장애 처리

  • 컨트롤러는 장애가 발생한 브로커를 감지 후, 해당 브로커가 리더로 맡고 있던 모든 파티션을 순회
  • 파티션의 레플리카 목록에서 다음 레플리카를 리더로 선출

리더 전환 요청

  • 컨트롤러는 LeaderAndISR 요청을 통해 새로운 리더와 팔로워 정보를 업데이트

브로커 추가 시 처리

  • 새 브로커가 추가될 경우 리더 선출 없이 모든 파티션 레플리카를 팔로워로 설정

ZooKeeper 기반 컨트롤러의 한계

메타데이터 불일치

  • 컨트롤러가 주키퍼에 메타데이터를 쓰는 작업은 동기적
  • 반면, 업데이트 및 메시지 전달 작업은 비동기적 → 불일치 가능성

컨트롤러 재시작 문제

  • 재시작 시 모든 브로커와 파티션 메타데이터를 다시 읽고 전송해야 하므로 시간이 지연됨

ZooKeeper 학습 부담

  • ZooKeeper는 분산 시스템으로 설계되어 추가 학습이 필요하며, 카프카 학습 곡선을 높이는 원인이 됨

6.2.1 KRaft: 카프카의 새로운 Raft 기반 컨트롤러 🚀

로그 기반 아키텍처

  • 기존 주키퍼 기반 컨트롤러는 메타데이터를 관리하며 다양한 기능 수행
  • KRaft는 이벤트 로그 기반 설계로 여러 컨트롤러 노드가 메타데이터의 이벤트 로그를 관리

다중 컨트롤러 구성

  • 리더 컨트롤러(액티브 컨트롤러)와 여러 팔로워 컨트롤러로 구성
  • 리더 컨트롤러가 장애 시 팔로워 컨트롤러 중 하나를 리더로 승격
  • 장애 발생 시에도 빠른 재시작이 가능

메타데이터 일관성

  • 리더 컨트롤러가 메타데이터 로그를 작성하고 팔로워 컨트롤러가 이를 복제
  • 주키퍼와 컨트롤러가 분리되어 동작하던 문제를 해결하여 메타데이터 불일치 문제를 방지

브로커와의 연동

  • 브로커는 컨트롤러의 MetadataFetch API를 사용해 메타데이터를 읽고 동작

ZooKeeper와 KRaft 컨트롤러 비교

ZooKeeper 기반 컨트롤러

  • 브로커 중 하나가 컨트롤러 역할을 수행
  • 주키퍼와 함께 파티션 리더 선출메타데이터 관리 담당

KRaft 컨트롤러

  • 브로커와 별개의 카프카 프로세스로 동작
  • 리더 컨트롤러에서 파티션 리더 선출과 메타데이터 관리를 전담
  • Raft 기반 설계로 다수의 컨트롤러가 존재하여 고가용성 제공

6.3 복제 🌀

복제 기능의 중요성

  • 카프카는 다수의 레플리카를 통해 데이터의 신뢰성지속성을 보장
  • 장애 발생 시에도 데이터 손실 없이 클러스터가 안정적으로 동작하도록 설계됨

리더 레플리카

  • 파티션에 대한 모든 읽기 및 쓰기 요청을 처리함

팔로워 레플리카

  • 리더 레플리카 데이터를 복제하여 최신 상태를 유지
  • 리더 레플리카가 장애를 겪으면 팔로워 레플리카 중 하나가 리더로 승격

팔로워 레플리카의 읽기 요청

  • rack 설정값에 따라 팔로워 레플리카도 읽기 요청을 처리할 수 있음
  • 장점: 부하를 분산하고 가까운 레플리카에서 데이터를 읽어와 트래픽 비용 절감
  • 단점: 리더 커밋 상태를 확인해야 하므로 일관성을 유지하는 과정에서 지연 발생 가능

레플리카 상태 확인

  • 리더 레플리카는 팔로워 레플리카의 복제 상태를 확인
  • 복제 상태에 따라 in-sync 레플리카out-of-sync 레플리카로 분류
  • 복제 지연 기준은 replica.lag.time.max.ms 설정으로 관리됨

리더 선출 기준

  • 리더 레플리카가 장애를 겪으면 in-sync 레플리카 중에서 새로운 리더를 선출함

선호 리더 레플리카

  • 선호 리더 레플리카는 파티션 생성 시 리더로 설정된 레플리카를 의미함
  • 선호 리더가 in-sync 상태일 경우, 리더로 승격되어 부하 분산 유지가 가능함

6.4 요청 처리 📩

브로커와 클라이언트 통신

  • 카프카 브로커는 TCP 이진 프로토콜을 사용해 클라이언트와 통신함

내부 요청 처리 로직

  • Acceptor 스레드
    • 브로커는 연결을 받는 포트별로 acceptor 스레드를 실행
    • 요청을 processor 스레드(네트워크 스레드)로 전달
  • Processor 스레드
    • 받은 요청을 요청 큐에 넣음
    • 완료된 응답을 응답 큐에서 받아 클라이언트로 전송
  • I/O 스레드
    • 요청 큐에서 요청을 가져와 처리
    • 완료된 요청의 응답을 응답 큐에 추가
  • Purgatory
    • 응답이 지연되는 상황(예: 데이터 준비 중)에서는 요청을 purgatory에 임시 저장
    • 요청 처리가 완료되면 purgatory에서 응답을 꺼내 클라이언트로 전송

메타데이터 요청 처리

  • 리더 파티션 정보 요청
    • 클라이언트는 리더 파티션이 있는 브로커로 읽기/쓰기 요청을 전송
    • 이를 위해, 클라이언트는 주기적으로 메타데이터 요청을 아무 브로커로 보냄
    • 반환된 메타데이터를 캐시하여 사용
  • 메타데이터 요청 응답
    • 메타데이터에는 토픽, 파티션, 레플리카, 리더 레플리카 정보 포함
    • 리더 변경 시, Not a Leader 에러 발생 → 메타데이터 최신화 후 재요청

6.4.1 쓰기 요청 ✍️

acks 설정에 따른 응답 시점

  • acks=0 → 요청 즉시 응답
  • acks=1 → 리더 브로커 쓰기 완료 시 응답
  • acks=all → 모든 레플리카 복제 완료 후 응답

Purgatory 사용

  • 리더 브로커는 쓰기 요청 처리 중 레플리카 응답 대기
  • 대기 중 응답을 Purgatory에 저장하며, 복제 완료 시 클라이언트로 응답 전송

6.4.2 읽기 요청 처리 📖

클라이언트 요청

  • 클라이언트는 읽고자 하는 토픽, 파티션, 오프셋, 데이터 한도 정보를 브로커에 전송

리더 브로커 동작

  • 요청 오프셋의 유효성 검사를 수행
  • 유효하지 않으면 에러 반환, 유효하면 데이터를 전송

Zero-Copy 최적화

  • 리더 브로커는 데이터를 읽을 때 Zero-Copy 최적화를 사용
  • 데이터 중간 버퍼 없이 네트워크 채널로 직접 전송 → 오버헤드 감소

Fetch Session Cache

  • 클라이언트가 많은 파티션 데이터를 읽는 경우 변경 사항만 업데이트하여 요청 크기를 최소화
  • 세션 생성/해제 제한 발생 시 적절한 에러 반환

6.4.3 기타 요청 🛠️

다양한 요청 유형

  • 메타데이터, 쓰기, 읽기 외에도 다양한 요청이 존재하며, 지속적으로 새로운 요청이 추가됨

하위 호환성 유지

  • 클라이언트는 상위 버전 브로커와 호환 가능
  • 상위 버전 클라이언트는 하위 버전 브로커와 호환되지 않음
    • 브로커를 우선적으로 업데이트하는 것이 권장됨

6.5 물리적 저장소 💾

카프카의 기본 저장 단위

  • 파티션 레플리카
    • 카프카는 파티션 레플리카를 기본 저장 단위로 사용
    • 데이터를 저장할 때 계층화된 저장소 구조를 활용

6.5.1 계층화된 저장소 📂

기존 저장소의 문제점

  • 저장 한도 제한
    • 최대 보존 기한, 파티션 수, 디스크 크기에 따라 저장이 제한됨
  • 확장성 문제
    • 디스크 및 클러스터 크기를 과도하게 늘려야 할 수 있음
  • 리밸런싱 비용 증가
    • 파티션 크기가 증가하면 리밸런싱 작업에 많은 시간이 소요됨

계층화된 저장소 동작 방식

  • 로컬 저장소
    • 기존 방식대로 로컬 디스크에 데이터를 저장
  • 원격 저장소
    • 완료된 로그 세그먼트를 HDFS, S3와 같은 외부 시스템에 저장
  • 보존 정책 분리
    • 로컬과 원격 저장소의 보존 정책을 각각 설정 가능
  • 데이터 처리 흐름
    • 로컬 저장소 → 빠르게 처리해야 할 데이터를 보관
    • 원격 저장소 → 완료된 데이터를 저장하고 복구 등에 활용

계층화된 저장소의 장점

  • 저장소 확장이 카프카 클러스터의 CPU, 메모리 리소스와 무관
  • 로컬 저장소의 부담이 줄어들어 복구 및 리밸런싱 속도 향상
  • 오래된 데이터를 접근할 때 네트워크 비용만 추가되어 효율적

6.5.2 파티션 할당 📈

할당 목표

  • 균등 분산
    • 파티션 레플리카를 브로커에 고르게 분배
  • 다른 브로커 배치
    • 같은 파티션의 레플리카를 각각 다른 브로커에 배치
  • 랙 분산
    • 브로커에 랙 정보가 설정된 경우, 레플리카를 서로 다른 랙에 배치

추가 파티션 할당

  • 새로 생성된 파티션은 가장 적은 파티션 수를 가진 브로커에 할당됨
  • 파티션 크기 및 서버 부하는 고려하지 않으므로 주의 필요

6.5.3 파일 관리 📑

보존 정책

  • 특정 기간을 초과하거나 데이터 양이 초과되면 파일을 삭제

세그먼트 구조

  • 파티션은 여러 세그먼트로 나뉘어 저장되며, 각 세그먼트는 별도의 파일로 관리
  • active 세그먼트(현재 사용 중인 세그먼트)는 삭제되지 않음

파일 디스크립터 관리

  • 각 파티션의 모든 세그먼트 파일 디스크립터를 열어 유지
  • 시스템의 파일 디스크립터 한도에 맞게 OS를 튜닝해야 함

6.5.4 파일 형식 📜

데이터 일관성

  • 프로듀서가 생성한 데이터는 세그먼트 파일에 저장된 후 컨슈머로 전송됨
  • zero-copy 최적화를 통해 중간 버퍼 없이 데이터 전송 가능

메시지 구조

  • 사용자 페이로드
    • 키값, 밸류값, 헤더를 포함
  • 시스템 헤더
    • 전송 관련 키/밸류 순서쌍 포함

메시지 배치

  • 여러 메시지를 묶어 배치 단위로 전송
  • 배치 헤더
    • 오프셋, 타임스탬프, 체크섬, 에포크 값 등 다양한 정보를 포함
  • 컨트롤 배치
    • 트랜잭션 커밋/롤백, 오프셋 커밋 등 시스템 동작 관리를 위한 메시지 묶음

6.5.5 인덱스 🔍

인덱스 역할

  • 특정 오프셋 메시지의 조회 속도를 개선
  • 오프셋 및 타임스탬프와 세그먼트 파일 위치를 매핑

파일 구조

  • 각 세그먼트 파일에는 오프셋 인덱스타임스탬프 인덱스가 별도로 존재
  • 세그먼트가 삭제되면 해당 인덱스도 함께 삭제됨

인덱스 복구

  • 인덱스가 오염될 경우 메시지를 재처리하여 인덱스를 재생성

6.5.6 압착 📦

보존 정책

  • 카프카는 두 가지 보존 정책을 지원
    • 삭제 보존 정책: 메시지를 기한에 따라 삭제
    • 압착 보존 정책: 키 값에 대해 가장 최근의 밸류값만 유지, 이전 메시지 삭제

혼용 옵션

  • 두 가지 정책을 혼합하여 사용 가능
  • 최신 메시지라도 보존 기한이 초과되면 삭제됨

6.5.7 압착의 작동 원리 ⚙️

압착 활성화

  • log.cleaner.enabled 설정으로 관리
  • 활성화 시
    • 압착 매니저 스레드 시작됨
    • 여러 압착 스레드 병렬로 실행

메시지 상태

  • 클린 상태: 압착이 완료된 메시지
  • 더티 상태: 압착이 미완료된 메시지
  • 클린/더티 분류 기준: 마지막 압착이 일어난 오프셋 기준을 기준으로 구분

압착 프로세스

  • 클리너 스레드 역할
    • 더티 영역 메시지를 읽고 키값-오프셋 맵 생성
    • 최신 메시지만 새로운 세그먼트로 복제
  • 작업 방식
    • 오래된 클린 세그먼트부터 읽기 시작
    • 키값 확인
      • 인메모리 맵에 없는 키값 → 최신 메시지 → 복제
      • 인메모리 맵에 있는 키값 → 오래된 메시지 → 건너뛰기

메모리 제한

  • 클리너 스레드의 인메모리 맵은 메모리 제한 설정 가능
  • 한 번에 처리 가능한 세그먼트 수가 적을 경우, 가장 오래된 세그먼트부터 압착

6.5.8 삭제된 이벤트 🗑️

삭제 방식

  • 키값에 대한 데이터를 삭제하려면 밸류를 null로 설정
  • 밸류가 null인 메시지는 tombstone 메시지라고 불림

tombstone 메시지 처리

  • tombstone 메시지는 설정된 보존 시간 동안 유지 후 삭제
  • 압착 후 결과
    • 키 값의 밸류는 null로 유지
    • 이후 tombstone 메시지가 삭제되면 해당 키값도 영구 삭제됨

컨슈머와 tombstone 메시지

  • 데이터베이스 동기화
    • tombstone 메시지를 읽은 컨슈머가 데이터베이스에서도 삭제를 수행
  • 문제 발생 가능성
    • tombstone 메시지 삭제 전 컨슈머가 중단되면 데이터베이스에 데이터가 남을 수 있음

완전 삭제를 위한 방법

  • deleteRecords 메서드 활용
    • 카프카 어드민 클라이언트를 사용하여 최하위 오프셋 증가
    • 지정 오프셋보다 작은 메시지는 접근 불가 → 완전 삭제 보장

6.5.9 토픽은 언제 압착되는가? ⏳

active 세그먼트와 압착

  • active 세그먼트에서는 압착이 실행되지 않음
    • 삭제 보존 정책에서도 active 세그먼트는 삭제되지 않는 것과 동일함

압착 대상 세그먼트 조건

  • 기본적으로 세그먼트의 50% 이상이 더티 상태일 경우 압착 대상이 됨
  • 모든 세그먼트 압착의 문제점
    • 디스크 공간 절약 가능
    • 그러나 압착 과정에서 I/O 성능 저하가 발생할 수 있으므로 무분별한 사용은 권장하지 않음

압착 관련 설정

  • min.compaction.lag.ms
    • 메시지가 쓰여진 후 최소 경과 시간을 설정
    • 설정된 시간 이전에는 압착 실행 불가
  • max.compaction.lag.ms
    • 메시지가 쓰여진 후 압착이 최대 지연될 수 있는 시간을 설정
    • 설정된 시간 내에 압착이 반드시 실행되도록 보장
This post is licensed under CC BY 4.0 by the author.