Todas las buenas nuevas empresas mueren rápidamente o crecen a escala. Modelaremos una startup de este tipo, que primero trata sobre características y luego sobre rendimiento. Mejoraremos el rendimiento con MongoDB, una popular solución de almacenamiento de datos NoSQL. MongoDB es fácil de comenzar y muchos problemas tienen soluciones listas para usar. Sin embargo, cuando aumenta la carga, sale un rastrillo que nadie le advirtió antes ... ¡hasta hoy!
Sergey Zagursky , responsable de la infraestructura de back-end en general, y MongoDB en particular en
Joom, realizan el modelado. También se vio en el lado del servidor del desarrollo de MMORPG Skyforge. Como Sergei se describe a sí mismo, él es "un tomador de cono profesional con su propia frente y rastrillo". Bajo un microscopio, un proyecto que utiliza una estrategia de acumulación para gestionar la deuda técnica. En esta versión de texto del
informe en HighLoad ++, nos moveremos en orden cronológico desde la aparición del problema hasta la solución usando MongoDB.
Primeras dificultades
Estamos modelando una startup que llena los baches. La primera etapa de la vida: las características se lanzan en nuestro inicio y, inesperadamente, llegan los usuarios. Nuestro pequeño y pequeño servidor MongoDB tiene una carga que nunca soñamos. Pero estamos en la nube, ¡somos una startup! Hacemos las cosas más simples posibles: mira las solicitudes, oh, y aquí tenemos la corrección completa restada para cada usuario, aquí construiremos los índices, agregaremos el hardware allí, y aquí almacenaremos en caché.
¡Todo, vivimos!
Si los problemas pueden resolverse por medios tan simples, deben resolverse de esta manera.
Pero el camino futuro de un inicio exitoso es un retraso lento y doloroso del momento de escala horizontal. Intentaré dar consejos sobre cómo sobrevivir a este período, escalar y no pisar el rastrillo.
Grabación lenta
Este es uno de los problemas que puede encontrar. ¿Qué hacer si la conoces y los métodos anteriores no te ayudan? Respuesta:
modo de garantía de durabilidad en MongoDB por defecto . En tres palabras, funciona así:
- Llegamos a la línea primaria y dijimos: "¡Escribe!".
- Réplica primaria registrada.
- Después de eso, se leyeron réplicas secundarias de ella y dijeron primaria: "¡Grabamos!"
En el momento en que la mayoría de las réplicas secundarias hicieron esto, la solicitud se considera completa y el control vuelve al controlador en la aplicación. Dichas garantías nos permiten estar seguros de que cuando el control haya regresado a la aplicación, la durabilidad no irá a ningún lado, incluso si MongoDB se acuesta, excepto por desastres absolutamente terribles.
Afortunadamente, MongoDB es una base de datos que le permite reducir las garantías de durabilidad para cada solicitud individual.
Para solicitudes importantes, podemos dejar las garantías de máxima durabilidad por defecto, y para algunas solicitudes podemos reducirlas.
Solicitar clases
La primera capa de garantías que podemos eliminar es
no esperar la confirmación del registro por la mayoría de las réplicas . Esto ahorra latencia, pero no agrega ancho de banda. Pero a veces la latencia es lo que necesita, especialmente si el clúster está un poco sobrecargado y las réplicas secundarias no funcionan tan rápido como nos gustaría.
{w:1, j:true}
Si escribimos registros con tales garantías, en el momento en que tengamos el control en la aplicación, ya no sabemos si el registro estará vivo después de algún tipo de accidente. Pero por lo general, ella todavía está viva.
La siguiente garantía, que también afecta el ancho de banda y la latencia, es
deshabilitar la confirmación de registro . Una entrada de diario se escribe de todos modos. La revista es uno de los mecanismos fundamentales. Si desactivamos la confirmación de escritura, no hacemos dos cosas:
fsync en el registro y
no esperamos a que termine . Esto puede
ahorrar muchos recursos de disco y obtener un
aumento múltiple en el rendimiento simplemente cambiando la durabilidad de la garantía.
{w:1, j:false}
Las garantías de durabilidad más estrictas están
deshabilitando cualquier reconocimiento . Solo recibiremos confirmación de que la solicitud ha llegado a la réplica principal. Esto ahorrará latencia y no aumentará el rendimiento de ninguna manera.
{w:0, j:false} — .
También recibiremos varias otras cosas, por ejemplo, la grabación falló debido a un conflicto con una clave única.
¿A qué operaciones se aplica esto?
Te contaré sobre la aplicación para la configuración en Joom. Además de la carga de los usuarios, en la que no hay concesiones de durabilidad, hay una carga que puede describirse como una carga por lotes en segundo plano: actualización, recuento de clasificaciones, recopilación de datos analíticos.
Estas operaciones en segundo plano pueden llevar horas, pero están diseñadas de modo que si se rompe un corte, por ejemplo, un backend, no perderán el resultado de todo su trabajo, sino que se reanudarán desde el punto en el pasado reciente. Reducir la garantía de durabilidad es útil para tales tareas, especialmente porque fsync en el registro, como cualquier otra operación, aumentará la latencia también para la lectura.
Leer escalado
El siguiente problema es el
ancho de banda de lectura insuficiente . Recuerde que en nuestro clúster no solo hay réplicas primarias, sino también secundarias de las
que puede leer . Hagámoslo
Puedes leer, pero hay matices. Los datos ligeramente desactualizados provendrán de réplicas secundarias, por 0.5–1 segundos. En la mayoría de los casos, esto es normal, pero el comportamiento de la réplica secundaria es diferente del comportamiento de las réplicas primarias.
En secundaria, está el proceso de usar oplog, que no está en la réplica primaria. Este proceso no está diseñado para baja latencia, solo los desarrolladores de MongoDB no se molestaron con esto. Bajo ciertas condiciones, el proceso de usar oplog de primario a secundario puede causar demoras de hasta 10 s.
Las réplicas secundarias no son adecuadas para las consultas de los usuarios: las experiencias de los usuarios dan un paso rápido hacia el contenedor.
En grupos sin sombra, estos picos son menos notables, pero aún están allí. Los grupos de fragmentos sufren porque el oplog se ve particularmente afectado por la eliminación, y la
eliminación es parte del trabajo del equilibrador . El equilibrador elimina de manera confiable y de buen gusto documentos por decenas de miles en un corto período de tiempo.
Numero de conexiones
El siguiente factor a considerar es el
límite en el número de conexiones en instancias de MongoDB . De manera predeterminada, no hay restricciones,
excepto los recursos del sistema operativo: puede conectarse mientras lo permita.
Sin embargo, cuanto más concurrentes solicitudes concurrentes, más lento se ejecutan.
El rendimiento se degrada de forma no lineal . Por lo tanto, si nos llega un aumento de solicitudes, es mejor atender el 80% que no atender el 100%. El número de conexiones debe limitarse directamente a MongoDB.
Pero hay errores que pueden causar problemas debido a esto. En particular, el
grupo de conexiones en el lado MongoDB es común tanto para las conexiones intracluster de usuarios como de servicios . Si la aplicación "comió" todas las conexiones de este grupo, se podría violar la integridad en el clúster.
Aprendimos sobre esto cuando íbamos a reconstruir el índice, y dado que necesitábamos eliminar la unicidad del índice, el procedimiento pasó por varias etapas. En MongoDB, no puede construir al lado del índice de la misma manera, pero sin unicidad. Por lo tanto, queríamos:
- Construye un índice similar sin unicidad
- eliminar el índice con unicidad;
- Cree un índice sin unicidad en lugar de remoto;
- Eliminar temporal.
Cuando el índice temporal todavía se estaba completando en la secundaria, comenzamos a eliminar el índice único. En este punto, MongoDB secundario anunció su bloqueo. Se bloquearon algunos metadatos, y en la mayoría de los registros se detuvieron: colgaron en el
grupo de conexiones y esperaron a que confirmaran que el registro había pasado. Todas las lecturas en secundaria también se detuvieron porque se capturó el registro global.
El clúster en un estado tan interesante también perdió su conectividad. A veces aparecía y cuando dos comentarios se conectaban entre sí, intentaban tomar una decisión en su estado que no podían hacer, porque tenían un bloqueo global.
Moraleja de la historia: el número de conexiones debe ser monitoreado.
Hay un rastrillo conocido de MongoDB, que todavía es atacado con tanta frecuencia que decidí dar un breve paseo.
No perder documentos
Si envía una solicitud por índice a MongoDB, es posible que la
solicitud no devuelva todos los documentos que satisfacen la condición, y en casos completamente inesperados. Esto se debe al hecho de que cuando vamos al principio del índice, el documento, que al final, se mueve al principio para aquellos documentos que pasamos. Esto se debe únicamente
a la mutabilidad del índice . Para una iteración confiable, use
índices en campos no estables y no habrá dificultades.
MongoDB tiene sus propias vistas sobre qué índices usar. La solución es simple:
con la ayuda de $ hint, forzamos a MongoDB a usar el índice que especificamos .
Tamaños de colección
Nuestra startup se está desarrollando, hay muchos datos, pero no quiero agregar discos, ya hemos agregado tres veces en el último mes. Veamos qué se almacena en nuestros datos, observe el tamaño de los documentos. ¿Cómo entender en qué parte de la colección puedes reducir el tamaño? Según dos parámetros.
- El tamaño de documentos específicos para jugar con su longitud:
Object.bsonsize()
;
- Según el tamaño promedio del documento en la colección :
db.c.stats().avgObjectSize
.
¿Cómo afectar el tamaño del documento?
Tengo respuestas no específicas a esta pregunta. Primero, un
nombre de campo largo aumenta el tamaño del documento. En cada documento, se copian todos los nombres de campo, por lo que si el documento tiene un nombre de campo largo, entonces el tamaño del nombre debe agregarse al tamaño de cada documento. Si tiene una colección con una gran cantidad de documentos pequeños en varios campos, nombre los campos con nombres cortos: "A", "B", "CD": un máximo de dos letras.
En el disco, esto se compensa con la compresión , pero todo se almacena en la memoria caché tal como está.
El segundo consejo es que a veces
algunos campos con baja cardinalidad se pueden colocar en el nombre de la colección . Por ejemplo, dicho campo puede ser un idioma. Si tenemos una colección con traducciones al ruso, inglés, francés y un campo con información sobre el idioma almacenado, el valor de este campo se puede poner en el nombre de la colección. Por lo tanto,
reduciremos el tamaño de los documentos y podemos
reducir el número y el tamaño de los índices : ¡un
gran ahorro! Esto no siempre se puede hacer, porque a veces hay índices dentro del documento que no funcionarán si la colección se divide en diferentes colecciones.
Último consejo sobre el tamaño del documento:
use el campo _id . Si sus datos tienen una clave única natural, colóquela directamente en id_field. Incluso si la clave es compuesta, use una identificación compuesta. Está perfectamente indexado. Solo hay un pequeño rastrillo: si su jefe de clasificación a veces cambia el orden de los campos, la identificación con los mismos valores de campo, pero con un orden diferente se considerará una identificación diferente en términos de un índice único en MongoDB. En algunos casos, esto puede suceder en Go.
Tamaños de índice
El índice almacena una copia de los campos que están incluidos en él . El tamaño del índice consta de los datos que se indexan. Si estamos tratando de indexar campos grandes, prepárese para que el tamaño del índice sea grande.
El segundo momento infla fuertemente los índices: los
campos de matriz en el índice multiplican otros campos del documento en este índice . Tenga cuidado con las matrices grandes en los documentos: o no indexe otra cosa a la matriz, o juegue con el orden en que se enumeran los campos en el índice.
El orden de los campos es importante ,
especialmente si uno de los campos de índice es una matriz . Si los campos difieren en cardinalidad, y en un campo el número de valores posibles es muy diferente del número de valores posibles en otro, entonces tiene sentido construirlos aumentando la cardinalidad.
Puede guardar fácilmente el 50% del tamaño del índice si intercambia campos con diferente cardinalidad. La permutación de los campos puede dar una reducción más significativa en el tamaño.
A veces, cuando el campo contiene un valor grande, no necesitamos comparar este valor más o menos, sino una comparación clara de igualdad. Luego, el
índice en el campo con contenido pesado puede ser
reemplazado por el índice en hash de este campo . Se almacenarán copias del hash en el índice, no copias de estos campos.
Eliminar documentos
Ya mencioné que eliminar documentos es una operación desagradable y
es mejor no eliminarlos si es posible. Al diseñar un esquema de datos, intente considerar minimizar la eliminación de datos individuales o eliminar colecciones enteras. podrían eliminarse con colecciones completas. Eliminar colecciones es una operación barata, y eliminar miles de documentos individuales es una operación difícil.
Si aún necesita eliminar muchos documentos, asegúrese de
acelerar , de lo contrario, la eliminación masiva de documentos afectará la latencia de la lectura y será desagradable. Esto es especialmente malo para la latencia en secundaria.
Vale la pena hacer algún tipo de "bolígrafo" para acelerar el acelerador: es muy difícil aumentar el nivel la primera vez. Lo pasamos tantas veces que se acelera la tercera, cuarta vez. Inicialmente, considere la posibilidad de apretarlo.
Si elimina más del 30% de una colección grande, transfiera documentos en vivo a la colección vecina y elimine la colección anterior en su conjunto. Está claro que hay matices, porque la carga cambia de la colección antigua a la nueva, pero cambia si es posible.
Otra forma de eliminar documentos es el índice
TTL , que es un índice que indexa el campo que contiene la marca de tiempo Mongo, que contiene la fecha en que murió el documento. Cuando llegue este momento, MongoDB eliminará este documento automáticamente.
El índice TTL es conveniente, pero
no hay limitación en la implementación. MongoDB no se preocupa por cómo eliminar estas eliminaciones. Si intenta eliminar un millón de documentos al mismo tiempo, durante unos minutos tendrá un clúster inoperable que solo se ocupa de la eliminación y nada más. Para evitar que esto suceda, agregue algo de
aleatoriedad ,
difunda el TTL tanto como lo permita su lógica comercial y los efectos especiales sobre la latencia. Es imperativo difuminar TTL si tiene razones lógicas comerciales naturales que concentran la eliminación en un momento dado.
Sharding
Intentamos posponer este momento, pero ha llegado, todavía tenemos que escalar horizontalmente. Para MongoDB, esto es fragmentación.
Si duda de que necesita fragmentación, entonces no lo necesita.
Sharding complica la vida de un desarrollador y devops en una variedad de formas. En una empresa, lo llamamos impuesto de fragmentación. Cuando fragmentamos una colección, el
rendimiento específico de la colección disminuye : MongoDB requiere un índice separado para la división, y se deben pasar parámetros adicionales a la solicitud para que pueda ejecutarse de manera más eficiente.
Algunas cosas de fragmentación simplemente no funcionan bien. Por ejemplo, es una mala idea usar consultas con
skip
, especialmente si tiene muchos documentos. Usted da el comando: "Saltar 100,000 documentos".
MongoDB piensa de esta manera: “Primero, segundo, tercero ... cien milésimos, vamos más allá. Y se lo devolveremos al usuario ".
En una colección no compartida, MongoDB realizará una operación en algún lugar dentro de sí mismo. En forma de fragmento, ella realmente lee y envía todos los 100,000 documentos a un proxy de fragmentación en
mongos , que de alguna manera filtrará y descartará los primeros 100,000. Una característica desagradable a tener en cuenta.
El código ciertamente se volverá más complicado con el fragmentación: tendrá que arrastrar la clave de fragmentación a muchos lugares. Esto no siempre es conveniente, y no siempre es posible. Algunas consultas irán ya sea de difusión o multidifusión, lo que tampoco agrega escalabilidad. Llegue a la elección de una clave mediante la cual el fragmentación será más preciso.
En colecciones de fragmentos, la operación de count
rompe . Ella comienza a devolver un número más que en la realidad: puede mentir 2 veces. La razón radica en el proceso de equilibrio, cuando los documentos se vierten de un fragmento a otro. Cuando los documentos se vierten en el fragmento vecino, pero aún no se han eliminado en el original, el
count
todos modos. Los desarrolladores de MongoDB no llaman a esto un error, es una característica. No sé si lo arreglarán o no.
Un clúster aleatorio es mucho más difícil de administrar . Devops dejará de saludarte, porque el proceso de eliminar una copia de seguridad se vuelve radicalmente más complicado. Al fragmentar, la necesidad de automatización de infraestructura parpadea como una alarma de incendio, algo que podría haber hecho sin antes.
Cómo funciona el sharding en MongoDB
Hay una colección, queremos dispersarla de alguna manera por fragmentos. Para hacer esto,
MongoDB divide la colección en fragmentos utilizando la clave de fragmento, tratando de dividirlos en partes iguales en el espacio de la clave de fragmento. Luego viene el equilibrador, que
distribuye diligentemente
estos trozos de acuerdo con los fragmentos en el grupo . Además, al equilibrador no le importa cuánto pesan estos trozos y cuántos documentos hay en ellos, ya que el equilibrio se realiza pieza por pieza.
Clave de fragmentación
¿Todavía decides qué fragmentar? Bueno, la primera pregunta es cómo elegir una clave de fragmentación. Una buena clave tiene varios parámetros:
alta cardinalidad ,
falta de estabilidad y se
adapta bien en solicitudes frecuentes .
La elección natural de una clave de fragmentación es la clave principal: el campo id. Si el campo id es adecuado para fragmentar, entonces es mejor fragmentar directamente en él. Esta es una excelente opción: tiene una buena cardinalidad, no es estable, pero lo bien que se adapta a las solicitudes frecuentes es la especificidad de su negocio. Construye sobre tu situación.
Daré un ejemplo de una clave de fragmentación fallida. Ya mencioné la colección de traducciones - traducciones. Tiene un campo de idioma que almacena el idioma. Por ejemplo, la colección admite 100 idiomas y compartimos el idioma. Esto es malo: cardinalidad, el número de valores posibles es de solo 100 piezas, lo cual es pequeño. Pero esto no es lo peor, tal vez la cardinalidad sea suficiente para estos fines. Peor aún, tan pronto como barajamos el idioma, inmediatamente descubrimos que tenemos 3 veces más usuarios de habla inglesa que el resto. Tres veces más solicitudes llegan al fragmento desafortunado en el que se encuentra el inglés que a todos los demás combinados.
Por lo tanto, debe tenerse en cuenta que a veces una clave de fragmento tiende naturalmente a una distribución de carga desigual.
Equilibrio
Llegamos a fragmentar cuando la necesidad ha madurado para nosotros: nuestro clúster MongoDB cruje, cruje con sus discos, procesador, con todo lo que podemos. A donde ir En ninguna parte, y heroicamente barajamos los talones de las colecciones. Fragmentamos, lanzamos y de repente descubrimos que el
equilibrio no es gratis .
El equilibrio pasa por varias etapas. El equilibrador elige trozos y fragmentos, de dónde y dónde se transferirá. El trabajo adicional se realiza en dos fases: primero, los
documentos se copian del origen al destino, y luego los documentos que se copiaron
se eliminan .
Nuestro fragmento está sobrecargado, contiene todas las colecciones, pero la primera parte de la operación es fácil para él. Pero el segundo, la extracción, es bastante desagradable, ya que pondrá un fragmento en los omóplatos y ya sufrirá bajo carga.
El problema se agrava por el hecho de que si equilibramos muchos fragmentos, por ejemplo, miles, entonces con la configuración predeterminada, todos estos fragmentos se copian primero, y luego entra un eliminador y comienza a eliminarlos en masa. En este punto, el procedimiento ya no se ve afectado y solo tiene que mirar tristemente lo que está sucediendo.
Por lo tanto, si se acerca a fragmentar un clúster sobrecargado, debe planificar, ya que el
equilibrio lleva tiempo. Es aconsejable aprovechar este tiempo no en horario de máxima audiencia, sino en períodos de baja carga. Balancer: una pieza de repuesto desconectada. Puede acercarse al equilibrio primario en modo manual, apagar el equilibrador en horario de máxima audiencia y encenderlo cuando la carga haya disminuido para permitirse más.
Si las capacidades de la nube aún le permiten escalar verticalmente, es mejor mejorar la fuente de fragmentos de antemano para reducir ligeramente todos estos efectos especiales.
El fragmentación debe estar cuidadosamente preparado.HighLoad ++ Siberia 2019 llegará a Novosibirsk los días 24 y 25 de junio. HighLoad ++ Siberia es una oportunidad para que los desarrolladores de Siberia escuchen informes, hablen sobre temas de alta carga y se sumerjan en el entorno "donde todos tienen lo suyo", sin volar más de tres mil kilómetros a Moscú o San Petersburgo. De las 80 solicitudes, el Comité del Programa aprobó 25, y le informamos sobre todos los demás cambios en el programa, anuncios de informes y otras noticias en nuestra lista de correo. Suscríbase para mantenerse informado.