본문 바로가기
데이터 엔지니어링

Kafka란? (3) 내부 동작 원리 replication/controller/log segment

by 내기록 2022. 8. 7.
반응형

 

카프카의 내부 동작 원리와 구현에서 가장 중요한 부분 중 하나는 리플리케이션 동작이다.

 

1. 카프카 리플리케이션

카프카는 안정성을 확보하기 위해 리플리케이션이라는 동작을 한다.

 

1.1 리플리케이션 동작 개요

describe 옵션을 이용해 토픽의 상세보기 출력

$ kafka-topics.sh --bootstrap-server ... --topic test_topic --describe

[output]
Topic: test_topic	partitionCount:1	ReplicationFactor:3	Configs:segment.bytes=100324543
Topic: test_topic	partition:0	Leader:1	Replicas:1,2,3	Isr:1,2,3

위에서 알 수 있는 내용은 파티션 0의 리더는 브로커1이고, replication들은 브로커 1,2,3에 있음을 나타낸다.

여기서 실제로 replication되는 것은 토픽이 아니라 토픽을 구성하는 각각의 파티션들이다.

 

메시지를 topic_test로 전송한 후 각 브로커의 세그먼트 파일(../topic_test-0/000000000000000.log)의 내용을 확인해보면,

브로커 1,2,3 모두 동일한 메시지를 갖고 있음을 알 수 있다.

 

N개의 리플리케이션이 있는 경우 N-1까지의 브로커 장애가 발생해도 메시지 손실 없이 안정적으로 메시지를 주고받을 수 있다.

 

1.2 리더와 팔로워

리더는 replication 중 하나가 선정되며, 모든 읽기와 쓰기는 그 리더를 통해서만 가능하다.

즉, 프로듀서는 모든 리플리케이션에 메시지를 보내는 것이 아니라 리더에게만 메시지를 전송하며 컨슈머도 오직 리더로부터 메시지를 가져온다.

 

https://velog.io/@hyun6ik/Apache-Kafka-Replication

리더가 읽고 쓰기 동작을 할 때 팔로워들 역시 그저 대기만 하는 것이 아니라, 리더에 문제가 발생하거나 이슈가 있을 경우를 대비해 언제든지 새로운 리더가 될 준비를 해야한다. 컨슈머가 토픽의 메시지를 꺼내 가는 것과 비슷한 동작으로 지속적으로 파티션의 리더가 새로운 메시지를 받았는지 확인하고, 새로운 메시지가 있다면 해당 메시지를 리더로부터 복제한다.

 

1.3 복제 유지와 커밋

리더와 팔로워는 ISR(InSyncReplica)라는 논리적인 그룹으로 묶여 있다. 이렇게 별도의 그룹으로 나누는 이유는 해당 그룹 안에 속한 팔로워들만이 새로운 리더의 자격을 가질 수 있기 때문이다.

 

ISR 내의 팔로워들은 리더와의 데이터 일치를 유지하기 위해 지속적으로 리더의 데이터를 복제하고, 리더는 ISR 내 모든 팔로워가 메시지를 받을 때까지 기다린다.

하지만 팔로워가 네트워크 오류, 브로커 장애 등의 이유로 리플리케이션을 하지 못하는 경우가 발생한다면 이 팔로워는 리더와의 데이터 불일치가 발생한 상태이다. 만약 이 팔로워에게 리더를 넘겨준다면 데이터의 정합성이나 메시지 손실 등의 문제가 발생할 수 있으므로 파티션의 리더는 팔로워들이 위쳐지지 않고 리플리케이션 동작을 잘 하고 있는지 감시한다.

 

만약 팔로워가 특정 주기 내 복제 요청을 하지 않는다면 리더는 해당 팔로워의 리플리케이션 동작에 문제가 생겼다고 판단해 ISR 그룹에서 추방한다. (리더 후보 자격 박탈)

 

* ISR 내에서 모든 팔로워의 복제가 완료되면, 리더는 내부적으로 커밋되었다는 표시를 한다. 마지막 커밋 오프셋의 위치는 하이워터마크(high water mark)라고 부른다. 즉 커밋되었다는 것은 ISR 내의 모든 팔로워들이 메시지를 저장했음을 의미한다.

그리고 이렇게 커밋된 메시지만 컨슈머가 읽어갈 수 있다. 커밋되지 않은 메시지를 읽을 수 없게 하는 이유는 메시지의 일관성을 유지하기 위해서이다.

 

아래 그림에서 컨슈머는 6 offset 까지의 메시지만 읽을 수 있다.

https://developpaper.com/kafka-replica-synchronization-mechanism/

 

 

커밋된 위치는 어떻게 알 수 있는가?

모든 브로커는 재시작될 때, 커밋된 메시지를 유지하기 위해 로컬 디스크의 repliation-offset-checkpoint 파일에 마지막 커밋 오프셋 위치를 저장한다.

 

cat ../kafka-logs/replication-offset-checkpoint

test_topic 0 1

test_topic : 토픽 명

0: 파티션 번호

1: offset

 

test_topic에 새로운 메시지를 전송하고 다시 확인해보면 offset이 2로 변경되어 있음을 확인할 수 있다.

 

1.4 리더와 팔로워의 단계별 리플리케이션 동작

카프카로 향하는 수많은 메시지의 읽고 쓰기 처리를 하는 리더는 매우 바쁘다. 리더가 replication 동작을 위해 팔로워들과 많은 통신을 주고받거나 replication 동작에 많은 관여를 한다면 결과적으로 리더의 성능은 떨어지고 카프카의 장점인 빠른 성능을 내기도 어려워진다.

따라서 카프카는 리더와 팔로워 간의 replication 동작을 처리할 때 서로의 통신을 최소화할 수 있도록 설계함으로써 리더의 부하를 줄였다.

 

리더에게 message1(offset 0)이 들어오면 팔로워들은 리더에게 0번 오프셋 가져오기(fetch) 요청을 보낸 후 새로운 메시지인 message1(offset0)이 있다는 사실을 인지하고 message1을 리플리케이션 한다.

리더는 모든 팔로워가 0인 오프셋 메시지를 리플리케이션하기 위한 요청을 보냈다는 사실을 알고 있다. 하지만 리더는 팔로워들이 0번 오프셋에 대한 리플리케이션 동작의 성공/실패 여부를 알지 못한다.

카프카는 리더와 팔로워 사이에 ACK 통신을 제거함으로써 리플리케이션 동작의 성능을 더욱 높였다.

 

예)

리더 브로커에 message2(offset 1)이 들어오면 팔로워들은 offset 1에 대한 리플리케이션을 요청한다. 팔로워들로부터 offset1에 대한 요청을 받은 리더는 팔로워들이 offset 0에 리플리케이션 동작이 성공했음을 인지하고, offset0에 대해 커밋 표시를 한 후 하이워터마크를 증가시킨다.

즉, 리더는 팔로워들이 보내는 리플리케이션 요청의 오프셋을 보고, 팔로워들이 어느 위치의 오프셋까지 리플리케이션을 성공했는지 알 수 있다.

 

1.5 LeaderEpoch(리더에포크)와 복구

카프카의 파티션들이 복구 동작을 할 때 메시지의 일관성을 유지하기 위한 용도로 이용된다.

리더에포크는 복구 동작 시 하이워터마크를 대체하는 수단으로도 활용된다.

 

LeaderEpoch 관련 내용은 도서(실전 카프카 개발부터 운영까지, 고승범) 를 보거나 다른 예제를 찾아서 보는걸 추천한다.

 

2. Controller(컨트롤러)

컨트롤러는 리더 선출을 맡고 있다. 카프카 클러스터 중 하나의 브로커가 컨트롤러 역할을 하게 되며, 파티션의 ISR 리스트 중에서 리더를 선출한다. 

 

리더를 선출하기 위한 ISR 정보는 안전한 장소에 보관되어야 하기 때문에 가용성 보장을 위해 주키퍼에 저장되어 있다.

컨트롤러는 브로커를 예의주시하다가 실패가 감지되면 ISR 리스트 중 하나를 새로운 파티션 리더로 선출한다.

 

파티션 리더가 다운됐다는 것은 해당 파티션의 리더가 없는 상태를 의미하며, 카프카 클라이언트인 프러듀서나 컨슈머가 해당 파티션으로 읽거나 쓰기가 불가능해진다..!

 

2.1 예기치 않은 장애로 인한 리더 선출 과정

 

 

리더를 선출하는 과정은 아래와 같다.

1) 파티션 0번의 리더가 있는 브로커 1번이 예기치 않게 다운된다.

2) 주키퍼는 1번 브로커와 연결이 끊어진 후, 0번 파티션의 ISR에 변화가 생겼음을 감지한다.

3) 컨트롤러는 주키퍼 워치를 통해 0번 파티션에 변화가 생김을 감지하고, 해당 파티션 ISR 중 다른 후보를 리더로 선출한다.

4) 컨트롤러는 0번 파티션의 새로운 리더에 대한 정보를 주키퍼에 기록한다.

5) 갱신된 정보는 활성화 상태인 모든 브로커에게 전파된다.

 

파티션이 하나인 경우 위 작업은 오랜 시간이 걸리지 않는다. 하나의 파티션 리더에 대한 선출 작업이 0.2초가 걸린다고 가정하면, 파티션이 1개일 경우에는 0.2초가 걸리지만 1만개의 파티션에 대해 선출이 이뤄져야 한다면 약 30분이 조금 넘게 걸린다.

1대의 브로커 장애가 발생하고, 새로운 리더 선출 작업으로 카프카와 클라이언트 간에 약 30여분간 통신이 끊어지는 사태가 발생하게 된다..

 

이런 상황은 현재 개선된 상태이다. 카프카 버전 1.0.0에서 6분 30초 소요되던 작업이 불필요한 로깅을 없애고 주키퍼 비동기 API가 반영된 카프카 버전 1.1.0에서는 약 3초만에 완료된다. 따라서 최신 버전을 사용하고 있다면 이런 문제에 대해서는 안심할 수 있다.

 

2.2 관리자에 의해 이뤄지는 graceful(자연스러운) 종료

 

1) 관리자가 브로커 종료 명령어를 실행하고 SIG_TERM 신호가 브로커에게 전달된다.

2) SIG_TERM 신호를 받은 브로커는 컨트롤러에게 알린다.

3) 컨트롤러는 리더 선출 작업을 진행하고 해당 정보를 주키퍼에 기록한다.

4) 컨트롤러는 새로운 리더 정보를 다른 브로커들에게 전송한다.

5) 컨트롤러는 종료 요청을 보낸 브로커에게 정상 종료를 해도 된다는 응답을 보낸다.

6) 응답을 받은 브로커는 캐시에 있는 내용을 디스크에 저장하고 종료한다.

 

갑작스러운 종료와의 가장 큰 차이는 바로 다운타임(downtime)이다.

제어된 종료를 사용하면 브로커가 종료되기 전 컨트롤러가 새로운 리더 선출 작업을 진행한다.

리더 선출 작업 대상인 파티션들의 리더들이 활성화된 상태에서 컨트롤러가 순차적으로 각 파티션마다 리더를 선출하므로 결과적으로 각 파티션들은 다운타임을 최소화할 수 있다.

또한, 제어된 종료의 경우 브로커는 모든 로그를 디스크에 동기화한 후 종료되므로 다시 브로커가 재시작할 때 로그 복구 시간이 짧다.

 

3. 로그(로그 세그먼트)

카프카의 토픽으로 들어오는 메시지(레코드)는 세그먼트(=로그 세그먼트, segment)라는 파일에 저장된다.

메시지는 순차적으로 로그 세그먼트 파일에 저장된다. 여기에는 메시지의 내용 뿐 아니라 메시지의 key, value, offset, message size와 같은 정보가 함께 저장되며, 로그 세그먼트 파일들은 브로커의 로컬 디스크에 보관된다.

 

하나의 로그 세그먼트 크기가 너무 커져버리면 파일을 관리하기 어렵기 때문에, 최대 크기는 default 1GB로 설정되어 있다.

하지만 1GB 크기의 로그 세그먼트 파일이 무한히 늘어날 경우를 대비해 로그 세그먼트에 대한 관리 계획을 수립해야 한다.

 

3.1 로그 세그먼트 삭제

retention.ms 설정은, 이 설정보다 로그 세그먼트 보관 시간이 크면 세그먼트를 삭제하는 설정이다.

00000000000000000000.index는 로그 세그먼트에 저장된 위치와 오프셋 정보를 기록하는 파일

00000000000000000000.log는 실제 메시지들이 저장되는 파일

00000000000000000000.timeindex는 메시지의 타임스탬프를 기록하는 파일

 

retention.ms 시간이 지나고 세그먼트가 삭제되었다면 기존 파일은 삭제되고 아래와 같은 파일이 있을것이다.

00000000000000000137.log, 00000000000000000137.timeindex

 

여기서 137은 오프셋 시작 번호를 이용해 파일 이름을 생성하는 규칙을 따른다.

카프카의 기본 설정값은 7일으로, 모든 세그먼트 파일은 7일이 지남과 동시에 전부 삭제된다. retention.bytes 옵션을 이용해 지정된 크기를 기준으로도 로그 세그먼트를 삭제할 수 있다.

 

3.2 로그 세그먼트 컴팩션(compaction)

로그를 삭제하지 않고 컴팩션하여 보관할 수 있다. 로컬 디스크에 저장되어 있는 세그먼트를 대상으로 실행되는데, 현재 활성화돤 세그먼트는 제외하고 나머지 세그먼트들을 대상으로 컴팩션이 실행된다.

로그 세그먼트를 컴팩션하면 메시지(레코드)의 키값을 기준으로 마지막 데이터만 보관한다.

예) __consumer_offset topic : key(컨슈머 그룹명, 토픽명), value(오프셋 커밋 정보)

 

https://kafka.apache.org/documentation/#compaction

키값을 기준으로 최종값만 필요한 워크로드에 적용하는 것이 바람직하다.

 

 

 

참고하면 좋은 사이트

https://velog.io/@hyun6ik/Apache-Kafka-Replication

 

References

실전 카프카 개발부터 운영까지, 고승범 (추천!)

반응형

댓글