La Diferencia Arquitectónica Que Importa
Las diferencias arquitectónicas fundamentales son el punto de partida, ya que todas las características de rendimiento se derivan de ellas.
Apache Kafka es una aplicación JVM. Está escrito en Scala y Java, corre en el JVM con su garbage collector asociado, y hasta hace poco requería ZooKeeper como servicio de coordinación separado. El modo KRaft (el consenso Raft built-in de Kafka) está production-ready desde Kafka 3.5, y ZooKeeper está oficialmente deprecado desde la 3.7.
Redpanda es una aplicación C++ construida sobre el framework Seastar, el mismo framework de I/O asíncrono que utiliza ScyllaDB. Sin JVM, sin garbage collector, sin ZooKeeper. Usa Raft para consenso nativamente, con cada partición teniendo su propio grupo Raft.
Esto no es solo una diferencia de lenguaje. Es una diferencia fundamental en cómo interactúan con el hardware.
Thread-Per-Core vs Thread Pool
El modelo thread-per-core de Seastar significa que cada core de CPU corre un único thread que es dueño de su memoria, sus conexiones de red y su I/O de disco. Sin locks, sin context switches entre threads, sin memoria compartida entre cores.
Redpanda (Seastar/thread-per-core):
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Core 0 │ │ Core 1 │ │ Core 2 │ │ Core 3 │
│ Shard 0 │ │ Shard 1 │ │ Shard 2 │ │ Shard 3 │
│ Mem prop │ │ Mem prop │ │ Mem prop │ │ Mem prop │
│ I/O prop │ │ I/O prop │ │ I/O prop │ │ I/O prop │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
↕ message passing (sin estado compartido)
Kafka (JVM/thread pool):
┌───────────────────────────────────────────────────┐
│ JVM Heap (compartido) │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │Thread 1│ │Thread 2│ │Thread 3│ │Thread N│ │
│ └────┬───┘ └────┬───┘ └────┬───┘ └────┬───┘ │
│ └──────────┴──────────┴──────────┘ │
│ Acceso sincronizado │
│ Las pausas de GC afectan a TODOS │
└───────────────────────────────────────────────────┘
Los threads de Kafka comparten memoria del heap y coordinan a través de locks y el modelo de memoria del JVM. Cuando el collector G1 o ZGC arranca, todos los threads pausan (o al menos se ralentizan durante las fases concurrentes). Redpanda simplemente no tiene este problema, no hay GC porque no hay managed heap.
Los Benchmarks
Las siguientes pruebas se corrieron en hardware idéntico: clusters de 3 nodos, cada nodo con 16 cores (AMD EPYC 7313), 64GB RAM, 2x NVMe SSDs en RAID 0, red de 25Gbps. Esto representa un setup "serio pero no extravagante".
Test de Throughput
Configuración de producer: mensajes de 1KB, batching habilitado (linger.ms=5, batch.size=65536), acks=all, 6 particiones, factor de replicación 3.
# Benchmark Kafka
bin/kafka-producer-perf-test.sh \
--topic bench-topic \
--num-records 10000000 \
--record-size 1024 \
--throughput -1 \
--producer-props \
bootstrap.servers=kafka1:9092,kafka2:9092,kafka3:9092 \
acks=all \
linger.ms=5 \
batch.size=65536
# Benchmark Redpanda (la misma herramienta funciona, API compatible)
bin/kafka-producer-perf-test.sh \
--topic bench-topic \
--num-records 10000000 \
--record-size 1024 \
--throughput -1 \
--producer-props \
bootstrap.servers=redpanda1:9092,redpanda2:9092,redpanda3:9092 \
acks=all \
linger.ms=5 \
batch.size=65536
Resultados (10M mensajes, 1KB cada uno):
| Métrica | Kafka 3.7 (KRaft) | Redpanda 24.3 |
|---|---|---|
| Throughput (MB/s) | 850 | 920 |
| Latencia avg | 3.2ms | 1.8ms |
| Latencia p50 | 2.1ms | 1.4ms |
| Latencia p99 | 28ms | 4.2ms |
| Latencia p99.9 | 145ms | 8.1ms |
Notemos el número de p99.9. 145ms vs 8.1ms. Eso no es un error de redondeo, es el G1GC del JVM haciendo una mixed collection. Y esto fue después de un extenso tuning de flags del JVM:
# Tuning de JVM para Kafka (KAFKA_HEAP_OPTS)
-Xms12g -Xmx12g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=20
-XX:G1HeapRegionSize=16M
-XX:InitiatingHeapOccupancyPercent=35
-XX:G1ReservePercent=25
-XX:+ParallelRefProcEnabled
-XX:+ExplicitGCInvokesConcurrent
Sin ese tuning, el p99.9 de Kafka pasaba los 400ms. El JVM es poderoso, pero lo pagás en la cola.
Throughput de Consumer
Para consumo, se testeó un grupo de 3 consumers leyendo del mismo topic:
| Métrica | Kafka 3.7 | Redpanda 24.3 |
|---|---|---|
| Throughput consumer (MB/s) | 1,100 | 1,250 |
| Tiempo de rebalanceo (3→4 consumers) | 12s | 3s |
| Latencia end-to-end (produce→consume) | 8ms avg | 3ms avg |
La diferencia en tiempo de rebalanceo es significativa en la práctica. Cada rebalanceo significa un período donde las particiones no están asignadas y no se procesan mensajes. El cooperative sticky assignor de Kafka mejoró las cosas, pero la asignación de particiones de Redpanda es directamente más rápida porque no hay coordinación con un servicio de metadata separado.
La Migración: Un Caso de Estudio
Consideremos un cluster de 5 nodos de Kafka manejando unos 2TB de eventos por día, analytics de usuarios, eventos de órdenes, updates de inventario, todo el paquete. La decisión de probar Redpanda vino de dos cosas: el equipo de on-call pasaba el 30% del tiempo de incidentes en problemas de ZooKeeper o tuning de JVM, y el SLO de latencia p99 se estaba poniendo cada vez más difícil de cumplir.
Paso 1: Cluster Shadow
Primero se levantó un cluster Redpanda de 3 nodos y usamos MirrorMaker 2 para replicar todos los topics:
# mm2.properties
clusters = kafka, redpanda
kafka.bootstrap.servers = kafka1:9092,kafka2:9092,kafka3:9092
redpanda.bootstrap.servers = rp1:9092,rp2:9092,rp3:9092
kafka->redpanda.enabled = true
kafka->redpanda.topics = .*
sync.topic.configs.enabled = true
sync.topic.acls.enabled = false
replication.factor = 3
# Crítico: configurar esto para evitar que manee los nombres de topic
replication.policy.class = org.apache.kafka.connect.mirror.IdentityReplicationPolicy
Esto funcionó out of the box. MirrorMaker 2 no sabe ni le importa que el target es Redpanda, simplemente habla el protocolo Kafka.
Paso 2: Validación de Consumers
Se corrieron consumers shadow en el cluster Redpanda y comparamos conteos de mensajes, orden y contenido contra los consumers de Kafka. Esto detectó dos problemas:
-
Manejo de timestamps: El
LogAppendTimede Redpanda tenía diferencias de precisión en microsegundos respecto a Kafka. La lógica de deduplicación que usaba comparaciones de timestamp se rompió. -
Marcadores de transacción: Algunos producers estaban usando transacciones (
enable.idempotence=true+transactional.id). Los marcadores de commit de transacción aparecían distinto en la representación interna de Redpanda. Los consumers usando aislamientoread_committedfuncionaban bien, pero una herramienta custom de tracking de offsets que parseaba los datos crudos de la partición se rompió.
Paso 3: El Switchover
Se realizó una migración rolling, moviendo un consumer group a la vez:
# Script de migración simplificado
import confluent_kafka
def migrate_consumer_group(group_id, kafka_brokers, redpanda_brokers, topics):
# 1. Obtener offsets committed actuales de Kafka
admin = confluent_kafka.admin.AdminClient({
'bootstrap.servers': kafka_brokers
})
committed = admin.list_consumer_group_offsets([
confluent_kafka.admin.ConsumerGroupTopicPartitions(group_id)
])
# 2. Mapear offsets a Redpanda (MM2 preserva offsets con IdentityReplicationPolicy)
rp_admin = confluent_kafka.admin.AdminClient({
'bootstrap.servers': redpanda_brokers
})
# 3. Pre-commitear offsets a Redpanda antes de arrancar consumers
offsets = []
for tp, offset_meta in committed[group_id].result().topic_partitions:
offsets.append(confluent_kafka.TopicPartition(
tp.topic, tp.partition, offset_meta.offset
))
rp_admin.alter_consumer_group_offsets([
confluent_kafka.admin.ConsumerGroupTopicPartitions(group_id, offsets)
])
print(f"Migrado {group_id}: {len(offsets)} offsets de partición committeados a Redpanda")
Paso 4: Switchover de Producers
Se cambiaron los bootstrap servers de los producers durante una ventana de bajo tráfico:
# Antes
kafka:
bootstrap-servers: kafka1:9092,kafka2:9092,kafka3:9092
# Después
kafka: # mantuvimos el nombre del config key para minimizar cambios de código
bootstrap-servers: rp1:9092,rp2:9092,rp3:9092
Funcionó sin problemas. Sin cambios de código necesarios. La librería cliente de Kafka se conectó a Redpanda y empezó a producir. Producers idempotentes, producers transaccionales, producers con particiones, todos.
Schema Registry: La Historia de Compatibilidad
Redpanda incluye su propio schema registry que es API-compatible con el de Confluent. La migración cubrió ~200 schemas Avro:
# Exportar schemas del Confluent Schema Registry
for subject in $(curl -s http://kafka-sr:8081/subjects | jq -r '.[]'); do
schema=$(curl -s "http://kafka-sr:8081/subjects/${subject}/versions/latest")
echo "$schema" > "schemas/${subject}.json"
done
# Importar en el schema registry de Redpanda
for file in schemas/*.json; do
subject=$(basename "$file" .json)
schema=$(cat "$file" | jq -c '{schema: .schema, schemaType: .schemaType}')
curl -X POST "http://rp1:8081/subjects/${subject}/versions" \
-H "Content-Type: application/vnd.schemaregistry.v1+json" \
-d "$schema"
done
Los modos de compatibilidad (BACKWARD, FORWARD, FULL) funcionaron todos. El único gotcha fue que el schema registry de Redpanda guarda datos internamente en un topic _schemas, mientras que Confluent usa _schemas también, pero el formato de los mensajes difiere. No es posible simplemente replicar el topic, teners que re-registrar.
Diferencias Operacionales Que Importan Día a Día
Monitoreo
Redpanda expone métricas Prometheus en el puerto 9644 por defecto. Sin JMX, sin exporters JMX-to-Prometheus, sin andar peleando con autenticación de JMX remoto:
# prometheus.yml
scrape_configs:
- job_name: redpanda
static_configs:
- targets:
- rp1:9644
- rp2:9644
- rp3:9644
metrics_path: /public_metrics
Con Kafka, obtener buenas métricas requiere un sidecar de JMX exporter, que en sí consume recurse es y agrega complejidad. Ese setup no se extraña.
Uso de Disco
Redpanda usa alrededor de 20-30% menos de disco para los mismos datos, principalmente porque no tiene el overhead de archivos de índice y timeindex que Kafka crea por segmento. Redpanda mantiene su índice en memoria usando su propio motor de storage.
Memoria
Acá resulta interesante. Kafka necesita tuning del heap del JVM, muy poco y las pausas de GC te matan, demasiado y las pausas de GC también te matan (pero de otra forma). Con Redpanda, le decís cuánta memoria usar y la maneja solo:
rpk redpanda config set --set redpanda.memory=48G
rpk redpanda config set --set redpanda.reserve_memory=4G
Sin tuning de heap, sin tuning de GC, sin flags -XX:. El allocator de Seastar maneja todo.
Donde Kafka todavía gana
Kafka sigue siendo una opción sólida, y existen razones sustanciales para continuar utilizándolo:
- Kafka Streams y ksqlDB: El ecosistema de procesamiento de streams no tiene rival. Redpanda no tiene equivalente, lo combinarías con Flink o tus propios consumers.
- Kafka Connect: Si bien Redpanda puede correr Kafka Connect (es solo el protocolo Kafka), el ecosistema de conectores testeados en producción es más profundo con Kafka.
- Madurez de KRaft: El modo KRaft elimina ZooKeeper, y está estable desde la 3.5. La brecha operacional se está achicando.
- Comunidad y contratación: Encontrar ingenieros de Kafka es más fácil. La experiencia en Redpanda es más rara.
- Tiered storage: El tiered storage de Kafka (escribir segmentos fríos a S3) está más maduro que la implementación de Redpanda.
Los Números Después de 8 Meses
Después de migrar a Redpanda:
- Latencia p99 de producción: Bajó de 28ms a 4ms (consistente, no solo al principio)
- Incidentes on-call relacionados a streaming: Bajaron 70%
- Costo de infraestructura: Bajó 40% (3 nodos Redpanda reemplazaron 5 Kafka + 3 ZooKeeper)
- Tiempo dedicado a tuning de JVM: Cero, porque no hay JVM
La migración valió la pena para este caso de uso. Sin embargo, no es universalmente recomendable. Si un cluster de Kafka está estable, la latencia es aceptable, y se usa Kafka Streams a fondo, quedarse con Kafka puede ser la mejor opción. La migración tiene costos reales y riesgos reales.
Para proyectos greenfield, sin embargo, Redpanda merece fuerte consideración. La simplicidad operacional sola justifica la elección.