Concurrencia PostgreSQL: no esférica, no caballo, no en el vacío



Escalar un DBMS es un futuro que avanza continuamente. Los DBMS mejoran y escalan mejor en plataformas de hardware, mientras que las plataformas de hardware aumentan la productividad, la cantidad de núcleos y la memoria: Aquiles se está poniendo al día con la tortuga, pero aún no lo ha hecho. El problema de escalar DBMS está en pleno apogeo.

Postgres Professional tuvo un problema con el escalado no solo teóricamente, sino también prácticamente: con sus clientes. Y más de una vez. Uno de esos casos será discutido en este artículo.

PostgreSQL escala bien en sistemas NUMA si es una placa base única con múltiples procesadores y múltiples buses de datos. Algunas optimizaciones se pueden leer aquí y aquí . Sin embargo, hay otra clase de sistemas, tienen varias placas base, el intercambio de datos entre ellas se realiza mediante interconexión, mientras que una instancia del sistema operativo está trabajando en ellas y para el usuario este diseño parece una sola máquina. Y aunque formalmente tales sistemas también pueden atribuirse a NUMA, en esencia están más cerca de las supercomputadoras, como El acceso a la memoria local del nodo y el acceso a la memoria del nodo vecino difieren radicalmente. La comunidad PostgreSQL cree que la única instancia de Postgres que se ejecuta en tales arquitecturas es una fuente de problemas, y aún no existe un enfoque sistemático para resolverlos.

Esto se debe a que la arquitectura de software que usa memoria compartida está diseñada fundamentalmente por el hecho de que el tiempo de acceso de los diferentes procesos a su propia memoria remota es más o menos comparable. En el caso de que trabajemos con muchos nodos, la apuesta por la memoria compartida como un canal de comunicación rápido deja de justificarse, porque debido a la latencia es mucho más "barato" enviar una solicitud de una determinada acción al nodo (nodo) donde se encuentra datos interesantes que enviar estos datos en el bus. Por lo tanto, para las supercomputadoras y, en general, los sistemas con muchos nodos, las soluciones de clúster son relevantes.

Esto no significa que sea necesario poner fin a la combinación de sistemas de múltiples nodos y la arquitectura típica de memoria compartida de Postgres. Después de todo, si los procesos postgres pasan la mayor parte de su tiempo haciendo cálculos complejos localmente, entonces esta arquitectura será incluso muy eficiente. En nuestra situación, el cliente ya había comprado un poderoso servidor de múltiples nodos, y tuvimos que resolver los problemas de PostgreSQL en él.

Pero los problemas eran graves: las solicitudes de escritura más simples (cambiar varios valores de campo en un registro) se ejecutaron en un período de varios minutos a una hora. Como se confirmó más tarde, estos problemas se manifestaron en todo su esplendor precisamente por la gran cantidad de núcleos y, en consecuencia, el paralelismo radical en la ejecución de solicitudes con un intercambio relativamente lento entre nodos.

Por lo tanto, el artículo resultará, por así decirlo, con dos propósitos:

  • Comparta experiencia: qué hacer si en un sistema de múltiples nodos la base de datos se ralentiza en serio. Por dónde empezar, cómo diagnosticar dónde moverse.
  • Describa cómo se pueden resolver los problemas del propio DBMS PostgreSQL con un alto nivel de concurrencia. Incluyendo cómo el cambio en el algoritmo para tomar bloqueos afecta el rendimiento de PostgreSQL.

Servidor y DB


El sistema constaba de 8 cuchillas con 2 enchufes en cada una. En total, más de 300 núcleos (excluyendo hypertreading). Un neumático rápido (tecnología patentada por el fabricante) conecta las cuchillas. No es que sea una supercomputadora, pero para una instancia del DBMS, la configuración es impresionante.
La carga también es bastante grande. Más de 1 terabyte de datos. Alrededor de 3000 transacciones por segundo. Más de 1000 conexiones a postgres.

Habiendo comenzado a lidiar con las expectativas de grabación por hora, lo primero que hicimos fue escribir en el disco como causa de demoras. Tan pronto como comenzaron los retrasos incomprensibles, las pruebas comenzaron a hacerse exclusivamente en tmpfs . La imagen no ha cambiado. El disco no tiene nada que ver con eso.

Primeros pasos con los diagnósticos: vistas


Dado que los problemas surgieron probablemente debido a la alta competencia de los procesos que "tocan" los mismos objetos, lo primero que debe verificar son los bloqueos. En PostgreSQL, hay una vista pg.catalog.pg_locks y pg_stat_activity para tal verificación. El segundo, ya en la versión 9.6, agregó información sobre lo que el proceso está esperando ( Amit Kapila, Ildus Kurbangaliev ) - wait_event_type . Los valores posibles para este campo se describen aquí .

Pero primero, solo cuenta:

 postgres=# SELECT COUNT(*) FROM pg_locks; count —---— 88453 (1 row) postgres=# SELECT COUNT(*) FROM pg_stat_activity; count —---— 1826 (1 row) postgres=# SELECT COUNT(*) FROM pg_stat_activity WHERE state ='active'; count —---— 1005 

Estos son números reales. Alcanzó hasta 200,000 cerraduras.
Al mismo tiempo, tales bloqueos colgaban de la solicitud desafortunada:

 SELECT COUNT(mode), mode FROM pg_locks WHERE pid =580707 GROUP BY mode; count | mode —-----+---------------— 93 | AccessShareLock 1 | ExclusiveLock 

Al leer el búfer, el DBMS usa el bloqueo share , mientras escribe, exclusive . Es decir, los bloqueos de escritura representaron menos del 1% de todas las solicitudes.
En la vista pg_locks , los tipos de bloqueo no siempre se ven como se describe en la documentación del usuario.

Aquí está la placa del partido:

 AccessShareLock = LockTupleKeyShare RowShareLock = LockTupleShare ExclusiveLock = LockTupleNoKeyExclusive AccessExclusiveLock = LockTupleExclusive 

La consulta SELECT mode FROM pg_locks mostró que CREATE INDEX (sin CONCURRENTLY) esperaría 234 INSERTs y 390 INSERTs para el buffer content lock . Una posible solución es "enseñar" INSERTs de diferentes sesiones para cruzarse menos en buffers.

Es hora de usar perf


La utilidad perf recopila mucha información de diagnóstico. En modo de record ... escribe estadísticas de eventos del sistema en archivos (de manera predeterminada están en ./perf_data ), y en modo de report analiza los datos recopilados, por ejemplo, puede filtrar eventos que conciernen solo postgres o un pid dado:

 $ perf record -u postgres  $ perf record -p 76876  ,  $ perf report > ./my_results 

Como resultado, veremos algo como



Cómo usar perf para diagnosticar PostgreSQL se describe, por ejemplo, aquí , así como en la página de wiki .

En nuestro caso, incluso el modo más simple proporcionó información importante: perf top , que funciona, por supuesto, en el espíritu del sistema operativo top . Con perf top vimos que la mayoría de las veces el procesador pasa en los PinBuffer() centrales, así como en las PinBuffer() y LWLockAttemptLock(). .

PinBuffer() es una función que aumenta el contador de referencias al búfer (mapeando una página de datos a la RAM), gracias a que los procesos de postgres saben qué búferes se pueden forzar y cuáles no.

LWLockAttemptLock() - LWLock de captura de LWLock . LWLock es un tipo de bloqueo con dos niveles de shared y exclusive , sin definir deadlock , los bloqueos se asignan previamente a la shared memory , los procesos de espera están esperando en una cola.

Estas funciones ya se han optimizado bastante en PostgreSQL 9.5 y 9.6. Los spinlocks dentro de ellos fueron reemplazados por el uso directo de operaciones atómicas.

Gráficos de llamas


Es imposible sin ellos: incluso si fueran inútiles, todavía valdría la pena contarlos: son inusualmente hermosos. Pero son útiles. Aquí hay una ilustración de github , no de nuestro caso (ni nosotros ni el cliente estamos listos para revelar detalles aún).



Estas bellas imágenes muestran muy claramente lo que toman los ciclos del procesador. El mismo perf puede recopilar datos, pero el flame graph visualiza de manera inteligente los datos y construye árboles en función de las pilas de llamadas recopiladas. Puede leer más sobre la creación de perfiles con gráficos de llama, por ejemplo, aquí , y descargar todo lo que necesita aquí .

En nuestro caso, una gran cantidad de nestloop era visible en los gráficos de llamas. Aparentemente, las uniones de una gran cantidad de tablas en numerosas solicitudes de lectura concurrentes causaron una gran cantidad de bloqueos de access share .

Las estadísticas recopiladas por perf muestran dónde van los ciclos del procesador. Y aunque vimos que la mayor parte del tiempo del procesador pasa por las cerraduras, no vimos qué conduce exactamente a expectativas tan largas de las cerraduras, ya que no vemos exactamente dónde ocurren las expectativas de las cerraduras, porque El tiempo de CPU no se pierde esperando.

Para ver las expectativas en sí mismas, puede crear una solicitud para la vista del sistema pg_stat_activity .

 SELECT wait_event_type, wait_event, COUNT(*) FROM pg_stat_activity GROUP BY wait_event_type, wait_event; 

reveló que:

 LWLockTranche | buffer_content | UPDATE ************* LWLockTranche | buffer_content | INSERT INTO ******** LWLockTranche | buffer_content | \r | | insert into B4_MUTEX | | values (nextval('hib | | returning ID Lock | relation | INSERT INTO B4_***** LWLockTranche | buffer_content | UPDATE ************* Lock | relation | INSERT INTO ******** LWLockTranche | buffer_mapping | INSERT INTO ******** LWLockTranche | buffer_content | \r 

(los asteriscos aquí simplemente reemplazan los detalles de la solicitud que no revelamos).

Puede ver los valores buffer_content (bloqueando el contenido de los buffers) y buffer_mapping (bloqueando los componentes de la placa hash shared_buffers ).

Para ayuda a gdb


Pero, ¿por qué hay tantas expectativas para este tipo de cerraduras? Para obtener información más detallada sobre las expectativas, tuve que usar el depurador GDB . Con GDB podemos obtener una pila de llamadas de procesos específicos. Mediante la aplicación de muestreo, es decir Después de recopilar un cierto número de pilas de llamadas aleatorias, puede hacerse una idea de qué pilas tienen las expectativas más largas.

Considere el proceso de compilación de estadísticas. Consideraremos la recopilación “manual” de estadísticas, aunque en la vida real se utilizan scripts especiales que lo hacen automáticamente.

Primero, gdb necesita estar adjunto al proceso PostgreSQL. Para hacer esto, encuentre el pid proceso pid servidor, digamos de

 $ ps aux | grep postgres 

Digamos que encontramos:

 postgres 2025 0.0 0.1 172428 1240 pts/17  S   23  0:00 /usr/local/pgsql/bin/postgres -D /usr/local/pgsql/data 

y ahora inserte el pid en el depurador:

 igor_le:~$gdb -p 2025 

Una vez dentro del depurador, escribimos bt [es decir, backtrace ] o where . Y obtenemos mucha información sobre este tipo:

 (gdb) bt #0 0x00007fbb65d01cd0 in __write_nocancel () from /lib64/libc.so.6 #1 0x00000000007c92f4 in write_pipe_chunks ( data=0x110e6e8 "2018‐06‐01 15:35:38 MSK [524647]: [392‐1] db=bp,user=bp,app=[unknown],client=192.168.70.163 (http://192.168.70.163) LOG: relation 23554 new block 493: 248.389503\n2018‐06‐01 15:35:38 MSK [524647]: [393‐1] db=bp,user=bp,app=["..., len=409, dest=dest@entry=1) at elog.c:3123 #2 0x00000000007cc07b in send_message_to_server_log (edata=0xc6ee60 <errordata>) at elog.c:3024 #3 EmitErrorReport () at elog.c:1479 

Una vez recopiladas las estadísticas, incluidas las pilas de llamadas de todos los procesos de postgres, recopiladas repetidamente en diferentes puntos en el tiempo, vimos que el buffer partition lock del buffer partition lock dentro del relation extension lock la relation extension lock duró 3706 segundos (aproximadamente una hora), es decir, se bloquea en un trozo de la tabla hash del búfer manager, que era necesario suplantar el búfer antiguo, para luego reemplazarlo por uno nuevo correspondiente a la parte extendida de la tabla. También se notó un cierto número de bloqueos del buffer content lock del buffer content lock , lo que correspondía a la expectativa de bloquear las páginas del índice del B-tree para su inserción.



Al principio, llegaron dos explicaciones para un tiempo de espera tan monstruoso:

  • Alguien más tomó este LWLock y se atascó. Pero esto es poco probable. Porque no ocurre nada complicado dentro del bloqueo de la partición del búfer.
  • Encontramos algún comportamiento patológico de LWLock . Es decir, a pesar de que nadie tomó la cerradura demasiado tiempo, su expectativa duró demasiado.

Parches de diagnóstico y tratamiento de árboles


Al reducir el número de conexiones simultáneas, probablemente descargaríamos el flujo de solicitudes a las cerraduras. Pero eso sería como rendirse. En cambio, Alexander Korotkov , el arquitecto jefe de Postgres Professional (por supuesto, ayudó a preparar este artículo), propuso una serie de parches.

En primer lugar, era necesario obtener una imagen más detallada del desastre. No importa cuán buenas sean las herramientas terminadas, los parches de diagnóstico de su propia fabricación también serán útiles.

Se escribió un parche que agrega un registro detallado del tiempo dedicado a la relation extension , lo que está sucediendo dentro de la función RelationAddExtraBlocks() . Entonces descubrimos qué tiempo pasa dentro de RelationAddExtraBlocks().

Y en apoyo de él, se escribió otro parche informando en pg_stat_activity sobre lo que estamos haciendo ahora en relation extension . Se hizo de esta manera: cuando la relation expande, application_name convierte en RelationAddExtraBlocks . Este proceso ahora se analiza convenientemente con los máximos detalles utilizando gdb bt y perf .

En realidad, los parches médicos (y no diagnósticos) se escribieron dos. El primer parche cambió el comportamiento de los bloqueos de la hoja del B‐tree : antes, cuando se le pedía que se insertara, la hoja se bloqueó como recurso share , y después de eso se volvió exclusive . Ahora inmediatamente se vuelve exclusive . Ahora este parche ya se ha confirmado para PostgreSQL 12 . Afortunadamente, este año Alexander Korotkov recibió el estado de un committer : el segundo committer PostgreSQL en Rusia y el segundo en la compañía.

El valor NUM_BUFFER_PARTITIONS también se aumentó de 128 a 512 para reducir la carga en los bloqueos de mapeo: la tabla hash del administrador de búfer se dividió en partes más pequeñas, con la esperanza de que la carga en cada parte específica se redujera.

Después de aplicar este parche, los bloqueos en el contenido de los búferes desaparecieron, pero a pesar del aumento en NUM_BUFFER_PARTITIONS , el buffer_mapping permaneció, es decir, le recordamos las piezas bloqueadas de la tabla hash del administrador de búfer:

 locks_count | active_session | buffer_content | buffer_mapping ----‐‐‐--‐‐‐+‐------‐‐‐‐‐‐‐‐‐+‐‐‐------‐‐‐‐‐‐‐+‐‐------‐‐‐ 12549 | 1218 | 0 | 15 

E incluso eso no es mucho. B - el árbol ya no es un cuello de botella. La extensión del heap- llegó a primer plano.

Tratamiento de la conciencia


A continuación, Alexander presentó la siguiente hipótesis y solución:

Esperamos mucho tiempo en el buffer parittion lock del buffer parittion lock al buffer parittion lock búfer. Quizás, en el mismo buffer parittion lock hay una página muy demandada, por ejemplo, la raíz de algún B‐tree En este punto, hay un flujo continuo de solicitudes de shared lock de las solicitudes de lectura.

La línea de espera en LWLock "no es justa". Dado que shared lock se pueden tomar tantos como sea necesario a la vez, entonces si el shared lock ya shared lock , los shared lock posteriores pasan sin hacer cola. Por lo tanto, si la secuencia de bloqueos compartidos es de suficiente intensidad para que no haya "ventanas" entre ellos, entonces esperar un exclusive lock casi al infinito.

Para solucionar esto, puede intentar ofrecer un parche de comportamiento "caballeroso" de las cerraduras. Despierta la conciencia de shared locker y honestamente hacen cola cuando ya hay un exclusive lock (curiosamente, los candados pesados, hwlock , no tienen problemas con la conciencia: siempre hacen cola honestamente)

 locks_count | active_session | buffer_content | buffer_mapping | reladdextra | inserts>30sec ‐‐‐‐‐‐-‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐--‐-‐+‐‐‐‐‐‐-‐‐‐‐‐‐+‐‐‐‐------ 173985 | 1802 | 0 | 569 | 0 | 0 

Todo esta bien! No hay insert largas. Aunque las cerraduras en las piezas de las placas hash permanecieron. Pero qué hacer, estas son las propiedades de los neumáticos de nuestra pequeña supercomputadora.

Este parche también se ofreció a la comunidad . Pero no importa cómo se desarrolle el destino de estos parches en la comunidad, nada les impide acceder a las próximas versiones de Postgres Pro Enterprise , que están diseñadas específicamente para clientes con sistemas muy cargados.

Moraleja


Los bloqueos share ligeros de alta moralidad (bloques exclusive se saltan la cola) han resuelto el problema de los retrasos por hora en un sistema de múltiples nodos. La etiqueta hash buffer manager no funcionó debido a un flujo de share lock demasiado, lo que no dejaba la posibilidad de que los bloqueos necesarios suplantaran los viejos búferes y cargaran otros nuevos. Los problemas con la extensión del búfer para las tablas de la base de datos fueron solo una consecuencia de esto. Antes de esto, era posible expandir el cuello de botella con acceso a la raíz del B-tree

PostgreSQL no fue diseñado para arquitecturas y supercomputadoras NUMA. Adaptarse a tales arquitecturas de Postgres es un trabajo enorme que requeriría (y posiblemente requeriría) los esfuerzos coordinados de muchas personas e incluso empresas. Pero las consecuencias desagradables de estos problemas arquitectónicos pueden mitigarse. Y tenemos que hacerlo: los tipos de carga que provocaron retrasos similares a los descritos son bastante típicos, las señales de socorro similares de otros lugares continúan llegando a nosotros. Problemas anteriores aparecieron antes: en sistemas con menos núcleos, solo las consecuencias no fueron tan monstruosas y los síntomas se trataron con otros métodos y otros parches. Ahora ha aparecido otro medicamento, no universal, pero claramente útil.

Entonces, cuando PostgreSQL funciona con la memoria de todo el sistema como local, ningún bus de alta velocidad entre nodos puede compararse con el tiempo de acceso a la memoria local. Las tareas surgen debido a esto difícil, a menudo urgente, pero interesante. Y la experiencia de resolverlos es útil no solo para los decisivos, sino también para toda la comunidad.

Source: https://habr.com/ru/post/es423685/


All Articles