Kafka 메시지 압축률 안나와서 고생한 이야기

카프카를 사용할 때 메시지를 압축하면 디스크와 네트워크 자원을 절약할 수 있다. 대신 압축을 하고 푸는 데 CPU를 좀 더 쓰게 되지만 메시지 압축은 대체로 프로듀서와 컨슈머의 처리량을 높여준다. 디스크 성능이 좋지 않거나, 네트워크 대역폭이 낮은 환경에서는 압축의 효과를 크게 볼 수 있다. 하지만 메시지를 압축하도록 설정하더라도 압축률(compression ratio)이 낮다면 디스크 절약이나 처리량 향상같은 효과를 볼 수 없다.

TL;DR

카프카 메시지의 압축률이 낮다면 linger.ms를 늘려보자. 그래도 안되면 batch.size를 늘려보자.

카프카 도입 배경

내가 속한 팀의 시스템 중에는 로그를 TCP/IP 연결을 통해 실시간으로 보내고 처리하는 모듈이 있다. TCP 연결을 통해 로그를 주고 받다보니 이중화나 장애 대응에 어려움이 있었다. 가끔 트래픽이 많은 날이면 로그를 수신하는 모듈이 부하를 견디지 못하고 죽기도 했다. 스케일 아웃도 문제였다. 송/수신 모듈이 데이터를 직접 연결을 맺고 데이터를 주고 받다보니 스케일 아웃은 많은 작업을 필요로 했다. 모듈 간 통신을 위해 IP와 포트를 알아야 하기 때문에 장비가 추가되면 설정파일이나 코드가 변경되어야 했다. 장애가 발생했을 때 유실된 데이터를 복구하기도 거의 불가능했다.
우리는 이 모듈이 갖고 있는 문제들을 해결하기 위해 카프카를 적용하기로 했다. 카프카는 이미 널리 사용되고 있었고, 유명했기 때문에 솔루션을 선택하는 데 크게 고민하지 않았다. 아래와 같은 카프카의 특징들은 우리가 정말 필요로 했던 것이었다.

  • 카프카의 컨슈머는 PULL 방식으로 동작하기 때문에 메시지를 수신하는 쪽에서 부하를 조절할 수 있다.
  • 수신 모듈이 죽는다 하더라도 가장 최근에 처리했던 메시지부터 다시 처리할 수 있다.
  • 스케일 아웃이 편리하다. 브로커가 늘어나더라도 기존의 모듈을 수정하거나 재배포하지않아도 된다.

메시지 압축

카프카는 메시지 압축을 지원한다. 프로듀서가 메시지를 압축해서 전송하면 브로커는 디스크에 저장하고, 컨슈머에 보낼 때도 압축한 메시지 그대로 전송한다. 구글에 “kafka compression benchmark”라고 검색하면, 카프카가 지원하는 압축 알고리즘을 비교/분석한 자료들을 볼 수 있다. 대체로 GZIP은 압축률이 높고, SNAPPY는 압축률은 다소 떨어지지만 처리량이 높다는 글이 많다. 그냥 SNAPPY를 사용할까 하다가 직접 우리 개발환경에서 테스트를 해보기로 했다. 테스트 당시 카프카의 버전은 2.0.0이었고, 프로듀서의 compression.type 설정에는 none(압축 안함), gzip, snappy, lz4 이렇게 네 가지 옵션이 있었다.(지금은 2.1.0이 나왔고 zstd가 추가되었다!)
테스트 시나리오는 간단하다. 프로듀서는 파일을 읽어서 메시지를 전송하고, 컨슈머는 메시지를 읽는다. 네 가지 알고리즘을 비교해보니 아래와 같은 결과를 얻을 수 있었다. 각 항목의 값은 none=1로 한 상대값이다.

프로듀서

type CPU 사용률 처리 시간 브로커 디스크 용량 압축률
none 1.00 1.00 1.00 1.00
gzip 1.70 0.84 0.31 3.17
lz4 1.54 0.61 0.46 2.16
snappy 1.54 0.60 0.46 2.16

컨슈머

type CPU 사용률 처리시간
none 1.00 1.00
gzip 1.54 0.65
lz4 1.35 0.68
snappy 1.35 0.72

프로듀서는 메시지를 압축해야하고, 컨슈머는 다시 풀어야하기 때문에 CPU 사용률은 올라간다. 하지만 CPU가 노력한만큼 I/O 비용은 줄어들기 때문에 압축은 성능 향상을 불러올 수 있다. 내 개발 환경에서는 압축을 하지 않는 none이 가장 좋지 않은 결과를 보였다. none만 아니면 무엇이든 괜찮은 선택으로 보였지만, 메시지를 좀 더 오래 보관하고 싶어서 압축률이 높은 gzip을 사용하기로 했다.

예상보다 높은 디스크 사용량

카프카 클러스터 스펙을 산정할 때 예상한대로라면 gzip 압축시 메시지를 3일간 보관해도 절반 이하의 디스크를 사용해야했다. 그런데 이틀이 조금 지난 시점에 이미 디스크를 80% 정도 사용하고 있었다. 압축률이 예상보다 너무 낮았다. 배치 사이즈가 작아서 그런가 싶어 batch.size 설정을 늘려보았지만 소용없었다. 사실 우리가 쓰는 로그 데이터는 batch.size 기본값인 16384바이트만 쌓여도 충분히 높은 압축률이 나와야했다. 토픽의 파티션을 나눠서 그런가 했더니 파티션 때문도 아니었다. Producer Configsbatch.size를 보면 다음과 같은 내용이 있다.

Requests sent to brokers will contain multiple batches, one for each partition with data available to be sent.

배치는 파티션마다 존재하기 때문에 파티션별로 batch.size를 따로 관리한다는 말이다. 한참동안 낮은 압축률의 원인을 찾아보다가 Producer Configs에서 linger.ms 옵션을 발견했다. 이 옵션은 프로듀서가 메시지를 브로커로 전송하기 전에 일정 시간 기다리게 한다. 이 옵션의 설명엔 다음과 같은 말이 나온다.

The producer groups together any records that arrive in between request transmissions into a single batched request. Normally this occurs only under load when records arrive faster than they can be sent out.

프로듀서는 메시지 도착 속도가 메시지 전송 속도보다 빠를 때만 배치로 묶어서 보낸다는 것이다.(유레카!) 즉, 메시지를 바로바로 브로커로 전송할 수 있는 상황에서는 배치가 쌓일 때까지 기다리지 않고 전송한다. 이를 방지하기 위해서는 linger.ms를 설정해서 메시지가 batch.size만큼 차기 전에 브로커로 전송하지 않도록 해야한다. linger.ms는 기본값이 0이기 때문에 명시적으로 값을 주지 않으면, 배치 크기가 줄어들어 낮은 압축률로 이어질 수 있다. 테스트할 때는 로컬에서 파일을 읽어서 메시지를 전송했기 때문에 다음 메시지를 전송하기 전에 배치 사이즈만큼의 메시지가 쌓였지만, 리얼 환경에서는 메시지 도착 속도가 로컬 파일에서 읽는 것만큼 빠르지 않았기 때문에 배치 크기가 작았던 것이다.

디스크 사용량 문제는 linger.ms를 설정하고 해결되었다. 높게 설정했었던 batch.size도 기본 값으로 복구했다. linger.ms는 높게 설정하더라도 배치가 batch.size만큼 쌓이게 되면 linger.ms만큼 대기하지 않고 브로커로 메시지를 전송하기 때문에 조금 높게 잡아도 상관없다.

정리

  • 카프카를 사용하고자 한다면 압축을 고려해보자.
  • 생각보다 압축률이 나오지 않는다면 linger.msbatch.size를 조절해보자.

참고 문서

Kafka Documentaion - Producer Configs