Escribimos en PostgreSQL en sublight: 1 host, 1 día, 1 TB

Hace poco os conté cómo, usando recetas estándar. aumentar el rendimiento de las consultas de lectura SQL de la base de datos PostgreSQL. Hoy hablaremos de cómo La grabación se puede realizar de forma más eficiente. en la base de datos sin utilizar ningún "giro" en la configuración, simplemente organizando correctamente los flujos de datos.

Escribimos en PostgreSQL en sublight: 1 host, 1 día, 1 TB

#1. Seccionamiento

Un artículo sobre cómo y por qué vale la pena organizarse. partición aplicada “en teoría” Ya lo ha sido, aquí hablaremos sobre la práctica de aplicar algunos enfoques dentro de nuestro servicio de monitoreo para cientos de servidores PostgreSQL.

"Cosas de tiempos pasados..."

Inicialmente, como cualquier MVP, nuestro proyecto comenzó con una carga bastante ligera: el monitoreo se llevó a cabo solo para los diez servidores más críticos, todas las tablas eran relativamente compactas... Pero a medida que pasó el tiempo, la cantidad de hosts monitoreados aumentó cada vez más. , y una vez más intentamos hacer algo con uno de tablas de 1.5 TB de tamaño, nos dimos cuenta de que aunque era posible seguir viviendo así, era muy inconveniente.

Los tiempos eran casi como tiempos épicos, diferentes versiones de PostgreSQL 9.x eran relevantes, por lo que todas las particiones debían realizarse "manualmente", mediante herencia de tablas y desencadenantes enrutamiento con dinámica EXECUTE.

Escribimos en PostgreSQL en sublight: 1 host, 1 día, 1 TB
La solución resultante resultó ser lo suficientemente universal como para poder traducirse a todas las tablas:

  • Se declaró una tabla principal de "encabezado" vacía, que describía todos índices y disparadores necesarios.
  • El registro desde el punto de vista del cliente se realizó en la tabla “raíz”, e internamente utilizando disparador de enrutamiento BEFORE INSERT el registro fue insertado “físicamente” en la sección requerida. Si aún no existía tal cosa, detectamos una excepción y...
  • … mediante el uso CREATE TABLE ... (LIKE ... INCLUDING ...) fue creado basándose en la plantilla de la tabla principal sección con una restricción en la fecha deseadade modo que cuando se recuperan datos, la lectura se realiza sólo en ellos.

PG10: primer intento

Pero históricamente la partición mediante herencia no ha sido adecuada para tratar con un flujo de escritura activo o una gran cantidad de particiones secundarias. Por ejemplo, puede recordar que el algoritmo para seleccionar la sección requerida tenía complejidad cuadrática, que funciona con más de 100 secciones, tú mismo entiendes cómo...

En PG10 esta situación se optimizó enormemente implementando soporte partición nativa. Por lo tanto, intentamos aplicarlo inmediatamente después de migrar el almacenamiento, pero...

Como resultó después de revisar el manual, la tabla particionada de forma nativa en esta versión es:

  • no admite descripciones de índice
  • no admite disparadores en él
  • no puede ser “descendiente” de nadie
  • no es compatible INSERT ... ON CONFLICT
  • no se puede generar una sección automáticamente

Después de recibir un doloroso golpe con un rastrillo en la frente, nos dimos cuenta de que sería imposible prescindir de modificar la aplicación y pospusimos la investigación adicional durante seis meses.

PG10: segunda oportunidad

Entonces, comenzamos a resolver los problemas que surgieron uno por uno:

  1. Porque los desencadenantes y ON CONFLICT Descubrimos que todavía los necesitábamos aquí y allá, así que hicimos una etapa intermedia para resolverlos. tabla de proxy.
  2. Se deshizo del "enrutamiento" en desencadenantes, es decir, desde EXECUTE.
  3. Lo sacaron por separado tabla de plantilla con todos los índicespara que ni siquiera estén presentes en la tabla de proxy.

Escribimos en PostgreSQL en sublight: 1 host, 1 día, 1 TB
Finalmente, después de todo esto, particionamos la tabla principal de forma nativa. La creación de una nueva sección aún queda a la conciencia de la aplicación.

Diccionarios de “aserrado”

Como en cualquier sistema analítico, también teníamos "hechos" y "cortes" (diccionarios). En nuestro caso, en esta capacidad actuaron, por ejemplo, cuerpo de la plantilla consultas lentas similares o el texto de la consulta misma.

Los "hechos" ya estaban divididos por día durante mucho tiempo, por lo que eliminamos con calma las secciones obsoletas y no nos molestaron (¡registros!). Pero había un problema con los diccionarios...

No quiere decir que fueran muchos, pero aproximadamente 100 TB de "hechos" dieron como resultado un diccionario de 2.5 TB. No es posible eliminar nada de una tabla de este tipo, no se puede comprimir en el tiempo adecuado y escribir en ella gradualmente se volvió más lento.

Como en un diccionario... en él cada entrada debe presentarse exactamente una vez... ¡y esto es correcto, pero!.. Nadie nos impide tener un diccionario separado para cada día! Sí, esto trae cierta redundancia, pero permite:

  • escribir/leer más rápido debido al tamaño de sección más pequeño
  • consumir menos memoria trabajando con índices más compactos
  • almacenar menos datos debido a la capacidad de eliminar rápidamente obsoletos

Como resultado de todo el conjunto de medidas La carga de la CPU disminuyó en ~30%, la carga del disco en ~50%:

Escribimos en PostgreSQL en sublight: 1 host, 1 día, 1 TB
Al mismo tiempo, continuamos escribiendo exactamente lo mismo en la base de datos, solo que con menos carga.

#2. Evolución y refactorización de bases de datos.

Así que nos decidimos por lo que tenemos. cada día tiene su propia sección con datos. De hecho, CHECK (dt = '2018-10-12'::date) – y hay una clave de partición y la condición para que un registro caiga en una sección específica.

Dado que todos los informes de nuestro servicio se crean en el contexto de una fecha específica, los índices para ellos desde "tiempos no particionados" han sido de todo tipo. (Servidor, fecha, Plantilla de plano), (Servidor, fecha, Nodo de plan), (fecha, Clase de error, Servidor), ...

Pero ahora viven en cada sección. tus copias cada uno de esos índices... Y dentro de cada sección la fecha es una constante... Resulta que ahora estamos en cada uno de esos índices. simplemente ingresa una constante como uno de los campos, lo que aumenta tanto su volumen como el tiempo de búsqueda, pero no arroja ningún resultado. Se dejaron el rastrillo solos, ups...

Escribimos en PostgreSQL en sublight: 1 host, 1 día, 1 TB
La dirección de la optimización es obvia: simple. eliminar el campo de fecha de todos los índices en tablas particionadas. Dados nuestros volúmenes, la ganancia es de aproximadamente 1TB/semana!

Ahora observemos que este terabyte todavía tenía que registrarse de alguna manera. Es decir, nosotros también el disco ahora debería cargar menos! Esta imagen muestra claramente el efecto obtenido con la limpieza, a la que dedicamos una semana:

Escribimos en PostgreSQL en sublight: 1 host, 1 día, 1 TB

#3. “Difundir” la carga máxima

Uno de los grandes problemas de los sistemas cargados es sincronización redundante algunas operaciones que no lo requieren. A veces “porque no se dieron cuenta”, a veces “así era más fácil”, pero tarde o temprano hay que deshacerse de él.

Hagamos zoom en la imagen anterior y veamos que tenemos un disco. “bombas” bajo la carga con doble amplitud entre muestras adyacentes, lo que claramente “estadísticamente” no debería suceder con tal número de operaciones:

Escribimos en PostgreSQL en sublight: 1 host, 1 día, 1 TB

Esto es bastante fácil de lograr. Ya hemos comenzado a monitorear casi 1000 servidores, cada uno es procesado por un hilo lógico separado, y cada hilo restablece la información acumulada para ser enviada a la base de datos con una frecuencia determinada, algo como esto:

setInterval(sendToDB, interval)

El problema aquí radica precisamente en el hecho de que Todos los hilos comienzan aproximadamente al mismo tiempo., por lo que sus tiempos de envío casi siempre coinciden “al grano”. Ups #2...

Afortunadamente, esto es bastante fácil de solucionar. agregar un período previo "aleatorio" por tiempo:

setInterval(sendToDB, interval * (1 + 0.1 * (Math.random() - 0.5)))

#4. Guardamos en caché lo que necesitamos

El tercer problema tradicional de alta carga es sin caché donde está podría ser

Por ejemplo, hicimos posible analizar en términos de nodos del plan (todos estos Seq Scan on users), pero inmediatamente piensan que son, en su mayor parte, iguales: lo olvidaron.

No, por supuesto, no se vuelve a escribir nada en la base de datos, esto corta el disparador con INSERT ... ON CONFLICT DO NOTHING. Pero estos datos aún llegan a la base de datos y son innecesarios. leer para comprobar si hay conflictos Tener que hacer. Ups #3...

La diferencia en la cantidad de registros enviados a la base de datos antes/después de habilitar el almacenamiento en caché es obvia:

Escribimos en PostgreSQL en sublight: 1 host, 1 día, 1 TB

Y esta es la consiguiente caída en la carga de almacenamiento:

Escribimos en PostgreSQL en sublight: 1 host, 1 día, 1 TB

En total

“Terabyte por día” suena aterrador. Si haces todo bien, entonces esto es solo 2^40 bytes / 86400 segundos = ~12.5 MB/sque incluso los tornillos IDE de escritorio se sujetaron. 🙂

Pero en serio, incluso con un "desvío" diez veces mayor de la carga durante el día, se pueden alcanzar fácilmente las capacidades de los SSD modernos.

Escribimos en PostgreSQL en sublight: 1 host, 1 día, 1 TB

Fuente: habr.com

Añadir un comentario