5 분 소요

image



카프카 살펴보기

토픽(Topic)과 파티션(Partition) 그리고 세그먼트 파일(Segment File)

카프카에 전달되는 메시지 스트림의 추상화된 개념을 토픽(Topic)이라고 한다. 프로듀서는 메시지를 특정 토픽에 발행한다. 컨슈머는 특정 토픽에서 발행되는 메시지를 구독할 수 있다. 토픽은 프로듀서와 컨슈머가 만나는 접점이라고 생각하면 된다.

image

프로듀서가 메시지를 특정토픽에 전송하면 카프카 클러스터는 토픽을 좀 더 세분화된 단위인 파티션(Partition)으로 나누어 관리한다. 기본적으로 프로듀서는 발행한 메시지가 어떤 파티션에 저장되는지 관여하지 않는다 (물론 메시지 키와 파티셔너를 이용하여 특정 파티션으로 메시지를 전송할 수 있도록 할 수도 있다.) 각 파티션은 카프카 클러스터를 구성하는 브로커들이 고루 나눠 가진다. (카프카 클러스터의 브로커 중 한 녀석이 컨트롤러(Controller)가 되어 이 분배 과정을 담당한다. 컨트롤러는 카프카 클러스터의 반장 역할이라고 보면된다)

특정 파티션으로 전달된 메시지에는 오프셋(Offset)이라는 숫자가 할당된다. 오프셋은 해당 파티션에서 몇 번째 메시지인지 알 수 있는 ID 같은 개념이라고 생각하면 된다. 오프셋을 이용해서 컨슈머가 메시지를 가져간다. 몇 번째 오프셋까지 읽었다, 몇 번째 오프셋부터 읽겠다는 요청을 할 수 있다. 오프셋은 파티션 내에서 유일한(Unique) 값을 갖는다.

카프카 브로커는 파티션에 저장된 메시지를 파일 시스템에 저장한다. 이 때 만들어지는 파일이 ‘세그먼트 파일(Segment File)’이다. 기본적으로 1GB까지 세그먼트 파일이 커지거나 일정 시간이 지나면 파일을 다시 만든다. 보존기간이 지난 메시지가 지워질 때 세그먼트 파일 단위로 지워진다.

파티션의 복제(Replication)

카프카는 고가용성(high Availability)을 제공하기 위해 파티션 데이터의 복사본(Replication)을 유지할 수 있다. 몇 개의 복사본을 저장할 것인지는 리플리케이션 팩터(Replication Factor)로 저장할 수 있으며 토픽별로 다르게 설정할 수 있다.

만약 토픽의 리플리케이션 팩터를 N으로 설정하면 N개의 파티션 데이터 복사본이 생성되고 카프카 브로커가 겹치지 않게 나눠갖는다. N개의 복사본은 리플리카(Replica)라고 하며 N개 중 1개의 리플리카가 리더(Leader)로 선정되어 클라이언트 요청을 담당한다. 나머지 N - 1개의 리플리카는 팔로워(Follower)가 되어 리더의 변경사항을 따라가기만 한다. 프로듀서와 컨슈머의 쓰기, 읽기 요청은 리더 리플리카에만 전송되며 클라이언트 설정에 따라 팔로워들에게 전송되기까지 기다릴 수도 있고, 리더에게만 전송될 수도 있다.

리더의 변경사항을 잘 따라가면서 복사를 하는 팔로워는 ISR(In-Sync Replica)를 구성하며 리더 리플리카를 담당하는 브로커에 장애가 생겼을 때, ISR에 속한 리플리카가 새로운 리더로 선정되어 클라이언트 요청을 담당하게 된다. 만약 ISR에 있는 리플리카가 리더의 변경 사항을 미처 따라가지 못하면 ISR에서 빠지게 된다. (리더의 변경을 따라가지 못한 팔로워가 새로운 리더가 되면 데이터가 유실되기 때문에..)

파티션의 리더와 팔로워는 다른 브로커에 할당해야 고가용성을 보장할 수 있다. 카프카 버전 0.10.0부터는 랙(Rack)을 식별할 수 있는 정보도 명시할 수 있다. 즉, 데이터 센터에서 같은 랙에 있는 서버는 전원이나 네트워크 스위치를 공유할 가능성이 있어 같이 장애가 발생할 수 있다. 리더와 팔로워 둘 중 하나는 장애에서 살아남아야 고가용성이 보장되기 때문에 다른 랙으로 할당할 수 있게 랙 정보를 입력할 수 있는 기능이 제공된다.

프로듀서(Producer)와 컨슈머(Consumer), 컨슈머 그룹(consumer Group)

카프카의 클라이언트는 기본적으로 프로듀서(Producer)와 컨슈머(Consumer)라는 두 가지 분류가 존재한다.

image

프로듀서(Producer)는 메시지를 생성하여 카프카에 전달하는 클라이언트를 의미한다. 프로듀서가 특정 토픽에 메시지를 전송하면 기본적으로 여러 파티션에 번갈아가며 전송되어 파티션을 골고루 사용하게 된다. 전송 순서가 중요한 메시지는 메시지에 키(Key)값을 할당하고 이 키를 기반으로 특정 파티션에 전송되도록 파티셔너를 작성할 수도 있다.

프로듀서에게 유의해야 할 점은 서로 다른 파티션으로 전송된 메시지의 소비 순서는 보장되지 않는다는 것이다. 첫 번째 메시지가 0번 파티션으로 전송되고 두 번째 메시지가 1번 파티션으로 전송되었을 때, 컨슈머가 1번 파티션에 있는 두 번째 메시지를 먼저 소비할 수도 있다. 만약 메시지의 처리 순서가 중요한 경우라면 메시지 키와 파티셔너를 이용해 두 개의 메시지가 같은 파티션으로 전송되도록 추가적인 작업이 필요하다. 즉, 카프카로 전송된 메시지는 같은 파티션일 경우에만 순서가 보장된다.

image

컨슈머(Consumer)는 메시지를 카프카로부터 읽어가는 클라이언트다. 카프카의 컨슈머는 컨슈머 그룹(consumer Group)을 형성한다. 카프카의 토픽은 컨슈머 그룹 단위로 구독된다. 토픽의 파티션은 컨슈머 그룹당 오로지 하나의 컨슈머의 소비만 소비될 수 있다. 파티션과 컨슈머의 이런 연결을 소유권 (Ownership)이라고 부른다. 다시말해서 같은 컨슈머 그룹에 속한 컨슈머들이 동시에 동일한 파티션에서 메시지를 읽어갈 수 없다.

파티션과 컨슈머의 Ownership 관계는 브로커와 컨슈머의 구성이 변경되지 않는 이상 계속 유지된다. 즉, 컨슈머 그룹에 컨슈머가 추가 혹은 제거된 경우 컨슈머 그룹내에서 파티션의 소유권을 재분배하는 리밸런싱(Rebalancing) 과정이 실행된다. 리밸런싱을 통해 컨슈머 그룹 내의 컨슈머들이 파티션을 고르게 할당받아 소비할 수 있게 된다. 카프카 클러스터에 브로커가 추가/제거되는 경우 전체 컨슈머 그룹들에서 리밸런싱이 발생한다.

컨슈머 그룹의 컨슈머 수가 토픽의 파티션 수보다 많은 경우, 파티션 개수만큼의 컨슈머만 동작하며 나머지 잉여 컨슈머들은 놀게 된다. 따라서 파티션 개수와 컨슈머 그룹내 컨슈머 개수의 적절한 조정이 필요하다.

컨슈머 그룹은 각 파티션에 대해 오프셋(Offset)값을 할당받는다. 이 오프셋은 파티션에 저장된 메시지에 할당된 오프셋 값으로 컨슈머 그룹이 해당 파티션에서 어디까지 읽었는지를 의미한다. 따라서 특정 컨슈머 그룹에 컨슈머가 추가, 제거되어 리밸런싱이 일어났을때 다른 컨슈머가 파티션을 할당받아도 내 컨슈머 그룹이 어디까지 읽었는지 기록이 유지되기 때문에 이어서 처리할 수 있게 된다.

컨슈머가 카프카로부터 메시지를 읽어서 처리한 다음 ‘여기까지 읽어서 처리했어요’라고 컨슈머 그룹에 할당된 오프셋을 변경하는 작업을 오프셋 커밋(Commit) 이라고 한다. 오프셋 커밋을 잘 처리하지 않으면 카프카 클라이언트를 사용하는 애플리케이션에서 메시지 누락이 발생하거나 불필요한 중복 처리가 발생할 수 있으므로 잘 이해하고 사용해야 한다.

Kafka와 Filesystem

카프카로 전송된 메시지는 카프카 내부에서 세그먼트 파일 형태로 저장된다. 파일을 파일 시스템에 기록하여 메시지의 영속성(Persistence)을 얻는다. 즉 나중에 다시 특정 메시지를 소비하고 싶을 때 파일 시스템에 저장된 메시지를 읽어서 컨슈머에게 전송할 수 있다.

파일 시스템을 사용할 때, 일반적으로 Durability는 보장되지만 속도가 느리다는 단점이 있다. 하지만 세대가 거듭되면서 운영체제(OS)와 파일 시스템의 하드디스크 최적화가 진행되었고, 하드디스크의 물리적인 구성에 따라 순차 읽기(Sequential read)와 미리 읽기(Read Ahead) 등의 최적화를 통해 제법 빠르게 사용할 수 있게 되었다. (Kafka 문서를 보면, ACM Queue에 게재된 글에서 하드디스크의 순차 읽기 성능이 메모리의 랜덤 읽기 성능보다 빠르다는 내용을 찾아볼 수 있다. 물론 메모리의 순차 읽기가 더 빠르며 하드디스크의 랜덤 읽기는 참혹할 정도로 느리다.)

카프카는 파일 시스템에 세그먼트 파일을 쓰는 동작에서 별도의 버퍼 캐시를 구현하는 대신 운영체제의 페이지 캐시를 사용했다. 운영체제는 사용하지 않는 메모리를 파일 시스템의 페이지 캐시로 사용하며, 사용자가 요청하지 않아도 미리 읽기(Read Ahead) 동작을 통해 앞으로 읽을 가능성이 있는 뒤쪽 내용을 미리 메모리를 읽어들이는 최적화를 진행한다. 또 카프카 내부에서 버퍼캐시를 운영하지 않기 때문에 JVM에 의해 발생할 수 있는 GC(Garbage Collection) 오버헤드도 줄인다. 또한 브로커를 재시작하는 경우에도 페이지 캐시는 커널 영역에 남아있으므로 웜업되어 있는 상태로 서비스를 시작할 수 있게 된다.

효율성(Efficiency)

카프카는 효율성(Efficiency)을 극대화하기 위해 매우 노력했다.

우선 메시지의 크기가 작은 경우 네트워크 오버헤드가 상대적으로 커질 수 있는 상황을 해소하기 위해 메시지 셋(Message Set) 단위로 메시지를 모아서 처리하는 배치(Batch) 처리를 가능하도록 기능을 제공했다. 메시지를 모아서 처리하는 배치를 이용해서 네트워크 오버헤드도 줄일 수 있고, 디스크에 최대한 연속적으로 메시지를 쓸 수 있는 순차처리의 장점을 얻을 수도 있다.

또한, 디스크와 네트워크 채널 사이에 데이터 전송 시 발생할 수있는 오버헤드를 줄이기 위해 sendfile 시스템 호출을 이용하는 제로카피(Zero-copy) 기법을 사용하여 성능향상을 도모했다.

아파치 카프카는 손쉽게 데이터 파이프라인을 구축할 수 있는 오픈소스 프로젝트다. 손쉽게 사용할 수 있지만 제대로 사용하고 튜닝하려면 내부 동작을 잘 알아야 한다. 관련 내부 동작 공부를 심도 있게 해야 할 필요가 있을 것 같다.




참고자료

  • https://www.confluent.io/resources/kafka-the-definitive-guide/
  • http://notes.stephenholiday.com/Kafka.pdf
  • https://kafka.apache.org/intro
  • https://soft.plusblog.co.kr/

태그:

카테고리:

업데이트:

댓글남기기