Para facilitar la comprensión de qué trata este artículo, empecemos con el cambio en la estadística media diaria de Stack Overflow. Las siguientes cifras proceden de las estadísticas a fecha de 12 de noviembre de 2013:
- El balanceador de carga aceptó 148.084.833 solicitudes HTTP
- De estos, 36.095.312 eran cargas de páginas
- Se utilizan 833.992.982.627 bytes (776 GB) de tráfico HTTP para enviar
- Se recibieron un total de 286.574.644.032 bytes (267 GB) de datos
- Se enviaron un total de 1.125.992.557.312 bytes (1.048 GB) de datos
- 334.572.103 consultas SQL (incluyendo solo de solicitudes HTTP)
- 412.865.051 solicitudes Redis
- 3.603.418 solicitudes de Motor de Etiquetas
- Tardó 558.224.585 ms (155 horas) en consultas SQL
- Tardó 99.346.916 ms (27 horas) en solicitudes de Redis
- Tardé 132.384.059 ms (36 horas) en la solicitud del motor de etiquetas
- Tardó 2.728.177.045 ms (757 horas) en ASP.Net procesamiento de proceso
Los siguientes datos muestran los cambios en las estadísticas a fecha del 9 de febrero de 2016, para que puedas comparar:
- Solicitudes HTTP recibidas por balanceador de carga: 209.420.973 (+61.336.090)
- 66.294.789 (+30.199.477) de los cuales se cargan la página
- Datos HTTP enviados: 1.240.266.346.053 (+406.273.363.426) bytes (1,24 TB)
- Cantidad total de datos recibidos: 569.449.470.023 (+282.874.825.991) bytes (569 GB)
- Cantidad total de datos enviados: 3.084.303.599.266 (+1.958.311.041.954) bytes (3,08 TB)
- Consultas SQL (solo desde peticiones HTTP): 504.816.843 (+170.244.740)
- Visitas a la caché de Redis: 5.831.683.114 (+5.418.818.063)
- Búsquedas elásticas: 17.158.874 (no registradas en 2013)
- Solicitudes de motores de etiquetas: 3.661.134 (+57.716)
- Tiempo acumulado consumido para ejecutar consultas SQL: 607.073.066 (+48.848.481) ms (168 horas)
- Tiempo consumido de la caché de Rederis: 10.396.073 (-88.950.843) ms (2,8 horas)
- Tiempo consumido por las solicitudes del motor de etiquetas: 147.018.571 (+14.634.512) ms (40,8 horas)
- Tiempo consumido en ASP.Net procesamiento: 1.609.944.301 (-1.118.232.744) ms (447 horas)
- 22,71 (-5,29) ms 49.180.275 páginas de edición Tiempo medio de renderizado (de las cuales 19,12 ms se consumen en ASP.Net)
- 11,80 (-53,2) ms 6.370.076 primeras páginas Tiempo medio de renderizado (de los cuales 8,81 ms se consumen en ASP.Net)
Quizá te preguntes por qué ASP.Net procesa 61 millones de solicitudes más al día pero reduce el tiempo de procesamiento en 757 horas (comparado con 2013). Esto se debe principalmente a las actualizaciones que hicimos en nuestros servidores a principios de 2015, así como a mucho trabajo de optimización del rendimiento dentro de la app. No olvides: el rendimiento sigue siendo un punto a favor. Si tienes más curiosidad por los detalles específicos del hardware, no te preocupes, te daré los detalles específicos del hardware de los servidores usados para gestionar estos sitios en forma de apéndice en el próximo artículo (actualizaré este enlace cuando llegue el momento).
¿Qué ha cambiado en los últimos dos años? No mucho, solo estoy reemplazando algunos servidores y equipos de red. Aquí tienes un resumen de los servidores que se usan para gestionar tu sitio web hoy en día (fíjate en cómo han cambiado desde 2013)
- 4 servidores Microsoft SQL Server (2 de los cuales utilizan hardware nuevo)
- 11 Servidores Web IIS (Nuevo Hardware)
- 2 servidores Redis (nuevo hardware)
- 3 servidores del Motor de Etiquetas (2 de los cuales utilizan hardware nuevo)
- 3 Servidores Elasticsearch (igual que antes)
- 4 servidores de balanceo de carga HAProxy (2 añadidos para soportar CloudFlare)
- 2 dispositivos de red (Nexus 5596 core + 2232TM Fabric Extender, todos los dispositivos actualizados a 10Gbps de ancho de banda)
- 2 x Fortinet 800C Firewall (sustituye a los Cisco 5525-X ASA)
- 2 routers Cisco ASR-1001 (sustituyen a los routers Cisco 3945)
- 2 routers Cisco ASR-1001-x (¡NUEVOS!) )
¿Qué necesitamos para que Stack Overflow funcione? No ha cambiado mucho desde 2013, pero gracias a las optimizaciones y al nuevo hardware mencionado anteriormente, ahora solo necesitamos un servidor web. Ya hemos probado esta situación sin querer, con éxito varias veces. Por favor, ten en cuenta: acabo de decir que funciona, no que sea buena idea. Pero cada vez que esto ocurre, es bastante interesante.
Ahora que tenemos algunas cifras básicas sobre ideas de escalado de servidores, veamos cómo hemos creado estas páginas web tan chulas. Pocos sistemas existen completamente de forma independiente (y los nuestros no son una excepción), y sin una visión holística que integre estas partes, el significado de la planificación arquitectónica se reduce considerablemente. Nuestro objetivo es comprender la situación general. Habrá muchos artículos que profundizarán en cada campo específico en el futuro. Este artículo es simplemente un resumen de la estructura lógica del hardware clave, y el siguiente artículo contendrá detalles específicos sobre este hardware.
Si quieres ver cómo es este hardware hoy en día, aquí tienes algunas fotos que tomé del Armario A (el Gabinete B es exactamente igual) cuando actualicé el servidor en febrero de 2015:
Ahora, vamos a profundizar en la disposición arquitectónica. A continuación se presenta un resumen de la arquitectura lógica de los principales sistemas existentes:
Principios básicos
Aquí tienes algunos principios comunes que no necesitan ser introducidos a su vez:
- Todo tiene copias de seguridad redundantes.
- Todos los servidores y dispositivos de red tienen al menos dos conexiones de ancho de banda de 10Gbps.
- Todos los servidores tienen dos fuentes de energía que suministran energía a través de dos unidades de UPS, dos generadores detrás y dos feedforwards de tensión de red.
- Todos los servidores tienen una copia de seguridad redundante ubicada en el Rack A y el Rack B.
- Todos los servidores y servicios tienen copias de seguridad redundantes dobles en un centro de datos separado (en Colorado), aunque principalmente cubro Nueva York.
- Todo tiene copias de seguridad redundantes.
Internet
Primero tienes que encontrar nuestra web, que es algo de DNS. Encontrar sitios web es rápido, así que ahora se lo damos a CloudFlare porque tienen servidores DNS en todos los rincones del mundo. Actualizamos los registros DNS a través de APIs, y ellos son responsables de "gestionar" el DNS. Sin embargo, en nuestra mente villana, seguimos teniendo nuestros propios servidores DNS debido a los profundos problemas de confianza. Cuando el apocalipsis es apocalíptico —quizá por la GPL, Punyon o problemas de caché— y la gente sigue queriendo programar para desviar su atención, cambiamos a nuestros propios servidores DNS.
Una vez que tu navegador encuentra nuestro escondite, el tráfico HTTP de nuestros cuatro ISP (Level 3, Zayo, Cogent y Lightower en Nueva York) entra en uno de nuestros cuatro routers avanzados. Utilizamos el Border Gateway Protocol (BGP, un protocolo muy estándar) para hacer peer-to-peer del tráfico de los proveedores de red para controlarlo y proporcionar la forma más eficiente de acceder a nuestros servicios. Los routers ASR-1001 y ASR-1001-X se dividen en dos grupos, cada uno de los cuales debe usar el modo activo/activo para gestionar el tráfico de ambos proveedores de red; aquí hay copias de seguridad redundantes. Aunque todos tienen el mismo ancho de banda físico de 10Gbps, el tráfico externo sigue siendo independiente del tráfico de la VLAN externa y está conectado al balanceo de carga por separado. Después de que el tráfico pase por el router, llegarás al balanceador de carga.
Creo que ya es hora de mencionar que tenemos MPLS con un ancho de banda de 10Gbps entre los dos centros de datos, aunque esto no está realmente directamente relacionado con los servicios web. Utilizamos esta tecnología para realizar la replicación fuera del sitio y la recuperación rápida de datos para hacer frente a ciertas emergencias. "¡Pero Nick, esto no tiene ninguna redundancia!" Bueno, desde un punto de vista técnico tienes razón (en un sentido positivo), en este nivel es realmente un punto único de fallo. ¡Pero espera! A través del proveedor de red, también disponemos de dos rutas adicionales de conmutación por fallo OSPF (MPLS es la primera opción, y estas son la segunda y tercera por razones de coste). Cada uno de los conjuntos de dispositivos mencionados anteriormente se conectará al centro de datos de Colorado para equilibrar la carga del tráfico de red en caso de conmutación por error. Por supuesto, podríamos haber conectado estos dos conjuntos de dispositivos entre sí, de modo que hubiera cuatro conjuntos de caminos, pero olvídalo, sigamos.
Balanceo de carga (HAProxy)
El balanceo de carga está implementado con HAProxy 1.5.15, que funciona en CentOS 7 (nuestra versión favorita de Linux). Y añadir el protocolo de transmisión segura TLS (SSL) en HAProxy. También estamos pendientes de HAProxy 1.7, que dará soporte para el protocolo HTTP/2 de inmediato.
A diferencia de otros servidores con conexiones de red LACP dobles de 10Gbps, cada balanceador de carga tiene dos conexiones de 10Gbps: una para la red externa y otra para la DMZ. Estos servidores disponen de 64GB o más de memoria para manejar la capa de protocolo SSL de forma más eficiente. Cuando podemos almacenar en caché y reutilizar más sesiones TLS en memoria, consumimos menos recursos de cómputo al conectarnos al mismo cliente. Esto significa que podemos restaurar las sesiones de forma más rápida y económica. La memoria es tan barata que es una elección fácil.
El balanceo de carga en sí es fácil de configurar. Escuchamos diferentes sitios web en múltiples IPs distintas (principalmente por motivos de gestión de certificados y DNS) y luego encaminamos el tráfico a diferentes backends (principalmente basados en cabeceras de host). Lo único que hacemos aquí es limitar la tasa y extraer información de cabecera (de la capa web) para acceder a los mensajes de registro del sistema de HAProxy; de esta manera podemos registrar métricas de rendimiento para cada solicitud. Lo mencionaremos en detalle más adelante.
Capa web (IIS 8.5, ASP.Net MVC 5.2.3 y .Net 4.6.1)
El balanceo de carga distribuye el tráfico entre 9 de lo que llamamos el servidor web principal (01-09) y 2 servidores web de desarrollo (10-11, nuestro entorno de prueba). El servidor principal ejecuta Stack Overflow, Careers y todos los sitios de Stack Exchange, mientras que meta.stackoverflow.com y meta.stackexchange.com se ejecutan en otros dos servidores. La app principal de preguntas y respuestas es multiinquilino, lo que significa que una sola aplicación gestiona todas las solicitudes del sitio de preguntas y respuestas. En otras palabras, podemos ejecutar toda la aplicación de preguntas y respuestas en un solo grupo de aplicaciones en un solo servidor. Otras aplicaciones como Careers, API v2, Mobile API, etc., son independientes. Esto es lo que ves en IIS para los servidores master y dev:
Aquí está la distribución de la capa web de Stack Overflow tal como se ve en Opserver (nuestro panel interno de monitorización):
Y aquí está el consumo de recursos de estos servidores web:
Entraré en más detalle en un artículo posterior sobre por qué estamos proporcionando tantos recursos en exceso, centrándonos en la construcción progresiva, la flexibilidad y la redundancia.
Capa de servicio (IIS, ASP.Net MVC 5.2.3, . NET 4.6.1 y HTTP. SYS)
Junto a la capa web está la capa de servicio. También funcionan sobre IIS 2012 en Windows 8.5R2. Esta capa ejecuta algunos servicios internos que soportan la capa web y otros sistemas internos del entorno de producción. Los dos servicios principales son: "Stack Server", que ejecuta un motor de etiquetas y se basa en http.sys (no en IIS); API de Providence (basada en IIS). Un dato interesante: tuve que correlacionar los dos procesos para conectarse a sockets diferentes, porque el Stack Server accedía a las cachés L2 y L3 con mucha frecuencia al actualizar la lista de problemas cada dos minutos.
Las máquinas que ejecutan estos servicios son críticas para el motor de etiquetas y las APIs de backend, por lo que deben ser redundantes, pero no 9 veces redundantes. Por ejemplo, cargamos todos los artículos y sus etiquetas de la base de datos cada n minutos (actualmente 2 minutos), lo cual no es poco. No queremos repetir esta operación de carga 9 veces en la capa web, 3 veces es suficiente para nosotros. También utilizamos diferentes configuraciones de hardware para estos servidores para optimizar mejor las características computacionales y de carga de datos del motor de etiquetas y los trabajos de índice elástico (que también se ejecutan en esta capa). El propio "motor de etiquetas" es un tema relativamente complejo que se tratará en un artículo dedicado. El principio básico es que cuando accedes a la dirección /questions/tagged/java, visitas el motor de etiquetado para obtener las preguntas que coinciden con él. El motor gestiona toda la coincidencia de etiquetas excepto /search, así que en todas partes, incluida la nueva navegación, se obtiene datos a través de este servicio.
Caché y Publicación/Suscripción (Redis)
Usamos Redis en algunos sitios y tiene una estabilidad muy sólida. Aunque hay hasta 160.000 millones de operaciones al mes, la CPU por instancia no supera el 2%, que suele ser menor:
Usamos Redis para sistemas de caché a nivel L1/L2. El nivel "L1" es la caché HTTP que funciona en un servidor web o en cualquier aplicación similar. El nivel "L2" es para obtener datos a través de Redis después de que falle la caché del nivel anterior. Nuestros datos se almacenan en formato Protobuf, implementado mediante protobuf-dot-net escrito por Marc Gravel. Para el cliente Redis, usamos la biblioteca StackExchange.Redis, que es una biblioteca de código abierto desarrollada internamente. Si un servidor web no accede tanto a la caché L1 como a L2, obtiene datos de sus fuentes (consultas a bases de datos, llamadas a API, etc.) y guarda los resultados en la caché local y en Redis. El siguiente servidor puede estar ausente de la caché L1 al recuperar los mismos datos, pero recuperará los datos en L2/Redis, eliminando la necesidad de consultas a la base de datos o llamadas a la API.
También ejecutamos muchos sitios de preguntas y respuestas, cada uno con su propia caché L1/L2: clave como prefijo en la caché L1 e ID de base de datos en la caché L2/Redis. Profundizaremos en este tema en próximos artículos.
Además de los dos servidores principales de Redis (uno maestro y un esclavo) que ejecutan todas las instancias del sitio, también configuramos una instancia para aprendizaje automático (principalmente por razones de memoria) usando otros dos servidores esclavos dedicados. Este grupo de servidores se utiliza para ofrecer servicios como recomendar preguntas en la página principal y mejorar la coincidencia de trabajos. Esta plataforma se llama Providence, y Kevin Montrose escribió sobre ella.
El servidor principal de Redis tiene 256GB de RAM (unos 90GB usados), y el servidor de Providence tiene 384GB de memoria (unos 125GB de uso).
Redis no es solo para caché, también tiene un mecanismo de publicación y suscripción donde un servidor puede publicar un mensaje y otros suscriptores pueden recibirlo (incluyendo Redis de clientes posteriores en el servidor). Utilizamos este mecanismo para limpiar la caché L1 en otros servicios y así mantener la consistencia de la caché en el servidor web. Pero tiene otro uso importante: los websockets.
Websockets (NetGain)
Utilizamos websockets para enviar actualizaciones en tiempo real a los usuarios, como notificaciones en la barra superior, votos, nueva navegación, nuevas respuestas, comentarios y más.
El servidor socket en sí funciona en la capa web, usando sockets nativos. Esta es una aplicación muy pequeña basada en nuestra implementación de biblioteca de código abierto: StackExchange.NetGain. En los momentos de mayor actividad, teníamos unas 500.000 conexiones websocket simultáneas, que son muchos navegadores. Dato curioso: algunos de estos navegadores llevan abiertos más de 18 meses, y tendrás que encontrar a alguien para ver si esos desarrolladores siguen vivos. El siguiente gráfico muestra el patrón de concurrencia de websockets esta semana:
¿Por qué usar websockets? A nuestra escala, es mucho más eficiente que las encuestas. De este modo, podemos simplemente enviar más datos con menos recursos y ser más en tiempo real para los usuarios. Sin embargo, este enfoque no está exento de problemas: puertos temporales, manos de archivo agotados en balanceadores de carga, son problemas muy interesantes, y hablaremos de ellos más adelante.
Búsqueda (Elasticsearch)
Spoiler: No hay mucho de lo que emocionarse aquí. La capa web utiliza Elasticsearch 1.4 e implementa un cliente ultraligero y de alto rendimiento llamado StackExchange.Elastic. A diferencia de la mayoría de las cosas, no planeamos abrir esta parte como código abierto, simplemente porque expone un subconjunto muy pequeño de las APIs que necesitamos usar. Estoy seguro de que hacerlo público compensa la pérdida y solo confundirá a los desarrolladores. En estos sitios usamos elastic:/search para calcular preguntas relacionadas y dar sugerencias al hacer preguntas.
Cada clúster Elastic (uno para cada centro de datos) contiene 3 nodos, cada uno con su propio índice. El sitio de Carreras también tiene algunos índices adicionales. Una parte un poco menos estándar de nuestra configuración en círculos elásticos es que nuestro clúster de 3 servidores es algo más potente que la configuración habitual: cada servidor usa almacenamiento SSD, 192GB de memoria y red dual de 10Gbps de ancho de banda.
El mismo dominio de aplicación de Stack Server (sí, nos ha pasado por .Net Core en este lugar) también aloja un motor de etiquetas, que también usa Elasticsearch para indexación continua. Aquí usamos un pequeño truco, como usar ROWVERSION en SQL Server (la fuente de datos) para comparar con el documento de "último lugar" en Elastic. Como aparentemente es secuencial, nos resulta fácil rastrear e indexar el contenido si se modifica tras la última visita.
La principal razón por la que usamos Elasticsearch en lugar de tecnologías como la búsqueda de texto completo en SQL es su escalabilidad y rentabilidad. SQL es relativamente caro en CPUs, mientras que Elastic es mucho más barato y últimamente tiene muchas funciones nuevas. ¿Por qué no usar Solr? Necesitamos buscar a lo largo de la red (con múltiples índices al mismo tiempo), y Solr no soporta este escenario en el momento de nuestras decisiones. La razón por la que aún no hemos usado la 2.x es porque los tipos han cambiado mucho en la 2.x, lo que significa que tenemos que reindexar todo si queremos actualizar. Simplemente no tengo tiempo suficiente para planificar cambios de requisitos y migraciones.
Base de datos (SQL Server)
Usamos SQL Server como una única fuente de verdad. Todos los datos en Elastic y Redis provienen de SQL Server. Tenemos dos clústeres SQL Server y estamos configurados con grupos de disponibilidad AlwaysOn. Cada clúster tiene un servidor principal en Nueva York (que asume casi toda la carga) y un servidor réplica, además de un servidor réplica en Colorado (nuestro centro de datos de recuperación ante desastres). Todas las operaciones de copia son asíncronas.
El primer clúster es un conjunto de servidores Dell R720xd, cada uno con 384GB de memoria, un SSD PCIe con 4TB de espacio y dos CPUs de 12 núcleos. Incluye Stack Overflow, Sites (es un mal nombre, lo explicaré más adelante), PRIZM y la base de datos de Mobile.
El segundo clúster es un conjunto de servidores Dell R730xd, cada uno con 768GB de memoria, un SSD PCIe con 6TB de espacio y dos CPUs de 8 núcleos. Este clúster contiene todas las demás bases de datos, incluyendo Careers, Open ID, Chat, registros de excepciones y otros sitios de preguntas y respuestas (por ejemplo, Super User, Server Fault, etc.).
A nivel de base de datos, queremos mantener la utilización de la CPU a un nivel muy bajo, aunque en la práctica el uso de la CPU será ligeramente mayor cuando ocurran algunos problemas de caché planificados (que estamos solucionando). Actualmente, NY-SQL02 y 04 son los servidores principales y 01 y 03 son los servidores réplica, y hoy los hemos reiniciado por la actualización del SSD. Así es como se comportaron en las últimas 24 horas:
Nuestro uso de SQL es muy sencillo. Simple significa rápido. Aunque algunas sentencias de consulta pueden ser pervertidas, nuestra interacción con SQL se realiza de forma bastante nativa. Tenemos algo de Linq2SQL heredado, pero todos nuestros nuevos desarrollos usan Dapper, nuestro framework micro-ORM de código abierto que utiliza POCO. Déjame explicarlo de otra manera: Stack Overflow solo tiene un procedimiento almacenado en su base de datos, y voy a eliminar este último procedimiento almacenado y reemplazarlo por código.
Biblioteca
Bueno, cambiemos de opinión, aquí tienes cosas que pueden ayudarte de forma más directa. Ya he mencionado algunas antes, pero te daré una lista de las muchas librerías .Net de código abierto que mantenemos y que todo el mundo utiliza. Los abrimos porque no tienen valor empresarial central, pero pueden ayudar a desarrolladores de todo el mundo. Espero que puedas usarlos ahora:
- Dapper (.Net Core) – Un marco micro-ORM de alto rendimiento para ADO.Net
- StackExchange.Redis – Un cliente Redis de alto rendimiento
- MiniProfiler – un perfilador ligero que usamos en todas las páginas (también soporta Ruby, Go y Node)
- Excepcional – Para registro de errores en SQL, JSON, MySQL, etc
- Jil – Serialización y deserializador JSON de alto rendimiento
- Sigil – .Net CIL Generation Helper (usado cuando C# no es lo suficientemente rápido)
- NetGain – Servidor websocket de alto rendimiento
- Opserver – Panel de control de monitorización que consulta directamente la mayoría de los sistemas y puede obtener información de Orion, Bosun o WMI
- Bosun – Sistema de monitorización en segundo plano, escrito en Go
|