Redpanda: JVM 없는 Kafka 호환 스트리밍, rpk CLI, 스키마 레지스트리, Tiered Storage, 마이그레이션
이 글의 핵심
Kafka 클러스터 운영 부담의 상당 부분은 JVM 튜닝, GC pause, ZooKeeper·KRaft 관리에서 나옵니다. Redpanda는 Kafka 프로토콜을 유지한 채 단일 바이너리와 thread-per-core 구조로 이 부분을 걷어내므로 기존 KafkaJS·confluent-kafka·franz-go 코드를 그대로 붙일 수 있습니다. 성능은 자기 워크로드로 직접 재 봐야 하는 이유와 Kafka를 유지하는 편이 나은 경우도 함께 짚습니다.
Redpanda가 해결하는 문제
Kafka는 10년 이상 이벤트 스트리밍의 표준이지만 운영 부담이 만만치 않습니다.
| Kafka 고통점 | Redpanda 해법 |
|---|---|
| ZooKeeper → KRaft 마이그레이션 복잡성 | 단일 바이너리, Raft 내장 |
| JVM GC pause로 p99 튐 | C++ + Seastar, GC 없음 |
| 브로커당 수백 토픽·파티션에서 성능 저하 | thread-per-core, 효율적 |
| 복잡한 운영 도구(Kafka Manager 등) 필요 | 내장 rpk CLI + 콘솔 |
| 스키마 레지스트리·REST 프록시 별도 배포 | 스키마 레지스트리·HTTP 프록시를 같은 바이너리에 내장(커넥터 런타임은 별도) |
Redpanda는 “Kafka API는 그대로 유지하고 내부는 새로 설계한다”는 접근입니다. 따라서 기존 클라이언트·도구 투자를 유지하면서 운영을 단순화합니다.
이 표는 Redpanda 쪽 관점의 요약이라는 점도 감안해야 합니다. Kafka도 3.x 이후 KRaft 모드가 정식화되면서 ZooKeeper 없이 운영할 수 있게 되었고, 4.0부터는 ZooKeeper 지원이 제거되었습니다. 그래서 “ZooKeeper가 없다”는 차이는 예전만큼 결정적이지 않고, 지금의 차이는 주로 JVM이 없다는 점, 스레드 모델, 그리고 스키마 레지스트리·HTTP 프록시가 한 바이너리에 들어 있다는 운영 편의에서 나옵니다. 반대로 Kafka는 가장 넓은 운영 커뮤니티와 문서, 검증된 장애 사례를 가지고 있다는 강점이 있습니다. 어느 쪽이 “낫다”기보다, 팀이 JVM 운영 경험과 Kafka 생태계에 얼마나 투자했는지가 선택을 좌우합니다.
아키텍처
┌──────────────────────────────────────────┐
│ Redpanda Broker (단일 바이너리) │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ core 0 │ │ core 1 │ │
│ │ (shard 0) │ │ (shard 1) │ │
│ │ 파티션 리더│ │ 파티션 리더│ ... │
│ └─────────────┘ └─────────────┘ │
│ │
│ Raft 복제 ◄──► 다른 브로커 │
│ Kafka API / Admin API / Schema Registry │
│ / Pandaproxy (HTTP) │
└──────────────────────────────────────────┘
핵심 설계:
- Thread-per-core: 각 코어가 shard → lock free
- Raft 합의: 메타데이터·로그 복제 모두 Raft (ZK 없음)
- 비동기 직접 I/O: Seastar 프레임워크의 비동기 디스크 I/O로 페이지 캐시와 컨텍스트 스위칭 의존을 줄임
- Tiered Storage: 오래된 세그먼트를 S3/GCS로 자동 오프로드
Thread-per-core는 C++ 프레임워크 Seastar에서 온 구조입니다. 코어마다 스레드 하나를 고정하고 각 스레드가 자기 몫의 파티션과 메모리를 전담하므로, 코어 사이에 공유 데이터와 락이 거의 없고 다른 코어에 일을 넘길 때는 메시지를 보냅니다. 이 구조의 전제는 Redpanda가 그 코어들과 메모리를 독점한다는 것입니다. 그래서 Redpanda는 기본적으로 시작할 때 가용 메모리 대부분을 미리 확보하고 코어를 바쁘게 폴링하며, 다른 프로세스와 CPU를 나눠 쓰는 환경에서는 성능이 크게 떨어집니다. 아래 Docker 예제의 --overprovisioned는 바로 이 가정을 끄는 옵션으로, 코어를 독점하지 않고 폴링을 줄여 개발용 노트북이나 공유 VM에서도 무리 없이 돌게 합니다. 운영 환경에서 이 옵션을 켜 두면 Redpanda의 성능 이점 상당 부분을 스스로 포기하는 셈입니다.
Kafka가 OS의 페이지 캐시에 크게 의존하는 것과 달리, Redpanda는 페이지 캐시를 우회하는 직접 I/O와 자체 캐시를 씁니다. 이 차이 때문에 Kafka에서 쓰던 “브로커 힙은 작게, 나머지 메모리는 페이지 캐시로” 같은 튜닝 공식은 Redpanda에 그대로 적용되지 않고, 대신 rpk redpanda tune으로 디스크 스케줄러, IRQ 분배, CPU 절전 모드 같은 OS 설정을 맞추는 단계가 중요합니다.
설치
Docker (1노드 개발)
docker run -d --name redpanda \
-p 9092:9092 -p 9644:9644 -p 8081:8081 -p 8082:8082 \
docker.redpanda.com/redpandadata/redpanda:latest \
redpanda start \
--overprovisioned \
--smp 1 --memory 1G --reserve-memory 0M \
--node-id 0 --check=false \
--kafka-addr PLAINTEXT://0.0.0.0:9092 \
--advertise-kafka-addr PLAINTEXT://localhost:9092
# rpk CLI
docker exec -it redpanda rpk cluster info
이 명령은 1노드 개발용 설정입니다. --smp 1은 코어 하나만 쓰게 하고, --memory 1G --reserve-memory 0M은 메모리를 1GB로 제한하며, --check=false는 운영 환경 적합성 검사(파일 시스템, 커널 설정 등)를 건너뜁니다. 버전에 따라 --node-id는 자동 할당으로 바뀌어 생략해도 되거나 경고가 나올 수 있습니다.
가장 흔히 막히는 곳은 --advertise-kafka-addr입니다. Kafka 프로토콜에서 클라이언트는 처음 접속한 부트스트랩 주소가 아니라 브로커가 메타데이터로 알려 주는 advertised 주소로 다시 연결합니다. 여기서는 localhost:9092를 알려 주므로, 호스트에서 실행한 클라이언트는 잘 되지만 같은 Docker 네트워크의 다른 컨테이너(아래 예제의 redpanda:9092)에서 접속하면 localhost가 그 컨테이너 자신을 가리켜 연결이 실패합니다. KafkaJS라면 Connection error: connect ECONNREFUSED 127.0.0.1:9092처럼 엉뚱한 주소로의 연결 오류가 반복되는 것이 전형적인 증상입니다. 해결책은 리스너를 두 개 두는 것입니다. --kafka-addr internal://0.0.0.0:29092,external://0.0.0.0:9092로 열고 --advertise-kafka-addr internal://redpanda:29092,external://localhost:9092로 각각 알려 주면, 컨테이너 안의 앱은 redpanda:29092, 호스트의 도구는 localhost:9092로 접속할 수 있습니다. 저는 Kafka든 Redpanda든 “부트스트랩은 되는데 produce가 타임아웃난다”는 증상을 보면 가장 먼저 advertised 주소를 확인합니다. latest 태그 대신 버전을 고정해 두는 것도 운영의 기본입니다.
Kubernetes (Helm)
helm repo add redpanda https://charts.redpanda.com
helm install redpanda redpanda/redpanda \
--version 5.9.0 \
--namespace redpanda --create-namespace \
--set statefulset.replicas=3 \
--set resources.cpu.cores=4 \
--set resources.memory.container.max=8Gi \
--set storage.persistentVolume.size=200Gi
Console UI도 함께:
helm install redpanda-console redpanda/console \
--namespace redpanda \
--set console.config.kafka.brokers='{redpanda-0.redpanda.redpanda.svc.cluster.local:9093}'
브라우저에서 토픽·메시지·컨슈머 그룹을 시각화해 관리합니다.
Helm 값에서 resources.cpu.cores와 메모리 설정은 단순한 쿠버네티스 요청·제한이 아니라 Redpanda의 --smp와 메모리 할당량으로 그대로 이어집니다. 앞서 설명한 thread-per-core 구조 때문에, CPU 제한을 요청보다 크게 잡아 다른 파드와 코어를 나눠 쓰게 하면 성능이 불안정해지므로 운영에서는 요청과 제한을 같게 두어 Guaranteed QoS로 코어를 확보하는 것이 권장됩니다. 차트 버전(--version)은 Redpanda 버전과 설정 키 구조를 함께 결정하므로, 차트를 올릴 때는 변경 기록에서 값 이름이 바뀌지 않았는지 확인해야 합니다. 내부 주소의 9093 포트는 차트가 클러스터 내부용 Kafka 리스너에 쓰는 기본값입니다.
rpk: 통합 CLI
# 클러스터 상태
rpk cluster info
rpk cluster health
# 토픽
rpk topic create orders --partitions 6 --replicas 3
rpk topic list
rpk topic describe orders
# 메시지 produce/consume
rpk topic produce orders
> {"id": 1, "amount": 100}
rpk topic consume orders --num 10
# 컨슈머 그룹
rpk group list
rpk group describe my-group
# ACL
rpk acl create --allow-principal User:alice --operation read --topic orders
# 스키마 레지스트리
rpk registry schema create orders-value --schema schema.avsc --type avro
Kafka의 kafka-topics.sh·kafka-console-producer.sh·여러 개 나뉜 도구가 한 CLI로 통합됩니다.
rpk는 Kafka API로 동작하는 명령(토픽, 그룹, produce/consume)과 Redpanda 전용 Admin API(기본 포트 9644)로 동작하는 명령(클러스터 설정, 헬스, 파티션 이동)을 함께 제공합니다. 그래서 Kafka 클러스터에 대해서도 rpk topic list -X brokers=kafka:9092처럼 Kafka API 쪽 명령은 쓸 수 있습니다. 여러 환경을 오간다면 rpk profile create로 브로커 주소와 인증 정보를 프로필로 저장해 두면 명령마다 -X 옵션을 반복하지 않아도 됩니다. --replicas 3은 브로커가 3대 이상일 때만 성공하므로, 1노드 개발 환경에서는 --replicas 1로 만들어야 INVALID_REPLICATION_FACTOR 오류를 피할 수 있습니다.
클라이언트: Kafka 코드 그대로
Node.js (KafkaJS)
import { Kafka } from "kafkajs"
const kafka = new Kafka({
clientId: "my-app",
brokers: ["redpanda:9092"],
})
const producer = kafka.producer()
await producer.connect()
await producer.send({
topic: "orders",
messages: [{ key: "user-42", value: JSON.stringify({ amount: 100 }) }],
})
const consumer = kafka.consumer({ groupId: "order-processor" })
await consumer.connect()
await consumer.subscribe({ topic: "orders", fromBeginning: true })
await consumer.run({
eachMessage: async ({ message }) => {
console.log(message.value?.toString())
},
})
Python (confluent-kafka)
from confluent_kafka import Producer, Consumer
p = Producer({"bootstrap.servers": "redpanda:9092"})
p.produce("orders", key="user-42", value='{"amount": 100}')
p.flush()
c = Consumer({
"bootstrap.servers": "redpanda:9092",
"group.id": "order-processor",
"auto.offset.reset": "earliest",
})
c.subscribe(["orders"])
while True:
msg = c.poll(1.0)
if msg: print(msg.value())
Go (franz-go)
cl, _ := kgo.NewClient(
kgo.SeedBrokers("redpanda:9092"),
kgo.ConsumerGroup("order-processor"),
kgo.ConsumeTopics("orders"),
)
defer cl.Close()
fetches := cl.PollFetches(ctx)
fetches.EachRecord(func(r *kgo.Record) {
fmt.Printf("%s\n", r.Value)
})
주소만 Redpanda로 바꾸면 끝입니다.
대부분은 정말 그렇지만, 클라이언트 쪽에서 확인할 부분이 몇 가지 있습니다. 파티셔너 차이가 대표적입니다. Java 클라이언트와 librdkafka 계열(confluent-kafka)은 기본 키 해시 알고리즘이 달라서(murmur2와 crc32 계열), Kafka에서 Redpanda로 옮기는 것과 별개로 클라이언트 언어를 섞으면 같은 키가 다른 파티션으로 갈 수 있습니다. 순서 보장이 키 단위 파티션 배치에 기대고 있다면 모든 프로듀서의 파티셔너를 맞춰야 합니다. 또 위 Go 예제는 오류를 _로 버리고 fetches.Errors()도 확인하지 않는 축약형이고, Python 예제의 poll() 결과는 msg.error()를 먼저 검사해야 파티션 끝 도달이나 브로커 오류를 메시지로 착각하지 않습니다. KafkaJS는 유지보수가 활발하지 않은 상태이므로 새 Node.js 프로젝트라면 librdkafka 기반 클라이언트도 함께 검토해 볼 만합니다.
스키마 레지스트리
# 스키마 등록
curl -X POST http://redpanda:8081/subjects/orders-value/versions \
-H "Content-Type: application/json" \
-d '{"schema": "{\"type\":\"record\",\"name\":\"Order\",\"fields\":[{\"name\":\"id\",\"type\":\"long\"},{\"name\":\"amount\",\"type\":\"double\"}]}"}'
# 호환성 체크
curl http://redpanda:8081/compatibility/subjects/orders-value/versions/latest \
-H "Content-Type: application/json" \
-d '{"schema": "..."}'
Confluent Schema Registry와 동일한 REST API로 기존 도구가 그대로 동작합니다.
스키마 레지스트리는 메시지 자체를 검사하지 않는다는 점을 오해하기 쉽습니다. 프로듀서의 직렬화기(Avro·Protobuf serializer)가 레지스트리에서 스키마 ID를 받아 메시지 앞에 붙이고, 컨슈머의 역직렬화기가 그 ID로 스키마를 조회하는 클라이언트 측 규약입니다. 따라서 레지스트리를 켜 두어도 직렬화기를 쓰지 않는 프로듀서는 아무 JSON이나 보낼 수 있습니다. 레지스트리가 실제로 막아 주는 것은 호환성 규칙을 어기는 스키마 변경입니다. 기본 호환성 모드는 BACKWARD라서 필드를 삭제하거나 기본값 없는 필드를 추가하는 변경은 등록 단계에서 거부되고, 위의 호환성 체크 API로 배포 전에 미리 확인할 수 있습니다. 이 검사를 CI에 넣어 두면 “새 버전 프로듀서가 배포되자 옛 컨슈머가 역직렬화에 실패하는” 사고를 막을 수 있습니다.
Redpanda Connect (구 Benthos)
Kafka Connect 대안으로 YAML 기반의 선언적 스트림 프로세싱 + 커넥터:
# connect.yaml
input:
kafka_franz:
seed_brokers: ["redpanda:9092"]
topics: ["orders"]
consumer_group: "order-enricher"
pipeline:
processors:
- bloblang: |
root = this
root.enriched_at = now()
root.region = metadata("kafka_key").string()
output:
kafka_franz:
seed_brokers: ["redpanda:9092"]
topic: "orders.enriched"
redpanda-connect run connect.yaml
수많은 입력·출력 커넥터(S3, Postgres CDC, HTTP, Elasticsearch, Snowflake, Pulsar, NATS 등)를 단일 바이너리로 제공해 Kafka Connect 클러스터를 별도 운영할 필요가 없습니다.
bloblang은 Redpanda Connect의 매핑 언어로, root는 출력 문서, this는 입력 문서를 가리킵니다. 위 예제는 입력을 그대로 복사한 뒤 두 필드를 추가하는데, metadata("kafka_key")는 키가 없는 메시지에서 null이 되어 .string() 변환이 실패할 수 있으므로 실제로는 .or("") 같은 기본값을 두는 편이 안전합니다. Kafka Connect와의 가장 큰 차이는 상태 관리 방식입니다. Kafka Connect는 오프셋과 설정을 Kafka 토픽에 저장하고 워커 클러스터가 작업을 분산하지만, Redpanda Connect는 각 프로세스가 독립적으로 동작하고 Kafka 입력의 오프셋은 컨슈머 그룹으로 관리합니다. 단순한 파이프라인은 훨씬 가볍게 운영되지만, 장기 실행되는 CDC처럼 복잡한 상태가 필요한 작업은 커넥터별 동작을 문서로 확인해야 합니다.
Tiered Storage: S3 오프로드
로그 보존을 길게 가져가면서 로컬 SSD 비용을 줄이는 핵심 기능:
# redpanda.yaml
redpanda:
cloud_storage_enabled: true
cloud_storage_bucket: "my-redpanda-archive"
cloud_storage_region: "ap-northeast-2"
cloud_storage_access_key: "..."
cloud_storage_secret_key: "..."
# 토픽별 설정도 가능
# topic level:
# retention.ms=604800000 (7일 로컬)
# retention.local.target.ms=86400000 (하루만 로컬, 나머지 S3)
“핫 데이터는 로컬, 콜드는 S3” 자동 계층화로 로컬 디스크 비용을 줄이면서 consumer가 오래된 데이터도 정상 접근 가능합니다.
설정할 때 알아 둘 점이 있습니다. cloud_storage_* 값은 최신 버전에서 브로커별 redpanda.yaml이 아니라 클러스터 속성이라, rpk cluster config set cloud_storage_enabled true처럼 설정하거나 최초 기동 시 bootstrap 파일로 넣는 것이 권장됩니다. 토픽이 실제로 업로드되려면 토픽 속성 redpanda.remote.write와 redpanda.remote.read도 켜져 있어야 합니다. 액세스 키를 설정 파일에 평문으로 두기보다 cloud_storage_credentials_source로 인스턴스 IAM 역할을 쓰는 편이 안전합니다. 또 S3에서 읽어 오는 오래된 데이터는 로컬 디스크보다 지연이 훨씬 크므로, 컨슈머가 처음부터 다시 읽는(재처리) 작업은 로컬 데이터를 읽을 때와 전혀 다른 속도로 진행된다는 점을 운영 계획에 반영해야 합니다. 객체 스토리지 요청 수에 따른 비용도 저장 용량과 별도로 발생합니다.
프로덕션 설정
인증·인가
# SASL/SCRAM-SHA-256 활성
rpk cluster config set enable_sasl true
rpk cluster config set superusers "admin"
# 최신 rpk에서는 rpk security user create / rpk security acl create 형태
rpk acl user create admin -p strong-password
# 일반 사용자
rpk acl user create alice -p alice-password
rpk acl create --allow-principal User:alice --operation read --topic orders
rpk acl create --allow-principal User:alice --operation write --topic orders
# 클라이언트 연결
rpk topic produce orders -X user=alice -X pass=alice-password -X sasl.mechanism=SCRAM-SHA-256
순서가 중요합니다. enable_sasl을 켜는 순간 인증 없는 연결이 거부되므로, 슈퍼유저 계정을 먼저 만들고 superusers 목록에 넣은 뒤 SASL을 켜야 스스로를 잠그는 일을 피할 수 있습니다. 위 명령은 설명을 위해 순서를 단순화했으므로, 실제로는 사용자 생성 → 슈퍼유저 지정 → SASL 활성화 순서로 실행하세요. 비밀번호를 명령줄 인자로 넘기면 셸 기록과 프로세스 목록에 남는다는 점도 주의해야 합니다. 컨슈머에게는 토픽 read 권한 외에 컨슈머 그룹에 대한 read 권한도 필요해서, 이를 빠뜨리면 GROUP_AUTHORIZATION_FAILED로 소비가 실패합니다.
TLS
redpanda:
kafka_api_tls:
- name: external
enabled: true
cert_file: /etc/redpanda/certs/tls.crt
key_file: /etc/redpanda/certs/tls.key
require_client_auth: false
토픽 보존·압축
rpk topic create events \
--partitions 12 --replicas 3 \
--config retention.ms=604800000 \
--config compression.type=zstd \
--config cleanup.policy=delete
# compact (키 최신값 유지)
rpk topic create user-profiles \
--config cleanup.policy=compact \
--config segment.ms=3600000
컴팩션은 닫힌 세그먼트에서만 일어나므로, segment.ms를 짧게 잡은 이유는 활성 세그먼트가 빨리 닫혀 오래된 값이 더 자주 정리되게 하기 위해서입니다. 대신 세그먼트 파일이 많아져 파일 핸들과 메타데이터 부담이 늘어납니다. 컴팩션 토픽은 “키마다 최신 값 하나만 남는다”를 보장하지 않고 “결국에는 남는다”만 보장하므로, 컨슈머는 같은 키의 옛 값이 여러 번 보일 수 있다고 가정해야 합니다. compression.type=zstd는 브로커가 토픽 수준에서 압축 방식을 지정하는 설정인데, 프로듀서가 다른 코덱으로 압축해 보내면 브로커가 재압축해야 하므로 보통은 프로듀서 쪽 설정과 맞추거나 producer로 두는 편이 CPU를 아낍니다.
Kafka에서 마이그레이션
방법 1: MirrorMaker 2
# mm2.properties
clusters = source, target
source.bootstrap.servers = kafka-old:9092
target.bootstrap.servers = redpanda:9092
source->target.enabled = true
source->target.topics = .*
bin/connect-mirror-maker.sh mm2.properties
방법 2: Redpanda Connect
input:
kafka_franz:
seed_brokers: ["kafka-old:9092"]
topics: [".*"]
regexp_topics: true
consumer_group: "mirror"
output:
kafka_franz:
seed_brokers: ["redpanda:9092"]
topic: "${! meta(\"kafka_topic\") }"
방법 3: 애플리케이션 cutover
트래픽을 Redpanda에도 dual-write → consumer를 Redpanda로 전환 → 기존 Kafka의 dual-write 제거. 가장 안전하지만 시간이 걸립니다.
어떤 방법을 쓰든 가장 까다로운 부분은 컨슈머 오프셋입니다. 미러링은 메시지를 복사하지만 대상 클러스터에서 같은 메시지의 오프셋 번호는 원본과 달라질 수 있으므로, 컨슈머를 옮길 때 “원본에서 어디까지 읽었는지”를 대상의 오프셋으로 번역해야 합니다. MirrorMaker 2는 체크포인트 토픽과 오프셋 동기화(sync.group.offsets.enabled)로 이를 지원하지만, 방법 2의 Redpanda Connect는 메시지만 옮기므로 컨슈머를 전환할 때 처음부터 다시 읽거나 타임스탬프 기준으로 위치를 다시 잡아야 합니다. 두 방법 모두 원본 토픽 이름에 접두사가 붙는지(MM2의 기본 복제 정책은 source.orders처럼 접두사를 붙임)를 확인해야 컨슈머 설정을 바꿀 범위를 정확히 알 수 있습니다. 컨슈머가 중복 메시지를 처리해도 안전하게(멱등하게) 만들어 두면 전환 시점의 경계 문제에 훨씬 여유가 생깁니다.
벤치마크 체크리스트
Kafka 대비 실제 이득 측정:
# 두 클러스터에 같은 도구·같은 설정으로 부하를 건다 (Kafka 프로토콜이므로 둘 다 사용 가능)
kafka-producer-perf-test.sh --topic bench --num-records 10000000 \
--record-size 1024 --throughput -1 \
--producer-props bootstrap.servers=redpanda:9092 acks=all
kafka-producer-perf-test.sh --topic bench --num-records 10000000 \
--record-size 1024 --throughput -1 \
--producer-props bootstrap.servers=kafka:9092 acks=all
비교 대상:
- 평균/최대 처리량
- p50/p95/p99 publish 지연
- consumer lag
- CPU·메모리 사용
벤치마크 수치는 공급사마다 자기에게 유리한 조건으로 발표하는 경우가 많으므로, 공개 비교 결과보다 자기 워크로드로 잰 값을 믿어야 합니다. 비교할 때는 몇 가지 조건을 반드시 맞춰야 합니다. 같은 하드웨어와 디스크, 같은 복제 계수와 acks=all, 같은 fsync 정책(Redpanda는 기본적으로 쓰기를 디스크에 fsync하지만 Kafka는 기본적으로 페이지 캐시에 맡기고 복제로 내구성을 얻음), 같은 메시지 크기와 배치 설정입니다. 특히 fsync 차이는 결과를 크게 바꾸므로, “어느 쪽이 빠른가”보다 “같은 내구성 조건에서 어느 쪽이 빠른가”를 물어야 합니다. 부하를 처리량 최대치로만 거는 대신 목표 처리량을 고정해 두고 p99 지연을 비교하는 것이 실제 서비스 조건에 더 가깝습니다. 여러 프로듀서·컨슈머와 장시간 부하를 재현하려면 OpenMessaging Benchmark 같은 전용 도구를 쓰는 것도 방법입니다.
문제 해결
브로커가 under-replicated 상태
- Raft 복제가 따라가지 못함 → 네트워크·디스크 확인
rpk cluster health로 상세 확인- 단일 노드에서 다시 balance 되도록
rpk cluster partitions movement status
Produce 실패 not_enough_replicas
- min.insync.replicas 설정과 실제 복제본 수 확인
rpk topic describe TOPIC로 ISR 확인
Consumer lag 증가
- 컨슈머 그룹 처리 성능 문제.
rpk group describe <group>의 LAG 열이나 컨슈머 그룹 lag 메트릭 확인 - 파티션 수가 적으면 병렬성 부족 → 토픽 파티션 증가(기존 데이터는 재분배 안 됨 유의)
파티션을 늘릴 때는 부작용을 이해해야 합니다. 키 기반 파티셔닝은 “키 해시 % 파티션 수”로 위치를 정하므로, 파티션 수가 바뀌는 순간 같은 키의 새 메시지가 다른 파티션으로 가서 키 단위 순서 보장이 전환 시점에 깨집니다. 순서가 중요한 토픽이라면 처음부터 여유 있게 파티션을 잡거나, 새 토픽을 만들어 옮기는 편이 안전합니다. 또 lag의 원인이 컨슈머 한 인스턴스의 느린 처리(외부 API 호출 등)라면 파티션을 늘려도 해결되지 않고, 컨슈머 그룹의 리밸런싱이 자주 일어나는 것이 원인일 수도 있으므로 max.poll.interval.ms 초과 로그도 함께 확인하세요.
Tiered Storage 업로드 실패
- S3 자격 증명·버킷 권한
cloud_storage_max_connections조정
JVM Kafka 클라이언트가 연결 안 됨
- Kafka 프로토콜 버전 협상 로그 확인
- Redpanda가 지원하지 않는 API 호출 시
UNSUPPORTED_VERSION반환
모니터링
- Prometheus endpoint:
:9644/public_metrics(운영 대시보드용redpanda_*메트릭),:9644/metrics(내부 상세 메트릭,vectorized_*접두사) - 내부 메트릭은 수가 많고 카디널리티가 높으므로 필요한 항목만 수집하는 편이 모니터링 비용을 줄임
- 공식 Grafana 대시보드 제공
- Redpanda Console에서 실시간 토픽·컨슈머 모니터링
언제 Kafka를 유지해야 하나
- Confluent Platform 독점 기능 의존: Stream Designer, 특정 커넥터
- 대규모 Kafka Streams 파이프라인: 테스트·검증 비용이 큼
- 팀의 Kafka 운영 경험이 풍부: 이미 잘 돌고 있다면 리스크 감수 불필요
- Redpanda 에코시스템 미지원 도구: 일부 엔터프라이즈 도구
그 외 대부분은 점진적 도입·부분 교체로 이점을 취할 수 있습니다.
체크리스트
- 토픽 설계: 파티션·복제·보존·압축
- SASL + ACL + TLS로 보안
- Tiered Storage로 장기 보존 비용 절감
- 스키마 레지스트리로 이벤트 계약 강제
- Redpanda Connect로 CDC·싱크 구성
- Prometheus + Grafana 대시보드
- Console UI로 운영자 편의성 확보
- 재해복구: 두 리전 간 MM2/Connect 미러링
- rpk로 운영 자동화(IaC 연동)
마무리
Redpanda는 “Kafka 생태계의 가치는 유지하면서 운영 복잡성은 줄인다”는 절충안입니다. 이벤트 드리븐 아키텍처를 새로 시작하는 팀이라면 JVM과 별도 컨트롤러 구성 없이 단일 바이너리로 시작할 수 있다는 점이 큰 장점이고, 기존 Kafka를 운영하는 팀이라면 자기 워크로드로 p99 지연과 운영에 드는 인력을 직접 비교해 본 뒤 전환 여부를 판단하는 것이 좋습니다. 대부분의 Kafka 클라이언트·도구를 그대로 쓰면서 인프라만 교체하는 “조용한 마이그레이션”이 가능하다는 점이 Redpanda의 가장 큰 매력입니다.
자주 묻는 질문 (FAQ)
Q. Produce가 not_enough_replicas 오류로 실패하면 무엇을 확인해야 하나요?
A. 토픽의 min.insync.replicas 설정이 현재 동기화된 복제본 수보다 크면 이 오류가 납니다. rpk topic describe로 ISR을 확인하고, 복제본이 빠진 이유가 Raft 복제 지연이라면 rpk cluster health로 브로커 상태와 네트워크·디스크를 함께 점검합니다. 개발용 1노드 클러스터에서 복제 계수나 min.insync.replicas를 프로덕션 값 그대로 쓰면 자주 만나는 오류입니다.