Apache Avro: Serialización de Datos Basada en Esquemas para Big Data
Apache Avro es un framework de serialización de datos desarrollado como parte del proyecto Apache Hadoop en 2009. Usa JSON para definir esquemas y produce salida binaria compacta, combinando la disciplina schema-first de Protobuf con la autoría de esquemas legible por humanos de JSON. Avro se ha convertido en el formato de serialización dominante en los ecosistemas de Apache Kafka, especialmente cuando se combina con un Schema Registry — convirtiéndolo en el estándar para arquitecturas orientadas a eventos, pipelines de datos en streaming y mensajería de microservicios a escala.
Qué Hace Diferente a Avro
Esquema en el archivo: A diferencia de Protobuf (que almacena el esquema separadamente en archivos .proto), Avro serializa el esquema junto con los datos en los archivos contenedor Avro (.avro). Esto significa que un archivo Avro es autónomo y puede decodificarse sin archivos de esquema externos.
Soporte de tipado dinámico: Los esquemas Avro son JSON, legibles sin compilador, y pueden generarse o modificarse programáticamente en tiempo de ejecución.
Nativo en Kafka: El Schema Registry de Confluent almacena esquemas Avro centralmente, asignando a cada esquema un ID numérico. Los mensajes Kafka llevan entonces solo un prefijo de 5 bytes con el ID del esquema + la carga útil binaria Avro.
Orientado a filas: A diferencia de Parquet (columnar), Avro almacena datos fila por fila, haciéndolo eficiente para el streaming registro a registro pero menos eficiente para consultas analíticas selectivas por columnas.
Lenguaje de Esquema Avro
Los esquemas Avro se escriben en JSON. Los tipos primitivos son:
"null", "boolean", "int", "long", "float", "double", "bytes", "string"
Los tipos complejos incluyen records, enums, arrays, maps, unions y secuencias de bytes de tamaño fijo:
{
"type": "record",
"name": "Usuario",
"namespace": "com.ejemplo.usuarios",
"fields": [
{"name": "id", "type": "long"},
{"name": "nombre_usuario", "type": "string"},
{"name": "email", "type": "string"},
{
"name": "estado",
"type": {
"type": "enum",
"name": "EstadoUsuario",
"symbols": ["ACTIVO", "INACTIVO", "SUSPENDIDO", "ELIMINADO"]
},
"default": "ACTIVO"
},
{
"name": "roles",
"type": {"type": "array", "items": "string"},
"default": []
},
{
"name": "metadatos",
"type": {"type": "map", "values": "string"},
"default": {}
},
{
"name": "creado_en",
"type": {"type": "long", "logicalType": "timestamp-millis"}
},
{
"name": "notas",
"type": ["null", "string"],
"default": null
}
]
}
Unions y Campos Anulables
El patrón ["null", "string"] para campos anulables es la forma idiomática de Avro — una unión de null y string. El primer tipo en una unión es el tipo predeterminado, por lo que "default": null requiere que null aparezca listado primero. Esta es la fuente más común de confusión para los nuevos usuarios de Avro.
Tipos Lógicos
Los tipos lógicos de Avro anotan tipos primitivos con significado semántico:
| Tipo Lógico | Tipo Base | Semántica |
|---|---|---|
date |
int | Días desde la época Unix |
timestamp-millis |
long | Milisegundos desde la época Unix |
timestamp-micros |
long | Microsegundos desde la época Unix |
decimal |
bytes/fixed | Número decimal de precisión arbitraria |
uuid |
string | Representación UUID |
Python: fastavro
import fastavro
import io
from datetime import datetime, timezone
# Definir esquema
schema = fastavro.parse_schema({
"type": "record",
"name": "Pedido",
"namespace": "com.ejemplo.pedidos",
"fields": [
{"name": "pedido_id", "type": "long"},
{"name": "cliente_id", "type": "long"},
{"name": "sku_producto", "type": "string"},
{"name": "cantidad", "type": "int"},
{"name": "precio_centimos", "type": "long"},
{"name": "moneda", "type": "string", "default": "EUR"},
{"name": "realizado_en", "type": {"type": "long", "logicalType": "timestamp-millis"}},
{"name": "notas", "type": ["null", "string"], "default": None},
]
})
# Registros de ejemplo
registros = [
{
"pedido_id": 1001,
"cliente_id": 42,
"sku_producto": "WIDGET-A",
"cantidad": 3,
"precio_centimos": 2999,
"moneda": "EUR",
"realizado_en": int(datetime(2024, 3, 15, 10, 30, tzinfo=timezone.utc).timestamp() * 1000),
"notas": None,
},
]
# Escribir en archivo contenedor Avro
with open('pedidos.avro', 'wb') as f:
fastavro.writer(f, schema, registros, codec='snappy')
# Leer desde archivo contenedor Avro
with open('pedidos.avro', 'rb') as f:
lector = fastavro.reader(f)
for registro in lector:
print(f"Pedido {registro['pedido_id']}: {registro['sku_producto']} x{registro['cantidad']}")
# Serialización sin esquema en el payload (para Kafka)
def avro_serializar(schema, registro):
buf = io.BytesIO()
fastavro.schemaless_writer(buf, schema, registro)
return buf.getvalue()
def avro_deserializar(schema, datos):
buf = io.BytesIO(datos)
return fastavro.schemaless_reader(buf, schema)
crudo = avro_serializar(schema, registros[0])
print(f"Tamaño serializado: {len(crudo)} bytes")
Avro con Kafka y Schema Registry
La característica más valiosa de Avro en producción es su integración con el Confluent Schema Registry:
from confluent_kafka.schema_registry import SchemaRegistryClient
from confluent_kafka.schema_registry.avro import AvroSerializer, AvroDeserializer
from confluent_kafka.serialization import SerializationContext, MessageField
# Conectar al Schema Registry
cliente_sr = SchemaRegistryClient({'url': 'http://schema-registry:8081'})
avro_serializer = AvroSerializer(cliente_sr, schema_str)
avro_deserializer = AvroDeserializer(cliente_sr)
El formato wire para un mensaje Avro de Schema Registry es:
[0x00][4 bytes ID de esquema][payload binario Avro]
El byte mágico 0x00 identifica el mensaje. Los 4 bytes de ID de esquema permiten al consumidor obtener el esquema correcto del registro y decodificar el payload.
Reglas de Evolución de Esquemas
La evolución de esquemas en Avro es más flexible que en Protobuf:
Compatibilidad Hacia Atrás (nuevo lector, datos antiguos)
- ✅ Añadir un campo con un valor predeterminado
- ✅ Eliminar un campo que tenía valor predeterminado
- ❌ Añadir un campo sin valor predeterminado
- ❌ Eliminar un campo requerido (sin predeterminado)
Compatibilidad Hacia Adelante (lector antiguo, datos nuevos)
- ✅ Eliminar un campo
- ✅ Añadir un campo con un valor predeterminado
Compatibilidad Total (ambas direcciones)
- ✅ Solo añadir/eliminar campos que tienen valores predeterminados
El Schema Registry impone estas reglas de compatibilidad automáticamente cuando intentas registrar una nueva versión de esquema, evitando que los cambios que rompen la compatibilidad lleguen a los consumidores en producción.
Avro vs Parquet vs Protobuf
| Característica | Avro | Parquet | Protobuf |
|---|---|---|---|
| Orientación del almacenamiento | Filas | Columnar | Filas |
| Ubicación del esquema | En archivo + Registry | En archivo | .proto externo |
| Lenguaje del esquema | JSON | Tipo JSON | IDL Proto |
| Esquema legible por humanos | ✅ Sí | ✅ Sí | ✅ Sí |
| Consultas analíticas | ❌ Deficiente | ✅ Excelente | ❌ Deficiente |
| Registros en streaming | ✅ Excelente | ⚠️ Sobrecarga | ✅ Excelente |
| Ecosistema Kafka | ✅ Nativo | ❌ No típico | ✅ Soportado |
Conclusión
Apache Avro ocupa un nicho específico e importante: serialización en streaming orientada a filas con evolución de esquemas flexible basada en JSON e integración nativa con Kafka/Schema Registry. Mientras que Protobuf es más eficiente para RPC de microservicios y Parquet es superior para el almacenamiento analítico, la combinación de Avro de esquemas legibles por humanos, reglas de evolución robustas impuestas por el Schema Registry, y codificación orientada a filas optimizada para el streaming registro a registro lo convierte en el estándar práctico para las arquitecturas de eventos basadas en Kafka. Comprender el lenguaje de esquemas de Avro, la codificación binaria, los patrones union y la integración con Schema Registry te prepara para construir sistemas orientados a eventos de grado de producción donde los cambios de esquema no deben romper a los consumidores en ejecución.
Conversiones relacionadas
Conversiones frecuentes del catálogo: