Escribimos en PostgreSQL en sublight: 1 host, 1 day, 1TB

Hai pouco díxenche como, usando receitas estándar aumentar o rendemento das consultas de lectura SQL de base de datos PostgreSQL. Hoxe falaremos de como a gravación pódese facer de forma máis eficiente na base de datos sen usar ningún tipo de "torsión" na configuración, simplemente organizando correctamente os fluxos de datos.

Escribimos en PostgreSQL en sublight: 1 host, 1 day, 1TB

#1. Seccionamento

Un artigo sobre como e por que paga a pena organizalo partición aplicada "en teoría" xa foi, aquí falaremos da práctica de aplicar algúns enfoques dentro do noso servizo de monitorización para centos de servidores PostgreSQL.

"Cousas de tempos pasados..."

Inicialmente, como calquera MVP, o noso proxecto comezou cunha carga bastante lixeira: a monitorización levouse a cabo só para os dez servidores máis críticos, todas as táboas eran relativamente compactas... Pero co paso do tempo, o número de anfitrións monitorizados foi cada vez máis. , e unha vez máis tentamos facer algo cun de táboas de 1.5 TB de tamaño, decatámonos de que aínda que era posible seguir vivindo así, era moi incómodo.

Os tempos eran case como tempos épicos, as diferentes versións de PostgreSQL 9.x eran relevantes, polo que todas as particións tiñan que facerse "manual" - a través de herdanza de táboas e disparadores enrutamento con dinámica EXECUTE.

Escribimos en PostgreSQL en sublight: 1 host, 1 day, 1TB
A solución resultante resultou ser o suficientemente universal como para poder traducirse a todas as táboas:

  • Declarouse unha táboa principal de "cabeceira" baleira, que describiu todas índices e disparadores necesarios.
  • O rexistro desde o punto de vista do cliente realizouse na táboa "raíz", e utilizando internamente disparador de enrutamento BEFORE INSERT o rexistro foi inserido "fisicamente" na sección requirida. Se aínda non houbese tal cousa, captamos unha excepción e...
  • … mediante o uso CREATE TABLE ... (LIKE ... INCLUDING ...) creouse a partir do modelo da táboa principal sección cunha restrición na data desexadade xeito que cando se recuperan datos, a lectura realízase só nel.

PG10: primeiro intento

Pero a partición a través da herdanza non foi historicamente moi adecuada para xestionar un fluxo de escritura activo ou un gran número de particións fillas. Por exemplo, pode lembrar que o algoritmo para seleccionar a sección requirida tiña complexidade cuadrática, que funciona con máis de 100 seccións, vostede mesmo comprende como...

No PG10 esta situación foi moi optimizada mediante a implementación do soporte partición nativa. Polo tanto, intentamos aplicalo inmediatamente despois de migrar o almacenamento, pero...

Como se viu despois de explorar o manual, a táboa particionada de forma nativa nesta versión é:

  • non admite descricións de índice
  • non admite disparadores nel
  • non pode ser o "descendente" de ninguén
  • non admite INSERT ... ON CONFLICT
  • non pode xerar unha sección automaticamente

Despois de recibir un doloroso golpe na fronte cun anciño, decatámonos de que sería imposible prescindir de modificar a aplicación e aprazamos a investigación durante seis meses.

PG10: segunda oportunidade

Entón, comezamos a resolver os problemas que xurdiron un por un:

  1. Porque desencadea e ON CONFLICT Comprobamos que aínda os necesitabamos aquí e alí, así que fixemos unha etapa intermedia para elaboralos táboa de proxy.
  2. Desfíxose do "routing" en disparadores - é dicir, de EXECUTE.
  3. Sacárono por separado táboa modelo con todos os índicespara que nin sequera estean presentes na táboa de proxy.

Escribimos en PostgreSQL en sublight: 1 host, 1 day, 1TB
Finalmente, despois de todo isto, particionamos a táboa principal de forma nativa. A creación dunha nova sección aínda queda á conciencia da aplicación.

Dicionarios de “serrado”.

Como en calquera sistema analítico, tamén tivemos "feitos" e "recortes" (dicionarios). No noso caso, nesta calidade actuaron, por exemplo, corpo da plantilla consultas lentas similares ou o propio texto da consulta.

Os "feitos" foron seccionados por día xa desde hai moito tempo, polo que eliminamos con calma as seccións obsoletas e non nos molestaron (rexistros!). Pero houbo un problema cos dicionarios...

Non quere dicir que fosen moitos, senón aproximadamente 100 TB de "feitos" resultaron nun dicionario de 2.5 TB. Non podes eliminar nada convenientemente desta táboa, non podes comprimilo no tempo adecuado e escribir nela foi gradualmente máis lenta.

Como un dicionario... nel, cada entrada debe ser presentada exactamente unha vez... e isto é correcto, pero!.. Ninguén nos impide ter un dicionario separado para cada día! Si, isto trae unha certa redundancia, pero permite:

  • escribir/le máis rápido debido ao menor tamaño da sección
  • consumir menos memoria traballando con índices máis compactos
  • almacenar menos datos debido á capacidade de eliminar rapidamente os obsoletos

Como resultado de todo o complexo de medidas A carga da CPU reduciuse nun ~30 %, a carga do disco nun ~50 %:

Escribimos en PostgreSQL en sublight: 1 host, 1 day, 1TB
Ao mesmo tempo, seguimos escribindo exactamente o mesmo na base de datos, só con menos carga.

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

Así que decidimos co que temos cada día ten a súa propia sección con datos. En realidade, CHECK (dt = '2018-10-12'::date) — e hai unha clave de partición e a condición para que un rexistro caia nunha sección específica.

Dado que todos os informes do noso servizo están construídos no contexto dunha data específica, os índices para eles desde "horas non particionadas" foron de todos os tipos (Servidor, Data, Modelo de plan), (Servidor, Data, nodo Plan), (Data, clase de erro, servidor), ...

Pero agora viven en todas as seccións as súas copias cada un destes índices... E dentro de cada sección a data é unha constante... Resulta que agora estamos en cada un destes índices simplemente introduza unha constante como un dos campos, que aumenta tanto o seu volume como o tempo de busca do mesmo, pero non trae ningún resultado. Deixáronlles o rastrillo para eles, oops...

Escribimos en PostgreSQL en sublight: 1 host, 1 day, 1TB
A dirección da optimización é obvia, sinxela eliminar o campo de data de todos os índices en táboas particionadas. Tendo en conta os nosos volumes, a ganancia é sobre 1 TB/semana!

Agora imos notar que este terabyte aínda tiña que ser gravado dalgún xeito. É dicir, nós tamén agora o disco debería cargar menos! Esta imaxe mostra claramente o efecto obtido da limpeza, á que dedicamos unha semana:

Escribimos en PostgreSQL en sublight: 1 host, 1 day, 1TB

#3. "Difundir" a carga máxima

Un dos grandes problemas dos sistemas cargados é sincronización redundante algunhas operacións que non o requiren. Ás veces “porque non se decataron”, outras “era máis doado así”, pero tarde ou cedo hai que desfacerse del.

Acheguemos a imaxe anterior e vexamos que temos un disco "bombas" baixo a carga con dobre amplitude entre mostras adxacentes, o que claramente "estatisticamente" non debería ocorrer con tal número de operacións:

Escribimos en PostgreSQL en sublight: 1 host, 1 day, 1TB

Isto é bastante fácil de conseguir. Xa comezamos o seguimento case 1000 servidores, cada un é procesado por un fío lóxico separado, e cada fío restablece a información acumulada que se enviará á base de datos cunha frecuencia determinada, algo así:

setInterval(sendToDB, interval)

O problema aquí reside precisamente no feito de que todos os fíos comezan aproximadamente ao mesmo tempo, polo que os seus tempos de envío case sempre coinciden "ata o punto". Vaia #2...

Afortunadamente, isto é bastante fácil de solucionar, engadindo un avance "aleatorio". por tempo:

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

#4. Almacenamos en caché o que necesitamos

O terceiro problema tradicional de alta carga é sen caché onde está podería ser.

Por exemplo, fixemos posible analizar en termos de nodos de plano (todos estes Seq Scan on users), pero inmediatamente pense que son, na súa maioría, iguais - esquecéronse.

Non, por suposto, nada se escribe na base de datos de novo, isto corta o disparador con INSERT ... ON CONFLICT DO NOTHING. Pero estes datos aínda chegan á base de datos e son innecesarios ler para comprobar se hai conflitos ter que facer. Vaia #3...

A diferenza no número de rexistros enviados á base de datos antes/despois de activar o caché é obvia:

Escribimos en PostgreSQL en sublight: 1 host, 1 day, 1TB

E esta é a caída que acompaña na carga de almacenamento:

Escribimos en PostgreSQL en sublight: 1 host, 1 day, 1TB

En total

"Terabyte-per-day" só parece asustado. Se fai todo ben, isto é só 2^40 bytes/86400 segundos = ~12.5 MB/sque ata os parafusos IDE do escritorio sostiñan. 🙂

Pero en serio, mesmo cunha "sesgada" dez veces da carga durante o día, podes cumprir facilmente as capacidades dos SSD modernos.

Escribimos en PostgreSQL en sublight: 1 host, 1 day, 1TB

Fonte: www.habr.com

Engadir un comentario