Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Designing Data-Intensive Applications: 4. Encoding and Evolution #7

Open
guilleiguaran opened this issue May 3, 2019 · 3 comments
Open
Labels

Comments

@guilleiguaran
Copy link
Collaborator

Esta semana a cargo @julianduque
Siguiente semana @fmauricios

@julianduque
Copy link
Member

El summary lo estaré publicando el domingo, esta semana tuve cirugía y ando lento ;)

@julianduque
Copy link
Member

julianduque commented May 13, 2019

Capítulo 4 - Encoding y Evolución

Conceptos Claves

  • Evolvability - facilidad de adaptación al cambio
  • schema-on-read: schemaless (Flexibilidad de Schema)
  • rolling upgrade - staged rollout

Cuando el formato de los datos cambia se debe hacer cambio del código pero no siempre se puede hacer inmediatamente.

  • Aplicaciones server-side: requieren un rolling upgrade para garantizar que el cambio no afecta el sistema
  • Aplicaciones client-side: depende del usuario realizando actualización

Viejas y nuevas versiones del código y los datos pueden existir al mismo tiempo, por eso de debe garantizar compatibilidad en ambas vías

  • Backwards compatibility: Código nuevo puede leer data vieja
  • Forward compatibility: Código viejo puede leer data nueva

Backwards compatibility usualmente no es dificil pues se conoce la forma de datos anterior. Forward compatibility es dificil pues el código debe ignorar adiciones a los datos en nuevas versiones del código.

Formatos de Encoding

  • JSON
  • XML
  • Protocol Buffers
  • Thrift
  • Avro

Aplicaciones usualmente trabajan con datos en dos representaciones:

  • En memoria, la data se representa en estructuras de datos (objetos, listas, arreglos, mapas) y son optimizadas para ser manipuladas por la CPU
  • Cuando se quiere escribir los datos a un archivo o transferir en la red se necesita codificar como una secuencia de bytes utilizando algún formato (por ejemplo JSON)

El proceso de tradución de la data en memoria hacia una secuencia de bytes se conoce como codificación (encoding, serialización o marshalling).
El proceso inverso se conoce como decodificación (decoding, parsing, deserialization, unmarshalling).

Formatos específicos de un lenguaje

Varios lenguajes tienen sus propios formatios para codificación:

  • Java - java.io.Serializable
  • Ruby - Marshall
  • Python - pickle

Aunque pueden llegar a ser convenientes tienen sus limitaciones:

  • Solo se puede usar con el mismo lenguaje de programación o plataforma
  • Puede representar problemas de seguridad pues el proceso de decodificación requiere instanciar una clase de manera arbitraria
  • No manejan bien el tema de versionamiento y compatibilidad (backwards and forward)
  • Performance: No siempre es el método mas eficiente

Conclusión: Evitar usar este tipo de formatos, solo para cosas pequeñas / no importantes.

JSON, XML y CSV

JSON, XML y CSV son formatos de codificación de tipo texto, bastante populares, de alguna u otra forma legibles (human readable) y con una síntaxis no muy poderosa.

Algunos de los problemas relacionados con estos formatos:

  • Codificación de números: XML y CSV no puede distinguir entre String y Número, JSON aunque hace la distinción no puede trabajar con punto flotante, precisión y números grandes (2^53)
  • JSON y XML tienen buen soporte de Unicode (human readable) pero no tiene soporte de binary strings, se puede hacer el workaround codificando estos en base64 pero aumenta el tamaño de los datos en un 33%
  • JSON y XML tienen soporte para schema, son bastante poderosos pero dificiles de aprender e implementar. Es mucho mas común en XML que en JSON.
  • CSV no posee esquema, depende de la implementación de la aplicación en como va a interpretar los datos.

A pesar de sus fallas, son suficientemente buenos para la mayoría de aplicaciones siempre y cuando se mantenga un formato definido.

Codificación Binaria

Son formatos mas compactos y rápidos de parsear, óptimos para grandes cantidades de información (mejor performance) y recomendados para uso interno dentro de una organización. Aunque son utilizados no son tan populares como sus versiones basadas en texto.

  • JSON: MessagePack, BSON, BJSON, UBJSON, BISON, Smile
  • XML: WBXML, Fast Infoset

Algunos de estos formatos poseen tipos de datos cono enteros, punto flotante, strings binarios, etc, pero, al no tener un schema definido deben incluir también el nombre de las propiedades dentro de la data codificada.

MessagePack por ejemplo utiliza secuencias de byte para identificar los tipos de datos y el tamaño de la información que va a codificar, permitiendo así tener reducciones de tamaño de la información.

Thrift y Protocol Buffers

Apache Thrift (Desarrollada originalmente por Facebook) y Protocol Buffers (Desarrollada por Google) son tecnologías open source para codificar datos de manera binaria, ambos requieren un Schema para cualquier tipo de información que será codificada. Usualmente utiliza un IDL (Interface Definition Language) para definir el Schema, ejemplo:

Thrift

struct Person {
    1: required string         userName,
    2: optional i64            favoriteNumber,
    3: optional list<string>   interests
}

Protobuf

message Person {
    required string user_name       = 1;
    optional int64 favorite_number  = 2;
    repeated string interests       = 3;
}

Ambas tecnologías cuentan con herramientas para generar código a partir del Schema.

Thrift cuenta con dos tipos de codificación: BinaryProtocol y CompactProtocol, en este caso en el proceso de codificación no se utiliza el nombre de los campos como en MessagePack sino que se utiliza un field tag (aliases), el cual correspondería al número que aparece en el esquema. La principal diferencia entre BinaryProtocol y CompactProtocol es que este último codifica en un solo byte el field tag y el tipo de dato y también utiliza tamaño variable para enteros.

Protocol Buffers solo utiliza un tipo de codificación binaria utilizando un método muy similar a CompactProtocol de Thrift.

Aunque en el Schema se definan campos requeridos estos no interfieren en el proceso de codificación, solamente es utilizado en runtime como un check de validación.

Los field tags son clave para el tema de la evolución de los Schema, siempre y cuando se conserve el mismo identificador de campo (field tag) el código será capaz de interpretar la información correctamente. En el caso de backwards compatibility, nuevos campos no pueden ser requeridos y tampoco se pueden eliminar campos que sean requeridos, solo aquellos que son opcionales. Respecto al cambio de tipo de dato, aunque algunos protocolos lo pueden soportar no es recomendado pues se puede perder información.

Avro

Apache Avro es otro formato de codificación binaria, diferente a Thrift y Protocol Buffers, nace en 2009 como un sub-proyecto de Hadoop.

Avro utiliza un schema para definir la estructura de los datos y se puede escribir en dos lenguajes: Avro IDL y JSON.

Avro IDL

record Person {
    string                  userName;
    union { null, long }    favoriteNumber = null;
    array<string>           interests;
}

Avro JSON

{
    "type": "record",
    "name": "Person",
    "fields": [
        { "name": "userName", "type": "string" },
        { "name": "favoriteNumber", "type": ["null", "long"], "default": null },
        { "name": "interests", "type": "array", "items": "string" }
    ]
}

Avro no utiliza field tags o identificadores de tipo, simplemente codifica los valores concatenados uno del otro. Un string consiste en su tamaño seguido de la información en UTF-8. Un entero se codifica como Thrift CompactProtocol utilizando tamaño variable.

Avro decodifica la información utilizando el orden de los campos según aparecen en el esquema, de tal manera que el mismo esquema debe ser usado por ambas partes o sino Avro no es capaz de realizar el proceso correctamente.

Para temas de Evolución de Schema, Avro utiliza el esquema de escritura y el esquema de escritura, estos no tienen que ser exactamente el mismo pero deben ser compatibles, Avro realiza un proceso de traducción del esquma de escritura al esquema de lectura.

Para mantener compatibilidad y evolución solo se pueden quitar y agregar campos que tengan un valor por defecto.

Cómo el lector (reader) conoce sobre el esquema de escritura (writers)? Depende del contexto en el cual se utilice Avro:

  1. Un archivo grande con muchos registros: Se puede incluir el esquema al inicio del archivo y de esta forma el lector sabe con que esquema se escribieron los registros
  2. Base de datos con registros individuales: Se puede incluir un campo con la versión del esquema utilizada para ese registro en especifico.
  3. Enviar registros por la red: Se puede enviar el esquema al inicio de la conexión (Así funciona Avro RPC)

Para aplicaciones grandes se recomienda tener una base de datos con diferentes esquemas y sus versiones, este puede actuar como documentación y mecanismo para verificar compatibilidad.

Avro también cuenta con mecanismos para generar esquemas dinámicamente facilitando el proceso de evolución.

Meritos de Esquemas

  • Pueden ser mucho mas compactos que las diferentes variedades de "JSON binario"
  • Un esquema es una forma de documentación
  • Mantener una base de datos de esquemas es bueno para temas de compatibilidad (backward/forward)
  • La posibilidad de generar código a partir de un esquema es muy útil en lenguajes estáticamente tipados

Modos de Flujo de Datos

  • Flujo de datos en una base de datos
  • Flujo de datos a través de llamados a un servicio (RPC / REST)
  • Flujo de datos a través de mensajes asíncronos (Message Queues)

Base de datos

  • Compatibilidad hacia atrás es muy importante pues los datos viven mucho mas tiempo que el código
  • Aunque migrar los datos es posible es un proceso bastante costoso
  • Muchos motores de base de datos permiten modificar el esquema sin tener que migrar toda una base de datos al utilizar valores por defecto

Servicios (REST / RPC)

Web

  • Para servicios web existen REST y SOAP
  • SOAP utiliza WSDL (XML) para describir los servicios y se basa en herramientas y generación de código, ha perdido popularidad
  • REST es mas sencillo pero su API depende de la aplicación que la implemente, existen formatos para describir APIs como OpenAPI (Swagger)

RPC

  • Remote Procedure Call or Remote Method Invocation, pretende simular ejecutar una función a través de la red pero funcionan de manera muy diferente
  • Problemas de red son comunes y deben ser manejados correctamente
  • Es muy dificil saber si el método se ha ejecutado correctamente o no, aunque se reintente el llamado la respuesta se puede perder generando doble ejecución
  • El tiempo de ejecución no es comparable con la ejecución de una función local

@guilleiguaran
Copy link
Collaborator Author

guilleiguaran commented May 13, 2019

Hace varios años Uber publico un interesante articulo sobre el tema: How Uber Engineering Evaluated JSON Encoding and Compression Algorithms to Put the Squeeze on Trip Data[1]

[1] https://eng.uber.com/trip-data-squeeze/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants