Cómo Instagram escaló a 14 millones de usuarios con sólo 3 programadores
Tiempo de lectura: 4.17 minutos
Instagram creció de 0 a 14 millones de usuarios en poco más de un año, desde octubre de 2010 hasta diciembre de 2011.
Lograron esto con solo 3 ingenieros.
Lo lograron siguiendo 3 principios clave.
Los principios de Instagram
- Mantener las cosas simples.
- No reinventar la rueda.
- Usar tecnologías probadas y sólidas.
El stack explicado de forma sencilla
La infraestructura inicial de Instagram funcionaba en AWS, usando EC2 con Ubuntu Linux.
Como referencia: EC2 es el servicio de Amazon que permite a los desarrolladores alquilar computadoras virtuales.
Para simplificar, veamos cómo luce una sesión de usuario, desde la perspectiva de un ingeniero de software.
A continuación se marcará como Sesión
, al inicio de cada sección.
Frontend
Sesión: Un usuario abre la aplicación de Instagram.
Instagram inicialmente se lanzó como una aplicación para iOS en 2010.
Desde que Swift fue lanzado en 2014, podemos asumir que Instagram fue escrito usando Objective-C y una combinación de otras cosas como UIKit.
Balanceo de carga
Sesión: Después de abrir la aplicación, se envía una solicitud al backend para obtener las fotos del feed principal. Instagram cuenta con un balanceador de carga.
Instagram utilizó el servicio "Elastic Load Balancer" de Amazon.
Tenían 3 instancias de NGINX que se intercalaban a conveniencia.
Cada solicitud pasaba por el balanceador de carga antes de ser dirigida al "servidor de aplicación".
Backend
Sesión: El balanceador de carga envía la solicitud al servidor de aplicación, que tiene la lógica para procesar la solicitud correctamente.
El "servidor de aplicación" de Instagram utilizaba Django y estaba escrito en Python, con Gunicorn como su servidor WSGI.
A modo de recordatorio:
- WSGI significa "Web Server Gateway Interface".
- Y se encarga de redirigir las solicitudes de un servidor web a una aplicación web.
Instagram utilizaba Fabric para ejecutar comandos en paralelo en muchas instancias a la vez. Esto permitía hacer deploy en segundos.
Contaban con 25 máquinas "High-CPU Extra-Large" provistas por Amazon.
Gracias a que el servidor es stateless (sin estado), cuando necesitaban manejar más solicitudes, podían agregar más máquinas sin problema (escalar horizontalmente).
Almacenamiento de datos
Sesión: El servidor de aplicación ve que la solicitud necesita datos para el feed principal.
Para esto, digamos que necesita:
- IDs de las fotos relevantes más recientes.
- Las fotos reales que coincidan con esos IDs de fotos.
- Datos de usuario para esas fotos.
Base de datos: Postgres
Sesión: El servidor de aplicación toma los IDs de fotos relevantes más recientes de Postgres.
El servidor de aplicación obtiene datos de PostgreSQL, que almacenaba la mayoría de los datos de Instagram, como usuarios y metadatos de fotos.
Las conexiones entre Postgres y Django se agruparon usando Pgbouncer.
Instagram fragmentó sus datos debido al alto tráfico que estaban recibiendo (más de 25 fotos y 90 me gusta por segundo). Usaron código para mapear varios miles de fragmentos 'lógicos' a unos pocos fragmentos físicos.
Un desafío interesante que Instagram enfrentó y resolvió es la generación de IDs que pudieran ordenarse por tiempo.
Sus IDs ordenables por tiempo, constaban de:
- 41 bits para el tiempo en milisegundos (equivalente a 41 años de IDs, con un custom epoch).
- 13 bits que representan el ID de fragmento lógico.
- 10 bits que representan una secuencia autoincrementable, módulo 1024. Esto significa que podemos generar 1024 IDs, por fragmento, por milisegundo.
Gracias a los IDs ordenables por tiempo en Postgres, el servidor de aplicación recibía con éxito los IDs de las fotos relevantes más recientes.
Almacenamiento de fotos: S3 y Cloudfront
Sesión: El servidor de aplicación luego obtiene las fotos reales que coincidan con esos IDs de fotos con enlaces de CDN, para que carguen rápidamente para el usuario.
Varios terabytes de fotos se almacenaron en Amazon S3.
Estas fotos se sirvieron rápidamente a los usuarios utilizando Amazon CloudFront.
Almacenamiento en caché: Redis y Memcached
Sesión: Para obtener los datos de usuario de Postgres, el servidor de aplicación (Django) asociaba los IDs de fotos con IDs de usuario usando Redis.
Instagram usó Redis para almacenar un mapeo de aproximadamente 300 millones de fotos al ID de usuario que las creó, con el fin de saber qué fragmento consultar cuando se obtienen fotos para el feed principal, feed de actividad, etc.
Todo Redis se almacenó en memoria para disminuir la latencia y se dividió en varios fragmentos.
Con un hashing inteligente, Instagram pudo almacenar 300 millones de mapeos de claves en menos de 5 GB.
Este mapeo clave-valor de ID de foto a ID de usuario fue necesario para saber qué fragmento de Postgres consultar.
Sesión: Gracias al almacenamiento en caché eficiente usando Memcached, obtener datos de usuario de Postgres fue rápido, ya que las respuesta recientes se leían de caché.
Para el almacenamiento en caché general, Instagram usó Memcached.
- Tenían 6 instancias de Memcached en ese momento.
- Memcached es relativamente simple de implementar sobre Django.
Dato interesante: 2 años después, en 2013, Facebook publicó un paper sobre cómo escaló Memcached, para ayudarles a manejar miles de millones de solicitudes por segundo.
Sesión: El usuario ahora ve el feed de inicio, poblado con las últimas imágenes de personas a las que sigue.
Configuración Maestro-Réplica
Tanto Postgres como Redis se ejecutaron en una configuración master-replica, y usaron instantáneas de Amazon EBS (Elastic Block Store) para realizar copias de seguridad frecuentes de los sistemas.
Notificaciones push y tareas asíncronas
Sesión: Ahora, supongamos que el usuario cierra la aplicación, pero luego recibe una notificación push de que un amigo publicó una foto.
Esta notificación push sería enviada usando pyapns, junto con las más de mil millones de notificaciones push que Instagram ya había enviado.
Pyapns es un proyecto open source, que facilita la integración y uso del Servicio de Notificaciones Push de Apple (APNS).
Sesión: ¡Al usuario le encantó esta foto! Entonces decidió compartirla en Twitter.
En el backend, la tarea se envía a Gearman, una cola de tareas que distribuyó el trabajo a máquinas mejor adaptadas.
Instagram tenía alrededor de 200 workers escritos en Python, consumiendo la cola de tareas definida con Gearman.
Gearman se usó para múltiples tareas asíncronas, como distribuir actividades (como una nueva foto publicada) a todos los seguidores de un usuario (esto se llama fanout).
Monitoreo
Sesión: ¡Oh no! La aplicación de Instagram se bloqueó porque algo falló en el servidor y envió una respuesta errónea. Los tres ingenieros de Instagram son alertados instantáneamente.
Instagram usó Sentry, una aplicación de Django de código abierto, para monitorear errores de Python en tiempo real.
Munin se utilizó para graficar métricas a nivel del sistema y alertar anomalías. Instagram tenía un montón de plugins para monitorear métricas a nivel de aplicación (como fotos publicadas por segundo).
Pingdom se utilizó para la monitorización de servicios externos, y PagerDuty se utilizó para manejar incidentes y notificaciones.
Aprende viendo
Si la arquitectura inicial de Instagram te parece interesante:
Te invito a ver el siguiente video en YouTube, donde se explica paso a paso las tecnologías usadas, y se cuenta un poco más sobre la historia de Instagram.