개발놀이터
카프카는 어떻게 수십만 TPS에 도달할 수 있었을까? 본문
요즘은 카프카와 NoSQL을 공부하고 있는데 카프카는 특히 공부하면 공부할수록 왜 개발자들이 많이 사용하는지 알 것 같습니다. 공부하면 공부할수록 왜 쓰는지 이해가 된달까요..
이번 포스팅에선 카프카와 AWS SQS, SNS와 비교하면서 왜 카프카가 대용량 메세지 처리에 능하게 되었는지에 대해서 정리해봤습니다.
카프카 vs SQS, SNS
왜 다른 메세지 큐도 아니고 SQS냐하면 RabbitMQ는 메세지를 소비할 곳을 세밀하게 조정하고 싶을 때 사용하는 것이 바람직하지 대용량으로 비동기 메세지 처리를 하는데 특화되어있지는 않습니다. 다른 메세지 큐인 Redis의 Pub/Sub 또한 대용량 비동기 메세지 처리에 특화되어있지 않기도 하구요.
그런 의미에서 SQS와 SNS를 조합한 메세지 큐잉 서비스는 완전 관리형 메세지 큐잉 서비스를 표방하는만큼 카프카와 비교할만하죠.
이번 섹션에서는 카프카와 SQS, SNS를 여러가지 측면에서 비교해보고 최종적으로 하고싶었던 얘기인 "카프카는 어떻게 수십만 TPS에 도달했는지" 에 대해서 얘기해보겠습니다.
메세지 실패에 대한 재시도 전략
메세지 큐잉 서비스에서 실패한 메세지에 대한 재시도 전략은 반드시 필요하다고 볼 수 있습니다. 카프카에선 현재 컨슈머가 소비하고 있는 offset (이하 오프셋) 을 stateful하게 관리하고 있어 컨슈머가 오프셋을 커밋해주지 않으면 카프카는 실패로 간주, 컨슈머에게 다시 메세지를 전송해줍니다.
SQS는 기본적으로 이런 재시도 전략이 없습니다. 그런데 실패한 메세지만 따로 관리하는 DLQ (Dead Letter Queue) 가 존재하죠. DLQ로 들어간 메세지는 메세지를 한 번 재시도해서 전송할 수 있습니다.
하지만 아직까진 DLQ에서마저도 실패한 메세지에 대해서 자동으로 처리해주진 않습니다. 애초에 DLQ는 종착점이기 때문에 일정 횟수 이상 컨슈머가 처리에 실패한 메세지를 저장하는 곳이지 여기서 자동으로 다시 메인 큐로 이동하는건 불가능합니다.
이를 해결하기 위해선 CloudWatch를 이용해서 모니터링하다 Lambda같은 서비스로 DLQ에서 실패한 메세지에 대해서 SQS로 다시 올려주는 작업이 필요합니다. 즉, 수동으로 해줘야한다는 의미이죠.
Retention 정책
카프카에서는 다양한 Retention 정책이 있습니다. 이런 메세지 보관 정책들은 주로 메세지를 재시도할 때 사용하고 어떤 메세지가 처리되었는지에 대한 로그 기록으로도 사용됩니다.
카프카에선 몇초가 지나면 삭제할 것인지, 크기가 어느정도 되면 삭제할 것인지에 대한 "삭제 정책"과 메세지 키를 중복해서 관리하지 않고 최신 상태로만 관리하는 "압축 정책"이 존재합니다.
그럼 SQS는 어떨까요? SQS는 기본적으로 메세지가 14일동안 유지됩니다. 그리고 삭제되죠. 그럼 삭제된 메세지는 어떻게 하냐? 어떻게 못하죠 그냥 삭제된겁니다.
흠.. 여기서도 카프카 승리일까요? 조금 더 지켜보죠
메세지 관리
SQS에서 할 말이 있는 섹션이 등장했습니다. AWS의 SQS부분 공식 문서를 보면 SQS는 무한에 가까운 큐를 가질 수 있다고 나와있습니다.
진짜 무한은 아니겠지만 그만큼 그런거 확장하는데 신경쓰지 않아도 된다고 얘기하는 것 같습니다. "이게 클라우드야." 라는 느낌을 강하게 주죠.
카프카는 어떨까요?
카프카는 ZooKeeper부터 시작해서 관리해야할게 정말 많죠. 사실 위에서 Retention 정책에 대해서도 카프카가 시간단위, 공간단위 삭제 정책과 압축 정책이 있긴 하지만 이만큼 자유롭다는건 그만큼 관리를 개발자가 직접 해줘야한다는 것이죠. 이것도 다 자원이 들어가는 것이고 이런건 무시하지 못하죠.
만약 카프카의 메세지 브로커를 모니터링 해야하고 상황에 따라서 브로커를 추가해줘야할 수도 있고 컨슈머 그룹, 파티션, 토픽 이런걸 다 관리해줘야하죠.
물론 AWS에서 완전 관리형 MSK라고 카프카를 자동으로 관리해주는 서비스가 있긴 합니다만... 카프카를 쓸 정도 되는 큰 서비스를 관리하는데 이거 쓸바엔 그냥 사람한테 시키는게 더 싸게 먹힐수도 있습니다.
메세지 처리
SQS의 TPS는 평균 300TPS, 배치처리를 하는 경우 3000TPS입니다. 반면 카프카는 수만~수십만 TPS까지 나오게 됩니다.
이게 사실이라면 대규모 비동기 메세지 처리에 SQS를 쓰는걸 진지하게 고려해봐야할 것 같은데... 왜 이런 차이가 생기게 된걸까요?
먼저 SQS는 컨슈머가 메세지를 처리할 때 HTTP Polling을 사용하기 때문에 레이턴시가 존재하게 됩니다. 그리고 SQS가 뭐 무한한 큐를 가질수 있네 없네를 따지고 있지만 SQS는 단순한 메세지 처리, 높은 확장성의 메세지 큐, 다양한 AWS 서비스와 연동에 초점이 맞춰져있습니다.
그럼 카프카는 어떻게 수십만 TPS를 달성할 수 있었을까요?
zero copy
운영체제에 대한 로우레벨 얘기를 하지 않을 수 없겠습니다. 결국 카프카도 "메세지"라는 상태를 보관해야하는 입장에서 어느정도 stateful하고 데이터베이스정도는 아니지만 상태관리를 해줘야합니다.
카프카는 로그의 형태로 메세지를 데이터로 관리하고 있고 이를 결국 디스크에 써야합니다. 우리는 운영체제에서 파일시스템이 접근해 파일에 접근하는 경우를 봐야합니다.
https://coding-review.tistory.com/563
데이터베이스 쿼리를 실행하면 내부적으로는 어떤 일이 벌어질까?
오랜만에 포스팅을 쓰는 것 같습니다. 거의 열흘만인 것 같은데 요즘 쿠버네티스를 공부하느라고 실습을 위주로 공부하고 있느라 공부할 시간이 마땅히 나지 않았네요... 이번 포스팅은 평소
coding-review.tistory.com
https://coding-review.tistory.com/565
리눅스의 파일 입출력 (feat. 파일시스템)
이번 포스팅에선 리눅스가 어떻게 파일을 읽고 쓰는지에 대해서 공부해본 내용을 정리하려합니다. 요즘 리눅스 커널에 대해서 공부하고있는데요. 아직 배경지식이 전무하다시피해서 공부하
coding-review.tistory.com
제가 작성한 두개의 포스팅을 먼저 읽고 오시면 앞으로 할 얘기가 쉽게 다가오실 것 같습니다. 물론 읽지 않으셔도 상관없습니다.
파일시스템이 파일에 접근하기 위해서 Buffered I/O를 진행하게 됩니다. Buffered I/O란 메모리에서 메모리로 데이터가 이동하면서 순차적으로 데이터를 로우레벨에서 하이레벨로 이동시키는 것을 이야기합니다.
이를 운영체제에선 copy라고 부르고 최상단 하이레벨인 애플리케이션부터 최하단 로우레벨인 디스크에 있는 데이터가 이동하기까지 여러번의 copy가 일어납니다.
운영체제에 커널모드, 유저모드, 애플리케이션 이렇게 세 단계에서 copy가 일어나게 되는데 이를 순서로 표현하면 다음과 같습니다.
- 애플리케이션에서 디스크에 있는 데이터에 접근하려고 합니다.
- 운영체제가 유저모드에서 커널모드로 전환됩니다.
- 커널모드에서 시스템콜을 호출해서 파일시스템에 접근합니다.
- 파일시스템이 추상화된 VFS가 디스크에 접근해 디스크에 들어있는 데이터를 메모리에 적재합니다.
- 메모리에 적재된 데이터가 커널모드에 copy됩니다. (1)
- 커널모드에서 다시 유저모드로 전환될 때 또 다시 copy 됩니다 (2)
- 유저모드에서 애플리케이션에 다시 copy 됩니다 (3)
이때서야 비로소 애플리케이션이 디스크에 있던 데이터를 사용할 수 있게됩니다.
그럼 zero copy가 뭔지 직관적으로 이해할 수 있을 것 같습니다. 앞에서 언급한 copy가 일어나지 않게 되는것이죠.
메모리에 적재하는 순간! 바로 데이터를 전송해버리는것이죠.
엥? 그래도 되나?
사실 애플리케이션이 디스크에 있는 데이터를 사용해야할 때 이런 copy가 필요한데 카프카 입장에선 디스크에 있는 정보가 카프카에게 필요가 없습니다. 그냥 consumer에게 던지기만 하면 되죠.
때문에 운영체제에서 이런 zero copy를 이루기위해 sendFile()이라던가 mmap()같은 시스템콜을 만들어두어 카프카와 같은 서비스들이 이용할 수 있도록 준비되어있습니다.
PageCache
여기서 한 단계 더 나아가 운영체제에 PageCache가 존재합니다. 이건 파일시스템이 자주 사용되는 데이터에 대해 캐싱해둔 공간으로 주로 메모리에 위치하게됩니다.
운영체제는 기본적으로 20퍼센트 정도 되는 메모리를 dirty page로 지정하는데 dirty page는 디스크에서 데이터를 메모리에 적재한 뒤 flush하기 전 "변경된 데이터이지만 디스크에 반영되지 않은 데이터"로서 캐시되어잇습니다.
카프카는 이 dirty page에 접근해서 캐싱된 데이터가 있으면 그걸 바로 컨슈머에게 던져버리는거죠.
그럼 순서가 이렇게 변합니다.
- 애플리케이션 (카프카) 가 디스크에 접근하려고 합니다.
- 운영체제가 유저모드에서 커널모드로 전환됩니다.
- 커널모드에서 시스템콜을 호출해서 파일시스템에 접근합니다.
- dirty page를 조회합니다. 만약 있으면 바로 consumer에게 데이터가 전송됩니다.
- 만약 없다면 파일시스템이 추상화된 VFS가 디스크에 있는 데이터를 메모리에 적재합니다. 이후 바로 consumer에게 전송합니다.
mmap과 DirectByteBuffer
mmap은 디스크에 있는 데이터를 메모리로 올리는 역할을 수행하고 이를 통해 메모리에서 직접 데이터를 가져오는 효과가 생겨 속도가 빨라집니다.
즉, 카프카가 Producer로부터 받은 데이터를 디스크에 넣기 전에 mmap으로 메모리에 올리고 곧바로 sendFile (zero copy) 시스템콜로 컨슈머에게 던져버리는 것이죠.
그 과정에서 DirectByteBuffer라는 개념이 나오는데, 카프카는 JVM 기반이기에 GC에 부담을 주는 것이 싫어 Off-Heap 즉, JVM에 할당된 Heap 메모리가 아닌 외부 메모리 (OS에 할당된 메모리) 를 이용하고 싶었습니다.
이런 니즈가 있었기에 java 1.4에서 DirectByteBuffer라는 개념이 생겨 JVM 기반 애플리케이션이 직접 외부 메모리에 붙을 수 있게 되었습니다.
하지만 이렇게 Non Blocking으로 데이터에 붙기 시작하니까 메모리 누수가 생겨 메모리를 관리하기 힘들어졌습니다. 이를 또 개선한 것이 java 9에 생긴 cleaner이고 이를 호출해 지속적으로 메모리를 청소해줄 수 있는 API가 생기게 되었습니다.
이게 뭐 얼마나된다고 수십만 TPS까지 나오겠어?
이게 꽤 얼마가 됩니다.
CPU가 메모리에 접근해서 데이터를 처리하는 과정이 수백번 수천번의 이동이 있기에 그 데이터가 처리되는 것인데 이런 순환이 하나하나 쌓이기 시작하면 정말 커지게 되는 것이죠.
그래서 카프카의 성능은 CPU의 성능보다 메모리의 크기와 PageCache의 hit 비율에 따라 성능 차이가 크게 달라집니다. 카프카의 성능이 정말정말 중요하다면 커널의 PageCache비율을 줄이는 것도 고려해볼만한 튜닝 방식이라고 생각합니다.
마치며
이렇게 카프카가 어떻게 수십만 TPS에 도달하게 되었는지 로우레벨에서 정리해봤습니다. 카프카는 정말 팔방미인에 다재다능하지만 유지보수 측면에서는 최악이라는 사실을 오늘도 깨닫게 됩니다. 역시 완벽한 기술은 없다는게 이번에도 증명되었네요.
이번 포스팅은 여기서 마치도록 하겠습니다. 오늘도 즐거운 하루 되세요!
'배포 > Apache Kafka' 카테고리의 다른 글
아파치 카프카 (응용) (0) | 2025.03.28 |
---|---|
아파치 카프카 (심화) (0) | 2025.03.18 |
메세지 브로커의 근심과 걱정 (0) | 2024.11.07 |
아파치 카프카 (개념) (0) | 2024.05.18 |