Escalabilidad en
Proyectos Software

Fundamentos, consejos y herramientas útiles para crear sistemas capaces de ofrecer un servicio a millones de usuarios

Javier Matos Odut

iam@javiermatos.com

¿Qué es la escalabilidad?

Escalabilidad ≠ Velocidad

Un sistema veloz hace algo en poco espacio de tiempo, pero esto no es realmente escalabilidad...

¿Qué es la escalabilidad?

La escalabilidad es la propiedad deseable de un sistema, una red o un proceso, que indica su habilidad para...

  • reaccionar y adaptarse sin perder calidad
  • manejar el crecimiento continuo de trabajo de manera fluida
  • estar preparado para hacerse más grande sin perder calidad en los servicios ofrecidos

¿Cuándo logramos un
sistema escalable?

En computación, decimos que logramos un sistema escalable cuando su rendimiento se incrementa de forma proporcional a la capacidad hardware que se le agrega


¿De qué manera podemos agregar
capacidad hardware?

Escalabilidad vertical

  • Fácil y rápido de escalar
  • Un servidor no se puede ampliar indefinidamente
  • Si optamos por uno nuevo, el precio de los servidores no es lineal respecto a su rendimiento
  • Punto de fallo único
  • Si compramos uno nuevo el anterior deja de utilizarse (no podemos sumar rendimientos)

Escalabilidad horizontal

  • Podemos escalar agregando nuevos nodos
  • Desaparece el punto de fallo único
  • Los nuevos nodos agregan potencia a los que ya están funcionando (se suman los rendimientos)
  • Se suele utilizar commodity hardware
  • Es complicado el desarrollar aplicaciones distribuídas

Commodity hardware

Commodity hardware

Commodity hardware para dedicar todo el rendimiento del equipo en la actividad que necesitamos



Virtualización

Equipos virtuales para particionar un servidor potente y sacarle todo el partido en tareas específicas (aprovecha la concurrencia)

¿Por qué es necesario hacer los sistemas escalables?

El fenómeno de las startups

Cómo está organizada la presentación

Se parte de un sistema desplegado en un único nodo. Siguiendo un proceso iterativo y evolutivo mejoramos el sistema para hacerlo más escalable y superar los diferentes cuellos de botella.

Cómo resolvemos un
problema de escalabilidad

Distribuímos el trabajo en nuestro sistema de acuerdo a los siguientes principios (se pueden combinar)

  • Diseña para clonar cosas
    • Servidores web, servidores de aplicación, servidores de bases de datos, servidores de caché...
  • Diseña para separar cosas diferentes
    • Login, creación de usuarios, búsquedas, órdenes de compra, subida de fotos...
  • Diseña para dividir cosas similares
    • Bases de datos de usuarios, transacciones costosas, colas de procesamiento, contenido estático...

Estado inicial del sistema

  • Todo el sistema reside en un único nodo o máquina
  • El servidor web procesa las peticiones del usuario. Los archivos estáticos los sirve sin más. El contenido dinámico lo genera el servidor de aplicación.
  • El servidor de aplicación es nuestro proyecto. Es responsable de generar el contenido dinámico cuando se le requiere.
  • El servidor de base de datos contiene los objetos y entidades que manipulamos en la aplicación.

Descomponemos el sistema

  • Separar cosas diferentes

  • Rendimientos diferentes

  • Requisitos diferentes

    Servidor CPU MEM HDD
    Web bajo bajo bajo
    Aplicación medio medio bajo
    DB alto alto alto

Nuestro sistema
no puede ser una caja negra

  • Sabemos que existen elementos diferentes que interactuan
  • Los usuarios hacen peticiones a nuestro sistema y se responden
  • Y bueno... esto... mmmm... creo que ya con eso está todo, ¿no?


¡En realidad no sabemos nada!

Medir y conocer el estado del sistema

Si no tenemos mediciones de todos los aspectos del sistema no sabemos cómo se comporta cuando funciona bien, cómo lo hace cuando funciona mal y mucho menos podemos identificar los problemas que ocurren para hallar soluciones. Las mediciones nos ayudan a detectar tendencias y adelantarnos a los sucesos.


¡Sin mediciones estamos perdidos!

Protocolo SNMP

Facilita el intercambio de información de administración entre dispositivos de red

Comandos: snmpwalk, snmpbulkwalk, snmp*

Mediciones en el tiempo: SNMP

Es importante capturar el estado y la evolución de todos los aspectos de nuestro sistema a lo largo del tiempo

Nagios, Cacti, Zenoss, Zabbix y otros muchos más...

Logs en tiempo real: Syslog

Hay que conocer los eventos de todo el sistema en tiempo real...

logstash, fluentd, splunk, Flume

Logs en tiempo real: Syslog

...y poder recogerlos en un lugar centralizado

ElasticSearch, Kibana, Sentry, Graphite

Logs en tiempo real: Agregación

Si trabajamos con demasiados logs es importante utilizar estrategias de agregación para evitar vernos abrumados

ElasticSearch, Kibana, Sentry, Graphite

Primer cuello de botella:
la base de datos

  • Notamos que las peticiones de los usuarios se resuelven muy lento por culpa de la base de datos
  • El servidor de aplicaciones pasa la mayor parte del tiempo esperando
  • Las peticiones se acumulan a mayor velocidad de lo que se responden


¡Tenemos que resolver el problema de la base de datos!

Las lecturas son lentas (1)

  • Descubrimos que los usuarios consumen datos en una proporción mucho mayor a la que los generan
  • Una vez publicado un contenido por parte de un usuario, muchos otros usuarios leen ese contenido
  • Las operaciones de escritura no son un problema
  • ¿Escalamos el sistema para mejorar la lectura de datos?
  • Utilizamos un esquema de replicación master/slave en SQL

Las lecturas son lentas (2)

Sincronizamos los servidores de bases de datos para que se hagan las escrituras en el master y las lecturas en los slaves

Todas las bases de datos en el cluster son réplicas exactas y con ellas el rendimiento en lectura se multiplica por 3

Demasiados usuarios para una BD (1)

  • Nuestro web, nuestro sistema... ¡todo funcionaba bien...
  • ...hasta que llegaron los usuarios!
  • Ya no podemos almacenar todos los usuarios en una única base de datos (la tabla de usuarios es enooooooooorme)
  • ¿Y si repartimos la tabla de usuarios en múltiples servidores?
  • Utilizamos una partición de la tabla en shards

Demasiados usuarios para una BD (2)

Dividimos la tabla de usuarios de acuerdo a alguna estrategia (aleatorio, módulo, hash...) y la repartimos en varios servidores

Ahora podemos almacenar más usuarios y además el rendimiento se multiplica por 4

Decisiones complicadas (1)

  • Pensábamos que estaba todo resuelto, pero ahora teníamos un reto nuevo que era difícil de resolver
  • Los usuarios se escriben mensajes entre sí y...
    • puedo almacenar los mensajes de acuerdo al destinatario y facilitar la operación de lectura de mensajes recibidos
    • o puedo almacenar los mensajes según emisor y facilitar la lectura de mensajes enviados
    • si no lo hago de alguna de las formas anteriores entonces ambas operaciones serán ineficientes
  • Caso de muros o conversaciones de Facebook, Twitter...
  • ¿Y qué tal si duplico información y almaceno lo mismo de las dos formas? Sacrifico almacenamiento y gano rendimiento

Decisiones complicadas (2)

La ventaja de esta duplicidad es que la lectura de mensajes enviados y recibidos es eficiente. Además, un usuario puede borrar sus mensajes sin afectar a los demás usuarios.

Alternativas más allá de SQL

  • Hasta el momento hemos escalado bases de datos SQL
  • Hay muy buenas bases de datos SQL como son MySQL, Percona (buen clustering), MaríaDB y PostgreSQL
  • Sin embargo, existen otras alternativas "mejores"
  • ¿Qué hay de las bases de datos NoSQL? ¿acaso no permiten escalar más fácil? Además, tenemos muchas opciones: HBase (BigTable de Google), Cassandra (Facebook, Twitter, Digg), MongoDB, CouchDB, Redis, ElasticSearch, Neo4j...
  • Ah, y que no se nos olviden los archivos en disco duro, que para ciertos casos pueden ser la mejor opción

Características de nuestros datos

  • ¿Conocemos bien cómo son nuestros datos y cómo han de ser manipulados? ¿utilizar X es la mejor forma para almacenarlos?
  • ¿Es mejor SQL o NoSQL? (o inclusive un archivo)
  • Las bases de datos SQL fueron diseñadas y pensadas con un propósito diferente a las bases de datos NoSQL, aunque pensemos en ambas opciones como equivalentes

Un poco de teoría sobre bases de datos

En informática, el teorema CAP, también llamado Teorema de Brewer, establece que es imposible para un sistema de cómputo distribuido garantizar simultáneamente:

  1. La consistencia (Consistency), es decir, que todos los nodos vean la misma información al mismo tiempo
  2. La disponibilidad (Availability), es decir, la garantía de que cada petición a un nodo reciba una confirmación de si ha sido o no resuelta satisfactoriamente
  3. La tolerancia a fallos (Partition Tolerance), es decir, que el sistema siga funcionando a pesar de algunas pérdidas arbitrarias de información o fallos parciales del sistema

Caché

  • Almacenamos todo en la base de datos aunque...
    • no todos los datos se consumen
    • sólo un subconjunto de datos se consume de forma intensiva (más recientes, más referenciados...)
    • la frecuencia de acceso a cada dato es diferente, y todos se sirven de la misma forma → ineficiencia
  • Observamos que con el tiempo varían los datos que consumen nuestros usuarios de forma intensiva
  • El valor de los datos que almacenamos decrece con el tiempo


¡Optimicemos el acceso a los datos más utilizados!

Utilidad de la caché

  • Caché hit: el dato está contenido en la caché y se sirve desde memoria, con el consiguiente aumento de velocidad
  • Caché miss: el dato no está contenido en la caché. Se lee el dato desde su orígen, se sirve el dato solicitado y además de actualiza la caché para la próxima lectura.
  • Nos podemos permitir políticas complejas de actualización de caché debido al elevado coste de leer el dato de origen
  • Heurística para saber cuántos servidores de caché utilizar:
    si caché hit < 85%, entonces ponemos más servidores

Utilización de caché en Facebook

A finales de 2008 Facebook utilizaba más de 800 servidores de caché con una capacidad de más de 28 TB de memoria (enlace)

Han desarrollado Claspin, que es una aplicación que representa de forma visual e intuitiva el estado de la caché de su sistema

Escalando en manejo de datos

Segundo cuello de botella:
el servidor de aplicación

  • La mejora en la arquitectura de las bases de datos ha multiplicado el rendimiento de acceso a los datos
  • Con la caché servimos desde memoria la mayoría de datos solicitados
  • El servidor de aplicación trabaja a pleno rendimiento y aún así es insuficiente


¡Tenemos que escalar el servidor de aplicaciones!

Contenido dinámico (1)

  • Se ha mejorado el acceso a bases de datos y se utiliza caché
  • Sabemos que las peticiones de usuarios son independientes
  • Podemos escalar el sistema desplegando más servidores de aplicación y configurando el servidor web
    
    # Configuración con Nginx para balanceo de carga
    http {
        upstream app_cluster {
            server app_server1.example.com;
            server app_server2.example.com;
            server app_server3.example.com;
        }
    
        server {
            listen 80;
    
            location / {
                proxy_pass http://app_cluster;
            }
        }
    }
                    

Contenido dinámico (2)

  • El contenido dinámico depende del servidor de aplicación, que es nuestra creación y depende por completo de nosotros
  • Hay que implementar el servidor de aplicación siguiendo buenas prácticas de programación y buscar la eficiencia, pero no realizar optimizaciones tempranas (perdemos simplicidad)
  • Sólo genera contenido cuando se producen cambios, sino usa la caché y almacéna los datos para la siguiente consulta
  • Organiza el sistema en componentes aislados: por un lado los que generan contenido muy cambiante, y por el otro los que generan contenido poco cambiante
  • El mejor consejo: evita los problemas y no dediques esfuerzo en tener que resolverlos

Escalando en procesamiento

Tercer cuello de botella:
interacción del usuario

  • Todas las peticiones de usuarios se responden rápido, menos aquellas relativas a acciones lentas
  • Parece que a veces es inevitable hacer que algunas peticiones tomen su tiempo, y más si se refieren a IO
  • El usuario debería concienciarse de que ciertas acciones suyas toman un tiempo para realizarse


¡Hemos de hacer lo que sea para evitar las esperas del usuario!

Un usuario borra una foto (1)

  1. Recibimos la petición de borrado en un servidor de aplicación
  2. Realizamos una llamada al servicio de almacenamiento
    1. El servicio busca la foto consultando una base de datos
    2. Marca la foto como eliminada para que no esté disponible
    3. Ordena el borrado de la foto al servidor donde se está almacenando
      1. El servidor elimina la foto
      2. Devuelve el resultado de la operación
    4. Si se borró con éxito elimina la referencia en base de datos
    5. Recibe la respuesta y notifica al servidor de aplicación
  3. El servicio responde con el resultado de la operación
  4. Enviamos el resultado de la operación al usuario


¿Alguien ha dicho Timeout Error?

Un usuario borra una foto (2)

  • A nadie le gusta esperar, ¡reconozcámoslo!
  • Los navegadores web (lado usuario) y los servidores web (nuestro lado) lanzan Timeout Error
  • En los servidores web matamos a los procesos bloqueados. ¿Terminará o no el proceso? (problema de la parada)
  • Existen procesos o tareas lentas cuyo requisito temporal excede el tiempo de vida de una petición

Un usuario borra una foto (3)

  • Al usuario no le gusta esperar, ¡nunca!
  • ¿Tenemos que hacer esperar su petición hasta tener la confirmación de que su foto ha sido eliminada del sistema?
  • A ver... idealmente, el usuario espera pulsar el botón de borrado, que la foto "le aparezca como que ya no está" y que pueda seguir navegando en nuestra aplicación
  • Qué tal si...

Un usuario borra una foto (4)

  1. Recibimos la petición de borrado en un servidor de aplicación
  2. Deshabilitamos la foto para no seguir sirviéndola
  3. Creamos una tarea para borrar realmente la foto
  4. Respondemos al usuario que su foto ha sido borrada


La creación de la tarea es inmediata y no bloquea el flujo de ejecución. La percepción del usuario es que hemos sido más rápidos borrando su foto. La foto ya no está disponible para otros usuarios, y la tarea de borrado se encarga de eliminarla.


/path/to/command arg1 arg2 &  # Tarea en segundo plano
            

Un usuario borra una foto (5)

  • En realidad podemos hacerlo mejor, pues el usuario espera menos pero de todas formas sigue teniendo que esperar
  • Desarrollamos todo el proceso mediante llamadas asíncronas para notificar al usuario con el resultado de la tarea
  • No bloqueamos el flujo de ejecución y permitimos al usuario seguir navegando mientras está atento a un evento
  • En algún momento el usuario recibe un evento con el resultado de la operación que procesamos con un callback

function slowTaskCallback() {
    console.log(this.responseText);
}

var oReq = new XMLHttpRequest();
oReq.onload = slowTaskCallback;
oReq.open("get", "http://api.example.com/a/restful/resource", true);
oReq.send();
            

Un usuario borra una foto (6)

Finalmente, la solución ideal combina dos elementos diferentes en el lado del usuario y en el lado de nuestro sistema:

  • Navegador del usuario
    • Llamadas asíncronas al servidor. Enviamos la petición y esperamos respuesta en forma de evento asíncrono.
  • Nuestro sistema
    • Llamadas asíncronas entre servicios internos. Enviamos la petición sin bloquear ni hacer esperar al procesador.
    • Para las operaciones costosas creamos una tarea mediante el envío de un mensaje que se encola en alguna parte
    • Si no queremos hacer algo ahora tenemos que decirle a alguien que lo haga después, y esto es la mensajería
    • El mensaje se encola y cuando llegue su turno se ejecuta la tarea asociada

Un usuario borra una foto (7)

  • Llamadas asíncronas en el lado del servidor
    • Podemos utilizar JavaScript en el lado del servidor con Node.js. La ventaja principal es que las bibliotecas para este servidor de aplicación son asíncronas y no bloquean.
    • También hay opciones en Python, con bibliotecas como Twisted, Tornado o Gunicorn entre otros.
    • Las anteriores son en realidad bibliotecas de networking que implementan el patrón reactor
  • Procesamiento de tareas en colas
    • Existen muchas alternativas para desplegar un sistema de mensajería con colas: AMPQ, JMS y Spread
    • Una buena opción es RabbitMQ, que implementa AMPQ con el lenguaje Erlang

Diagrama de eventos

Agregamos servidores de tareas

Cuarto cuello de botella:
Descargas de archivos estáticos

¡Sirvamos archivos estáticos más rápido!

  • Vale, parece que lo tenemos todo bajo control pero...
  • Nuestra aplicación es lo importante y lo esencial, aunque también hemos de enviar contenido estático
  • Empleamos demasiados recursos para servir archivos estáticos que deberían distribuirse más fácilmente

Contenido estático

  • El contenido estático se refiere a los archivos que se envían al usuario sin realizar computación alguna sobre ellos, y que por tanto se pueden enviar muy rápido
  • Ejemplos de archivos estáticos son los siguientes: HTML, CSS, JS, imágenes, videos, mp3, zip, pdf...
  • Se deben minificar los archivos de código y reducir su tamaño
  • Si se configura de forma adecuada, un único servidor puede distribuir archivos estáticos a velocidades de Gbits/s
  • Escalar un servidor de contenido estático es fácil, mucho más que en los casos anteriores

Estrategia de distribución (1)

  • El servidor web debe enviar estos archivos sin que el servidor de aplicación tome parte
  • Una vez enviados los archivos estáticos el usuario debe utilizar la copia local que tiene
  • Podemos tomar provecho de las siguientes cabeceras HTTP:
    • Cache-Control: define el comportamiento de las cachés intermedias desde nuestro sistema hasta el usuario.
    • Expires: fecha de expiración de la respuesta. Decimos al navegador cuánto tiempo de validez tiene lo descargado.
    • Etag: entity-tag. Nos ayuda a evitar la descarga del archivo si es igual al que posee el usuario.
    • Last-Modified: evita la descarga del archivo por el usuario si no ha habido modificaciones respecto a su versión.

Estrategia de distribución (2)

  • Resulta sencillo configurar Nginx para que cumpla con la estrategia de distribución anterior:
    
    # Servir archivos estáticos directamente
    location ~* ^.+\.(?:css|js|jpe?g|gif|ico|png|html|xml|svg)$ {
      add_header Cache-Control public;  # public, private, max-age...
      expires 30d;
      etag on;  # on, off
    }
                    
  • No sólo no procesamos los archivos estáticos, sino que además hacemos que el usuario los almacene durante 30 días
  • Notificamos a las cachés intermedias que el contenido es de carácter público y que pueden cachearlo de forma local
  • Se asigna un identificador a cada archivo de forma automática

Estrategia de distribución (3)

  • Archivos estáticos en nuestro dominio example.com, o en el subdominio específico static.example.com
  • Existe un problema: los navegadores limitan el número de conexiones simultáneas por dominio y subdominio: Chrome (6), Firefox 3+ (6), Opera 12 (6), IE 7 (2), IE 8 (6), IE 10 (8)
  • Creamos múltiples subdominios: static0.example.com, static1.example.com, static2.example.com...
  • Hay que tener cuidado: cada dominio/subdominio implica una resolución DNS, que incrementa el tiempo de carga.

Estrategia de distribución (4)

Estrategia de distribución (5)

  • Servir archivos estáticos es fácil y rápido, pero podemos saturar nuestra conexión
  • Usar una red de distribución de contenidos (CDN):
  • El CDN recibe los archivos de nuestro servidor y los guarda en caché para distribuirlos

Archivos estáticos en LinkedIn (1)

LinkedIn utiliza su propio CDN: static.licdn.com

Además del CDN, ¿sacan provecho de las cabeceras HTTP para reducir la transferencia de archivos?

Archivos estáticos en LinkedIn (2)

Fijémonos en un archivo estático JS y hagamos la prueba...

Con cabeceras HTTP reducen el tiempo de carga de 262 ms a 75.5 ms, y evitan el envío de 177 KB (comprimidos a 62.1 KB)

Realizar cómputo y almacenamiento en el navegador del usuario

  • Tal vez no seamos conscientes del todo, pero los navegadores web son una de las aplicaciones software más complejas que existen en la actualidad
  • Debemos sacar todo el partido a los navegadores si queremos escalar nuestro sistema
  • No hagamos trabajo en nuestros servidores que pueda hacer el usuario con su navegador

El navegador es esencial

Quinto cuello de botella:
El servidor web

La red es parte de la arquitectura

  • A estas alturas un único servidor web no nos permite escalar nuestro sistema
  • Ocurre algo aún peor: el servidor web es nuestro punto de fallo único
  • Necesitamos utilizar varios servidores web y repartir la carga de trabajo entre ellos
  • Ya puestos, también necesitamos aumentar la seguridad a nivel de red

Balanceadores de carga

  • Los balanceadores de carga pueden ser hardware o software
  • Los implementados en hardware son caros
  • Se consigue un resultado similar mediante servidores de DNS:
    • Round-robin DNS
    • Si distribuímos geográficamente, entonces podemos delegar el subdominio www.example.org y hacer que su zona se sirva por nuestros servidores web. De esta forma el subdominio tendrá múltiples direcciones IPs asociadas que serán las de nuestros servidores web.

one.example.org A 192.0.2.1
two.example.org A 203.0.113.2
www.example.org NS one.example.org
www.example.org NS two.example.org
            

Firewalls

  • Los firewalls son útiles para solucionar problemas de seguridad en nuestro sistema
  • Pero por favor, no abusemos de ellos
  • Si no se configuran como es debido dan tantos problemas como soluciones
  • El rigor en la seguridad ha de ser diferente para archivos estáticos que para datos de las bases de datos

Nuestro sistema es escalable

El data center

  • Toda nuestra infraestructura se encuentra en un mismo lugar
  • Podemos identificar el data center como punto de fallo único
  • Si queremos disponibilidad hemos de distribuir el sistema en varios data centers en diferentes puntos de la geografía

Nuestro sistema es muy escalable

Sistema escalable (0)

Sistema escalable (1)

Sistema escalable (2)

Sistema escalable (3)

Sistema escalable (4)

Sistema escalable (5)

Sistema escalable (6)

Sistema escalable (7)

Infraestructura de Wikimedia

Llegar hasta aquí es complicado

¿Hemos de pasar por todo lo anterior para poder lanzar nuestro proyecto y hacerlo disponible a gran escala?

Simplificación operativa

  • Escalar un sistema es caro y requiere de experiencia
  • La ventaja competitiva de nuestro sistema se encuentra en el código del servidor de aplicación, que es lo que nosotros programamos y lo que nos hace diferentes y únicos
  • El resto de elementos que rodean al servidor de aplicación podrían verse como no más que "males necesarios"



¡Externalizar y usar servicios de terceros!

Algunas precauciones importantes

  • Elegir un buen proveedor. La disponibilidad de nuestro sistema dependerá por completo de ellos.
  • El proveedor buscará la manera más barata y sencilla (tal vez inapropiada) de darnos el servicio
  • Nosotros somos los responsables del sistema de cara a nuestros usuarios
  • Las soluciones del proveedor han de ser abiertas para no atarnos a ellos y poder migrar en caso de necesidad
  • No es una elección todo o nada: podemos utilizar terceros para afrontar picos de carga junto a nuestro sistema

Proveedores

Unos últimos consejos

Como era de esperar he tenido que dejar cosas importantes fuera de la charla por limitaciones de tiempo

  • Evita la sobreingeniería
  • No permitas los puntos de fallo únicos (redundancia como garantía para hacer robusto a tu sistema)
  • Los fallos y las excepciones de tu sistema son un flujo de ejecución más y se han de tratar de forma conveniente
  • La herramienta concreta para el problema específico
  • Sé ahorrativo en todo cuanto hagas: procesos simples, algoritmos sencillos, utilización intensiva de caché...

Enlaces de interés

Gracias por vuestra atención

¿Preguntas?


Javier Matos Odut

iam@javiermatos.com