Loading
2022. 4. 23. 19:29 - lazykuna

Kafka는 어떻게 고성능을 달성하는가?

Kafka는 아파치 재단의 pub/sub 모델의 메시지 큐입니다. 분산환경 및 대규모 트랜잭션에 대해서 높은 성능을 발휘하는 것으로 알려져 있는데, 여기서는 어째서 Kafka가 이렇게 빠른지에 대해서 간단하게 정리해 두었습니다.
의외로 low-level 측면에서 최적화 한 부분이 많은 게 흥미로워서 정리해 보게 되었네요 ㅎㅎ.

Prologue: Kafka의 기본 특징

먼저 설명에 앞서 Kafka가 기본적으로 추구하고 있는 특징에 대해서 짚고 넘어가면,

  • 영속성 (Persistence)
    메시지를 메모리가 아닌 하드디스크에 저장하여, 메시지 유실을 방지
  • 확장성
    클러스터로 쉽게 확장 가능
  • 높은 성능
    최대한으로 많은 데이터 처리를 할 수 있게끔 설계

여기에서 마지막 “높은 성능"을 위해서, 카프카는 아키텍쳐적인 설계 뿐만 아니라 프로토콜 및 시스템 레벨에서의 설계까지 관여하고 있는 점이 인상깊습니다. 그 점들에 대해서 짚어볼 생각입니다.

Pull vs. Push

카프카는 Consumer이 메시지를 요청하는 Pull 방식을 사용하고 있습니다.
”흠, 그냥 Producer이 Consumer에게 Push 하는 쪽이랑 무슨 차이가 있지?”라고 생각할 수 있는데, 아래와 같은 시나리오로 예를 들면...

  • Consumer 10개에게 Producer이 Round-robin으로 메시지를 Push한다고 가정하면,
  • 특정 Consumer에게 계속해서 부하가 심한 메시지가 들어와서, 해당 Consumer은 처리가 느려지는 반면 다른 Consumer은 낮은 부하의 메시지만 받아서 Starvation이 들어올 수도 있습니다.

이를 위해서는 Producer이 Consumer의 상태를 트래킹하는 시스템을 추가로 또 구현해야 하는데, 굳이 그러느니 Consumer이 직접 Pull 하는 것이 시스템 자원을 최대한으로 활용하는 데 더 적합합니다.

  • 물론 Push가 중앙집중형 시스템이라는 이점을 가지고 있어 메시지의 관리 및 로깅에는 유리하지만, 성능 최우선인 카프카의 특징을 고려하면 고려 대상은 아닙니다.
    또 생각해보면 이미 Broker과 Producer이 중앙에서 해야 하는 기능을 충실히 수행하도록 이미 설계되어 있기도 합니다.

프로토콜

카프카의 프로토콜은 다음과 같은 특징이 있습니다. 공통점으로는 부하를 최소화하고 메시지전송 외 불필요한 블로킹/핸드쉐이크 등 요소를 최대한으로 배제하여 최대한의 throughput을 사용할 수 있도록 설계되어 있다는 점이겠네요.

  • 컨슈머당 단일 TCP 연결 유지
    하나의 메시지에 하나의 TCP 연결이 아닌, 컨슈머에 단일 TCP 연결을 사용하여 전송합니다. 이를 통해 TCP handshaking 등의 부하도 없앨 수 있어 최선의 성능을 보여줍니다.
  • Binary
    Serialize/Deserialize 할 필요 없이 그대로의 데이터를 보내어 부하 최소화
  • Non-blocking I/O
    Kafka는 응답을 대기하지 않고서도 계속해서 메시지를 송신 할 수 있는 구조로 되어 있습니다.

파일 시스템: Sequencial I/O와 Zero-copy

사실 이 내용을 정리하려고 글을 쓴 거긴 한데 ^^; 서론이 길었네요...

카프카는 디스크에 정보를 저장하는 persistent 방식의 메시지 큐인데, 디스크는 속도가 제일 느린 저장장치 중 하나입니다. 디스크로 인해 성능의 병목현상이 생기지 않도록 성능을 최대한으로 끌어다 쓸 수 있는 구조를 고려할 필요가 있는데, 카프카는 Sequencial I/O와 Zero copy를 통해서 이를 구현해냅니다.

Sequential I/O

디스크는 항상 순차적 쓰기가 훨씬 빠릅니다. 따라서, 카프카도 그 이점을 최대화하고자 순차적 쓰기를 하도록 만들어집니다. 여전히 메모리보다는 훨씬 느리지만 그래도 RAID 구성한 하드디스크의 순차 쓰기 속도는 제법 엄청난 대역폭을 보여줍니다.

하드디스크의 순차적 읽기 성능은 메모리에 대한 랜덤 읽기 성능보다 뛰어나며 메모리의 순차적 읽기 성능보다 7배 정도 느리다. 물론 하드디스크의 랜덤 읽기 성능은 메모리의 랜덤 읽기 성능보다 10만 배나 느리다. #

그리고, 이는 카프카의 유효기간에 따른 토픽 관리하는 데에도 큰 이점으로 작용합니다. 아주 단순하게 순차쓰기 된 데이터를 순차적으로 다시 덮어씌우면 되니까요.

Zero-copy란 무엇인가?

제로카피라는 개념은 2008년 IBM의 "Efficient data transfer through zero copy" 에서 처음 제시된 개념입니다. 간단히 말하면, 일반적인 파일 전송에서 발생하는 Context switching을 최소화하여 최대한의 파일 전송 대역폭을 만들어보자는 취지에서 도입된 개념입니다.

트위터의 Alex Xu 글에서 가져온 그림인데, 위의 그림이 일반적인 프로그램에서 발생하는 파일 I/O의 다이어그램입니다. OS에 파일 읽기를 요청하여, OS 버퍼로 읽어오고, 이를 어플리케이션으로 읽어들입니다. 그리고 이걸 다시 네트워크 버퍼로 쏘게 되는 구조입니다. 코드로 보면 아래와 같을 것입니다.

byte[] applicationBuffer = new byte[1024];

read(fileFd, applicationBuffer, len);
send(socketFd, applicationBuffer, len);

하지만 Kafka에서는 아래 그림과 같이 커널 콘텍스트를 여러번 타지 않고 바로 한번에 쓰기를 수행하는 모습을 볼 수 있습니다. 이를 위해 Linux에서는 sendfile 이라는 별도의 API를 제공하고 있습니다.

#include <sys/sendfile.h>

ssize_t sendfile(int out_fd, int in_fd, off_t * offset, size_t count);

최근에는 하드웨어에서 지원하는 경우 DMA에서 바로 NIC Buffer로 쏴 준다고 합니다. (위처럼) 이러한 최적화가 모두 들어갈 경우 파일 전송 수행 시간이 대략 65%까지 줄어든다고 합니다.

아직도 이런 context switching과 하드웨어적인 최적화까지 할 수 있는 여지가 많이 남아있었다니, 최적화의 길은 멀고도 험한 것 같네요.

참고

'개발 > Engineering' 카테고리의 다른 글

Nginx가 고가용성을 유지하는 방법?  (0) 2022.04.25
Django는 과연 무거운가?  (0) 2022.04.23
[Go] Golang은 어떻게 효율성을 달성하는가  (2) 2022.04.17
Address Sanitizer과 그 원리  (0) 2022.04.09
Rolling Update  (0) 2022.03.13