-
카프카의 내부 동작 원리와 구현Backend/kafka 2023. 8. 9. 01:21
이번 글은 카프카의 내부 동작 원리에 대한 챕터에 대한 정리입니다.
1. 카프카 리플리케이션
카프카는 안정성을 확보하기 위해 카프카 내부에서 리플리케이션이라는 동작을 하게 됩니다.
카프카는 브로커의 장애에도 불구하고 연속적으로 안정적인 서비스를 제공함으로써 데이터 유실을 방지하며 유연성을 제공합니다.
카프카의 리더 파티션에 메시지를 보내게 되면 해당 메시지를 모든 리플리케이션 팩터 수만큼의 브로커에서 가질 수 있게 됩니다. 즉, 리플리케이션이 있는 경우 N개의 브로커 중에 N-1 까지의 브로커 장애가 발생해도 메시지 손실 없이 안정적으로 메시지를 주고받을 수 있습니다.
[ 리더와 팔로워 ]
- 카프카는 내부적으로 모두 동일한 리플리케이션들을 리더와 팔로워로 구분하고, 각자의 역할을 분담시킵니다.
- 리더는 리플리케이션 중 하나가 선정되며, 모든 읽기와 쓰기는 그 리더를 통해서만 가능합니다.
프로듀서는 모든 리플리케이션에 메시지를 보내는 것이 아니라 리더에게만 메시지를 전송합니다. 또한 컨슈머도 오직 리더로부터 메시지를 가져옵니다.
팔로워들 역시 그저 대기만 하는 것이 아니라, 리더에 문제가 발생하거나 이슈가 있을 경우를 대비해 언제든지 새로운 리더가 될 준비를 해야 합니다. 따라서 컨슈머가 토픽의 메시지를 꺼내 가는 것과 비슷한 동작으로 지속적으로 파티션의 리더가 새로운 메시지를 받았는지 확인하고, 새로운 메시지가 있다면 해당 메시지를 리더로부터 복제합니다.
[ 복제 유지와 커밋 ]
리더와 팔로워는 ISR 이라는 논리적 그룹으로 묶여 있습니다. 이렇게 리더와 팔로워를 별도의 그룹으로 나누는 이유는 기본적으로 해당 그룹 안에 속한 팔로워들만이 새로운 리더의 자격을 가질 수 있기 때문입니다.
즉, ISR 그룹에 속하지 못한 팔로워는 새로운 리더의 자격을 가질 수 없습니다.
파티션의 리더는 팔로워들이 뒤쳐지지 않고 리플리케이션 동작을 잘하고 있는지 감시합니다.
리더에 뒤쳐지지 않고 잘 따라잡고 있는 팔로워들만이 ISR 그룹에 속하게 되며, 리더에 장애가 발생할 경우 새로운 리더의 자격을 얻을 수 있는 것입니다.
리더는 읽고 쓰는 동작은 물론, 팔로워가 리플리케이션 동작을 잘 수행하고 있는지도 판단합니다.
- 만약 팔로워가 특정 주기의 시간만큼 복제 요청을 하지 않는 다면, 리더는 해당 팔로워가 리플리케이션 동자겡 문제가 발생했다고 판단해 ISR 그룹에서 추방합니다.
ISR 내에서 모든 팔로워의 복제가 완료되면, 리더는 내부적으로 커밋되었다는 표시를 하게됩니다. 마지막 커밋 오프셋 위치는 하이워터마크라고 부릅니다. 즉, 커밋되었다는 것은 리플리케이션 팩터 수의 모든 리플리케이션이 전부 메시지를 저장했음을 의미합니다.
그리고 이렇게 커밋된 메시지만 컨슈머가 읽어갈 수 있습니다. 카프카에서 커밋되지 않은 메시지를 컨슈머가 읽을 수 없게 하는 이유는 바로 메시지의 일관성을 유지하기 위해서입니다.
위 그림에서 리더 파티션에 test message2가 전송 된 이후 리플리케이션이 작동하지 않은 상태에서 리더 파티션에 문제가 생겼다고 가정해보겠습니다.
만약 허용되지 않은 커밋 읽기를 허용한다면 컨슈머는 test message2 까지 읽게 될 것이고 이후 새로운 리더 파티션을 컨슈밍 하게 될 때 메시지 일관성이 유지되지 않았다는 것을 알 수 있습니다. 따라서 카프카에서는 이러한 메시지 불일치 현상을 방지하고자 커밋된 메시지만 컨슈머가 읽어 갈 수 있도록 구현되어 있습니다.
모든 브로커는 재시작될 때, 커밋된 메시지를 유지하기 위해 로컬 디스크의 replication-offset-checkpoint라는 파일에 마지막 커밋 오프셋 위치를 저장합니다.
[ 리더와 팔로워의 단계별 리플리케이션 동작 ]
카프카는 리더와 팔로워 간의 리플리케이션 동작을 처리할 때 서로의 통신을 최소화할 수 있도록 설계함으로써 리더의 부하를 줄였습니다.
리더는 팔로워들이 보내는 리플리케이션 요청의 오프셋을 보고 팔로워들이 어느 위치의 오프셋까지 리플리케이션을 성공했는지를 인지할 수 있습니다.
만약 모든 리플리케이션에서 0번 오프셋을 저장한 상태에서 리더에 존재하는 1번 오프셋 메시지를 리플리케이션 하는 과정이라고 가정해보겠습니다.
- 팔로워들은 리더에게 0번 오프셋에 대한 리플리케이션 동작을 마쳤기 때문에 1번 오프셋에 대한 리플리케이션을 요청합니다.
- 팔로워들로부터 1번 오프셋에 대한 리플리케이션 요청을 받은 리더는 팔로워들의 0번 오프셋에 대한 리플리케이션 동작이 성공했음을 인지하고, 오프셋 0에 대해 커밋 표시를 한 후 하이우터마크를 증가시킵니다.
- 팔로워가 0번 오프셋에 대한 리플리케이션을 성공하지 못했다면, 팔로워는 1번 오프셋에 대한 리플리케이션 요청이 아닌 0번 오프셋에 대한 리플리케이션 요청을 보내게 됩니다. 따라서 리더는 팔로워들이 보내는 리플리케이션 요청의 오프셋을 보고, 팔로워들이 어느 위치의 오프셋까지 리플리케이션을 성공했는지를 인지할 수 있습니다.
- 리더의 응답을 받은 모든 팔로워는 0번 오프셋 메시지가 커밋되었다는 사실을 인지하게 되고, 리더와 동일하게 커밋을 표시합니다.
- 그리고 1번 오프셋 메시지인 message2를 리플리케이션합니다.
여타 메시징 시스템들은 리플리케이션 동작에서 리더와 팔로워가 메시지를 잘 받았는지 확인하는 ACK 통신을 하지만, 카프카는 ACK 통신 단계를 제거했습니다.
카프카의 또 다른 장점으로 리플리케이션 동작에서 ACK 통신을 제외했음에도 불구하고 리플리케이션 동작이 매우 빠르면서도 신뢰할 수 있다는 점입니다. 리더가 push 하는 것이 아니라 팔로워들이 pull 하는 방식으로 동작하는데, 이러한 방식은 리더의 부하를 줄여주기 위함입니다.
[ 리더에포크와 복구 ]
리더에포크는 카프카의 파티션들이 복구 동작을 할 때 메시지의 일관성을 유지하기 위한 용도로 이용됩니다.
팔로워는 자신의 하이워터마크보다 높은 오프셋의 메시지를 무조건 삭제하지 않고, 먼저 리더에게 리더에포크 요청을 보내 응답을 받아서 최종 커밋된 오프셋 위치를 확인합니다. 이로서 리더에포크를 활용하는 카프카는 안정적인 복구 동작을 할 수 있습니다.
2. 컨트롤러
컨트롤러는 레플리케이션들의 리더 선출을 맡고 있습니다.
카프카 클러스터 중 하나의 브로커가 컨트롤러 역할을 하게 되며, 파티션의 ISR 리스트 중에서 리더를 선출합니다.
리더를 선출하기 위한 ISR 리스트 정보는 안전한 저장소에 보관되어 있어야 하는데, 가용성 보장을 위해 주키퍼에 저장되어 있습니다.
컨트롤러는 브로커가 실패하는 것을 에의주시하고 있으며, 만약 브로커의 실패가 감지되면 즉시 ISR 리스트 중 하나를 새로운 파티션 리더로 선출합니다.
그러고 나서 새로운 리더의 정보를 주키퍼에 기록하고, 변경된 벙보를 모든 브로커에게 전달합니다.
[ 제어된 종료 vs 급작스러운 종료 ]
제어된 종료를 사용하면 카프카 내부적으로 파티션들의 다운타임을 최소화할 수 있습니다. 그 이유는 브로커가 종료되기 전, 컨트롤러는 해당 브로커가 리더로 할당된 전체 파티션에 대해 리더 선출 작업을 진행하기 때문입니다.
리더 선출 작업 대상 파티션들의 리더들이 활성화된 상태에서 컨트롤러는 순차적으로 하나의 파티션마다 리더를 선출하게 되므로, 결과적으로 각 파티션들은 다운타임을 최소화할 수 있습니다.
다양한 장점이 있는 제어된 종료를 사용하려면 controlled.shutdown.enable = true 설정이 브로커의 설정 파일인 server.properties에 적용되어야 합니다.
3. 로그(로그 세그먼트)
카프카의 토픽으로 들어오는 메시지는 세그먼트라는 파일에 저장됩니다. 메시지는 정해진 형식에 맞추어 순차적으로 로그 세그먼트 파일에 저장됩니다.
로그 세그먼트 파일은 브로커의 로컬 디스크에 보관됩니다.
카프카에 기본적으로 로그 세그먼트 파일에 대한 롤링 전략이 준비되어 있긴 하지만, 카프카 관리자는 1GB 크기의 로그 세그먼트 파일이 무한히 늘어날 경우를 대비해 로그 세그먼트에 대한 관리 계획을 수립해둬야 합니다.
[ 로그 세그먼트 삭제 ]
카프카에서는 로그 세그먼트 파일을 생성할 때 오프셋 시작 번호를 이용해 파일 이름을 생성하는 규칙을 따릅니다.
카프카의 관리자는 토픽마다 보관 주기를 조정해서, 얼마만큼의 기간 동안 카프카에 로그를 저장할지를 결정하고 관리할 수 있습니다.
retention.ms 옵션을 통해 보관 주기를 정할 수 있고 기본 설정값은 7일입니다.
추가로 retention.bytes라는 옵션을 이용해 지정된 크기를 기준으로도 로그 세그먼트를 삭제할 수 있습니다.
[ 로그 세그먼트 컴팩션 ]
컴팩션은 카프카에서 제공하는 로그 세그먼트 관리 정책 중 하나로, 로그를 삭제하지 않고 컴팩션하여 보관할 수 있습니다. 로그 컴팩션은 기본적으로 로컬 디스크에 저장되어 있는 세그먼트를 대상으로 실행되는데, 현재 활성화된 세그먼트는 제외하고 나머지 세그먼트들을 대상으로 컴팩션이 실행됩니다.
카프카에서 로그 세그먼트를 컴팩션하면 메시지의 키값을 기준으로 마지막의 데이터만 보관하게 됩니다.
로그 컴팩션은 메시지의 키값을 기준으로 과거 정보는 중요하지 않고 가장 마지막 값이 필요한 경우에 사용합니다. 한 예시로 카프카의 _consumer_offset 토픽입니다. 즉, 해당 컨슈머 그룹이 어디까지 읽었는지 나타내는 마지막 오프셋만 저장하면 되기 때문에 로그 컴팩션을 사용하는 대표적인 예시입니다.
- 일반적으로 카프카로 메시지를 전송할 때, 메시지에 키는 필숫값이 아니지만, 로그 컴팩션 기능을 사용하고자 한다면, 카프카로 메시지를 전송할 때 키도 필숫값으로 전송해야 합니다.
- 로그 컴팩션의 장점은 바로 빠른 장애 복구입니다.
- 빠른 재처리라는 장점이 있다고 해서 모든 토픽에 로그 컴팩션을 적용하는 것은 좋지 않습니다. 키 값을 기준으로 최종값만 필요한 워크로드에 적용하는 것이 바람직합니다.
카프카에서 로그 컴팩션 작업이 실행되는 동안 브로커의 과도한 입출력 부하가 발생할 수 있으니 유의해야 합니다. 따라서 반드시 브로커의 리소스 모니터링도 병행하여 로그 컴팩션을 사용하기를 권장합니다.
이번 글에서는 카프카의 내부 동작에 대해 알아봤는데 다음 글에서는 프로듀서에 대한 글로 이어가겠습니다.
'Backend > kafka' 카테고리의 다른 글
카프카 기본 개념과 구조 (0) 2023.07.26