카프카는 왜 빠른가

프로그램 구현 레벨까지 가면 여러가지 이유를 댈 수 있겠지만, 카프카를 고성능으로 만드는 결정적인 이유를 말하라면 네 가지를 들 수 있겠다.

 

1. 시퀀셜 IO

일반적으로 RAM은 랜덤 엑세스를 지원하지만, 디스크는 그렇지 않다. 디스크는 원하는 데이터의 위치한 블록을 찾기 위한 시간(seek time), 블록을 메모리에 카피하는 시간 등의 오버헤드가 존재한다. 데이터가 캐시나 램에 이미 존재하면 이 과정은 생략될 수 있지만, 그렇지 않을 경우(page fault) 이러한 동작을 반복적으로 필요로 한다. 이런 점으로 인해 디스크는 일반적으로 느리다고 인식되곤 한다.

 

용량을 기준으로 볼 때 RAM은 디스크보다 훨씬 비싸다. 대용량 데이터를 핸들링하는 카프카가 그 모든 데이터를 RAM에 올리기는 현실적으로 어려운 일이다. 때문에 카프카는 디스크를 저장소로 사용하여 작동하는데, 대신 시퀀셜 디스크 엑세스를 통해 seek time의 최소화를 도모한다.

 

이는 카프카가 데이터를 로그(segments)로 저장하기 때문에 가능한 일이다. 로그는 한 번 쓰여지면 변하지 않는 성질을 지닌다(immutable). 특히 카프카의 데이터는 컨슈머가 읽어도 지워지지 않는다. 데이터는 오로지 맨 끝에 추가될 뿐이다(append-only). 이로 인해 카프카의 데이터는 디스크에 조각(fragments)으로 나뉘어 저장되지 않고, 가능한 연속적인 블록에 저장된다. 때문에 카프카는 원하는 데이터에 순차적으로 접근할 수 있다. 즉 디스크 seek time이 최소화된다.

 

이 시퀀셜 엑세스가 항상 보장되는 것은 아니다. 카프카의 데이터가 위치하는 파일 시스템을 다른 어플리케이션이 함께 사용하는 경우, 다른 어플리케이션으로 인해 디스크 단편화가 발생할 소지는 존재한다. 카프카의 데이터를 가급적 독립된 파일시스템에 유지할 것을 권장하는 이유가 여기에 있다. 하나의 파일 시스템을 카프카가 온전히 홀로 사용하도록 하여 카프카의 시퀀셜 엑세스를 보장해야 한다.

 

또, 디스크 엑세스를 더 빠르게 하기 위해 reading and writing은 OS 페이지 캐시를 최대한 활용하도록 해야 한다. 그래서 카프카가 구동되는 머신은 커널 swapiness를 최대한 작게 설정하여 OS 페이지 캐시가 swap out됨을 최대한 방지하는 것이 좋다.

 

 

2. Zero-Copy

예로부터 데이터를 디스크에서 읽어 네트워크로 전송하는 작업은 아래와 같이 4번의 카피와 4번의 컨텍스트 스위칭을 요구한다:

- 디스크에서 read buffer로 (DMA)

  -> read buffer 에서 application buffer로 (CPU)

    -> application buffer 에서 socket buffer로 (CPU)

      -> socket buffer에서 nic buffer로 (DMA)

 

Zero copy는 이 과정을 대폭 축소한다. 위 과정에서 application 영역은 디스크(read buffer)에서 읽은 데이터를 그대로 socket buffer로 보내기만 할 뿐, 다른 하는 일이 없다. 즉 데이터를 read buffer에서 socket buffer로 곧바로 보낼 수 있으면 좋을 것이다. zero copy는 그대로 탄생했다. 그리고 다음과 같이 동작한다:

- 디스크에서 read buffer로 (DMA)

  -> read buffer에서 socket buffer로 (CPU)

    -> socket buffer에서 nic로 (DMA)

카피 횟수도 줄었지만, 컨텍스트 스위칭이 처음 call에서 한 번, 리턴에서 한 번, 총 2번으로 줄었다.

 

Zero copy는 OS의 sendfile() syscall를 통해 이루어지는데, 이는 OS에서 zero copy를 지원해야 함을 의미한다. 유닉스나 요즘의 웬만한 리눅스 버전은 대부분 지원한다고 한다.

 

그런데 여기서 한 번 더 나아간다. 위 과정에서, read buffer에서 nic로 바로 데이터를 보낼 수 있다면 더욱 좋을 것이다. 이 기능은 nic라 gather operation을 지원해야 한다. 리눅스는 커널 2.4 이후로 이 기능을 지원하기 위해 socker buffer descriptor에 변경이 있었다. 이렇게 하여 최종적으로 zero copy는 다음과 같이 동작한다:

- 디스크에서 read buffer로 (DMA)

  -> read buffer에서 파일 디스크립터 정보(파일 위치, 크기 등)만 socket buffer로 (CPU)

    -> read buffer에서 nic로 (DMA)

Socket buffer는 파일 메타데이터만 가지며, 실제 데이터 카피는 발생하지 않는다. Read buffer에서 nic로의 데이터 카피는 DMA를 통해서만 이루어진다.

 

 

3. Data Batch & Compression

카프카는 데이터 batching과 compression을 지원하여 네트워킹 횟수를 대폭 줄여 성능을 향상한다. 추가로 로그 데이터 디스크 사용량도 크게 감소한다. 이 기능은 카프카 설정에 따라 작동 여부와 그 정도가 달라지지만, 대부분의 경우 사용할 것을 권장한다.

 

 

4. Topic Partitioning

카프카의 데이터는 로그 세그먼트로 저장되는데, 이 데이터는 순차적이기 때문에 다수의 쓰레드가 동시에 접근하는 일은 의미가 없다. 데이터는 최근의 것만 한 번 읽으면 되기 때문에, 아무리 많은 쓰레드가 있어도 경합만 발생할 뿐, 이득이 없다고 할 수 있다. 즉 병렬 처리가 불가능하다.

 

카프카는 로그 파티셔닝을 통해 이를 개선했다. 같은 토픽이라도 데이터를 나누어(sharding) 저장하고, 각 파티션에는 독립된 쓰레드가 붙어 작업할 수 있도록 함으로써 결과적으로 하나의 토픽을 여러 쓰레드가 나누어 처리할 수 있도록 병렬성을 구현했다.

 

 

@개인적으로 1번과 4번이 카프카의 특이점이라고 생각된다. 나머지는 다른 메시징 어플리케이션에서도 얼마든지 구현할 수 있을 것 같은 느낌이다. 카프카가 결정적으로 차별화를 두는 점은 4번이 결정적이고, 다음이 1번이라고 할 수 있지 않을까. 이 두 특성으로 인해 카프카가 메시징 어플리케이션을 넘어 데이터 스트리밍 플랫폼으로 군림할 수 있게 되었다고 생각된다.

'Kafka' 카테고리의 다른 글

Kafka 설정 listeners vs. advertised.listeners  (1) 2020.09.08

두 설정 모두 모두 카프카 브로커 서버가 바인딩하는 주소를 나타낸다.

 

listeners

- 카프카 브로커가 내부적으로 바인딩하는 주소.

 

advertised.listeners

- 카프카 프로듀서, 컨슈머에게 노출할 주소. 설정하지 않을 경우 디폴트로 listners 설정이 적용된다.

 

카프카는 분산 시스템이다. 클라이언트(프로듀서, 컨슈머)는 분산된 파티션에 접근하여 write/read 를 수행한다. 카프카가 클러스터로 묶인 경우, 카프카 리더만이 write/read 요청을 받는데, 클라이언트는 클러스터의 브로커 중 누가 리더인지 알아야 하기 때문에 write/read 요청에 앞서 해당 파티션의 리더가 누구인지 알 수 있는 메타데이터를 요청한다. 이 메타데이터 요청은 클러스터의 브로커 중 아무나 받아서 응답할 수 있다. 메타데이터 요청을 받은 브로커는 요청된 파티션의 리더가 어떤 브로커인지와 그 브로커에게 접근할 수 있는 엔드포인트를 반환한다. 그러면 클라이언트는 이 반환된 메타데이터를 가지고 실제 요청을 수행한다.

 

머신 OS에 직접 설치되어 구동되는 카프카의 경우 이 엔드포인트는 단순히 호스트 주소가 되는데, 그렇지 않고 가상 환경이나 클라우드와 같이 복잡한 네트워크 환경에서 구동되는 경우 상황이 달라진다.

 

이 경우, 카프카는 내부 브로커들 간의 통신을 위한 엔드포인트와 외부 클라이언트를 위한 엔드포인트를 구분할 필요가 생긴다. 네트워크 환경에 따라 내부의 서버 간 접근에 사용되는 호스트 주소와 외부에서 접근하기 위한 주소가 달리지기 때문이다. 그리고 성능 및 기타 비용을 고려할 때, 내부에서는 plaintext로, 외부에서는 SSL로 통신하도록 하는 등의 구분이 필요할 수 있다.

 

lisnters 와 advertised.listeners 는 그 구분을 위해 사용되는 설정이다.

 

카프카 공식 사이트에서 가져온 아래 설정 예시는 다양하게 구분된 엔드포인트를 보여준다:

listener.security.protocol.map=CLIENT:SASL_PLAINTEXT,REPLICATION:PLAINTEXT,INTERNAL_PLAINTEXT:PLAINTEXT,INTERNAL_SASL:SASL_PLAINTEXT
advertised.listeners=CLIENT://cluster1.foo.com:9092,REPLICATION://broker1.replication.local:9093,INTERNAL_PLAINTEXT://broker1.local:9094,INTERNAL_SASL://broker1.local:9095
listeners=CLIENT://192.1.1.8:9092,REPLICATION://10.1.1.5:9093,INTERNAL_PLAINTEXT://10.1.1.5:9094,INTERNAL_SASL://10.1.1.5:9095

 

 

모든 설정은 key-value 로 매핑된다. CLIENT 라는 key 의 내부 바인딩은 192.1.1.8:9092 로, 외부에서 접근하기 위한 엔드포인트는 cluster1.foo.com:9092 가 되며, 사용되는 프로토콜은 SASL_PLAINTEXT 를 사용한다.

 

내부 사용을 위한 엔드포인트로 보이는 INTERNAL_PLAINTEXT 는 내부 바인딩으로 10.1.1.5:9094 를, 외부에서의 접근은 broker1.local:9094 를 사용하며 PLAINTEXT 로 통신한다.

 

출처: 

cwiki.apache.org/confluence/display/KAFKA/KIP-103%3A+Separation+of+Internal+and+External+traffic

 

'Kafka' 카테고리의 다른 글

카프카는 왜 빠른가  (0) 2021.11.26

+ Recent posts