🥞 BE
home

Ch2. 카프카의 내부 동작 원리

Date
2025/01/08
Category
Data Engineering
Tag
Apache Kafka
Detail
실전 카프카 Study
목차

1. 카프카 리플리케이션

→ 분산 시스템에서 고가용성을 위해 사용.
카프카는 많은 데이터 파이프라인 중간에서 메인 허브 역할을 함. 만약 하드웨어 문제 등으로 정상 작동하지 못하면, 매우 심각한 문제가 야기됨. 때문에 안정적인 서비스 운영을 위해 리플리케이션을 활용.
1.
kafka01 서버에 토픽을 생성 (파티션 1개, 리플리케이션 팩터 3개)
--bootstrap-server kafka01:9092 --create --topic topic01 --partitions 1 --replication-factor 3
2.
생성된 토픽의 내부 파티션을 확인한다.
3.
test 메시지를 전송한다.
4.
kafka-dump-log.sh에서 세그먼트 내용을 확인한다.
kafka01에서 test 메시지 세그먼트를 저장하면, kafka02, 03에서도 똑같은 test 메시지를 확인할 수 있다.

1.1. 리더와 팔로워

파티션 내에서는 모두 동일한 리플리케이션이라 하더라도 리더만의 역할이 따로 있음.
모든 읽기와 쓰기는 리더를 통해서만 가능.
Producer는 리더에게 메시지를 전송하고, Consumer도 리더에서만 메시지를 가져옴.
리더 파티션과 팔로워 파티션을 합쳐 ISR(In Sync Replica)라고 부름.
ISR 내부의 파티션들은 모두 리더의 데이터 상황을 따라가려고 함. → 때문에 리더는 팔로워 파티션들을 모니터링함. → 만약 리더에 장애가 발생할 경우, 리더는 모니터링하던 팔로워들 중 리플리케이션이 제대로 되어있는 팔로워(ISR에 속한 팔로워)에게 리더를 넘김.
그럼 모니터링은 어떤 기준으로 수행될까?
팔로워가 특정 주기의 시간만큼 복제 요청 X → ISR에서 추방
모든 팔로워의 복제가 완료되면, 리더는 커밋을 수행함. 이렇게 커밋이 수행된 메시지 만을 Consumer가 읽어갈 수 있음.

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

1.
프로듀서가 특정 토픽의 (리더)파티션에 메시지를 전송
2.
리더가 메시지를 수신하고 로그에 기록 - uncommitted
3.
팔로워가 리더에게 메시지 리플리케이션 fetch 요청
4.
팔로워가 리더로부터 메시지를 pull, 로그에 기록
5.
팔로워가 리더에게 복제 완료 알림 → 성공하지 못했을 경우, 팔로워는 리더에게 이전 오프셋에 대한 리플리케이션 요청을 보내고, 리더는 이를 통해 ACK(acknowledgment).
6.
리더가 커밋 결정 → replication-factor, ISR을 기준으로 커밋 결정
7.
리더가 메시지를 커밋하고 컨슈머에게 알림 - committed
8.
커밋된 표시를 하게 되는데, 이를 high water mark라고 함.
→ point : 카프카는 기존 메시지 큐에서의 ACK 통신을 없애고, 간단한 내부 로직 처리를 통해 ACK를 구현. 이를 통해 대용량 메시지 처리에 있어서 성능 개선.

1.3. 리더에포크와 복구

리더에포크(Leader Epoch)는 파티션의 Replica들이 서로 일치하도록 하는 replication 프로토콜에서 사용.
컨트롤러에 의해 관리되는 32bit 숫자로 표현되며, 이 값은 파티션의 현재 상태를 판단하는데 사용됨. 리더에포크는 복구 동작 시 high water mark를 대체하는 수단으로도 활용됨.
리더에포크를 사용하지 않은 장애 복구 과정
리플리케이션 수행
1.
리더는 message-A를 받고, 0번 오프셋에 저장. 팔로워는 pull 요청을 하여 리플리케이션 수행.
2.
리더는 High-Water-Mark를 1로 올림.
3.
리더는 message-B를 받고, 1번 오프셋에 저장. 팔로워는 pull 요청을 보내고, 응답으로 High-Water-Mark 변경을 감지.
4.
팔로워는 본인의 High-Water-Mark를 1로 올림, 리플리케이션 수행.
5.
팔로워는 2번 오프셋에 대한 pull 요청도 보내게 됨. 리더는 이를 인지하고, High-Water-Mark를 2로 올림.
a.
이제 팔로워는 리더로부터 응답을 받아야하는데, 이때 예상치 못한 장애로 팔로워 다운.
장애 복구 완료
1.
팔로워는 자신이 갖고 있는 메시지들 중에서 본인의 High-Water-Mark보다 높은 메시지는 신뢰할 수 없다고 판단하여 삭제.
2.
팔로워는 리더에게 1번 오프셋의 새로운 메시지에 대해 pull 요청을 보냄.
3.
근데 이번엔 리더였던 브로커가 다운됨. → 해당 파티션에 유일하게 남아있던 팔로워가 승격.
기존 리더 다운되고 팔로워 승격
기존 팔로워들은 장애복구 과정에서 Message-B를 가지고 있지 않았었기에, 결국 Message-B는 최종적으로 손실됨.
리더에포크를 사용한 장애 복구 과정
장애 발생 후, 복구된 이후 상황
기존의 경우, High-Water-Mark보다 높으면 삭제. but 리더에포크를 사용하는 경우에는,
1.
팔로워가 메시지 복구 동작을 하며, 리더에포크 요청을 보냄.
2.
리더는 리더에포크에 대한 응답으로 오프셋 1의 Message-B까지라고 팔로워에게 보냄.
3.
팔로워는 이에 따라 Message-B까지 자신의 High-Water-Mark를 상향 조정함.
기존 리더 다운되고 팔로워 승격
리더에포크로 High-Water-Mark를 조정하고, Message-B를 보존했기에, 손실이 발생하지 않음. 마
만약 리더와 팔로워가 동시에 다운되면??
리더 종료 후 (리더에포크 사용 X)
1.
팔로워였던 브로커가 먼저 복구됨.
2.
토픽의 0번 파티션에 리더가 없기에, 팔로워는 리더로 승격됨.
3.
새로운 리더는 Producer에서 Message-C를 받고, 오프셋 1에 저장한 뒤, High-Water-Mark를 조정함.
기존 리더였던 브로커도 복구됨.
1.
이미 리더가 있으니, 복구된 브로커는 팔로워가 됨.
2.
리더와 메시지 정합성 확인을 위해, High-Water-Mark를 비교해봤는데, 둘이 똑같네? → Message-B를 삭제하지 않음.
3.
리더는 Message-D를 받고 오프셋 2에 저장, 팔로워는 이를 리플리케이션 하기 위해 준비.
→ 둘 다 동일한 High-Water-Mark 2를 가지고 있으나, 서로 가지고 있는 메시지는 달라져버림.
기존 리더였던 브로커 복구 (리더에포크 사용 O)
1.
이미 리더가 있으니, 복구된 브로커는 팔로워가 됨.
2.
팔로워는 리더에게 리더에포크 요청을 보내고, 리더는 오프셋 0까지 유효하다고 응답.
3.
그에 따라 팔로워는 오프셋 1번에 있는 Message-B를 삭제함.
4.
팔로워는 오프셋 1번에 있는 Message-C를 리플리케이션 하기 위해 준비.

1.4. 리더에포크 원리

그럼 리더에서는 어떻게 오프셋 0번까지 유효한지 알 수 있었을까?
→ 리더에포크 요청과 응답에는 리더에포크 번호와, 커밋된 오프셋 번호를 활용한다. 리더에포크 번호는 리더가 변경될 때마다 하나씩 숫자가 증가한다. 카프카는 리더에포크 번호를 유지하며, 장애 발생 시 새로운 리더가 선출될 때 리더에포크를 업데이트하며 리더에포크 번호 + 최종 커밋 오프셋 번호의 조합을 leader-epoch-checkpoint 파일에 기록한다. 다운되었던 브로커는 해당 파일에 기록된 정보를 통해 복구 동작을 진행하게 되는 것.

2. 컨트롤러

컨트롤러는 파티션의 리더 선출을 담당함. 카프카 클러스터 중 하나의 브로커가 컨트롤러 역할을 하게 되며, 파티션의 ISR 리스트 중에서 리더를 선출.
ISR 리스트 정보는 안전한 저장소에 보관되어 있어야 하는데, 가용성 보장을 위해 주키퍼에 저장.
1.
컨트롤러는 브로커가 실패하는 것을 예의주시 하고 있음.
2.
만약 브로커의 실패가 감지되면 즉시 ISR 리스트 중 하나를 새로운 파티션 리더로 산출.
3.
새로운 리더의 정보를 주키퍼에 기록.
4.
변경된 정보를 모든 브로커에게 전달.
리더가 다운되면, Producer나 Consumer가 해당 파티션으로 읽기/쓰기가 불가능하고, 클라이언트에 설정된 재시도 횟수만큼 재시도 하게 됨. 따라서 클라이언트들이 재시도 하는 시간 내에 리더 선출 작업이 빠르게 이루어져야 함.

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

1.
파티션 0번의 리더가 있는 브로커 1번이 예상치 못하게 다운.
2.
주키퍼는 1번 브로커와 연결이 끊어진 후, 0번 파티션의 ISR에서 변화가 생겼음을 감지.
3.
컨트롤러는 주키퍼 워치를 통해 0번 파티션에 변화가 생김을 감지. 해당 파티션 ISR 중 3번을 새로운 리더로 선출.
4.
컨트롤러는 0번 파티션의 새로운 리더가 3이라는 정보를 주키퍼에 기록.
5.
갱신된 정보를 현재 활성화 상태인 모든 브로커에 전파.

2.2. 리더 선출 진행 속도

파티션당 0.2 sec씩 소요. 파티션의 개수가 많아질수록 속도가 증가하게 되며 1만개의 파티션이 구성된 경우에는 약 30분의 시간이 소요됨.
만약 실시간 서비스를 운영중이라면 이러한 리더 선출 속도는 치명적일 수 있는데 다행히 2018년 11월에 릴리즈된 카프카 1.1.0 부터는 이를 개선한 상태.
기존에는 6분 30초 소요되던 작업이 1.1.0 부터는 약 3초만에 완료됨.

2.3. 제어된 종료 과정

위에서는 예기치 못한 브로커의 실패나 장애로 인해 리더 선출을 하게 되는 과정이었으나, 여기서는 관리자에 의한 제어된 종료 과정의 예시이다.
1.
관리자가 브로커 종료 명령어를 실행하고, SIG_TERM 신호가 브로커에게 전달.
2.
SIG_TERM 신호를 받은 브로커는 컨트롤러에게 알림.
3.
컨트롤러는 리더 선출 작업을 진행하고, 해당 정보를 주키퍼에 기록.
4.
컨트롤러는 새로운 리더 정보를 다른 브로커들에게 전송.
5.
컨트롤러는 종료 요청을 보낸 브로커에게 정상 종료한다는 응답을 보냄.
6.
응답을 받은 브로커는 캐시에 있는 내용을 디스크에 저장하고 종료.
제어된 종료와 급작스런 종료의 차이
→ Downtime. 제어된 종료를 사용하면 카프카 내부적으로 파티션들의 downtime을 최소화 할 수 있다. 그 이유는 활성된 리더 파티션을 기준으로 리더를 선출하기 때문이다. 예상치 못하게 브로커가 종료된다면 리더 선출 전까지는 downtime이 계속 누적이 되나, 의도된 종료에서는 그렇지 않다. 물론 제어된 종료도 downtime이 발생은 하지만, 비교적 빠르게 동작하게 된다. 제어된 종료를 사용하려면, server.properties에서 controlled.shutdown.enable = true 설정이 된 상태로 운영되어야 한다.

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

카프카의 토픽으로 들어오는 메시지(레코드)는 세그먼트(로그 세그먼트)라는 파일에 저장됨.
로그 세그먼트에는 메시지의 내용뿐 아니라 메시지의 Key, Value, Offset, Message 크기와 같은 정보들이 함께 저장되며 이들은 브로커의 로컬 디스크에 보관됨.
파일 관리의 어려움으로 인해 로그 세그먼트의 최대 크기는 1GB가 기본값.
만약 로그 세그먼트가 1GB보다 커지면, 롤링(다음 파일에 저장하는 형식) 전략을 사용.
이러한 파일들이 무한히 늘어날 경우를 대비하기 위해, 로그 세그먼트 삭제컴팩션을 제공함.

3.1. 로그 세그먼트 삭제

오래된 로그는 삭제하는 정책. server.properties에서 log.cleanup.policydelete로 명시되어야 함. 사실 따로 명시 안해도, 카프카는 기본적으로 로그 삭제 정책을 적용함.
retention.ms 을 통해서 관리하며, 설정 안하면 7일이 default. retention.ms 값보다 로그 세그먼트 보관 시간이 크면 세그먼트를 삭제한다는 의미.
카프카는 기본값 5분 주기로 삭제 작업을 수행하며, 이는 retention.ms=0 으로 설정할 경우, 하나의 로그가 적재된 이후 5분 뒤에 바로 삭제가 된다는 것을 뜻함. /data/kafka-logs/partition 위치를 보면 확인 가능.

3.2. 로그 세그먼트 컴팩션

컴팩션은 로그 세그먼트를 삭제하지 않고 저장을 해두는 정책. 모든 메시지를 저장하지 않고 Key값을 기준으로 마지막의 데이터만 보관함. → 과거 정보는 중요하지 않고 가장 마지막 값이 필요한 경우에만 사용
이후에 나오는 consumer group별 offset을 관리하는 __consumer_offset 토픽이 이와 동일한 방식으로 관리됩니다.
그렇다면 이렇게 컴팩션 해서 뭐가 좋은걸까?
→ 빠른 장애 복구!
장애 복구 시 전체 로그를 복구하지 않고, 메시지의 키를 기준으로 최신의 상태만 복구한다. 따라서 복구 시간을 줄일 수 있다는 장점이 있다.
물론 가장 키값을 기준으로 최종값만 필요한 워크로드에 적용하는 것이 바람직. 이외에는 쓰지말자.

4. 정리

위와 같은 기능들 덕분에 카프카 내부적으로 메시지의 신뢰성과 시스템 안정성이 보장됨.

Reference