Kafka 기초 다지기
목차
-
카프카 내부 메커니즘
카프카 내부 메커니즘
1. 클러스터와 컨트롤러
1) 주키퍼 (Zookeeper)와 클러스터 멤버십
- 내부적으로 주키퍼는 표준 파일 시스템의 디렉토리처럼 계층적인 트리 구조를 저장하고 사용한다.
- znode : 데이터를 저장하는 노드! 경로를 통해 각 노드의 위치를 식별한다.
- 각 노드에는 상태와 구성 정보, 위치 정보 등의 데이터가 저장되어 크기가 작고,
- 모든 노드가 메모리에 저장되어 처리되므로 속도가 빠르다
- Watch 기능 : 노드의 상태를 모니터링! (특정 노드의 Watch를 설정하면 해당 노드의 변경을 알려줌)
- 임시 (ephemeral) 노드 : 노드를 생성한 클라이언트가 연결되어 있을 때만 존재!
- 영구 (persistent) 노드 : 클라이언트가 삭제하지 않는 한 계속 보존!
- 카프카에서도 주키퍼를 사용해 클러스터 멤버인 브로커들의 메타데이터를 유지, 관리한다.
- 모든 카프카 브로커는 고유 식별자 (ID)를 가진다.
- 같은 ID를 갖는 다른 브로커를 시작하려고 하면 에러가 발생한다.
- 브로커가 중단되면 해당 브로커의 주키퍼 노드는 삭제된다.
- 하지만 해당 브로커의 ID는 여전히 다른 데이터 구조에 존재함!
- 각 토픽의 레플리카에는 브로커들의 ID가 포함되어 있다.
2) 컨트롤러
- 컨트롤러는 카프카 브로커 중 하나이다.
- 일반 브로커의 기능에 추가해 파티션 리더 (leader)를 선출하는 책임을 가진다.
- 클러스터에서 시작하는 첫 번째 브로커가 컨트롤러가 된다.
- 모든 브로커는
/controller
노드에 주키퍼의 Watch를 생성!- 이 노드에 변경이 생기는 것을 알 수 있다.
- 클러스터에는 항상 하나의 컨트롤러만 존재한다!
- 관련 주키퍼 경로를 Watch해 특정 브로커가 클러스터를 떠났다는 것을 인지하면,
- 해당 브로커가 리더로 할당되어있던 모든 파티션들에게 새로운 리더가 필요하다는 것을 알게 됨!
- 그럼 컨트롤러는 이 파티션들을 점검하고, 새로 리더가 될 브로커를 결정한다.
- 파티션들의 새로운 리더들과 팔로워들의 정보를 모든 브로커에게 전송한다.
2. 복제
1) 복제란?
-
복제 (replication)는 카프카 아키텍처의 핵심이다.
-
어쩔 수 없이 각 서버 노드에 장애가 생길 때 카프카가 가용성과 내구성을 보장하는 방법이므로 중요하다.
-
카프카 아키텍처
- 카프카 데이터는 토픽으로 구성됨
- 각 토픽은 여러 파티션에 저장됨
- 각 파티션은 다수의 리플리카를 가질 수 있음!
→ 각 브로커는 서로 다른 토픽과 파티션에 속하는, 수 백에서 많게는 수 천 개까지의 복제본을 저장한다.
2) 리플리카의 두 가지 형태
- 리더 리플리카 : 모든 프로듀서와 컨슈머 클라이언트의 요청은 일관성을 위해 리더를 통해 처리된다.
- 팔로어 중 어느 것이 최신의 리더 메시지를 복제하고 있는지 알아야 한다.
- 팔로어 리플리카 : 각 파티션의 리더를 제외한 나머지 리플리카들
- 팔로어는 클라이언트의 요청을 서비스하지 않는다.
- 리더의 메시지를 복제해 리더의 것과 동일하게 유지한다. → 리더의 모든 최신 메시지를 복제하려 한다.
- 특정 파티션의 리더 리플리카가 중단되면, 팔로어 리플리카 중 하나가 해당 파티션의 새로운 리더로 선출된다.
- 팔로어는 클라이언트의 요청을 서비스하지 않는다.
3) Fetch 요청
- 리더와 동기화를 하기 위해 팔로어 리플리카가 리더에게 보내는 요청
- 컨슈머가 메시지를 읽기 위해 전송하는 것과 같은 타입의 요청!
- 응답으로 리더는 리플리카에게 메시지를 전송한다.
- 리플리카가 다음으로 받기 원하는 메시지의 오프셋이 포함되고, 항상 수신된 순서대로 처리된다.
4) 리플리카의 동기화
- in-sync replica, ISR : 동기화 리플리카! 최신 메시지를 계속 요청하는 팔로어 리플리카.
- 기존 리더가 중단되는 경우, 동기화 리플리카만 리더로 선출될 수 있다.
- out-sync : 특정 시간 이상 메시지를 요청하지 않았거나, 최근 메시지를 복제하지 못한 경우!
- 리더에 장애가 생겼을 때 해당 리플리카는 더 이상 새로운 리더가 될 수 없다
- 선호 리더 (preferred leader) : 토픽이 생성될 때 각 파티션의 리더였던 리플리카.
3. 요청 처리
1) 요청의 특징
-
카프카 브로커가 하는 일은 대부분 클라와 파티션 리플리카 or 컨트롤러로부터 파티션 리더에게 전송되는 요청 (request)을 처리하는 것!
-
표준 헤더
- 요청 타입 ID : 어떤 요청인지를 나타냄! 16비트 정수 형식의 고유 번호
- 요청 버전 : 이 요청의 프로토콜 API 버전을 나타내는 16비트 정수 값
- cID (correlation ID) : 사용자가 지정한 32비트 정수 값. 각 요청의 고유 식별 번호로 사용!
- 클라이언트 ID : 사용자가 지정한 문자열 횽식의 값. 요청을 전송한 클라이언트 앱을 식별하는 데 사용!
-
생성되는 스레드
-
acceptor
스레드 : 연결을 생성하고, -
processor
스레드 : (= 네트워크 스레드) 연결을 받아 그 다음을 처리하도록 넘겨준다.- 클라이언트 연결로부터 요청을 받고 그것을 요청 큐 (request queue)에 넣는다.
- 응답 큐 (response queue)로부터 응답을 가져와 클라이언트에게 전송해준다.
-
2) 쓰기 요청
-
프로듀서가 전송하며, 카프카 브로커에게 쓰려는 메시지를 포함한다.
-
acks
: 메시지를 수신해야 하는 브로커의 수- 설정된 값의 브로커가 모두 메시지를 수신해야 쓰기 성공으로 간주한다.
acks=1
: 리더만 메시지를 받으면 됨acks=all
: 모든 동기화 리플리카가 메시지를 받아야 함acks=0
: 아예 브로커의 수신 응답을 기다리지 않음
3) 읽기 요청
-
카프카 브로커로부터 메시지를 읽을 때, 컨슈머의 팔로어 리플리카가 전송한다.
-
브로커는 쓰기 요청을 처리하는 법과 유사하게 읽기 요청을 처리한다!
-
각 요청은 파티션 리더에게 전달되어야 한다. → 메타데이터 요청을 거친다.
-
파티션에서 이미 삭제된 메시지나, 아직 존재하지 않는 오프셋을 클라가 요청하면 에러를 응답한다.
-
오프셋이 존재하면 클라가 요청에 지정한 제한 크기까지의 메시지들을 브로커가 해당 파티션에서 읽은 후
→ 클라에게 전송해준다!
-
-
제로 카피 : 파일 (or 파일 시스템 캐시)의 메시지를 중간 버퍼 메모리에 쓰지 않고 바로 네트워크 채널로 전송!
→ 클라에게 전송하기 전에 먼저 로컬 캐시 메모리에 저장하는 대부분의 DB와 다르다!
→ 메모리로부터 데이터를 복사하고 버퍼를 관리하는 부담을 제거해 성능이 훨씬 더 좋아짐!
-
크기 제한
-
상한 크기 : 브로커가 반환할 수 있는 데이터의 츼대 크기
-
하한 크기 : 적어도 __KB가 될 때만 전송해라. → CPU와 네트워크 사용률을 줄일 수 있다
-
원하는 양만큼 데이터가 채워지기를 기다렸다가 전송한다.
-
그 다음에 클라이언트가 추가로 읽기를 요청하면 된다.
→ 클라이언트와 브로커 간에 데이터를 주고 받는 횟수가 훨씬 줄어들어 부담이 적어진다.
-
타임아웃 : __밀리초 동안에도 데이터가 하한 크기많은 쌓이지 않으면 그냥 보내라
-
-
-
불안전한 메시지 : 리플리카들에게 아직 복제되지 않은 메시지들은 불안전한 것으로 간주한다.
→ 리더가 중단되어 새로 선출될 때 해당 메시지들이 중복되거나, 더 이상 존재하지 않을 수 있게 된다.
→ 모든 동기화 리플리카가 해당 메시지를 복제할 때까지 기다렸다가 복제된 다음에 컨슈머가 읽을 수 있게 한다.
4) 기타 요청
-
메타데이터 요청 (metadata request)
- 쓰기 요청과 읽기 요청은 모두 파티션의 리더 리플리카에게 전송되어야 한다.
- 요청 내용 : 클라이언트가 관심을 갖는 토픽 내역
- 응답 내용 : 각 파티션의 리플리카와 어떤 리플리카가 리더인지에 대한 정보…
- 클라이언트는 이런 정보들을 캐시에 보존한 후, 각 파티션의 올바른 브로커에게 쓰기와 읽기 요청을 전송한다.
-
클라이언트 버전을 업그레이드 하기 전에 브로커 버전을 먼저 업그레이드 하는 것이 좋다
- 구버전의 브로커는 신버전의 프로토콜 요청을 처리할 수 없기 때문!
4. 스토리지
1) 파티션 할당
-
토픽을 생성할 때 카프카는…
-
제일 먼저 여러 브로커 간 파티션을 할당하는 방법을 결정한다.
- 각 파티션의 리더를 결정하기 위해 라운드로빈 방식을 사용한다.
-
각 파티션의 팔로어를 결정한다.
- 랙-인식 방법을 고려할 때는 서로 다른 랙의 브로커가 번갈아 선택되도록 순서를 정해야 한다.
- 각 브로커 다음 순서는 그것과 다른 랙에 있는 브로커가 된다.
- 첫 번 째 랙이 오프라인이 되더라도 두 번째 랙의 리플리카들은 살아 있다! → 파티션은 여전히 사용 가능
-
새 파티션들에 사용할 디렉토리 결정
- 각 디렉토리의 파티션 개수를 계산하고, 가장 적은 수의 파티션을 갖는 디렉토리에 새 파티션 추가!
- 새로 디스크를 추가한 경우 모든 새 파티션을 그 디스크에 생성
-
2) 파일 관리와 파일 형식
-
보존 (retention) : 카프카의 중요한 개념!
- 카프카는 데이터를 영원히 보존하지 X, 메시지 삭제 전에 모든 컨슈머가 읽기를 기다리지도 X
- 메시지를 삭제하기 전에 보존하는 시간, 오래된 메시지 제거 전에 보존할 데이터의 크기
-
세그먼트 (segment) : 카프카에서는 각 파티션을 세그먼트로 나눈다.
-
큰 파일에서 제거해야 하는 메시지를 찾아 파일 일부분을 삭제하는 것은 시간이 많이 소요되기 때문…
-
액티브 세그먼트 : 메시지를 쓰기 위해 사용 중인 세그먼트
-
각 세그먼트는 하나의 데이터 파일로 생성되고, 카프카 메시지와 오프셋들이 저장된다.
-
제로 카피 : 디스크와 네트워크 모두에서 같은 메시지 형식을 사용하므로 가능하다!
→ 컨슈머에게 메시지를 전송할 때 별도의 버퍼 메모리를 사용하지 않고 디스크에서 바로 네트워크로 전송!
→ 프로듀서가 이미 압축해 전송한 메시지의 압축 해지와 재압축이 필요 X
-
-
파일 구조 : 키, 값, 오프셋에 추가해 각 메시지는…
- 메시지 크기
- 체크섬 (checksum) 코드 : 손상 여부 검출
- 매직 바이트 : 메시지 형식의 버전을 나타냄
- 압축 코덱 : Ex. Snappy, GZip, LZ4 …
- 타임스탬프 등을 포함한다.
-
래퍼 (Wrapper) 메시지 : 프로듀서가 압축된 메시지를 전송하면,
-
하나의 배치 (batch)에 포함된 모든 메시지가 같이 압축되어 래퍼 메시지의 값으로 전송된다.
-
브로커는 하나의 메시지를 수신하고, 컨슈머에게도 이렇게 전송할 수 있다.
→ 프로듀서가 압축을 사용하면, 더 큰 배치를 전송해도 네트워크와 브로커 디스크 모두에게 유리하다!
-
3) 인덱스
- 파티션의 인덱스 (index)
- 지정된 오프셋의 메시지를 브로커가 빨리 찾을 수 있도록 유지, 관리하는 것!
- 세그먼트 파일과 이 파일의 내부 위치로 오프셋을 연관시킨다.
- 인덱스도 세그먼트로 분할된다 → 메시지가 삭제되면 연관된 인덱스 항목도 삭제할 수 있다.
4) 압축
-
삭제 보존 정책 (delete) : 보존 기간 이전의 메시지를 삭제한다
-
압축 보존 정책 (compact) : 보존 기간 이전의 메시지를 각 키의 가장 최근 값만 토픽에 저장한다.
- 키와 값을 갖는 메시지를 생성하는 애플리케이션의 토픽에만 적용된다.
흔히 얘기하는 데이터 압축 (data compression)과 다르다.
- 데이터 압축 : 알고리즘으로 전체 데이터를 줄이는 목적
- 메시지 압축 : 같은 키를 갖는 메시지들이 토픽에 여러 번 저장됐을 때, 키를 중심으로 최근 것만 남기는 것
-
로그 세그먼트의 종류
- 클린 (Clean) : 이전에 이미 압축된 메시지. 각 키에 대해 하나의 값만 포함한다.
- 더티 (Dirty) **: 직전 압축 이후에 **추가로 쓴 메시지들이 저장된 부분.
-
모든 압축 작업이 끝나고 나면 같은 키를 갖는 메시지를 최근의 값 하나만 남게 된다!
-
압축 시점
- 현재 사용 중인 세그먼트는 압축되지 않고, 사용 중이 아닌 세그먼트를 압축 대상이 된다
- 보통, 토픽의 50%가 더티 레코드를 포함할 때 압축을 시작한다.
- 압축은 토픽의 I/O 성능에 영향을 주므로 너무 자주 하지 않는 것이 좋다
- 디스크 공간을 차지하므로 너무 많은 더티 레코드를 남겨두지 않는 것 또한 중요하다!