실전 카프카 개발부터 운영까지 - 카프카의 내부 동작 원리와 구현
들어가며
- 해당 포스팅은 실전 카프카 개발부터 운영까지 도서를 학습하며 정리한 글입니다.
실전 카프카 개발부터 운영까지
실전 카프카 개발부터 운영까지 작품소개: [도서 개요]아파치 카프카의 공동 창시자 준 라오(Jun Rao)가 추천한 책!국내 최초이자 유일한 컨플루언트 공인 아파치 카프카 강사(Confluent Certified Trainer for Apache Kafka)와 공인 관리자 자격(Confluent Certified Administrator for Apache Kafka)을 보유한 『카프카, 데이터 ...
카프카 리플리케이션
- 카프카 클러스터가 하드웨어 문제나 점검으로 인해 정상적으로 작동하지 않거나, 카프카와 연결된 전체 데이터 파이프라인에 영향을 미친다면 큰 문제가 발생할 수 있습니다.
- 하지만 일시적인 하드웨어 이슈로 일부 브로커에서 장애가 발생하더라도, 카프카는 중앙 데이터 허브로서 안정적인 서비스를 지속할 수 있도록 설계되었습니다. 이를 가능하게 하는 핵심 메커니즘 중 하나가 바로 리플리케이션입니다.
리플리케이션 동작 개요
- 리플리케이션 기능을 사용하려면, 토픽을 생성할 때 replication factor 옵션을 설정해야 합니다. 예를 들어, 토픽의 파티션 수를 1개, 리플리케이션 팩터를 3으로 설정했다고 가정해 보겠습니다.
- 이때 리플리케이션되는 대상은 토픽 자체가 아닌, 토픽을 구성하는 각각의 파티션입니다. 파티션 0의 리더는 브로커 1에 있고, 해당 파티션의 리플리케이션은 브로커 1, 2, 3에 분산되어 저장됩니다. 즉, 프로듀서가 보낸 하나의 메시지는 총 3개의 브로커에 저장됩니다.
- 리플리케이션 팩터 설정 덕분에, N개의 리플리케이션 중 N-1개가 장애를 일으키더라도 메시지 손실 없이 처리가 가능합니다.
리더와 팔로워
- 동일한 리플리케이션이 있더라도 리더는 특별한 역할을 맡습니다. 리플리케이션 중 하나가 리더로 선정되며, 읽기와 쓰기는 오직 리더를 통해서만 이루어집니다.
- 프로듀서는 모든 리플리케이션에 메시지를 보내는 것이 아니라, 리더에게만 메시지를 전송합니다.
- 컨슈머 역시 리더로부터만 메시지를 가져옵니다.
- 예를 들어, 프로듀서가 특정 토픽으로 메시지를 전송할 때, 해당 파티션의 리더만 읽기와 쓰기가 가능하기 때문에 0번 파티션의 리더에게 메시지를 전송합니다. 마찬가지로, 컨슈머도 0번 파티션의 리더로부터 메시지를 가져옵니다.
- 리플리케이션 중 나머지 팔로워들은 단순히 대기하는 것이 아닙니다. 리더에 문제가 발생하거나 이슈가 생길 경우, 언제든지 새로운 리더가 될 준비를 합니다. 이들은 지속적으로 파티션 리더가 새로운 메시지를 받았는지 확인하며, 새로운 메시지가 있을 경우 이를 복사해 동기화합니다.
복제
- 이번에는 리더와 팔로워 간의 복제 동작에 대해 알아보겠습니다. 리더와 팔로워는 ISR(In-Sync Replica)라는 논리적 그룹으로 묶여 있습니다.
- ISR 내의 팔로워들은 리더와 지속적으로 데이터 일치를 유지해야 합니다. 이를 위해 팔로워들은 리더의 데이터를 읽고 복제하며, 리더는 모든 팔로워가 메시지를 수신할 때까지 기다립니다.
- 그러나 네트워크 오류 등으로 인해 팔로워가 메시지를 받지 못하고 데이터 불일치가 발생할 수 있습니다. 이런 경우, 불일치가 발생한 팔로워가 리더로 승격되면 문제가 발생할 수 있습니다.
- 이를 방지하기 위해 리더는 팔로워들이 올바르게 리플리케이션을 수행하고 있는지 모니터링하며, 데이터 일치가 잘 되는 팔로워들만 ISR 그룹에 유지합니다.
- 팔로워가 일정 시간 내에 복제 요청을 하지 않으면, 문제가 발생했다고 판단하고 그 팔로워를 ISR 그룹에서 제외합니다.
- 모든 팔로워가 복제를 완료하면 리더는 이를 커밋하여 복제가 완료되었음을 표시합니다.
- 이때 커밋된 마지막 오프셋 위치를 하이워터마크라고 부릅니다.
- 커밋은 모든 리플리케이션 팩터에 해당하는 브로커들이 메시지를 성공적으로 저장했음을 의미합니다.
- 오직 커밋된 메시지만이 컨슈머에게 제공되며, 이를 통해 데이터 일관성이 유지됩니다.
리더와 팔로워의 단계별 리플리케이션 동작
- 리더와 팔로워 간 통신 과부하를 줄이기 위해, 두 요소 간의 통신을 최소화할 수 있도록 설계되었습니다.
- 리더가 데이터를 푸시(push)하는 방식이 아니라, 팔로워가 리더로부터 데이터를 풀(pull)하는 방식으로 리더의 부하를 줄였습니다.
- 예를 들어, RabbitMQ의 경우 트랜잭션 모드에서 모든 미러가 메시지를 수신하면, 각 미러가 리더에게 ACK를 반환합니다. 이를 통해 리더는 미러들이 메시지를 정상적으로 수신했는지 알 수 있습니다.
- 반면에, Kafka에서는 리더와 팔로워 사이에 ACK를 주고받는 통신이 없습니다. 이 방식 덕분에 리플리케이션 성능이 향상됩니다. 이때, ACK 없이도 팔로워가 메시지를 수신했는지 확인하는 방법은 하이워터마크를 통해 이루어집니다.
스텝 1
- peter-test01 토픽은 1개의 파티션과 3개의 리플리케이션 팩터를 가지고 있습니다.
- 현재 리더만이 오프셋에 message1 메시지를 저장한 상태입니다.
- 프로듀서가 message1 메시지를 peter-test01 토픽으로 전송했으며, 리더는 이를 저장했지만, 팔로워들은 아직 리더로부터 메시지를 복제하지 않은 상태입니다.
스텝 2
- 팔로워들은 리더에게 0번 오프셋의 메시지를 가져오기 위해 요청을 보내고, 그 후 새로운 메시지인 message1이 있다는 사실을 인지하여 해당 메시지를 복제(리플리케이션)합니다.
- 현재 리더는 모든 팔로워가 0번 오프셋 메시지를 복제하기 위한 요청을 보냈다는 사실을 알고 있습니다. 그러나 리더는 팔로워들이 0번 오프셋에 대한 리플리케이션이 성공했는지, 실패했는지는 알 수 없습니다.
- 이는 리더와 팔로워 간에 ACK를 주고받는 통신이 없기 때문입니다. 그럼에도 불구하고 카프카는 안정적으로 동작할 수 있으며, 그 이유는 이후 단계에서 설명됩니다.
스텝 3
- 현재 메시지가 저장된 상태에서, 새로운 메시지 message2 (오프셋 1)가 들어온다고 가정해 봅시다. 팔로워들은 리더에 맞춰 오프셋 0번 메시지 저장을 완료한 후, 새로운 메시지인 오프셋 1에 대한 리플리케이션 요청을 보냅니다.
- 이 요청을 통해 리더는 팔로워들이 오프셋 0번 리플리케이션을 성공적으로 마쳤음을 인지하고, 오프셋 0에 커밋 표시를 한 뒤 하이워터마크를 증가시킵니다.
- 만약 팔로워가 오프셋 0에 대한 리플리케이션을 성공하지 못하면, 1번 오프셋이 아닌 0번 오프셋에 대한 리플리케이션 요청을 다시 보냅니다.
- 팔로워들이 보낸 리플리케이션 요청을 통해 리더는 팔로워들이 어느 오프셋까지 리플리케이션을 성공했는지 알 수 있습니다.
- 리더는 팔로워들로부터 1번 오프셋에 대한 리플리케이션 요청을 받으면, 응답 시 0번 오프셋의 message1이 커밋되었음을 함께 알립니다.
스텝 4
- 모든 팔로워들은 리더의 응답을 통해 0번 오프셋 메시지가 커밋되었다는 사실을 확인하고, 리더와 동일하게 커밋을 표시합니다.
- 이후, 1번 오프셋의 메시지인 message2를 리플리케이션합니다.
- 이 과정이 반복되어, 리더가 1번 오프셋을 커밋하면 그 내용을 팔로워들에게 전송하고, 팔로워들도 이에 따라 동일하게 커밋을 완료합니다.
리더에포크와 복구
- 리더에포크는 카프카의 파티션들이 복구 동작을 할 때 메세지의 일관성을 유지하기 위한 용도로 이용됩니다.
- 리더에포크는 컨트롤러에 의해 관리되는 32비트 숫자로 표현됩니다.
리더에포크를 사용하지 않았을 때
1번 케이스 - 리더에포크 없이 메세지 손실이 일어나는 경우
- 리더는 프로듀서로부터 message1 메세지를 받았고, 0번 오프셋에 저장, 팔로워는 리더에게 0번 오프셋에 대한 가져오기 요청을 합니다.
- 가져오기 요청을 통해 팔로워는 message1 메세지를 리더로부터 리플리케이션합니다.
- 리더는 하이워터마크를 1로 올립니다.
- 리더는 프로듀서로부터 다음 메세지인 message2를 받은 뒤 1번 오프셋에 저장합니다.
- 팔로워는 다음 메세지인 message2에 대해 리더에게 가져오기 요청을 보내고, 응답으로 리더의 하이워터마크 변화를 감지하고 자신의 하이워터마크도 1로 올립니다.
- 팔로워는 1번 오프셋의 message2 메세지를 리더로부터 리플리케이션합니다.
- 요청을 받은 리더는 하이워터마크를 2로 올립니다.
- 리더는 프로듀서로부터 다음 메세지인 message3를 받은 뒤 2번 오프셋에 저장합니다.
- 팔로워는 1번 오프셋인 message2 메세지까지 리플리케이션을 완료했지만, 아직 리더로부터 하이워터마크를 2로 올리는 내용은 전달받지 못한 상황입니다.
- 이때 예상하지 못한 장애로 팔로워가 다운됩니다.
장애 복구 절차
- 팔로워는 자신이 갖고 있는 메세지들 중에서 자신의 워터마크보다 높은 메세지들은 신뢰할 수 없는 메세지라고 판단하고 삭제합니다. 따라서 1번 오프셋의 message2는 삭제됩니다.
- 팔로워는 리더에게 1번 오프셋의 새로운 메세지에 대한 가져오기 요청을 보냅니다.
- 이 순간 리더였던 브로커가 예상하지 못한 장애로 다운되면서, 해당 파티션에 유일하게 남아있던 팔로워가 새로운 리더로 승격됩니다.
- 팔로워가 새로운 리더로 승격된 후의 상태를 나타냅니다.
- 기존의 리더는 1번 오프셋의 message2를 가지고 있었지만, 팔로워는 message2 없이 새로운 리더로 승격됐습니다. 결국 새로운 리더는 message2를 가지고 있지 않습니다.
- 리더와 팔로워 간의 리플리케이션이 있음에도 불구하고, 리더가 변경되는 과정을 통해 최종적으로 1번 오프셋 message2 메세지가 손실된 것입니다.
2번 케이스 - 리더에포크 없이 메세지 불일치가 일어나는 경우
-
리더만 오프셋1까지 저장했고, 팔로워는 아직 1번 오프셋 메세지에 대해 리플리케이션 동작을 하지 못한 상태입니다.
-
이때 해당 브로커들의 장애가 발생하여 리더와 팔로워 모두 다운되었고, 팔로워가 있던 브로커만 장애에서 복구된 상태입니다.
-
팔로워(새로운 리더)의 복구 과정
- peter-test-01 토픽의 0번 파티션에 리더가 없으므로 팔로워는 새로운 리더로 승격됩니다.
- 새로운 리더는 프로듀서로부터 다음 메세지 message3를 전달받고 1번 오프셋에 저장한 후, 자신의 하이워터마크를 상향 조정합니다.
-
이전 리더의 복구 과정
- 이전에 리더였던 브로커도 장애에서 복구됩니다.
- peter-test-01 토픽의 0번 파티션에 이미 리더가 있으므로 복구 된 브로커는 팔로워가 됩니다.
- 리더와 메세지 정합성 확인을 위해 하이워터마크를 비교해보니 리더의 하이워터마크와 일치하므로, 브로커는 자신이 가지고 있던 메세지를 삭제하지 않습니다.
- 리더는 프로듀서로부터 message4 메세지를 받은 후 오프셋2 위치에 저장합니다.
- 팔로워는 오프셋2인 message4를 리플리케이션하기 위해 준비합니다.
-
이렇게 되면 새로운 리더는 1번 오프셋 위치에 message3를 갖고 있고, 팔로워는 1번 오프셋 위치에 message2를 갖고 있습니다. 리더와 팔로워 둘다 동일한 하이워터마크를 나타내고 있지만 서로의 메세지는 다릅니다.
-
리더와 팔로워가 메세지의 동일한 오프셋 위치를 이용해서 복구된다면 서로의 메세지가 불일치하는 경우가 발생합니다.
리더에포크를 사용하였을 때
1번 케이스 - 리더에포크를 통해 메세지 손실을 방지하는 경우
- 앞서 본 1번 절차에서 카프카 프로세스가 복구 동작을 통해 자신의 하이워터마크보다 높은 메세지를 즉시 삭제하기 이전 상황으로 돌아가겠습니다.
- 리더에포크를 사용하는 경우에는 하이워터마크보다 앞에 있는 메세지를 무조건 삭제하는 것이 아니라 리더에게 리더에포크 요청을 보냅니다.
- 팔로워는 복구 동작을 하면서 리더에게 리더에포크 요청을 보냅니다.
- 요청을 받은 리더는 리더에포크 응답으로 “1번 오프셋의 message2 까지” 라고 팔로워에게 보냅니다.
- 팔로워는 자신의 하이워터마크보다 높은 1번 오프셋의 message2를 삭제하지 않고, 리더의 응답을 확인 후 message2까지 자신의 하이워터마크를 상향 조정합니다.
- 하이워터마크를 높인 덕분에 팔로워가 메세지 손실을 발생시키지 않았습니다.
2번 케이스 - 리더에포크를 통해 메세지 불일치를 방지하는 경우
- 케이스 2번에서 리더에포크를 사용했을 때 입니다.
- 새로운 리더는 자신이 팔로워일 때의 하이워터마크와 새로운 리더가 되었을 때의 하이워터마크를 알고 있습니다.
- 이전에 리더였던 브로커가 장애에서 복구됩니다.
- peter-test-01 토픽의 0번 파티션에 이미 리더가 있고 자신은 팔로워가 됩니다.
- 팔로워는 새로운 리더에게 리더에포크 요청을 보냅니다.
- 새로운 리더는 0번 오프셋까지 유효하다고 응답합니다.
- 팔로워는 메세지 일관성을 위해 로컬 파일에서 1번 오프셋인 message2를 삭제합니다. (팔로워는 쓰기 권한이 없으므로 리더에게 message2를 추가할 수 없습니다.)
- 팔로워는 리더로부터 1번 오프셋인 message3을 리플리케이션하기 위해 준비합니다.
정리하며
- 카프카의 내부 구현과 원리에 대해서 알아보았습니다. 해당 내용들을 중간 정리하며 마치려고 합니다.
- 중간정리 내용을 도식화하면 위와 같습니다.
프로듀서 → 토픽
- 이벤트를 발행하는 프로듀서는 이벤트를 발행하기 위해 특정 토픽을 호출합니다. (send())
- 이때 파티션을 명시할 수 있습니다. 특정 토픽의 파티션을 사용하고 싶으면 명시하면 됩니다.
- 만약에 파티션을 명시하지 않으면 round-robin 순서로 파티션이 할당됩니다.
토픽
- 토픽은 엄밀히 말하자면 논리적인 존재입니다.
- 브로커 안에서 작업을 실제로 하는 것은 토픽의 파티션입니다. 레플리케이션 하는 것도 토픽의 파티션입니다.
- 토픽이라는 것은 어떠한 이벤트를 보내기 위한 대표 구독 장치인 셈입니다.
- 토픽에 대한 정보는 메타데이터를 관리하는 KRaft에 존재합니다.
- 해당 요소가 리더 선출 작업도 진행합니다.
토픽과 파티션
- 파티션이란 여러개의 작업을 병렬적으로 실행하기 위한 장치입니다.
- 파티션에도 작업들이 쌓이면 바로 나가는게 아니고, 파티션 안에 특정 주기로 모아두었다가 batch로 처리합니다.
- 이렇게 파티션 안에서도 대용량 데이터를 배치로 처리하고, 각 파티션이 더 있어서 해당 처리가 더 빠르게 동작할 수 있습니다.
- 실제 kafka 클러스터 안의 브로커에는 토픽이라는게 존재하는게 아니고, 파티션이 존재합니다. 이 파티션이 일들을 처리하는 것 입니다. (토픽은 논리적 개념)
토픽과 리플리케이션
- 토픽의 리플리케이션 팩터에 따라서 리플리케이션할 개수가 정해집니다.
- 파티션 1개에 리플리케이션 팩터 개수 만큼 브로커에 복제가 됩니다.
- 도식에서 볼 수 있듯이, partition 0에 리플리케이션 팩터에 대한 옵션을 3으로 준다면 아래와 같이 리플리케이션이 이루어집니다.
- 리더: 브로커 1 / 팔로워 1: 브로커 2 / 팔로워 2: 브로커 3
- 이렇게 되면 프로듀서에서 처리하는 작업을 리더가 저장하고, 컨슈머는 리더로부터 데이터를 읽습니다. 이 과정에서 다른 팔로워들에게 백업이 됩니다.
- 리플리케이션은 백업을 위해서 있는 것이며, 리더가 죽으면 다음 팔로워가 리더가 되어서 작업을 이어나갈 수 있게 하여 메세지 손실을 방지합니다.
브로커 ↔ 컨슈머
- 위에서 언급했듯이 컨슈머는 리더인 파티션의 값만 읽어서 컨슘하고 행동을 수행합니다.
- 컨슈머는 각각 하나의 파티션과 매칭되어서 작업을 수행합니다.
- 이때 컨슈머 끼리도 컨슈머 그룹이라는 것이 있습니다.
- 만약에 파티션 0에 매핑된 컨슈머 0이 있고, 파티션 1에 매핑된 컨슈머1이 있으며 두 컨슈머는 하나의 컨슈머 그룹에 있다고 가정합니다.
- 만약 컨슈머 0이 장애가 나면 더이상 파티션 0에 대한 메세지 컨슘을 하지 못합니다.
- 이때 같은 컨슈머 그룹인 컨슈머1이 컨슈머 0의 작업을 대신하여 파티션 0에 대한 메세지 컨슘을 하여 메세지 손실을 방지합니다.
마치며
- 중간정리를 통해서 카프카의 개념을 한번 더 정리할 수 있었습니다.
- 다음 글에서는 프로듀서와 컨슈머의 세부적인 내부 구현과 원리에 대해서 깊게 알아보려고 합니다.
