1. ¿Qué es la serialización (y por qué importa)?
Cuando dos servicios se comunican a través de Kafka, se envían bytes, no objetos. La serialización es el proceso de convertir un objeto en memoria a bytes para transmitirlo, y la deserialización es el proceso inverso: convertir esos bytes de vuelta a un objeto.
El enfoque más común es JSON: legible, universal, fácil de depurar. Pero tiene un problema: es verbose y no tiene un schema estricto impuesto a nivel de protocolo.
El mismo objeto de pedido — JSON ocupa 187 bytes, Avro binario solo 41 bytes (≈78% de reducción).
Apache Avro resuelve ambos problemas: serializa datos en formato binario compacto Y define un schema estricto que tanto el productor como el consumidor deben respetar.
2. ¿Qué es Apache Avro?
Apache Avro es un framework de serialización de datos creado por la Apache Software Foundation. Nació en el ecosistema de Hadoop pero se popularizó enormemente con Kafka por dos características clave:
- Serialización binaria: compacta, rápida y eficiente.
- Schema en JSON: la estructura de los datos se define en un archivo separado y explícito (
.avsc).
Ejemplo de Schema Avro
Un schema Avro es un archivo JSON que describe los campos, sus tipos y opcionalmente sus valores por defecto:
{
"type": "record",
"name": "Order",
"namespace": "com.faraujop.ecommerce",
"fields": [
{ "name": "orderId", "type": "string" },
{ "name": "product", "type": "string" },
{ "name": "quantity", "type": "int" },
{ "name": "price", "type": "double" },
{ "name": "status", "type": "string" }
]
}
El schema es el contrato entre productor y consumidor. Si el productor envía un campo que no existe en el schema, o omite uno requerido, el serializador lanza un error de inmediato, antes de que el mensaje llegue a Kafka.
3. Avro vs JSON vs Protobuf
Existen varios formatos de serialización. Aquí una comparación práctica:
| Característica | JSON | Apache Avro | Protobuf |
|---|---|---|---|
| Formato | Texto (legible) | Binario (compacto) | Binario (compacto) |
| Schema | ✗ Optional | ✓ Required | ✓ Required (.proto) |
| Tamaño | ✗ Large | ✓ Very small | ✓ Very small |
| Evolución de schema | ✗ Manual | ✓ Native | △ Partial |
| Ecosistema Kafka | △ Common | ✓ Native (Confluent) | △ Supported |
| Legibilidad humana | ✓ Yes | ✗ No | ✗ No |
Avro es el estándar de facto para Kafka, especialmente cuando se usa Confluent Platform, porque todo el ecosistema está diseñado a su alrededor (Schema Registry, connectors, KSQL).
4. El Schema Registry: Control Centralizado
Aquí hay un problema: si el schema Avro vive solo en el código del productor, ¿cómo sabe el consumidor cómo deserializar el mensaje? También necesita el schema.
La solución es el Schema Registry: un servicio centralizado (típicamente Confluent Schema Registry) que almacena todos los schemas y les asigna un ID numérico único.
El Schema Registry se consulta una vez por ID de schema, luego se cachea localmente. Kafka solo almacena bytes, no el schema.
¿Cómo se ve un mensaje dentro de Kafka?
Cada mensaje Avro con Schema Registry tiene un prefijo mágico de 5 bytes: 1 byte de número mágico (0x00) + 4 bytes con el ID del schema. El consumidor lee esos 4 bytes, obtiene el schema del registry y deserializa el resto.
// Estructura de un mensaje Avro en Kafka [ 0x00 ] // magic byte [ 0x00 0x00 0x00 0x2A ] // schema ID = 42 (4 bytes, big-endian) [ ... bytes de datos Avro ... ]
5. Avro en .NET: Ejemplo Práctico
Paquetes NuGet necesarios
dotnet add package Confluent.Kafka dotnet add package Confluent.SchemaRegistry dotnet add package Confluent.SchemaRegistry.Serdes.Avro dotnet add package Apache.Avro
Definición del Schema (order.avsc)
Primero crea el archivo de schema. En .NET se suele usar GenericRecord (dinámico, sin generación de código) o generar clases C# a partir del schema con las herramientas de Avro.
{
"type": "record",
"name": "Order",
"namespace": "com.faraujop.ecommerce",
"fields": [
{ "name": "orderId", "type": "string" },
{ "name": "product", "type": "string" },
{ "name": "quantity", "type": "int" },
{ "name": "price", "type": "double" },
{ "name": "status", "type": "string" }
]
}
Producer con Avro
using Confluent.Kafka; using Confluent.SchemaRegistry; using Confluent.SchemaRegistry.Serdes; using Avro; using Avro.Generic; var schemaRegistryConfig = new SchemaRegistryConfig { Url = "http://localhost:8081" }; var producerConfig = new ProducerConfig { BootstrapServers = "localhost:9092" }; // Leemos el schema desde el archivo .avsc var schemaJson = await File.ReadAllTextAsync("order.avsc"); var schema = (RecordSchema)Schema.Parse(schemaJson); using var schemaRegistry = new CachedSchemaRegistryClient(schemaRegistryConfig); using var producer = new ProducerBuilder<string, GenericRecord>(producerConfig) .SetValueSerializer(new AvroSerializer<GenericRecord>(schemaRegistry)) .Build(); // Creamos el mensaje con GenericRecord var order = new GenericRecord(schema); order.Add("orderId", "ORD-001"); order.Add("product", "Laptop"); order.Add("quantity", 1); order.Add("price", 999.99); order.Add("status", "PENDING"); await producer.ProduceAsync("orders", new Message<string, GenericRecord> { Key = order["orderId"].ToString()!, Value = order }); Console.WriteLine("✅ Pedido enviado con Avro");
Consumer con Avro
using Confluent.Kafka; using Confluent.SchemaRegistry; using Confluent.SchemaRegistry.Serdes; using Avro.Generic; var schemaRegistryConfig = new SchemaRegistryConfig { Url = "http://localhost:8081" }; var consumerConfig = new ConsumerConfig { BootstrapServers = "localhost:9092", GroupId = "inventory-service", AutoOffsetReset = AutoOffsetReset.Earliest }; using var schemaRegistry = new CachedSchemaRegistryClient(schemaRegistryConfig); using var consumer = new ConsumerBuilder<string, GenericRecord>(consumerConfig) .SetValueDeserializer(await new AvroDeserializer<GenericRecord>(schemaRegistry) .AsSyncOverAsync()) .Build(); consumer.Subscribe("orders"); while (true) { var result = consumer.Consume(); var order = result.Message.Value; Console.WriteLine($"Pedido: {order["orderId"]} | {order["product"]} x{order["quantity"]} | {order["status"]}"); }
Para mayor seguridad de tipos, puedes usar las herramientas de generación de código de Avro para crear clases C# fuertemente tipadas a partir de tus archivos .avsc, en lugar del enfoque dinámico con GenericRecord.
6. Evolución de Schemas: La Killer Feature
Aquí es donde Avro brilla de verdad. En un sistema real, los schemas cambian con el tiempo. Se añade un campo, se renombra, se elimina. Con JSON, hay que coordinar manualmente todos los servicios — una pesadilla en microservicios.
Avro soporta evolución de schemas con tres modos de compatibilidad:
Los campos opcionales nuevos con valores por defecto son retrocompatibles: los consumidores antiguos ignoran campos desconocidos sin errores.
Cómo añadir un campo de forma segura
{
"type": "record",
"name": "Order",
"namespace": "com.faraujop.ecommerce",
"fields": [
{ "name": "orderId", "type": "string" },
{ "name": "product", "type": "string" },
{ "name": "quantity", "type": "int" },
{ "name": "price", "type": "double" },
{ "name": "status", "type": "string" },
{
"name": "discount",
"type": ["null", "double"], // nullable
"default": null // valor por defecto → retrocompatible
}
]
}
Modos de compatibilidad en el Schema Registry
| Modo | Significado | Usar cuando |
|---|---|---|
| BACKWARD | Nuevo consumer lee mensajes viejos | Actualizas consumers primero |
| FORWARD | Viejo consumer lee mensajes nuevos | Actualizas producers primero |
| FULL | Ambas direcciones | Necesitas máxima flexibilidad |
Renombrar o eliminar campos requeridos (sin default) rompe la compatibilidad. Siempre añade campos nuevos con un valor default, y depreca los campos gradualmente en lugar de eliminarlos de inmediato.
7. ¿Cuándo usar Avro?
Avro es una excelente opción cuando tienes:
- Alto volumen de mensajes: el ahorro de espacio se traduce en reducción real de costes en almacenamiento y red.
- Múltiples equipos o servicios consumiendo el mismo topic: el schema actúa como contrato formal de API.
- Kafka en producción con Confluent Platform: las herramientas están diseñadas en torno a Avro.
- Necesidad de evolución de schema: añadir campos sin romper consumidores existentes.
Es posible que no necesites Avro si: estás haciendo un prototipo, tienes un solo consumidor, o tus mensajes ya son pequeños.
Puntos Clave
- Avro serializa datos en binario compacto — hasta 80% más pequeño que JSON
- Los schemas se definen en JSON (
.avsc) y se almacenan en Schema Registry - Cada mensaje Kafka lleva un prefijo de 5 bytes con el ID del schema
- Schema Registry cachea schemas localmente — overhead de red mínimo
- Añadir campos opcionales con defaults es seguro — retrocompatible
- En .NET: usar el paquete NuGet
Confluent.SchemaRegistry.Serdes.Avro