Diseño de aplicaciones mediante el uso intensivo de datos - Martin Kleppmann - E-Book

Diseño de aplicaciones mediante el uso intensivo de datos E-Book

Martin Kleppmann

0,0

Beschreibung

Los datos están en el centro de muchos desafíos que se presentan actualmente en el diseño de sistemas. Hay que resolver cuestiones complejas, como la escalabilidad, la coherencia, la fiabilidad, la eficiencia y el mantenimiento. Además, existe una abrumadora variedad de herramientas, incluyendo bases de datos relacionales, almacenes de datos NoSQL, procesadores de flujo o por lotes y gestores de mensajes. ¿Cuáles son las opciones correctas para nuestra aplicación? ¿Cómo podemos entender todos estos conceptos que están de moda? En esta guía práctica, el autor Martin Kleppmann le ayuda a navegar por este variado panorama examinando los pros y los contras de las distintas tecnologías destinadas al procesamiento y almacenamiento de datos. El software cambia constantemente, pero los principios fundamentales siguen siendo los mismos. Con este libro, los ingenieros y arquitectos de software aprenderán a aplicar esas ideas en la práctica y a aprovechar al máximo los datos en las aplicaciones modernas. "Analizar detalladamente el funcionamiento interno de los sistemas que ya utiliza, aprender a operar con ellos y utilizarlos con mayor eficacia. "Adoptar decisiones informadas, identificando los puntos fuertes y débiles de las diferentes herramientas. "Encontrar el equilibrio en relación con la coherencia, la escalabilidad, la tolerancia a fallos y la complejidad de las aplicaciones. "Comprender la investigación sobre sistemas distribuidos en la que se fundamentan las bases de datos modernas. "Echar un vistazo a lo que hay entre bambalinas en los principales servicios online y aprender de sus arquitecturas. Martin Kleppmann es investigador de sistemas distribuidos en la Universidad de Cambridge, Reino Unido. Antes desarrolló las funciones de ingeniero de software y empresario en empresas de Internet como Linkedln y Rapportive, donde trabajó en infraestructuras de datos a gran escala. Martin imparte habitualmente conferencias, es bloguero y desarrollador de código abierto.

Sie lesen das E-Book in den Legimi-Apps auf:

Android
iOS
von Legimi
zertifizierten E-Readern

Seitenzahl: 1190

Das E-Book (TTS) können Sie hören im Abo „Legimi Premium” in Legimi-Apps auf:

Android
iOS
Bewertungen
0,0
0
0
0
0
0
Mehr Informationen
Mehr Informationen
Legimi prüft nicht, ob Rezensionen von Nutzern stammen, die den betreffenden Titel tatsächlich gekauft oder gelesen/gehört haben. Wir entfernen aber gefälschte Rezensionen.



 

 

Primera edición original publicada en inglés por O’Reilly con el título Designing Data-Intensive Applications, ISBN 978-1-449-37332-0 ©Martin Kleppmann, 2017. Actualizada y corregida en enero de 2020.

This translation is published and sold by permission of O’Reilly Media, Inc., which owns or controls all rights to publish and sell the same.

Título de la edición en español:

Diseño de aplicaciones mediante el uso intensivo de datos

Primera edición en español, 2022

© 2022 MARCOMBO, S.L.

www.marcombo.com

Diseño de portada: Karen Montgomery

Ilustración: Rebecca Demarest

Traducción: Francisco Martínez Carreño

Corrección: Mónica Muñoz

Cualquier forma de reproducción, distribución, comunicación pública o transformación de esta obra solo puede ser realizada con la autorización de sus titulares, salvo excepción prevista por la ley. La presente publicación contiene la opinión del autor y tiene el objetivo de informar de manera precisa y concisa. La elaboración del contenido, aunque se ha trabajado de modo escrupuloso, no puede comportar una responsabilidad específica para el autor ni el editor de los posibles errores o imprecisiones que pudiera contener la presente obra.

ISBN: 978-84-267-3522-5

Producción del ePub: booqlab

 

 

 

La tecnología tiene una gran importancia en nuestra sociedad. Los datos, el software y la comunicación pueden utilizarse para hacer el mal: afianzar estructuras de poder injustas, socavar los derechos humanos y proteger intereses creados. Pero también puede utilizarse para hacer el bien: para que se escuchen las voces de las personas que están insuficientemente representadas, para crear oportunidades para todos y para evitar desastres. Este libro está dedicado a todos quienes trabajan para hacer el bien.

 

 

 

 

La informática es cultura pop […]. La cultura pop desprecia la historia. La cultura pop tiene que ver con la identidad y con sentirse partícipe. No tiene nada que ver con la cooperación, el pasado o el futuro: es vivir el presente. Creo que lo mismo ocurre con la mayoría de la gente que escribe código por dinero. No tienen ni idea de dónde viene [su cultura].

-Alan Kay, en una entrevista con el Dr. Dobb’s Journal (2012)

Contenidos

Prefacio

Parte I. Fundamentos de los sistemas de datos

1. Aplicaciones confiables, escalables y mantenibles

Reflexiones sobre los sistemas de datos

Confiabilidad

Fallos de hardware

Errores de software

Errores humanos

¿Cuál es la importancia de la confiabilidad?

Escalabilidad

Descripción de la carga

Descripción del rendimiento

Enfoques para hacer frente a la carga

Mantenimiento

Operatividad: facilitar la vida a las operaciones

Simplicidad: gestión de la complejidad

Evolución: facilitar el cambio

Resumen

Referencias

2. Modelos de datos y lenguajes de consulta

El modelo relacional frente al modelo de documentos

El nacimiento de NoSQL

El desajuste objeto-relacional

Relaciones de muchos a uno y muchos a muchos

¿Están las bases de datos de documentos repitiendo la historia?

Bases de datos relacionales frente a las de documentos en la actualidad

Lenguajes de consulta de datos

Consultas declarativas en la web

Consultas de MapReduce

Modelos de datos de tipo grafo

Grafos de propiedades

Lenguaje de consulta Cypher

Consulta de gráficos en SQL

Almacenes triples y SPARQL

Los fundamentos: Datalog

Resumen

Referencias

3. Almacenamiento y recuperación

Estructuras de datos que potencian la base de datos

Índices hash

SSTables y árboles LSM

Árboles B

Comparación de los árboles B con los árboles LSM

Otras estructuras de indexación

¿Procesamiento de transacciones o análisis?

Almacén de datos

Estrellas y copos de nieve: esquemas para el análisis

Almacenamiento orientado a columnas

Compresión de columnas

Orden de clasificación en el almacenamiento por columnas

Escritura en el almacenamiento orientado a columnas

Agregación: cubos de datos y vistas materializadas

Resumen

Referencias

4. Codificación y evolución

Formatos de codificación de datos

Formatos específicos para cada lenguaje

JSON, XML y variantes binarias

Thrift y Protocol Buffers

Avro

Méritos de los esquemas

Modos de flujo de datos

Flujo de datos a través de bases de datos

Flujo de datos a través de servicios: REST y RPC

Flujo de datos por paso de mensajes

Resumen

Referencias

Parte II. Datos distribuidos

5. Replicación

Líderes y seguidores

Replicación síncrona frente a asíncrona

Cómo configurar nuevos seguidores

Gestión de las interrupciones de los nodos

Implementación de logs de replicación

Problemas de retardo en la replicación

Lectura de nuestras propias escrituras

Lecturas monotónicas

Lecturas de prefijos coherentes

Soluciones para el retardo en la replicación

Replicación multilíder

Casos de uso de la replicación multilíder

Gestión de conflictos de escritura

Topologías de replicación multilíder

Replicación sin líder

Escritura en la base de datos cuando un nodo no funciona

Limitaciones de la coherencia del quorum

Quorum descuidados y transferencias indirectas

Detección de escrituras simultáneas

Resumen

Referencias

6. Particionado

Particionado y replicación

Particionado de datos clave-valor

Particionado por rangos de claves

Particionado por hash de claves

Cargas de trabajo desbalanceadas y mitigación de puntos calientes

Particionado e índices secundarios

Particionado de índices secundarios por documento

Particionado de índices secundarios por término

Rebalanceo de particiones

Estrategias de rebalanceo

Operaciones: rebalanceo automático o manual

Enrutamiento de solicitudes

Ejecución de consultas en paralelo

Resumen

Referencias

7. Transacciones

El resbaladizo concepto de «transacción»

El significado de ACID

Operaciones con un solo objeto y con varios objetos

Niveles de aislamiento débil

Lectura confirmada

Aislamiento de instantáneas y lectura repetitiva

Cómo evitar que se pierdan las actualizaciones

Escritura desviada y fantasmas

Serializabilidad

Ejecución en serie

Bloqueo en dos fases (2PL)

Aislamiento de instantáneas serializable (SSI)

Resumen

Referencias

8. El problema de los sistemas distribuidos

Fallos y averías parciales

Computación en la nube y supercomputación

Redes poco fiables

Fallos de red en la práctica

Detección de fallos

Tiempos de espera y retardos ilimitados

Redes síncronas frente a asíncronas

Relojes poco fiables

Relojes monotónicos frente a relojes en tiempo real

Sincronización y precisión del reloj

Confianza en los relojes sincronizados

Pausas del proceso

Conocimiento, verdades y mentiras

La verdad la define la mayoría

Fallos bizantinos

Modelos de sistemas y realidad

Resumen

Referencias

9. Coherencia y consenso

Garantías de coherencia

Linealizabilidad

¿Qué hace que un sistema sea linealizable?

Confianza en la linealizabilidad

Implementación de sistemas linealizables

El coste de la linealizabilidad

Garantías del ordenamiento

Ordenamiento y causalidad

Ordenamiento por números de secuencia

Difusión de orden total

Transacciones distribuidas y consenso

Confirmación atómica y confirmación en dos fases (2PC)

Transacciones distribuidas en la práctica

Consenso tolerante a fallos

Servicios de afiliación y coordinación

Resumen

Referencias

Parte III. Datos derivados

10. Procesamiento por lotes

Procesamiento por lotes con herramientas Unix

Análisis de un log sencillo

La filosofía Unix

MapReduce y sistemas de archivos distribuidos

Ejecución de trabajos MapReduce

Agrupaciones y uniones de lados reducidos

Uniones del lado del mapa

Resultado de los flujos de trabajo por lotes

Comparación de Hadoop con las bases de datos distribuidas

Más allá de MapReduce

Materialización del estado intermedio

Grafos y procesamiento iterativo

API y lenguajes de alto nivel

Resumen

Referencias

11. Procesamiento de flujos

Transmisión de flujos de eventos

Sistemas de mensajería

Logs particionados

Bases de datos y flujos

Necesidad de mantener los sistemas sincronizados

Captura de datos de cambios

Aprovisionamiento de eventos

Estado, flujos e inmutabilidad

Procesamiento de flujos

Usos del procesamiento de flujos

Razonamiento sobre el tiempo

Uniones de flujos

Tolerancia a fallos

Resumen

Referencias

12. El futuro de los sistemas de datos

Integración de datos

Combinación de herramientas especializadas mediante la derivación de datos

Procesamiento por lotes y procesamiento de flujos

Desagregación de bases de datos

Composición de las tecnologías de almacenamiento de datos

Diseño de aplicaciones en torno al flujo de datos

Observación del estado derivado

En busca de la corrección

Argumento de las bases de datos de extremo a extremo

Aplicación de restricciones

Puntualidad e integridad

Confíe, pero verifique

Hacer lo correcto

Análisis predictivo

Privacidad y seguimiento

Resumen

Referencias

Glosario

Prefacio

Si usted ha trabajado en ingeniería de software en los últimos años, especialmente en sistemas del lado del servidor y del backend, probablemente haya sido bombardeado con una plétora de palabras de moda relacionadas con el almacenamiento y el procesamiento de datos: «¡NoSQL!, ¡big data!, ¡escala web!, ¡fragmentación!, ¡coherencia eventual!, ¡ACID!, ¡teorema CAP!, ¡servicios en la nube!, ¡MapReduce!, ¡tiempo real!».

En la última década, hemos asistido a muchos avances interesantes en bases de datos, en sistemas distribuidos y en la forma de construir aplicaciones sobre ellos. Hay varias fuerzas que impulsan estos desarrollos:

• Empresas de Internet como Google, Microsoft, Amazon, Facebook, LinkedIn, Netflix y Twitter manejan enormes volúmenes de datos y tráfico, lo que las obliga a crear nuevas herramientas que les permitan manejar eficientemente tal escala.

• Las empresas necesitan ser ágiles, probar hipótesis a bajo coste y responder con rapidez a los nuevos conocimientos del mercado, manteniendo ciclos de desarrollo cortos y modelos de datos flexibles.

• El software libre y de código abierto ha tenido mucho éxito y ahora, en muchos entornos, se prefiere al software comercial o al hecho a medida.

• Las velocidades de reloj de las central processing units (CPU) apenas aumentan, pero los procesadores multinúcleo son un estándar y las redes se presentan cada vez más rápidas. Esto significa que el paralelismo solo va a aumentar.barra

• Incluso si se trabaja en un equipo pequeño, ahora se pueden construir sistemas distribuidos en muchas máquinas e incluso en varias regiones geográficas, gracias a la infraestructura como servicio (IaaS), como es Amazon Web Services.

• Ahora se espera que muchos servicios tengan una alta disponibilidad; el tiempo de inactividad prolongado debido a las interrupciones o al mantenimiento resulta cada vez más inaceptable.

Las aplicaciones con uso intensivo de datos están superando los límites de lo posible, al hacer uso de estos desarrollos tecnológicos. Llamamos a una aplicación de uso intensivo de datos si estos son su principal reto: la cantidad de datos, su complejidad o la velocidad a la que cambian, en contraposición a la de uso intensivo de computadores, en la que los ciclos de CPU son el cuello de botella.

Las herramientas y tecnologías que ayudan a las aplicaciones con uso intensivo de datos a almacenarlos y procesarlos se han adaptado rápidamente a estos cambios. Los nuevos tipos de sistemas de bases de datos («NoSQL») han recibido mucha atención, pero las colas de mensajes, las cachés, los índices de búsqueda, los marcos de trabajo para el procesamiento por lotes y flujos y las tecnologías relacionadas son también muy importantes. Muchas aplicaciones utilizan alguna combinación de ellas.

Las palabras de moda que llenan este espacio constituyen una señal de entusiasmo por las nuevas posibilidades, lo cual es algo estupendo. Sin embargo, como ingenieros y arquitectos de software, también necesitamos tener una comprensión técnicamente exacta y precisa de las distintas tecnologías y sus contrapartidas, si queremos construir buenas aplicaciones. Para ello, hemos de profundizar más allá de las palabras de moda.

Afortunadamente, detrás de los rápidos cambios en la tecnología, existen principios perdurables que siguen siendo válidos, independientemente de la versión de la herramienta concreta que se utilice. Si entendemos esos principios, estaremos en condiciones de ver dónde encaja cada herramienta, cómo hacer un buen uso de ella y cómo evitar sus trampas. Ahí es donde entra en juego este libro.

El objetivo de esta obra radica en ayudarlo a navegar por el variado y cambiante panorama de las tecnologías de procesamiento y almacenamiento de datos. Este libro no es el tutorial de una herramienta concreta, ni un libro de texto lleno de árida teoría. En su lugar, veremos ejemplos de sistemas de datos que han alcanzado el éxito; tecnologías que forman la base de muchas aplicaciones populares y que han de cumplir requisitos de escalabilidad, rendimiento y fiabilidad en la producción del día a día.

Nos adentraremos en las entrañas de estos sistemas, esclareceremos sus algoritmos clave, discutiremos acerca de sus principios y las contrapartidas que tienen que cumplir. En este viaje, trataremos de encontrar formas útiles de pensar en los sistemas de datos, no solo cómo funcionan, sino también por qué lo hacen, y qué preguntas debemos hacer.

Después de leer este libro, se hallará en una excelente posición para decidir qué tipo de tecnología resulta apropiada para cada propósito, y entender cómo se pueden combinar las herramientas para formar la base de una buena arquitectura de aplicación. No estará preparado para construir su propio motor de almacenamiento de bases de datos desde cero, pero, afortunadamente, eso rara vez resulta necesario. Sin embargo, desarrollará una buena intuición sobre lo que hacen sus sistemas internamente, de modo que pueda razonar sobre su comportamiento, tomar buenas decisiones de diseño y localizar cualquier problema que pueda surgir.

¿Quién debería leer este libro?

Si desarrolla aplicaciones con algún tipo de servidor/backend para almacenar o procesar datos, y sus aplicaciones utilizan Internet (por ejemplo, aplicaciones web y móviles o sensores conectados a Internet), este libro es para usted.

La presente obra se dirige a ingenieros y arquitectos de software, así como a directores técnicos a quienes les gusta codificar. Es especialmente relevante si tiene que tomar decisiones sobre la arquitectura de los sistemas con los que trabaja; por ejemplo, si debe elegir herramientas para resolver un problema determinado y averiguar la mejor manera de aplicarlas. Pero, incluso si no puede elegir sus herramientas, este libro lo ayudará a comprender mejor sus puntos fuertes y débiles.

Sería conveniente contar con alguna experiencia en la creación de aplicaciones basadas en la web o servicios de red, y debería estar familiarizado con las bases de datos relacionales y SQL. Las bases de datos no relacionales y otras herramientas vinculadas con los datos que pueda conocer constituyen una ventaja, pero no resultan necesarias. Será de utilidad un conocimiento general de los protocolos de red habituales, como TCP y HTTP. La elección que haga del lenguaje de programación o del marco de trabajo no supone ninguna diferencia a la hora de consultar este libro.

Si alguno de los siguientes puntos es cierto para usted, encontrará esta obra interesante:

• Quiere aprender a hacer que los sistemas de datos se presenten escalables; por ejemplo, para soportar aplicaciones web o móviles con millones de usuarios.

• Necesita hacer que las aplicaciones posean una alta disponibilidad (minimizando el tiempo de inactividad) y sean operativamente robustas.

• Busca formas de hacer que los sistemas resulten más fáciles de mantener a largo plazo, incluso a medida que crecen y cambian los requisitos y las tecnologías.

• Tiene una curiosidad natural acerca del funcionamiento de las cosas y quiere saber qué ocurre en los principales sitios web y servicios en línea. En este libro, se desglosan las interioridades de varias bases de datos y sistemas de procesamiento de datos, y resulta muy divertido explorar las brillantes ideas que se han utilizado en su diseño.

A veces, cuando se habla de sistemas de datos escalables, las personas hacen comentarios del tipo: «No eres Google o Amazon. Deja de preocuparte por la escala y utiliza una base de datos relacional». Esta afirmación resulta cierta: construir para una escala que no necesita supone un esfuerzo inútil y puede encerrarlo en un diseño inflexible. En efecto, es una forma de optimización prematura. Sin embargo, también resulta importante elegir la herramienta adecuada para el trabajo, y las diferentes tecnologías cuentan con sus propios puntos fuertes y débiles. Como veremos, las bases de datos relacionales son importantes, pero no son la última palabra en el tratamiento de datos.

Alcance del libro

Con esta obra, no se pretende dar instrucciones detalladas sobre cómo instalar o utilizar paquetes de software o interfaces de programación de aplicaciones (application programming interfaces, API) específicos, ya que existe mucha documentación al respecto. En su lugar, se abarcan los diversos principios y contrapartidas fundamentales para los sistemas de datos, y se exploran las diferentes decisiones de diseño que se han adoptado en distintos productos.

En las ediciones de libros electrónicos, hemos incluido enlaces al texto completo de los recursos en línea. En el momento de la publicación, se verificaron todos los enlaces, pero, desgraciadamente, los vínculos tienden a romperse con frecuencia, debido a la naturaleza de la web. Si encuentra un enlace roto, o si está leyendo una copia impresa de este libro, puede buscar las referencias utilizando un motor de búsqueda. En el caso de los artículos académicos, puede buscar el título en Google Scholar para encontrar archivos pdf de libre acceso. También puede encontrar todas las referencias en https://github.com/ept/ddia-references, donde mantenemos los enlaces actualizados.

Nos centramos, principalmente, en la arquitectura de los sistemas de datos y en la forma en que se integran en las aplicaciones con uso intensivo de datos. En este libro no disponemos de espacio para tratar el despliegue, las operaciones, la seguridad, la gestión y otras áreas; son temas complejos e importantes, y no les haríamos justicia convirtiéndolos en notas secundarias y superficiales. Merecen libros propios.

Muchas de las tecnologías descritas entran dentro del ámbito de las palabras de moda big data. Sin embargo, la expresión big data está tan excesivamente utilizada y tan pobremente definida que no es útil en una discusión seria de ingeniería. En este libro, se utilizan términos menos ambiguos, como «sistemas de un solo nodo» frente a «sistemas distribuidos», o «sistemas de procesamiento online/interactivo» frente a «sistemas fuera de línea/por lotes».

En esta obra, se manifiesta una inclinación hacia el software libre y de código abierto (free and open-source software, FOSS), porque leer, modificar y ejecutar el código fuente es una gran manera de entender cómo funciona algo en detalle. Las plataformas abiertas también reducen el riesgo de dependencia de un proveedor. Sin embargo, cuando se considera oportuno, también se habla de software propietario (software de código cerrado, software como servicio o software interno de las empresas, que solo se describe en la bibliografía, pero no se hace público).

Esquema del libro

El libro está organizado en tres partes:

1. En la parte I, analizamos las ideas fundamentales en las que se basa el diseño de aplicaciones con uso intensivo de datos. Comenzamos el capítulo 1 hablando de lo que realmente intentamos conseguir: fiabilidad, escalabilidad y mantenimiento; cómo debemos pensar sobre ello, y cómo podemos conseguirlo. En el capítulo 2, comparamos varios modelos de datos y lenguajes de consulta diferentes, y vemos cómo se muestran apropiados para diversas situaciones. En el capítulo 3, hablamos de los motores de almacenamiento: cómo las bases de datos organizan los datos en el disco, para que podamos encontrarlos de nuevo de forma eficiente. El capítulo 4 se centra en los formatos de codificación de datos (serialización) y en la evolución de los esquemas a lo largo del tiempo.

2. En la parte II, pasamos de los datos almacenados en una máquina a los distribuidos en varias. Esto se presenta a menudo necesario para la escalabilidad, pero trae consigo una variedad de desafíos singulares. En primer lugar, tratamos la replicación (capítulo 5), la partición/repartición (capítulo 6) y las transacciones (capítulo 7). A continuación, profundizamos en los problemas de los sistemas distribuidos (capítulo 8) y en lo que significa conseguir coherencia y consenso en un sistema distribuido (capítulo 9).

3. En la parte III, se tratan los sistemas que derivan unos conjuntos de datos de otros conjuntos de datos. Los datos derivados suelen aparecer en sistemas heterogéneos: cuando no existe una base de datos que pueda hacerlo todo bien, las aplicaciones han de integrar varias bases de datos, cachés, índices, etc. En el capítulo 10, comenzamos con un enfoque de procesamiento por lotes para los datos derivados y lo ampliamos con el procesamiento de flujos en el capítulo 11. Por último, en el capítulo 12, lo ponemos todo junto y discutimos acerca de los enfoques para construir aplicaciones fiables, escalables y mantenibles en el futuro.

Referencias y lecturas complementarias

La mayor parte de la materia que tratamos en este libro ya se ha expuesto en otros lugares de una forma u otra: presentaciones de conferencias, artículos de investigación, publicaciones en blogs, código, rastreadores de errores, listas de correo y folclore de ingeniería. En este libro, se resumen las ideas más importantes de muchas fuentes diferentes e incluye referencias a la bibliografía original a lo largo del texto. Las referencias que aparecen al final de cada capítulo son un gran recurso si se quiere profundizar en un área, y la mayoría de ellas se encuentran disponibles gratuitamente en Internet.

Aprendizaje O’Reilly online

Durante más de cuarenta años, O’Reilly Media ha proporcionado formación en tecnología y negocios, conocimientos y perspectivas para ayudar a las empresas a tener éxito.

Nuestra red única de expertos y personal innovador comparte sus conocimientos y experiencia a través de libros, artículos, conferencias y nuestra plataforma de aprendizaje en línea. La plataforma de aprendizaje en línea de O’Reilly le permite acceder a cursos de capacitación en vivo, rutas de aprendizaje en profundidad, entornos de codificación interactivos y una amplia colección de textos y vídeos de O’Reilly y de más de doscientos editores. Para más información, por favor, visite http://oreilly.com.

Cómo ponerse en contacto con nosotros

Tenemos una página web de este libro, en la que listamos las erratas, ejemplos y cualquier información adicional. Puede acceder a esta página en http://bit.ly/designing-data-intensive-apps.

Para comentar o hacer preguntas técnicas sobre el libro, envíe un correo electrónico a [email protected].

Reconocimientos

Este libro es una amalgama y sistematización de un gran número de ideas y conocimientos de otras personas, que combinan la experiencia tanto de la investigación académica como de la práctica industrial. En informática, tendemos a sentirnos atraídos por las cosas nuevas y brillantes, pero creo que tenemos mucho que aprender de las cosas que se han hecho antes. Este libro tiene más de ochocientas referencias a artículos, entradas de blog, charlas, documentación y mucho más, y han sido un recurso de aprendizaje inestimable para mí. Estoy muy agradecido a los autores de este material por compartir sus conocimientos.

También he aprendido mucho de las conversaciones personales, gracias a un gran número de personas que se han tomado el tiempo de discutir ideas o de explicarme pacientemente las cosas; en particular, me gustaría dar las gracias a Joe Adler, Ross Anderson, Peter Bailis, Márton Balassi, Alastair Beresford, Mark Callaghan, Mat Clayton, Patrick Collison, Sean Cribbs, Shirshanka Das, Niklas Ekström, Stephan Ewen, Alan Fekete, Gyula Fóra, Camille Fournier, Andres Freund, John Garbutt, Seth Gilbert, Tom Haggett, Pat Helland, Joe Hellerstein, Jakob Homan, Heidi Howard, John Hugg, Julian Hyde, Conrad Irwin, Evan Jones, Flavio Junqueira, Jessica Kerr, Kyle Kingsbury, Jay Kreps, Carl Lerche, Nicolas Liochon, Steve Loughran, Lee Mallabone, Nathan Marz, CaitieMcCaffrey, Josie McLellan, Christopher Meiklejohn, Ian Meyers, Neha Narkhede, Neha Narula, Cathy O’Neil, Onora O’Neill, Ludovic Orban, Zoran Perkov, Julia Powles, Chris Riccomini, Henry Robinson, David Rosenthal, Jennifer Rullmann, Matthew Sackman, Martin Scholl, Amit Sela, Gwen Shapira, Greg Spurrier, Sam Stokes, Ben Stopford, Tom Stuart, Diana Vasile, Rahul Vohra, Pete Warden y Brett Wooldridge.

Muchas otras personas han contribuido, de forma inestimable, a la redacción de este libro revisando borradores y proporcionando comentarios. Por estas contribuciones estoy especialmente en deuda con Raul Agepati, Tyler Akidau, Mattias Andersson, Sasha Baranov, Veena Basavaraj, David Beyer, Jim Brikman, Paul Carey, Raúl Castro Fernández, Joseph Chow, Derek Elkins, Sam Elliott, Alexander Gallego, Mark Grover, Stu Halloway, Heidi Howard, Nicola Kleppmann, Stefan Kruppa, Bjorn Madsen, Sander Mak, Stefan Podkowinski, Phil Potter, Hamid Ramazani, Sam Stokes y Ben Summers. Por supuesto, asumo toda la responsabilidad por cualquier error u opinión desagradable que quede en este libro.

Por ayudar a que este proyecto se convierta en realidad, y por su paciencia con mi lentitud al escribir y mis inusuales peticiones, doy las gracias a mis editores Marie Beaugureau, Mike Loukides, Ann Spencer y a todo el equipo de O’Reilly. Por ayudarme a encontrar las palabras adecuadas, doy las gracias a Rachel Head. Por darme tiempo y libertad para escribir a pesar de otros compromisos laborales, doy las gracias a Alastair Beresford, Susan Goodhue, Neha Narkhede y Kevin Scott.

Hay que agradecer, muy especialmente, a Shabbir Diwan y Edie Freedman, que ilustraron con gran esmero los mapas que acompañan a los capítulos. Es maravilloso que hayan asumido la idea poco convencional de crear mapas, y que los hayan hecho tan bellos y convincentes.

Por último, mi cariño va para mi familia y mis amigos, sin los cuales no habría podido superar este proceso de escritura que ha durado casi cuatro años. Sois los mejores.

PARTE I

Fundamentos de los sistemas de datos

En los cuatro primeros capítulos, se repasan las ideas fundamentales que se aplican a todos los sistemas de datos, tanto si se ejecutan en una sola máquina como si se distribuyen en un cluster de máquinas:

1. En el capítulo 1, se introduce la terminología y el enfoque que vamos a utilizar a lo largo del libro. Se examina lo que realmente queremos decir con palabras como fiabilidad, escalabilidad y mantenimiento, y cómo podemos intentar alcanzar estos objetivos.

2. En el capítulo 2, se comparan varios modelos de datos y lenguajes de consulta diferentes, el factor de distinción más visible entre las bases de datos desde el punto de vista del desarrollador. Veremos cómo los diferentes modelos son apropiados para distintas situaciones.

3. El capítulo 3 se centra en los aspectos internos de los motores de almacenamiento y se analiza también cómo las bases de datos distribuyen los datos en el disco. Los diferentes motores de almacenamiento están optimizados para diferentes cargas de trabajo, y la elección del correcto puede tener un gran efecto en el rendimiento.

4. En el capítulo 4, se comparan varios formatos de codificación de datos (serialización) y se examina especialmente su comportamiento, en un entorno en el que los requisitos de las aplicaciones cambian y los esquemas deben adaptarse con el tiempo.

Más tarde, en la parte II, se abordan los problemas particulares de los sistemas de datos distribuidos.

CAPÍTULO 1

Aplicaciones confiables, escalables y mantenibles

Internet se hizo tan bien que la mayoría de la gente piensa que es un recurso natural como el océano Pacífico, en lugar de algo hecho por el hombre. ¿Cuándo fue la última vez que una tecnología de esta envergadura estuvo tan libre de errores?

—Alan Kay, en una entrevista en Dr. Dobb’s Journal (2012)

Hoy día, muchas aplicaciones hacen un uso intensivo de datos, en lugar de uno intensivo de cálculo. La potencia bruta de la CPU rara vez es un factor limitante para estas aplicaciones: los problemas más importantes suelen estribar en la cantidad de datos, su complejidad y la velocidad a la que cambian.

Una aplicación con uso intensivo de datos se crea normalmente a partir de bloques de construcción estándar, que proporcionan la funcionalidad normalmente necesaria; por ejemplo, muchas aplicaciones precisan:

• Almacenar datos para que ellas, o cualquier otra aplicación, puedan volver a encontrarlos más tarde (bases de datos)

• Recordar el resultado de una operación onerosa, para acelerar las lecturas (cachés)

• Permitir a los usuarios buscar datos por palabras clave o filtrarlos de varias maneras (índices de búsqueda)

• Enviar un mensaje a otro proceso, para que lo gestione de forma asíncrona (procesamiento de flujos)

• Triturar periódicamente una gran cantidad de datos acumulados (procesamiento por lotes)

Que esto sea tan obvio se debe, precisamente, a que tales sistemas de datos representan una abstracción muy lograda: los usamos todo el tiempo, sin pensar demasiado. Cuando se construye una aplicación, a la mayoría de los ingenieros no se les ocurriría escribir un nuevo motor de almacenamiento de datos desde cero, porque las bases de datos son una herramienta perfectamente adecuada para realizar el trabajo.

Pero la realidad no es tan sencilla. Hay muchos sistemas de bases de datos con diferentes características, porque las distintas aplicaciones poseen diversos requisitos. Existen varios enfoques para el almacenamiento en caché y varias formas de construir índices de búsqueda. A la hora de crear una aplicación, tenemos que averiguar qué herramientas y qué enfoques son los más apropiados para la tarea en cuestión. Y puede resultar difícil combinar herramientas cuando se necesita hacer algo que una sola herramienta no puede hacer por sí sola.

Este libro constituye un viaje a través de los principios y los aspectos prácticos de los sistemas de datos, y de cómo se pueden utilizar para crear aplicaciones con uso intensivo de datos. Exploraremos qué poseen en común las distintas herramientas, qué las distingue y cómo consiguen sus características.

En este capítulo, empezaremos explorando los fundamentos de lo que estamos tratando de conseguir: sistemas de datos fiables, escalables y mantenibles. Aclararemos qué significan esas cosas, esbozaremos algunas formas de pensar sobre ellas y repasaremos los fundamentos que necesitaremos para abordar los capítulos posteriores. En los siguientes capítulos, continuaremos capa por capa, viendo las diferentes decisiones de diseño que se deben considerar cuando se trabaja en una aplicación intensiva de datos.

Reflexiones sobre los sistemas de datos

Normalmente pensamos que las bases de datos, las colas, las cachés, etc., son categorías de herramientas muy diferentes. Aunque una base de datos y una cola de mensajes presentan algunas similitudes superficiales, ambas almacenan datos durante algún tiempo (tienen patrones de acceso muy diferentes), lo que significa que poseen características de rendimiento distintas y, por tanto, implementaciones muy diferentes.

Entonces, ¿por qué debemos agruparlas todas bajo una expresión general como sistemas de datos?

En los últimos años, han surgido muchas herramientas nuevas para el almacenamiento y procesamiento de datos. Están optimizadas para una gran variedad de casos de uso y ya no se ajustan a las categorías tradicionales [1]; por ejemplo, algunos almacenes de datos también se utilizan como colas de mensajes (Redis), y hay colas de mensajes con garantías de durabilidad similares a las de las bases de datos (Apache Kafka). Los límites entre las categorías son cada vez más difusos.

En segundo lugar, cada vez son más las aplicaciones con requisitos tan exigentes o amplios que una sola herramienta ya no puede satisfacer todas sus necesidades de procesamiento y almacenamiento de datos. En su lugar, el trabajo se divide en tareas que pueden realizarse de forma eficiente con una sola herramienta, y esas diferentes herramientas se unen mediante el código de la aplicación.

Por ejemplo, si tenemos una capa de caché gestionada por la aplicación (usando Memcached o similar), o un servidor de búsqueda de texto completo (como Elasticsearch o Solr) separado de la base de datos principal, normalmente es responsabilidad del código de la aplicación mantener esas cachés e índices sincronizados con la base de datos principal. La figura 1.1 aporta una idea de cómo se puede conseguir esto (entraremos en detalle en capítulos posteriores).

Figura 1.1Una posible arquitectura para un sistema de datos que combina varios componentes.

Cuando se combinan varias herramientas para proporcionar un servicio, la interfaz del servicio o la API suelen ocultar los detalles de la implementación a los clientes. En este caso, básicamente, hemos creado un nuevo sistema de datos de propósito especial a partir de componentes más pequeños de propósito general. Nuestro sistema de datos compuesto puede proporcionar ciertas garantías: por ejemplo, que la caché se invalidará correctamente o se actualizará en lo que se refiere a las escrituras, para que los clientes externos vean resultados consistentes. Ahora no solo somos desarrolladores de aplicaciones, sino también diseñadores de sistemas de datos.

Si estamos diseñando un sistema o servicio de datos, surgen muchas preguntas complicadas. ¿Cómo se garantiza que los datos sigan siendo correctos y completos, incluso cuando las cosas van mal internamente? ¿Cómo proporcionar un buen rendimiento a los clientes, incluso cuando algunas partes del sistema se degradan? ¿Cómo se escala para manejar un aumento de la carga? ¿Qué aspecto tiene una buena API para el servicio?

Hay muchos factores que pueden influir en el diseño de un sistema de datos, como las habilidades y la experiencia de las personas involucradas, las dependencias del sistema heredado, la escala de tiempo para la entrega, la tolerancia de la organización a los diferentes tipos de riesgo, las restricciones reglamentarias, etc. Estos factores dependen, en gran medida, de la situación.

En este libro, nos centramos en tres preocupaciones que son importantes en la mayoría de los sistemas de software.

Confiabilidad

El sistema debe seguir funcionando adecuadamente (realizando la función correcta al nivel de rendimiento deseado) incluso ante la adversidad (fallos de hardware o software, e incluso errores humanos).

Escalabilidad

A medida que el sistema crece (en volumen de datos, de tráfico o complejidad), debe haber formas razonables de hacer frente a ese crecimiento.

Mantenimiento

Con el tiempo, muchas personas diferentes trabajarán en el sistema (ingeniería y operaciones, tanto para mantener el comportamiento actual como para adaptar el sistema a nuevos casos de uso), y todas ellas deberían ser capaces de trabajar en él de forma productiva.

A menudo, estas palabras se utilizan sin una comprensión clara de su significado. En aras de una ingeniería reflexiva, dedicaremos el resto de este capítulo a explorar formas de pensar en la fiabilidad, la escalabilidad y el mantenimiento. Luego, en los siguientes capítulos, veremos varias técnicas, arquitecturas y algoritmos que se utilizan para lograr esos objetivos.

Confiabilidad

Todo el mundo tiene una idea intuitiva de lo que significa que algo sea confiable o no. En el caso del software, las expectativas típicas son las siguientes

• La aplicación realiza la función que el usuario esperaba.

• Puede tolerar que el usuario cometa errores o utilice el software de forma inesperada.

• Su rendimiento es lo suficientemente bueno para el caso de uso requerido, bajo la carga y el volumen de datos esperados.

• El sistema impide cualquier acceso no autorizado, así como el abuso.

Si todas esas cosas juntas significan «funcionar correctamente», entonces, podemos entender la fiabilidad como «seguir funcionando correctamente, incluso cuando las cosas van mal».

Las cosas que pueden ir mal se llaman fallos, y a los sistemas que se anticipan a los fallos y pueden hacerles frente se los denomina tolerantes a fallos o resilientes. El primer término es ligeramente engañoso: sugiere que podríamos hacer un sistema tolerante a todos los tipos de fallos posibles, lo que en realidad no es factible. Si un agujero negro engullera a todo el planeta Tierra (y todos los servidores que hay en él), la tolerancia de ese fallo requeriría un alojamiento web en el espacio.

Se necesita algo de suerte para que se apruebe esa partida presupuestaria, así que solo tiene sentido hablar de tolerar determinados tipos de fallos.

Hay que tener en cuenta que un fallo no es lo mismo que una avería [2]. Un fallo suele definirse como un componente del sistema que se desvía de sus especificaciones, mientras que una avería se produce cuando el sistema en su conjunto deja de proporcionar el servicio requerido al usuario. Es imposible reducir la probabilidad de un fallo a cero, por lo que suele resultar mejor diseñar mecanismos de tolerancia a fallos que eviten que estos provoquen averías. En este libro, tratamos varias técnicas para construir sistemas fiables a partir de partes no fiables.

En contra de lo que sugiere la intuición, en estos sistemas tolerantes a fallos, puede tener sentido aumentar la tasa de fallos, provocándolos deliberadamente; por ejemplo, matando al azar procesos individuales sin previo aviso. Muchos fallos críticos se deben, en realidad, a una mala gestión de los errores [3]; al inducir deliberadamente los fallos, nos aseguramos de que la maquinaria de tolerancia a los fallos se ejercite y pruebe continuamente, lo que puede aumentar nuestra confianza en que los fallos se gestionarán correctamente cuando se produzcan de forma natural. Chaos Monkey de Netflix [4] es un ejemplo de este enfoque.

Aunque, en general, preferimos tolerar los fallos a prevenirlos, hay casos en los que es mejor prevenir que curar (por ejemplo, porque no exista cura). Es el caso de las cuestiones de seguridad; por ejemplo, si un atacante ha comprometido un sistema y ha accedido a datos sensibles, ese suceso no puede deshacerse. Sin embargo, en este libro nos ocupamos, sobre todo, de los tipos de fallos que se pueden curar, como se describe en las siguientes secciones.

Fallos de hardware

Cuando pensamos en las causas de los fallos del sistema, rápidamente nos vienen a la mente los fallos de hardware. Los discos duros se rompen, la memoria RAM se estropea, la red eléctrica sufre un apagón, alguien desenchufa el cable de red equivocado… Cualquiera que haya trabajado con grandes centros de datos puede decir que estas cosas suceden todo el tiempo cuando se tienen muchas máquinas.

Según los informes sobre discos duros, estos presentan un tiempo medio de fallo (mean time to failure, MTTF) de entre diez y cincuenta años [5, 6]. Por lo tanto, en un cluster de almacenamiento con 10 000 discos, deberíamos esperar que dejara de funcionar una media de un disco al día.

Nuestra primera respuesta suele ser añadir redundancia a los componentes de hardware individuales para reducir la tasa de fallos del sistema. Los discos pueden tener una configuración RAID, los servidores pueden presentar dos fuentes de alimentación y CPU intercambiables en caliente y los centros de datos pueden contar con baterías y generadores diésel como energía de reserva. Cuando un componente deja de funcionar, el componente redundante puede ocupar su lugar, mientras se sustituye el componente roto. Este enfoque no puede evitar por completo que los problemas de hardware provoquen averías, pero se comprende bien y, a menudo, puede mantener una máquina funcionando sin interrupciones durante años.

Hasta hace poco, la redundancia de componentes de hardware era suficiente para la mayoría de las aplicaciones, ya que hace que el fallo total de una sola máquina sea bastante raro. Mientras se pueda restaurar una copia de seguridad en una nueva máquina con bastante rapidez, el tiempo de inactividad en caso de fallo no es catastrófico, en la mayoría de las aplicaciones. Por lo tanto, la redundancia de varias máquinas solo es necesaria para un pequeño número de aplicaciones para las que la alta disponibilidad resulta absolutamente esencial.

Sin embargo, a medida que los volúmenes de datos y las demandas de computación de las aplicaciones han aumentado, cada vez más aplicaciones han comenzado a utilizar un mayor número de máquinas, lo que incrementa proporcionalmente la tasa de fallos de hardware. Además, en algunas plataformas en la nube, como Amazon Web Services (AWS), es bastante corriente que las instancias de máquinas virtuales dejen de estar disponibles sin previo aviso [7], ya que las plataformas están diseñadas para priorizar la flexibilidad y la elasticidad1 sobre la fiabilidad de una sola máquina.

De ahí que se esté avanzando hacia sistemas que puedan tolerar la pérdida de máquinas enteras, utilizando técnicas de tolerancia a fallos de software de forma preferente o complementaria a la redundancia de hardware. Estos sistemas también presentan ventajas operativas: un sistema de un solo servidor requiere un tiempo de inactividad planificado, si es necesario reiniciar la máquina (para aplicar parches de seguridad del sistema operativo, por ejemplo), mientras que, en un sistema que puede tolerar el fallo de la máquina, se puede parchear un nodo a la vez, sin que se produzca un tiempo de inactividad de todo el sistema (una actualización continua; véase el capítulo 4).

Errores de software

Solemos pensar que los fallos de hardware son aleatorios e independientes entre sí: que falle el disco de una máquina no implica que vaya a fallar el de otra. Puede haber correlaciones débiles (por ejemplo, debido a una causa común, como la temperatura en el rack del servidor), pero, por lo demás, resulta poco probable que un gran número de componentes de hardware fracasen al mismo tiempo.

Otra clase de fallo es un error sistemático dentro del sistema [8]. Estos problemas son más difíciles de prever y, al estar correlacionados entre nodos, tienden a causar muchas más averías en el sistema que los fallos de hardware no correlacionados [5]. Algunos ejemplos son:

• Un error de software que hace que todas las instancias de un servidor de aplicaciones se cuelguen cuando se les da una determinada entrada errónea; por ejemplo, consideremos el segundo intercalar del 30 de junio de 2012, que hizo que muchas aplicaciones se colgaran simultáneamente, debido a un error en el kernel de Linux [9].

• Un proceso fuera de control que utiliza algún recurso compartido: tiempo de la CPU, memoria, espacio en disco o ancho de banda de la red.

• Un servicio del que depende el sistema se ralentiza, deja de responder o empieza a devolver respuestas corruptas.

• Averías en cascada, en las que un pequeño fallo en un componente desencadena un error en otro componente que, a su vez, desencadena otros más [10].

Los problemas que causan este tipo de fallos en el software suelen permanecer latentes durante mucho tiempo, hasta que se desencadenan por una serie de circunstancias inusuales. En esos casos, se revela que el software está haciendo algún tipo de suposición sobre su entorno y, aunque esa suposición suele ser cierta, finalmente deja de serlo por alguna razón [11].

No existe una solución rápida al problema de los fallos sistemáticos en el software. Hay muchas cosas pequeñas que pueden ayudar: pensar cuidadosamente en las suposiciones e interacciones del sistema; realizar pruebas exhaustivas; aislar los procesos; permitir que estos se bloqueen y se reinicien; medir, supervisar y analizar el comportamiento del sistema en producción. Si se espera que un sistema ofrezca alguna garantía (por ejemplo, en una cola de mensajes, que el número de los entrantes sea igual al número de los salientes), puede comprobarse constantemente mientras se ejecuta y emitir una alerta si se encuentra una discrepancia [12].

Errores humanos

Las personas diseñan y construyen sistemas de software, y los operadores que mantienen los sistemas en funcionamiento también son personas. Incluso cuando tenemos las mejores intenciones, se sabe que las personas somos poco fiables; por ejemplo, en un estudio sobre grandes servicios de Internet, se descubrió que los errores de configuración de los operadores eran la principal causa de las interrupciones, mientras que los fallos del hardware (servidores o red) solo intervenían en el 10-25 % de las interrupciones [13].

¿Cómo podemos hacer que nuestros sistemas sean fiables, a pesar de que las personas no lo seamos? Los mejores sistemas combinan varios enfoques:

• Diseñar los sistemas de forma que se minimicen las posibilidades de error; por ejemplo, las abstracciones, las API y las interfaces de administración bien diseñadas facilitan que se haga «lo correcto» y desalientan «lo incorrecto». Sin embargo, si las interfaces son demasiado restrictivas, la gente las eludirá, anulando su beneficio, por lo que es un equilibrio difícil de conseguir.

• Desvincular los lugares en los que la gente comete más errores de los lugares en los que pueden provocar averías; en particular, proporcionar entornos sandbox sin producción donde la gente pueda explorar y experimentar con seguridad, utilizando datos reales, sin afectar a los usuarios reales.

• Probar a fondo todos los niveles, desde las pruebas unitarias hasta las de integración de todo el sistema y las pruebas manuales [3]. Las pruebas automatizadas se utilizan ampliamente, se comprenden bien y son especialmente valiosas para cubrir los casos límite que, rara vez, surgen en el funcionamiento normal.

• Permitir una recuperación rápida y sencilla de los errores humanos, para minimizar el impacto en caso de fallo; por ejemplo, hacer que los cambios de configuración sean rápidos de revertir, que el nuevo código se despliegue gradualmente (para que cualquier error inesperado afecte solo a un pequeño subgrupo de usuarios) y proporcionar herramientas para volver a calcular los datos (en caso de que resulte que la antigua computación fuera incorrecta).

• Establecer un seguimiento detallado y claro, como son las métricas de rendimiento y las tasas de error. En otras disciplinas de ingeniería, esto se denomina «telemetría» (una vez que el cohete ha abandonado el suelo, la telemetría es esencial para hacer un seguimiento de lo que está ocurriendo y para entender las averías [14]). La supervisión puede mostrarnos señales de alerta temprana y permitirnos comprobar si se están violando las suposiciones o restricciones. Cuando ocurre un problema, las métricas pueden ser inestimables para diagnosticarlo.

• Implantar buenas prácticas de gestión y formación, un aspecto complejo e importante, que va más allá del alcance de este libro.

¿Cuál es la importancia de la confiabilidad?

La confiabilidad no es algo que se utilice solo para las centrales nucleares y el software de control del tráfico aéreo: también se espera que las aplicaciones más mundanas funcionen de forma confiable. Los fallos en las aplicaciones empresariales provocan pérdidas de productividad (y ocasionan riesgos legales, si las cifras se informan de modo incorrecto), y las interrupciones de los sitios de comercio electrónico pueden acarrear enormes costes en términos de pérdida de ingresos, así como afectar negativamente a su reputación.

Incluso en las aplicaciones «no críticas», tenemos una responsabilidad con nuestros usuarios. Pensemos en un padre que almacena todas las fotos y vídeos de sus hijos en su aplicación de fotos [15]. ¿Cómo nos sentiríamos si esa base de datos se corrompiera de repente? ¿Sabríamos cómo restaurarla a partir de una copia de seguridad?

Hay situaciones en las que podemos optar por sacrificar la confiabilidad para reducir el coste de desarrollo (por ejemplo, al desarrollar el prototipo de un producto para un mercado en el que no se ha probado) o el coste operativo (por ejemplo, para un servicio con un margen de beneficio muy estrecho), pero debemos ser muy conscientes de cuándo estamos recortando gastos.

Escalabilidad

Aunque un sistema funcione hoy de forma fiable, eso no significa que vaya a hacerlo necesariamente en el futuro. Una razón frecuente para la degradación es el aumento de la carga: tal vez, el sistema ha crecido de 10 000 usuarios simultáneos a 100 000, o de 1 millón a 10. Quizá esté procesando volúmenes de datos mucho mayores que antes.

Escalabilidad es el término que utilizamos para describir la capacidad de un sistema para hacer frente a una mayor carga. Sin embargo, se debe tener en cuenta que no es una etiqueta unidimensional que podamos poner a un sistema: no tiene sentido decir «X es escalable» o «Y no es escalable». Hablar de escalabilidad significa, más bien, plantearse preguntas como «si el sistema crece de una manera determinada, ¿qué opciones tenemos para hacer frente al crecimiento?» y «¿cómo podemos añadir recursos informáticos para manejar la carga adicional?».

Descripción de la carga

En primer lugar, hay que describir de forma sucinta la carga actual del sistema; solo entonces, podremos hablar de cuestiones de crecimiento (¿qué pasa si nuestra carga se duplica?). La carga puede describirse con unos pocos números, a los que llamamos parámetros de carga. La mejor elección de los parámetros depende de la arquitectura del sistema: pueden ser las peticiones por segundo a un servidor web, la proporción de lecturas y escrituras en una base de datos, el número de usuarios activos simultáneamente en una sala de chat, la tasa de aciertos en una caché o cualquier otra cosa. Quizá lo que nos importe sea el caso medio, o quizá el cuello de botella esté dominado por un pequeño número de casos extremos.

Para concretar esta idea, consideremos Twitter como ejemplo, utilizando datos publicados en noviembre de 2012 [16]. Dos de las principales operaciones de Twitter son:

Publicación de un tuit

El usuario puede publicar un nuevo mensaje a sus seguidores (4,6k peticiones/segundo de media, más de 12k peticiones/segundo de pico).

Línea de tiempo de inicio

El usuario puede ver los tuits publicados por las personas a quienes sigue (300k peticiones/segundo).

Sencillamente, gestionar 12 000 escrituras por segundo (la tasa máxima de publicación de tuits) sería bastante fácil. Sin embargo, el reto del escalamiento de Twitter no se debe principalmente al volumen de tuits, sino al fan-out2: cada usuario sigue a muchas personas, y cada usuario es seguido por muchas personas. A grandes rasgos, hay dos formas de implementar estas dos operaciones:

1. Publicar un tuit, simplemente, consiste en insertar el nuevo tuit en una colección global de tuits. Cuando un usuario solicita su línea de tiempo de inicio, buscar todas las personas a quienes sigue, encontrar todos los tuits de cada uno de esos usuarios y se fusionan (ordenados por tiempo). En una base de datos relacional como la de la figura 1.2, se podría escribir una consulta como:

2. Mantener una caché para la línea de tiempo de inicio de cada usuario, como un buzón de tuits para cada usuario destinatario (véase figura 1.3). Cuando un usuario publica un tuit, se busca a todas las personas que siguen a ese usuario y se inserta el nuevo tuit en cada una de las cachés de su línea de tiempo. La solicitud de lectura de la línea de tiempo de inicio resulta entonces poco costosa, porque su resultado se ha calculado con antelación.

Figura 1.2Sencillo esquema relacional para la implementación de una línea de tiempo de inicio en Twitter.

Figura 1.3 Pipeline de datos de Twitter para la entrega de tuits a los seguidores, con parámetros de carga en noviembre de 2012 [16].

En la primera versión de Twitter, se utilizó el enfoque 1, pero los sistemas experimentaron problemas para mantener la carga de consultas en la línea de tiempo de inicio, por lo que la empresa cambió al enfoque 2. Esta opción funciona mejor porque la tasa media de tuits publicados es casi dos órdenes de magnitud inferior a la tasa de lecturas de la línea de tiempo de inicio, por lo que, en este caso, resulta preferible hacer más trabajo en tiempo de escritura y menos en tiempo de lectura.

Sin embargo, la desventaja del enfoque 2 radica en que la publicación de un tuit requiere ahora mucho trabajo extra. De media, un tuit se entrega a unos setenta y cinco seguidores, por lo que 4,6k tuits por segundo se convierten en 345k escrituras por segundo en las cachés de la línea de tiempo de inicio. Pero esta media oculta el hecho de que el número de seguidores por usuario varía enormemente, y algunos usuarios tienen más de treinta millones de seguidores. Esto significa que un solo tuit puede dar lugar a más de treinta millones de escrituras en las líneas de tiempo de inicio. Hacer esto de manera oportuna (Twitter trata de entregar los tuits a los seguidores en cinco segundos) es un reto importante.

En el ejemplo de Twitter, la distribución de seguidores por usuario (quizá ponderada por la frecuencia con la que esos usuarios tuitean) supone un parámetro de carga clave para discutir sobre la escalabilidad, ya que determina la carga de fan-out. Nuestra aplicación puede tener características muy diferentes, pero nosotros podemos aplicar principios similares para razonar sobre la carga.

La última vuelta de tuerca de la anécdota de Twitter: ahora que el enfoque 2 está sólidamente implantado, Twitter ha pasado a un híbrido de ambos enfoques. La mayoría de los tuits de los usuarios se siguen distribuyendo en las líneas de tiempo de inicio en el momento en que se publican, pero un pequeño número de usuarios con un gran número de seguidores (es decir, las celebridades) son excepciones a esta distribución. Los tuits de las celebridades a las que sigue un usuario se obtienen por separado y se combinan con la línea de tiempo de ese usuario cuando se leen, como en el enfoque 1. Con dicho enfoque híbrido, se ofrece un buen rendimiento constante. Volveremos a examinar este ejemplo en el capítulo 12, una vez que hayamos cubierto un poco más de terreno técnico.

Descripción del rendimiento

Una vez que hayamos descrito la carga de nuestro sistema, podemos investigar qué ocurre cuando la carga aumenta. Podemos verlo de dos maneras:

• Cuando aumenta un parámetro de carga y mantenemos los recursos del sistema (CPU, memoria, ancho de banda de red, etc.) sin cambios, ¿cómo se ve afectado el rendimiento de su sistema?

• Cuando se incrementa un parámetro de carga, ¿cuánto hay que ampliar los recursos si se quiere mantener el rendimiento sin cambios?

Ambas preguntas requieren números en lo que se refiere al rendimiento, así que veamos brevemente cómo describir el rendimiento de un sistema.

En un sistema de procesamiento por lotes como Hadoop, normalmente nos importa el rendimiento; es decir, el número de registros que podemos procesar por segundo, o el tiempo total que se tarda en ejecutar un trabajo en un conjunto de datos de cierto tamaño3. En los sistemas en línea, lo que suele ser más importante es el tiempo de respuesta del servicio; es decir, el tiempo que transcurre entre el instante en el que un cliente envía una solicitud y el instante en el que recibe la respuesta.

Latencia y tiempo de respuesta

La latencia y el tiempo de respuesta, a menudo, se utilizan como sinónimos, pero no son lo mismo. El tiempo de respuesta es lo que ve el cliente: además del tiempo real para procesar la solicitud (el tiempo de servicio), incluye los retardos de la red y los retardos de las colas. La latencia es el tiempo que una solicitud está esperando a ser tratada, durante el cual está latente, a la espera de ser atendida [17].

Incluso si solo realizamos la misma petición una y otra vez, obtendremos un tiempo de respuesta ligeramente diferente en cada intento. En la práctica, en un sistema en el que se maneja una variedad de peticiones, el tiempo de respuesta puede variar mucho. Por lo tanto, hay que pensar en el tiempo de respuesta no como un número único, sino como una distribución de valores que se pueden medir.

En la figura 1.4, cada barra gris representa una petición a un servicio, y su altura muestra el tiempo que ha tardado esa petición. La mayoría de las respuestas a las peticiones son razonablemente rápidas, pero hay valores atípicos ocasionales que tardan mucho más. Quizá las peticiones lentas son intrínsecamente más caras; por ejemplo, porque procesan más datos. Pero, incluso en un escenario en el que se pensaría que todas las peticiones deberían tardar lo mismo, se producen variaciones: se podría introducir la latencia adicional aleatoria, debido a un cambio de contexto a un proceso en segundo plano, la pérdida de un paquete de red y la retransmisión TCP, una pausa en la recogida de basura, un fallo de página que obligue a una lectura desde el disco, vibraciones mecánicas en el rack del servidor [18] o muchas otras causas.

Figura 1.4Ilustración de la media y los porcentajes: tiempos de respuesta para una muestra de 100 peticiones a un servicio.

Es habitual ver cómo se informa del tiempo medio de respuesta de un servicio (en sentido estricto, el término «medio» no se refiere a ninguna fórmula concreta, pero, en la práctica, suele entenderse como la media aritmética: dados n valores, se suman todos los valores y se dividen por n). Sin embargo, la media no es una métrica muy buena, si se quiere conocer el tiempo de respuesta «típico», porque no indica cuántos usuarios han experimentado realmente ese retraso.

Normalmente, es mejor utilizar los porcentajes. Si tomamos la lista de tiempos de respuesta y la ordenamos del más rápido al más lento, entonces, la mediana es el punto medio: por ejemplo, si la mediana del tiempo de respuesta es de 200 milisegundos, significa que la mitad de las peticiones vuelven en menos de 200 milisegundos, y la otra mitad tarda más de los 200.

Esto hace que la mediana sea una buena métrica, si se quiere saber cuánto tiempo suelen esperar los usuarios: la mitad de las peticiones de los usuarios se atiende en menos de la mediana del tiempo de respuesta, y la otra mitad tarda más que la mediana. La mediana también se conoce como porcentaje 50 y, a veces, se abrevia como p50. Hay que tener en cuenta que la mediana se refiere a una sola petición; si el usuario realiza varias peticiones (a lo largo de una sesión, o porque se incluyen varios recursos en una misma página), la probabilidad de que, al menos, una de ellas sea más lenta que la mediana es mucho mayor que el 50 %.

Para averiguar la gravedad de los valores atípicos, podemos fijarnos en los porcentajes más altos: los porcentajes 95, 99 y 99,9 son comunes (abreviados: p95, p99 y p99,9). Son los umbrales de tiempo de respuesta en los que el 95 %, el 99 o el 99,9 de las solicitudes son más rápidas que ese umbral concreto; por ejemplo, si el tiempo de respuesta del porcentaje 95 es de 1,5 segundos, eso significa que 95 de cada 100 peticiones tardan menos de 1,5 segundos, y 5 de cada 100 peticiones tardan 1,5 segundos o más. Esto se ilustra en la figura 1.4.

Los porcentajes altos de los tiempos de respuesta, también conocidos como latencias de cola, son importantes, porque afectan directamente a la experiencia de los usuarios del servicio; por ejemplo, Amazon describe los requisitos de tiempo de respuesta para los servicios internos en términos del porcentaje 99,9, aunque solo afecte a 1 de cada 1000 solicitudes. Esto se debe a que los clientes con las solicitudes más lentas suelen ser quienes disponen de más datos en sus cuentas, porque han hecho muchas compras; es decir, son los clientes más valiosos [19]. Es importante mantener a esos clientes contentos, asegurando que el sitio web sea rápido para ellos: Amazon también ha observado que un aumento de 100 milisegundos en el tiempo de respuesta reduce las ventas en un 1 % [20], y otros informan de que una ralentización de 1 segundo reduce la métrica de satisfacción del cliente en un 16 % [21, 22].

Por otro lado, se consideró que optimizar el porcentaje 99,99 (el más lento, 1 de cada 10 000 solicitudes) resultaba demasiado caro y no aportaba suficientes beneficios para los objetivos de Amazon. Reducir los tiempos de respuesta en porcentajes muy altos resulta difícil, porque se ven fácilmente afectados por eventos aleatorios fuera de nuestro control, y los beneficios disminuyen.

Por ejemplo, los porcentajes se utilizan a menudo en los objetivos de nivel de servicio (service level objectives, SLO) y en los acuerdos de nivel de servicio (service level agreements, SLA), contratos que definen el rendimiento y la disponibilidad esperados de un servicio. En un SLA, se puede establecer que se considera que el servicio está activo si cuenta con una mediana del tiempo de respuesta inferior a 200 milisegundos y un porcentaje 99 inferior a 1 segundo (si el tiempo de respuesta aparece mayor, también podría estar inactivo), y se puede exigir que el servicio esté activo al menos el 99,9 % del tiempo. Estos parámetros fijan las expectativas de los clientes del servicio y les permiten exigir un reembolso, si no se cumple el SLA.

Los retardos en las colas suelen representar la causa de una gran parte del tiempo de respuesta en porcentajes elevados. Como un servidor solo puede procesar un pequeño número de cosas en paralelo (limitado, por ejemplo, por el número de núcleos de CPU), solo se necesita un pequeño número de peticiones lentas para retrasar el procesamiento de las siguientes; un efecto que, a veces, se conoce como bloqueo de cabecera. Incluso si esas peticiones posteriores son rápidas de procesar en el servidor, el cliente verá un tiempo de respuesta general lento, debido al tiempo de espera para que se complete la petición anterior. A causa de este efecto, es importante medir los tiempos de respuesta en el lado del cliente.

Cuando se genera carga de forma artificial para probar la escalabilidad de un sistema, el cliente que genera la carga tiene que seguir enviando peticiones, independientemente del tiempo de respuesta. Si el cliente espera a que se complete la solicitud anterior antes de enviar la siguiente, ese comportamiento conlleva el efecto en la prueba de mantener artificialmente las colas más cortas de lo que serían en la realidad, lo que produce un sesgo en las mediciones [23].

Porcentajes en la práctica

Los porcentajes altos son especialmente importantes en los servicios de backend, a los que se llama varias veces como parte del servicio de una única solicitud del usuario final. Aunque se realicen las llamadas en paralelo, la solicitud del usuario final tiene que esperar a que se complete la más lenta de las llamadas en paralelo. Basta con una sola llamada lenta para que toda la petición del usuario final lo sea también, como se ilustra en la figura 1.5. Incluso si solo un pequeño porcentaje de las llamadas del backend son lentas, la posibilidad de obtener una llamada lenta aumenta si una petición del usuario final requiere múltiples llamadas del backend, por lo que una mayor proporción de peticiones del usuario final acaban siendo lentas (un efecto conocido como amplificación de la latencia de cola [24]).

Si queremos añadir porcentajes de tiempo de respuesta a los cuadros de mando de supervisión de los servicios, necesitamos calcularlos de forma eficiente y prolongada; por ejemplo, puede que deseemos mantener una ventana continua de los tiempos de respuesta de las solicitudes en los últimos 10 minutos. Cada minuto, se calcula la mediana y varios porcentajes sobre los valores de esa ventana y se trazan esas métricas en un gráfico.

La implementación ingenua consiste en mantener una lista de tiempos de respuesta para todas las peticiones dentro de la ventana de tiempo y ordenar esa lista cada minuto. Si eso no es lo suficientemente eficiente para lo que necesitamos, existen algoritmos con los cuales calcular una buena aproximación de los porcentajes con un coste mínimo de CPU y memoria, como forward decay [25], t-digest [26] o HdrHistogram [27