kafka 첫번째, 개념 정리

kafka 첫번째, 개념 정리

카프카 첫 번째, 상세 개념 다루기

카프카

데이터 엔지니어를 하면서 꼭 다뤄봐야할 것 중에 하나가 실시간 처리입니다. 이 실시간 처리에 다양한 기술들이 사용되지만, 카프카가 특히 많이 다뤄지고 있습니다. 카프카는 LinkedIn의 점차 복잡해지고 거대해지는 아키텍쳐에 따라 개발되었습니다. 가면 갈수록 LinkedIn의 데이터 파이프라인이 복잡해지면서 소스와 타깃의 연결을 정리할 필요성이 생겼고, 데이터를 한 곳에 모아 처리할 수 있도록 중앙집중화 해, 관리할 수 있도록 만들었던 것이 카프카 입니다.

Kafak가 적용되면 한 곳에 데이터를 모아 처리할 수 있다

이런 식으로 한 번 정리가 된다면, 복잡성을 떨어트릴 수 있고, 시스템간 의존도를 줄일 수 있겠습니다. 이런 구조를 통해서 카프카는 대용량 데이터를 수집하고, 이 데이터를 실시간 스트림으로 소비할 수 있도록 만들어주게 되었고, 이외에도 다양한 기능을 통해서 카프카는 실시간 처리 분야에서 중요한 플랫폼으로서 기능하게 되었습니다.



카프카의 기본 개념

카프카는 발행-구독(Publish & Subscribe)의 구조로 이루어져 있습니다. 쉽게 말해서 한 쪽에서 어떤 주제에 대해서 메세지를 보내면 이것을 구독했을때, 관련 주제의 메세지만을 받는 구조인 것입니다. 따라서 이 구조에서는 세 컴포넌트가 등장합니다. 바로 Producer, Consumer, Broker 입니다.

카프가 기본 구조

위 그림에서처럼, 프로듀서가 메세지를 브로커에 보내면, 주제에 맞게 이 메세지들을 보관하고 있다가 컨슈머 그룹에서 이 메세지를 가져가는 구조입니다. 프로듀서와 컨슈머는 대충 이름만 보고도 어떤 일을 하는지는 알 수 있을 것 같습니다. 그런데 브로커는 정확히 어떤 일을 하는 것일까요?



카프카 브로커, 클러스터, 주키퍼

카프카 브로커는 데이터를 주고받기 위한 주체입니다. 이 브로커를 통해서 데이터를 분산 저장하며, 이를 통해 장애가 발생하더라도 안정적으로 서비스를 운영할 수 있습니다. 하나의 서버에는 한 개의 카프카 브로커 프로세스가 실행됩니다. 카프카 브로커 서버는 1대로도 기능을 할 수 있지만, 안전성을 위해서 3대 이상의 서버를 1개의 클러스터로 묶어서 운영하는 것을 권장합니다. 이 클러스터 안에 있는 브로커들은 프로듀서가 보낸 데이터를 분산해서 저장해서 안전하게 서비스를 운영할 수 있도록 합니다.

브로커, 클러스터, 주키퍼

브로커가 데이터를 프로듀서로부터 전달받으면 프로듀서가 요청한 토픽의 파티션에 데이터를 저장하고, 컨슈머가 데이터를 요청하게되면 파티션에서 저장된 데이터를 전달합니다. 이렇게 전달된 데이터는 파일 시스템에 저장되고 이는 /tmp/kafka-logs에서 확인할 수 있습니다. 이 설정은 config/server.propertieslog.dir옵션에 정의된 디렉토리기 때문에, 원한다면 설정을 변경해도 무방합니다. log에는 메시지와 메타데이터를 저장하고 index에는 오프셋을 인덱싱한 정보를 담았습니다. timeindex에는 메시지에 포함된 timestamp값을 기준으로 인덱싱한 정보가 담겨있는데, timestamp는 브로커가 적재한 데이터를 삭제하거나 압축하는 데 사용합니다.

카프카를 좀 더 살펴보다 보면 속도 이슈에 대한 의문이 들 수 있습니다. 왜냐하면 메세지가 저장된 다음 다시 읽어야 하기 때문입니다. 보통 디스크 IO는 속도 이슈에 치명적인 요소로 보이곤 합니다. 하지만 카프카는 페이지 캐시를 사용해 디스크 입출력 속도를 높였습니다. 이 페이지 캐시는 OS에서 파일 입출력의 성능 향상을 위해 만들어 놓은 메모리 영역으로, 한 번 읽은 것은 페이지 캐시 영역에 저장하고 추후에 동일 파일 접근이 일어나면 여기에서 읽게 됩니다. 그래서 브로커를 실행하는데 힙 메모리 사이즈를 크게 가져갈 필요가 없게되는 것입니다.

replication은 장애 허용 시스템으로 동작하게 하는 원동력으로 데이터 복제는 파티션 단위로 이루어집니다. 만약 토픽 생성할 옵션을 직접 선택하지 않으면 브로커에 있는 설정대로 구동하게 됩니다. 이 복제의 개수의 최대 값은 브로커 개수만큼 주는 것이 좋은데, 복제 개수만큼 저장용량이 증가하는 단점이 있습니다. 하지만 그 만큼 안정성이 증가하는 장점이 있습니다.

파티션에는 리더와 팔로워 개념이 등장합니다. 복제된 파티션의 구분으로 둘을 나눠놓았는데, 여기서 리더란 프로듀서 또는 컨슈머와 직접 통신하는 파티션을 뜻하는 것이고, 나머지가 팔로워 파티션이 됩니다. 팔로워는 리더 파티션의 오프셋을 확인해서 현재 자신이 갖고 있는 오프셋과 차이가 나게 된다면, 리더 파티션으로부터 데이터를 갖고 와서 자신의 파티션에 저장합니다. 이렇게 데이터를 갖고와서 자기 파티션에 저장하는 것이 복제라 하는 것입니다. 만약 리더가 죽는다면 어떻게 될까요? 카프카의 장점은 안정성입니다. 만약에 리더가 다운되면 팔로워 파티션 중 하나가 리더 지위를 넘겨받게되어 정상적으로 기능하게 됩니다.

여기서, 브로커 중에 한 대는 컨트롤러의 역할을 맡게 됩니다. 이 컨트롤러는 중요한 요소로서, 다른 브로커들의 상태를 체크하고 브로커가 클러스터에서 빠지게 되었을 때, 해당 브로커에 있는 리더 파티션을 재분배하는 기능을 하게 됩니다. 카프카는 지속적으로 데이터를 처리해야 해서 브로커의 상태가 비정상이라면 빠르게 클러스터에서 빼내고, 새로운 브로커를 지정해 문제를 해결합니다. 만약 컨트롤러 브로커에 장애가 생기면, 다른 브로커가 컨트롤러를 맡게됩니다.

카프카 토픽의 데이터는 참고로 삭제가 되지 않습니다. 브로커만 데이터를 삭제할 수 있습니다. 또한 데이터 삭제는 파일 단위로 이루어지는데, 이 단위를 로그 세그먼트라고 부릅니다. 이 세그먼트에는 다수의 데이터가 들어있어서, 데이터를 구분해서 삭제할 수는 없습니다. 세그먼트에 데이터가 쌓이는 동안 파일이 열려있는 상태가 되고, log.segment.bytes, log.segment.ms 옵션에 따라 조건이 충족되면, 세그먼트 파일이 닫히게 됩니다. log.segment.bytes의 기본값은 1GB입니다. 이 용량 설정에서 너무 작은 용량을 설정하게 되면, 세그먼트 파일을 자주 여닫게 되어 부하가 발생할 수 있습니다.

컨슈머 오프셋 저장을 할때, log.dir 옵션에 지정된 경로로 가면 consumer_offset 디렉토리가 있는 것을 볼 수 있습니다. 컨슈머 그룹은 토픽이 특정 파티션으로 부터 데이터를 가져가서 처리하고 나서 이 파티션의 어느 레코드까지 가져갔는지 확인하기 위해 오프셋을 커밋합니다. 커밋한 오프셋이 바로 consumer_offset 디렉토리에 저장됩니다. 여기에 저장된 오프셋을 토대로 컨슈머 그룹은 다음 레코드를 가져가서 처리하게 됩니다.

아까 브로커 중 한 대는 컨트롤러를 맡는다고 했는데, 다수 브로커 중 한 대는 또 코디네이터 역할을 맡습니다. 코디네이터는 컨슈머 그룹의 상태를 체크하고 파티션을 컨슈머와 매칭되도록 분배합니다. 컨슈머가 컨슈머 그룹에서 빠지게 되면, 매칭되지 않은 파티션을 정상 동작하는 컨슈머로 할당하여 데이터 처리가 이어지도록 하는 거입니다. 파티션을 컨슈머로 재 할당하는 이 과정을 리밸런스라고 합니다.

참고로, 주키퍼는 카프카의 메타데이터를 관리합니다. bin/zookeeper-shell.sh 을 통해서 주키퍼로 직접 접속할 수 있습니다. ls /를 통해서 원하는 정보를 탐색해서 볼 수 있다. 주키퍼에서 다수의 카프카 클러스터를 사용하려면 주키퍼의 서로 다른 znode에 카프카 클러스터들을 설정하면 됩니다. znode란 주키퍼가 사용하는 데이터 저장 단위입니다. znode는 znode간에 tree구조와 같은 계층 구조를 가집니다. 이 구조에서 root znode가 아닌 한 단계 아래의 znode를 카프카 브로커 옵션으로 지정하도록 합니다. 이렇게 설정되었을 때, 각기 다른 하위 znode로 설정된 서로 다른 카프카 클러스터는 각 클러스터의 데이터에 영향을 미치지 않고 정상 동작하게 됩니다.



카프카 토픽, 파티션, 컨슈머

토픽파티션은 카프카에서 데이터를 구분하기 위해 사용하는 단위로, 토픽은 1개 이상의 파티션을 소유하고 있습니다. 프로듀서가 보낸 데이터들이 파티션에 저장되는데 이것을 레코드라고 부릅니다. 결국 파티션에는 오프셋, 메세지 키, 메세지 값으로 되어있는 레코드가 저장되는 것입니다. 이렇게 레코드가 저장된 파티션을 여러개로 나눔으로써 카프카의 병렬처리의 핵심으로 기능하게 됩니다. 파티션은 큐와 비슷한 구조로, 먼저 들어간 레코드를 컨슈머가 먼저 가져갑니다. pop이 되지 않는 것이 큐와의 차이점이다. 데이터가 삭제되지 않기 때문에 다양한 목적을 가진 여러 컨슈머 그룹들이 토픽의 데이터를 여러 번 가져갈 수 있는 것입니다.

컨슈머그룹은 레코드를 병렬로 처리할 수 있도록 파티션과 매칭됩니다. 만약 컨슈머 처리량이 제한되어 있다면, 최대한 많은 레코드를 병렬로 처리하는 가장 좋은 방법 중 하나는 컨슈머 개수를 늘려 스케일 아웃 하는 것입니다. 모니터링을 하며 컨슈머 개수를 늘리면서 동시에 파티션 개수도 늘리게 되면 처리량이 증가합니다.

토픽 이름을 지을 때는 이름을 잘 지어야 합니다. 이해하기 어려운 토픽이름은 기술 부채가 되기도 하며, 토픽 이름 변경은 카프카에서 지원이 되지 않으므로 변경이 필요하면 삭제 후 다시 생성해야 하기 때문입니다. Jira 티켓 번호를 넣는 것도 괜찮은 방법 중 하나이고, <환경>.<팀명>.<애플리케이션명>.<메시지타입>등 카프카를 관리하고 사용하는 그룹에서 규칙을 정하고 잘 따라야 합니다.

레코드는 타임스탬프, 메세지 키, 값, 오프셋, 헤더로 구성되어 있습니다. 프로듀서가 생성한 레코드가 브로커로 전송되면 오프셋과 타임스탬프가 지정되어 저장됩니다. 브로커에 한 번 적재된 레코드는 수정이 불가능하고 로그 리텐션 기간 또는 용량에 따라서 자동으로 삭제됩니다. timestamp는 프로듀서에서 해당 레코드가 생성된 시점의 유닉스 타임이 설정됩니다. 여기서 프로듀서가 레코드를 생성할 때 임의의 타임스탬프 값을 설정할 수 있고, 토픽 설정에 따라 브로커에 적재된 시간으로 설정될 수 있습니다. 메세지 키는 메시지 값을 순서대로 처리하거나 메세지 값의 종류를 나타내기 위해 사용됩니다. 키의 해시값을 토대로 파티션을 지정하게 되는데, 동일 메세지 키는 동일 파티션에 들어가고 파티션 개수가 변경되면 메세지 키와 파티션 매칭이 달라집니다. 만약 키를 설정 하지 않으면 null로 설정됩니다. 기본 설정 파티셔너에 따라 파티션에 분배되며, 메세지 키와 값은 직렬화되어 브로커로 전송되어 컨슈머가 이용할 때는 역직렬화를 수행합니다.

컨슈머 운영 방법은 크게 2가지입니다. 1개 이상의 컨슈머로 이루어진 컨슈머 그룹 운영하거나, 토픽의 특정 파티션만 구독하는 컨슈머 운영하는 것입니다.

컨슈머 그룹으로 운영

컨슈머 그룹으로 운영하면, 각 컨슈머 그룹으로 부터 격리된 환경에서 안전하게 운영할 수 있습니다. 이렇게 컨슈머 그룹으로 묶인 컨슈머들은 토픽의 1개이상 파티션들에 할당되어 데이터를 가져갈 수 있게 됩니다. 1개의 파티션은 최대 1개의 컨슈머에 할당될 수 있으며 1개 컨슈머는 여러 파티션에 할당 될 수 있습니다. 컨슈머 그룹의 컨슈머 개수는 가져가고자 하는 토픽의 파티션 개수보다 같거나 작아야 하는데 만약 컨슈머가 더 많다면, 남는 컨슈머는 파티션을 할당받지 못하고 유휴상태로 남게 되는 것을 주의해야 합니다. 이렇게 되면 스레드만 차지하고 실질적인 처리를 못하므로, 불필요한 스레드로 남게됩니다. 하지만, 컨슈머 그룹으로 운영하면 다른 컨슈머 그룹의 영향을 받지 않고 처리할 수 있습니다. 컨슈머 그룹이 아니라면, 동기적으로 처리를 해야할 수 있는데, 이렇게 되면 어떤 한 적재 파이프라인에 장애가 발생했을때 다른 곳에 적재가 불가능해집니다. 따라서 컨슈머 그룹으로 나눌 수 있는 것은 최대한 나누는 것이 좋습니다.

그런데 만약, 컨슈머 그룹 컨슈머에 장애가 발생한다면 어떻게 될까요? 그렇게 되면, 장애가 발생한 컨슈머에 할당된 파티션은 장애가 없는 컨슈머에 소유권이 넘어가게 됩니다. 이렇게 되면 리밸런싱이 일어나게 됩니다. 리밸런싱이 일어나는 경우는 크게 2가지 경우입니다.

  • 컨슈머가 추가 될 때
  • 컨슈머가 제외될 때

다만, 리밸런싱은 자주 일어나서는 안됩니다. 왜냐하면 리밸런싱이 발생할 때 재할당하는 과정에서 해당 컨슈머 그룹의 컨슈머들이 토픽의 데이터를 읽을 수 없기 때문입니다. group coordinator는 리밸런싱을 발동시키는데 컨슈머 그룹의 컨슈머가 추가되고 삭제되는 것을 감지합니다. 카프카 브로커 중 한 대가 그룹 조정자 역할을 수행하며 __consumer_offsets 에 기록된 커밋을 통해서 레코드의 어디 까지 읽었는지 파악해서 데이터 처리 중복을 피합니다.



Reference

Author

SangHyub Lee, Jose

Posted on

2021-12-19

Updated on

2023-12-08

Licensed under

Comments