La primera sintonización de forma proporciona a las organizaciones un medio para reducir los gastos de MongoDB en un 79%

Tl; Dr

Saas se despertó a un auto -escala silencioso de M20 → M60agregando 20 % a su factura de nubes durante la noche. En un frenético sprint de 48 horas: nosotros:

  • Aplanado n + 1 cascadas con $lookup ,
  • Malditos cursores sin problemas con proyección, limit() y ttl,
  • Docios “Jumbo” divididos de 16 MB en manchas de metadatos Lean + Gridfs,
  • reordenó un puñado de índices de sueño

Y observado $ 15 284 → $ 3 210/mes (‑79 %) mientras que la latencia de P95 se derrumbó desde 1.9 s → 140 ms.

Todo en un conjunto de réplica simple.


Paso 1: El día en que la factura se fue supernova

02:17 am – El teléfono de entrada se encendió como una máquina de pinball. Atlas había sometido en silencio a nuestro confiable M20 para un M60 maxed -out. Slack llena de 🟥 Choque de facturas Alertas mientras los gráficos de la fila de Grafana pintaron una película de terror en tiempo real.

“Finanzas dice que la nueva toallita de gasto de nueve meses de pasarela. Necesitamos una solución antes de mantener una posición”.
– COO, 02:38

Medio despierto, el ingeniero abrió el perfilador. Tres culpables saltaron de la pantalla:

  • Consulta cascada – Cada llamada de API de pedido activó una recuperación adicional para sus líneas. 1 000 órdenes? 1 001 Trips redondos.
  • Cursor de manguera de fuego – Se transmitió un punto de finalización de Click -Stream 30 meses de eventos en cada carga de página.
  • Documentos gigantes – Las facturas de 16 MB (completas con PDF) volaron el caché a bits y de regreso.

Atlas trató de ayudar arrojando hardware al fuego, superando de 64 GB de RAM a 320 GB, aumentando IOPS y, por supuesto, aumentando la factura.

Para el desayuno, las reglas de la sala de guerra eran claras: Cortar el 70 % del gasto en 48 horas, cero tiempo de inactividad, sin armas nucleares de esquema. El juego de juego comienza a continuación.


Paso 2: Tres crímenes de forma y cómo arreglarlos

2.1 n + 1 consulta tsunami

Síntoma: Para cada pedido, la API disparó una segunda consulta para sus elementos de pedido. 1 000 pedidos ⇒ 1 001 trips redondos.

// Old (painful)
const orders = await db.orders.find({ userId }).toArray();
for (const o of orders) {
  o.lines = await db.orderLines.find({ orderId: o._id }).toArray();
}

Tarifas ocultas: 1 000 caminatas por índice, 1 000 apretones de manos TLS, 1 000 interruptores de contexto.

Remedio (4 líneas):

// New (single pass)
db.orders.aggregate([
 { $match: { userId } },
 { $lookup: {
   from: 'orderLines',
   localField: '_id',
   foreignField: 'orderId',
   as: 'lines'
 } },
 { $project: { lines: 1, total: 1, ts: 1 } }
]);

Latencia P95: 2 300 ms → 160 ms. Leer Ops: 101 → 1 ( – 99 %).

2.2 Consulta ilimitada Hosto de fuego

Síntoma: Un punto final transmitió 30 meses de historial de clics en un solo cursor.

// Before
const events = db.events.find({ userId }).toArray();

Arreglar: Cape la ventana y el proyecto solo renderizó los campos.

const events = db.events.find(
  {
   userId,
   ts: { $gte: new Date(Date.now() - 30*24*3600*1000) }
  },
  { _id: 0, ts: 1, page: 1, ref: 1 }
).sort({ ts: -1 }).limit(1_000);

Entonces deja que Mongo poda por ti:

// 90‑day TTL
db.events.createIndex({ ts: 1 }, { expireAfterSeconds: 90*24*3600 });

Un cliente de FinTech recortado 72 % fuera de su almacenamiento durante la noche usando nada más que TTL.

2.3 Jumbo Document Money Pit

Cualquier cosa arriba 256 kb ya cepa las líneas de caché; Una colección almacenó facturas de Multi -MB completadas con PDF y 1 historias de 200 años.

Solución: Dividir por patrón de acceso: metadatos en las facturas, manchas de frío en S3/Gridfs.

graph TD
  Invoice[(invoices <2 kB)] -->|ref| Hist[history <1 kB * N]
  Invoice -->|ref| Bin[pdf‑store (S3/GridFS)]

SSD gasta nieve con nieve; La relación de golpe de caché saltó 22 pp


Paso 3: Pecados de cuatro formas escondidos a la vista

La forma no se trata solo del tamaño del documento, así es como las consultas, los índices y los patrones de acceso se entrelazan.

Estos cuatro anti -Paternos acechan en la mayoría de los grupos de producción y drenan silenciosamente el efectivo.

3.1 Clave de índice líder de baja consumo

Síntoma El índice comienza con un campo que tiene <10 % de valores distintos, por ejemplo, {Tipo: 1, TS: -1} . El planificador debe atravesar grandes franjas antes de aplicar la parte selectiva.

Costo Fan -by -stree alto, localidad de caché de mala, busca el disco adicional.

Arreglar Mueva la tecla selectiva (ID de usuario, Orgid, inquilino) primero: {UserId: 1, TS: -1} . Reconstruya en línea, luego suelte el viejo índice.

3.2 ciego $regex Escanear

Síntoma $regex: /foo/i en un campo no indicado Fuerza un escaneo de recolección completo; CPU Spikes, batallas de caché.

Costo de cada patrón coincide con cada documento y decodifica BSON en el camino caliente.

Arreglar preferir patrones anclados ( /^foo/ ) con un índice de soporte, o agregue un campo de babosa de búsqueda ( lower(name) ) e indexe eso en su lugar.

3.3 FUNDONEANDUPDATE como una cola de mensajes

Síntoma Encuesta de trabajadores con findOneAndUpdate({ status: 'new' }, { $set: { status: 'taken' } }).

Costo Locks de nivel de documento Los escritores serializan; El rendimiento se derrumba más allá de unos pocos miles de operaciones.

Arreglar Utilice una cola de propósito (transmisión redis, kafka, sqs) o MongodbLos cambios de cambio nativos para impulsar eventos, manteniendo escritos solo.

3.4 trampa de paginación compensada

Síntoma find().skip(N).limit(20) donde n puede alcanzar compensaciones de seis figuras.

Costo Mongo todavía cuenta y descarta todos los documentos omitidos: tiempo lineal. Los globos de latencia y la facturación cuentan cada lectura.

Arreglar Cambiar cursores de rango Usando el índice de compuesto (ts, _id) :

// page after the last item of previous page
find({ ts: { $lt: lastTs } })
 .sort({ ts: -1, _id: -1 })
 .limit(20);

Domete estos cuatro y reclamarás RAM, baja unidades de lectura y pospondrás el fragmento de fragmentos por cuartos.


Paso 4: Costo de anatomía 101

Métrico Antes Unidad $ Costo Después Δ %
Lectura (3 k/s) 7.8 b 0.09/m $ 702 2.3 b -70
Escribe (150/s) 380 m 0.225/m $ 86 380 m 0
Xfer 1.5 TB 0.25/GB $ 375 300 GB -80
Almacenamiento 2 TB 0.24/GB $ 480 800 GB -60
Total $ 1,643 -66

Paso 5: línea de tiempo de rescate de 48 horas

Hora Acción Herramienta Ganar
0‑2 Habilitar Profiler (Slowms = 50) cáscara de mongo Top 10 operaciones lentas ubicadas
2-6 Reemplazar n + 1 con $ búsqueda VS CODE + PRUEBAS 90 % menos lecturas
6‑10 Agregar proyecciones y límite () Capa API Ram Estable, API 4 × más rápido
10-16 Docios Jumbo Split ETL con guión El conjunto de trabajo se ajusta en RAM
16‑22 Dejar/volver a ordenar índices débiles Brújula Disco encogido, caché golpea ↑
22-30 Crear TTLS / Archivo en línea Atlas ui −60 % de almacenamiento
30‑36 Paneles de grafana de alambre Prometeo Vivir las advertencias tempranas
36‑48 Prueba de carga con k6 K6 + Atlas P95 <150 ms @ 2 × carga

Paso 6: Lista de verificación de autoagio

  • Doc más grande ÷ mediana> 10? → Refactor.
  • ¿Algún cursor> 1 000 documentos? → paginar.
  • Ttl en cada colección de eventos? (S/N)
  • Cardinalidad del índice <10 %? → Soltar o reordenar.
  • Profiler “Slow” OPS> 1 %? → Optimizar o caché.

Pasee esto a su monitor antes de que se despliegue el viernes.


Paso 7: Por qué forma> índices (la mayoría de los días)

Agregar un índice es como comprar una carretilla elevadora más rápida para el almacén: acelera la selección, pero lo hace nada Si los pasillos están abarrotados de cajas de gran tamaño. En términos de MongoDB, la fórmula de costo del planificador es más o menos:

workUnits = ixScans + fetches + sorts + returnedDocs

Recorte de índices todavía y todavía puede dominar cuando los documentos están hinchados, escasamente accessos o mal agrupados.

Una historia de dos consultas

Skinny Doc (2 kb) Jumbo Doc (16 MB)
ixscans 1 000 1 000
recursos 1 000 × 2 kb = 2 mb 1 000 × 16 MB = 16 GB
Tiempo neto 80 ms 48 S + tormentas de desalojo

Mismo índice, el mismo patrón de consulta: la única diferencia es forma.

La regla general

Arregle la forma primero, luego indexe una vez.
– Cada documento reestructurado encoge cada futuro en la línea, la línea de caché y el paquete de replicación.

Tres formas de victorias superan fácilmente a una docena de bordos B adicionales.


Paso 8: Métricas en vivo en las que debe alertar (PROMQL)

# Cache miss ratio (>10 % for 5 m triggers alert)
 (rate(wiredtiger_blockmanager_blocks_read[1m]) /
 (rate(wiredtiger_blockmanager_blocks_read[1m]) +
 rate(wiredtiger_blockmanager_blocks_read_from_cache[1m]))) > 0.10

# Docs scanned vs returned (>100 triggers alert)
 rate(mongodb_ssm_metrics_documents{state="scanned"}[1m]) /
 rate(mongodb_ssm_metrics_documents{state="returned"}[1m]) > 100

Paso 9: Script de migración de deslizamiento delgado

Necesito romper un 1 -TB events ¿Colección en sub -colecciones sin tiempo de inactividad? Use dos escrituras + relleno:

// 1) Forward writes
const cs = db.events.watch([], { fullDocument: 'updateLookup' });
cs.on('change', ev => {
 db[`${ev.fullDocument.type}s`].insertOne(ev.fullDocument);
});

// 2) Backfill history
let lastId = ObjectId("000000000000000000000000");
while (true) {
 const batch = db.events
  .find({ _id: { $gt: lastId } })
  .sort({ _id: 1 })
  .limit(10_000)
  .toArray();
 if (!batch.length) break;
 db[batch[0].type + 's'].insertMany(batch);
 lastId = batch[batch.length - 1]._id;
}

Paso 11: cuando se requiere fragmentos en realidad

El fragmento es un táctica de última variedad, no una cura de primera línea. Fractura los datos, multiplica los modos de falla y complica cada migración. Actualizaciones verticales de escape y optimizaciones basadas en forma primero. Alcanzar una llave de fragmentos solo cuando al menos uno de los umbrales a continuación es sostenido bajo carga real y no se puede resolver más barato.

Techos de capacidad dura

Síntoma Regla general Por qué la división horizontal ayuda
El conjunto de trabajo se encuentra por encima del 80 % de la RAM física durante 24 h+ <60 % es saludable; 60–80 % se puede enmascarar con una caja más grande; > 80 % páginas constantemente La división coloca particiones calientes en nodos separados, restaurando la relación de caché
Rendimiento de escritura primaria> 15 000 OPS/s Después de ajuste del índice Por debajo de 10 000 ops/s a menudo puede sobrevivir por lotes o upserts a granel Aislar los trozos de alta velocidad reduce el retraso del diario y la contención de bloqueos
Necesidades del producto multi -región <70 ms P95 Lectura de lectura Conjuntos de velocidad de la velocidad ~ 80 ms US↔EU Floor Los datos de pasadores de fragmento de zona cerca de los usuarios sin recurrir a los cachés de borde

Se acerca el fragmento de señales suaves

  • Las compilaciones de índice exceden las ventanas de mantenimiento incluso con la indexación en línea.
  • El tiempo de compactación se come en la recuperación de desastres SLA.
  • Un solo inquilino posee> 25 % del volumen del clúster.
  • Profiler muestra> 500 ms de bloqueo de transacciones largas.

Lista de verificación antes de cortar

  • Remodelar documentos: Si el Doc más grande es 20 × la mediana, refactor primero.
  • Habilitar compresión ( ZSTD o rápido ) a menudo compra 30 % de espacio para la cabeza de almacenamiento.
  • Archivar datos de frío a través del archivo en línea o almacenamiento de S3 escalonado.
  • Reescribe los puntos finales más calientes en Go/Rust si JSON PARSING domina la CPU.
  • Correr mongo‑perf; Si la carga de trabajo se ajusta a una sola réplica establecida después de los niños, aborta el plan de fragmentos.

Elegir una llave de fragmentos

  • Usar de alta consumo, aumentando monotónicamente campos ( ObjectId prefijo de marca de tiempo).
  • Evite las teclas de baja entropía (status , country ) Ese embudo escribe en algunos trozos.
  • Ponga primero el predicado de consulta más común para evitar la dispersión.

El fragmento es cirugía; Una vez que cortas, vives con la cicatriz. Asegúrese de que el paciente realmente necesite la operación.


Conclusión: se perfila antes de que vence el proyecto de ley

Cuando el M60 La actualización aterrizó con un boom silencioso, no fue culpa del hardware que fue una llamada de atención. No se trataba de CPU, memoria o disco, se trataba de forma. Forma de los documentos. Forma de las consultas. La forma de las suposiciones que silenciosamente se hincharon durante meses de “simplemente enviarlo”, se acelera.

Arreglarlo no tomó una nueva base de datos, una migración de fin de semana o un ejército de consultores. Se necesitó un equipo dispuesto a mirar hacia adentro, cambiar el pánico por el perfil y remodelar lo que ya tenían.

Los resultados fueron innegables: latencia hacia abajo por 92%costos reducidos por casi 80%y una base de código ahora lo suficientemente delgada como para respirar.

Pero aquí está la verdadera comida para llevar: la deuda técnica en forma no es solo un problema de rendimiento, es financiero. Y a diferencia de los índices o los trucos de almacenamiento en caché, dar forma a las cosas por adelantado paga cada vez que se ejecuta su consulta, cada vez que se replican sus datos, cada vez que escala.

Entonces, antes de su próximo ciclo de facturación, pregúntese:

  • ¿Cada punto final necesita el documento completo?
  • ¿Estamos diseñando para lecturas o simplemente escribiendo rápido?
  • ¿Están funcionando nuestros índices o simplemente trabajando duro?

Forma-primero no es una técnica, es una mentalidad. Un hábito. Y cuanto antes lo adopte, más tiempo durará su sistema, y ​​su pista.


Fuentes y lecturas adicionales

Sobre el autor

Hayk Ghukasyan es un jefe de ingeniería en Hexact, donde ayuda a construir plataformas de automatización como hexomática y hexopark. Tiene más de 20 años de experiencia en arquitectura de sistemas a gran escala, bases de datos en tiempo real e ingeniería de optimización.