Permítame recordarle que ya hablamos sobre
bloqueos de relación , bloqueos de nivel de fila , sobre
bloqueos de otros objetos (incluidos los predicados) y sobre la relación entre diferentes tipos de bloqueos.
Hoy termino esta serie con un artículo sobre
bloqueos de memoria . Hablaremos sobre spinlocks, bloqueos livianos y bloqueo de búfer, así como herramientas de monitoreo y muestreo de expectativas.

Bloqueo de giro
A diferencia de las cerraduras "pesadas" comunes, las cerraduras más ligeras y más baratas (en términos de gastos generales) se utilizan para proteger las estructuras en la RAM compartida.
El más simple de ellos son
cerraduras giratorias o
cerraduras giratorias . Están diseñados para capturar durante muy poco tiempo (varias instrucciones de procesador) y proteger secciones individuales de memoria de cambios simultáneos.
Los bloqueos de giro se implementan en base a instrucciones atómicas del procesador, como comparar e intercambiar. Soportan un solo modo exclusivo. Si el bloqueo está ocupado, el proceso de espera realiza una espera activa: el comando se repite ("gira" en el bucle, de ahí el nombre) hasta que se ejecuta con éxito. Esto tiene sentido, ya que los bloqueos de giro se utilizan cuando se estima que la probabilidad de conflicto es muy baja.
Los bloqueos de giro no proporcionan detección de puntos muertos (los desarrolladores de PostgreSQL están monitoreando esto) y no proporcionan ninguna herramienta de monitoreo. En general, lo único que podemos hacer con las cerraduras giratorias es saber acerca de su existencia.
Cerraduras de luz
Luego vienen las llamadas
cerraduras ligeras (cerraduras livianas, lwlocks).
Se capturan por el corto tiempo que lleva trabajar con la estructura de datos (por ejemplo, una tabla hash o una lista de punteros). Como regla general, un bloqueo de luz no se mantiene por mucho tiempo, pero en algunos casos, un bloqueo de luz protege las operaciones de E / S, por lo que, en principio, el tiempo puede llegar a ser significativo.
Se admiten dos modos: exclusivo (para cambiar datos) y compartido (solo lectura). Como tal, no hay cola de espera: si varios procesos están esperando que se libere el bloqueo, uno de ellos obtendrá acceso más o menos al azar. En sistemas con un alto grado de paralelismo y carga pesada, esto puede conducir a efectos desagradables (ver, por ejemplo,
discusión ).
No se proporciona un mecanismo para verificar puntos muertos, esto permanece en la conciencia de los desarrolladores del kernel. Sin embargo, las cerraduras ligeras tienen herramientas de monitoreo, por lo tanto, a diferencia de las cerraduras giratorias, se pueden "ver" (un poco más adelante mostraré cómo).
Clip buffer
Otro tipo de bloqueo que ya hemos discutido en el artículo sobre la
memoria caché del
búfer es
la fijación del búfer .
Con un búfer fijo, puede realizar varias acciones, incluido el cambio de datos, pero con la condición de que estos cambios no sean visibles para otros procesos debido a las versiones múltiples. Es decir, puede agregar una nueva línea a la página, pero no puede reemplazar la página en el búfer por otra.
Si el proceso se ve obstaculizado por el enlace, generalmente solo omite dicho búfer y selecciona otro. Pero en algunos casos, cuando se requiere este búfer en particular, el proceso se pone en cola y se queda dormido: el sistema lo activará cuando se elimine la fijación.
Las expectativas de consolidación están disponibles para el monitoreo.
Ejemplo: caché de búfer

Ahora, para obtener información (¡incompleta!) Sobre cómo y dónde se usan los bloqueos, considere un ejemplo de caché de búfer.
Para acceder a una tabla hash que contiene referencias a buffers, el proceso debe capturar un bloqueo de asignación de buffer ligero en modo compartido, y si la tabla necesita ser cambiada, entonces en modo excepcional. Para reducir la granularidad, esta cerradura está dispuesta como un
tramo , que consta de 128 cerraduras separadas, cada una de las cuales protege su propia parte de la tabla hash.
El proceso obtiene acceso al encabezado del búfer utilizando spin-lock. Las operaciones individuales (como incrementar el contador) también se pueden realizar sin bloqueos explícitos utilizando instrucciones atómicas del procesador.
Para leer el contenido de un búfer, se requiere un bloqueo del contenido del búfer. Por lo general, se captura solo durante el tiempo necesario para leer los punteros a la versión de las líneas, y luego la protección proporcionada por el clip de búfer es suficiente. Para modificar el contenido del búfer, este bloqueo debe capturarse en modo excepcional.
Al leer un búfer del disco (o escribir en el disco), también se captura el bloqueo de E / S en progreso, lo que indica a otros procesos que la página se está leyendo (o escribiendo); pueden hacer cola si también necesitan hacer algo con esta página.
Los punteros para liberar buffers y para la siguiente víctima están protegidos por un único bloqueo de estrategia de bloqueo de spin.
Ejemplo: buffers de registro

Otro ejemplo: búferes de registro.
Para el caché del diario, también se usa una tabla hash que contiene la asignación de páginas a buffers. A diferencia de la memoria caché del búfer, esta tabla hash está protegida por el único bloqueo ligero de WALBufMappingLock, ya que el tamaño de la memoria caché del diario es menor (generalmente 1/32 de la memoria caché del búfer) y el acceso a los búferes está más optimizado.
La escritura de páginas en el disco está protegida por un ligero bloqueo WALWriteLock para que solo un proceso pueda realizar esta operación a la vez.
Para crear una entrada de diario, el proceso primero debe reservar un espacio en la página WAL. Para hacer esto, captura el bloqueo de posición de inserción de bloqueo de giro. Después de reservar un lugar, el proceso copia el contenido de su registro en el lugar designado. La copia puede ser realizada por varios procesos al mismo tiempo, para lo cual el registro está protegido por un tramo de 8 bloqueos de inserción de bloqueo fácil (el proceso debe capturar
cualquiera de ellos).
La figura no muestra todos los bloqueos relacionados con el registro de pregrabación, pero este y el ejemplo anterior deberían dar alguna idea sobre el uso de bloqueos en la RAM.
Monitoreo de expectativas
Comenzando con PostgreSQL 9.6, las herramientas de monitoreo de espera están integradas en la vista pg_stat_activity. Cuando un proceso (sistema o mantenimiento) no puede hacer su trabajo y está esperando algo, esta expectativa se puede ver en la vista: la columna wait_event_type indica el tipo de expectativa, y la columna wait_event indica el nombre de una expectativa específica.
Tenga en cuenta que una vista muestra solo aquellas expectativas que se manejan adecuadamente en el código fuente. Si la vista no muestra la expectativa, esto generalmente no significa con una probabilidad del 100 por ciento de que el proceso realmente no espera nada.
Desafortunadamente, la única información disponible sobre las expectativas es
la información
actual . No se mantienen estadísticas. La única forma de obtener una imagen de las expectativas a lo largo del tiempo es
muestreando el estado de la vista en un intervalo específico. No hay medios integrados para esto, pero puede usar extensiones, por ejemplo,
pg_wait_sampling .
Es necesario tener en cuenta la naturaleza probabilística del muestreo. Para obtener una imagen más o menos confiable, el número de mediciones debe ser lo suficientemente grande. El muestreo a baja frecuencia puede no proporcionar una imagen confiable, y aumentar la frecuencia conducirá a un aumento de la sobrecarga. Por la misma razón, el muestreo es inútil para analizar sesiones de corta duración.
Todas las expectativas se pueden dividir en varios tipos.
Las expectativas de las cerraduras consideradas constituyen una gran categoría:
- esperando bloqueos de objetos (valor de bloqueo en la columna wait_event_type);
- esperando cerraduras de luz (LWLock);
- esperando un búfer anclado (BufferPin).
Pero los procesos pueden esperar otros eventos:
- Las expectativas de E / S (IO) ocurren cuando un proceso necesita escribir o leer datos;
- el proceso puede esperar los datos necesarios para el trabajo del cliente (Cliente) o de otro proceso (IPC);
- Las extensiones pueden registrar sus expectativas específicas (Extensión).
Hay situaciones en las que un proceso simplemente no hace un trabajo útil. Esta categoría incluye:
- esperando procesos en segundo plano en su bucle principal (Actividad);
- esperando un temporizador (Tiempo de espera).
Como regla, tales expectativas son "normales" y no hablan de ningún problema.
El tipo de expectativa es seguido por el nombre de la expectativa particular. La tabla completa se puede encontrar
en la documentación .
Si no se especifica ningún nombre de espera, el proceso no está en estado de espera. Se debe considerar que ese momento no se tiene en cuenta, ya que de hecho no se sabe qué está sucediendo exactamente en este momento.
Sin embargo, es hora de mirar.
=> SELECT pid, backend_type, wait_event_type, wait_event FROM pg_stat_activity;
pid | backend_type | wait_event_type | wait_event -------+------------------------------+-----------------+--------------------- 28739 | logical replication launcher | Activity | LogicalLauncherMain 28736 | autovacuum launcher | Activity | AutoVacuumMain 28963 | client backend | | 28734 | background writer | Activity | BgWriterMain 28733 | checkpointer | Activity | CheckpointerMain 28735 | walwriter | Activity | WalWriterMain (6 rows)
Se puede ver que todos los procesos de servicio en segundo plano están "jugando". Los valores vacíos en wait_event_type y wait_event indican que el proceso no espera nada; en nuestro caso, el proceso de publicación está ocupado ejecutando la solicitud.
Muestreo
Para obtener una imagen más o menos completa de las expectativas utilizando el muestreo, utilizamos la extensión
pg_wait_sampling . Debe compilarse a partir del código fuente; Omitiré esta parte. Luego registramos la biblioteca en el parámetro
shared_preload_libraries y reiniciamos el servidor.
=> ALTER SYSTEM SET shared_preload_libraries = 'pg_wait_sampling';
student$ sudo pg_ctlcluster 11 main restart
Ahora instale la extensión en la base de datos.
=> CREATE EXTENSION pg_wait_sampling;
La extensión le permite ver el historial de expectativas, que se almacena en un búfer circular. Pero lo más interesante es ver el perfil de las expectativas: las estadísticas acumuladas para todo el tiempo de trabajo.
Esto es lo que veremos en unos segundos:
=> SELECT * FROM pg_wait_sampling_profile;
pid | event_type | event | queryid | count -------+------------+---------------------+---------+------- 29074 | Activity | LogicalLauncherMain | 0 | 220 29070 | Activity | WalWriterMain | 0 | 220 29071 | Activity | AutoVacuumMain | 0 | 219 29069 | Activity | BgWriterMain | 0 | 220 29111 | Client | ClientRead | 0 | 3 29068 | Activity | CheckpointerMain | 0 | 220 (6 rows)
Como no ha sucedido nada desde que se inició el servidor, las principales expectativas son del tipo Actividad (los procesos de servicio esperan hasta que aparezca el trabajo) y Cliente (psql espera a que el usuario envíe una solicitud).
Con la configuración predeterminada (parámetro
pg_wait_sampling.profile_period ), el período de muestreo es de 10 milisegundos, es decir, los valores se guardan 100 veces por segundo. Por lo tanto, para estimar la duración de la espera en segundos, el valor del recuento debe dividirse por 100.
Para comprender a qué pertenecen las expectativas del proceso, agregamos la vista pg_stat_activity a la solicitud:
=> SELECT p.pid, a.backend_type, a.application_name AS app, p.event_type, p.event, p.count FROM pg_wait_sampling_profile p LEFT JOIN pg_stat_activity a ON p.pid = a.pid ORDER BY p.pid, p.count DESC;
pid | backend_type | app | event_type | event | count -------+------------------------------+------+------------+----------------------+------- 29068 | checkpointer | | Activity | CheckpointerMain | 222 29069 | background writer | | Activity | BgWriterMain | 222 29070 | walwriter | | Activity | WalWriterMain | 222 29071 | autovacuum launcher | | Activity | AutoVacuumMain | 221 29074 | logical replication launcher | | Activity | LogicalLauncherMain | 222 29111 | client backend | psql | Client | ClientRead | 4 29111 | client backend | psql | IPC | MessageQueueInternal | 1 (7 rows)
Carguemos con pgbench y veamos cómo cambia la imagen.
student$ pgbench -i test
Restablecemos el perfil recopilado a cero y ejecutamos la prueba durante 30 segundos en un proceso separado.
=> SELECT pg_wait_sampling_reset_profile();
student$ pgbench -T 30 test
La solicitud debe completarse antes de que se complete el proceso pgbench:
=> SELECT p.pid, a.backend_type, a.application_name AS app, p.event_type, p.event, p.count FROM pg_wait_sampling_profile p LEFT JOIN pg_stat_activity a ON p.pid = a.pid WHERE a.application_name = 'pgbench' ORDER BY p.pid, p.count DESC;
pid | backend_type | app | event_type | event | count -------+----------------+---------+------------+------------+------- 29148 | client backend | pgbench | IO | WALWrite | 8 29148 | client backend | pgbench | Client | ClientRead | 1 (2 rows)
Por supuesto, las expectativas del proceso pgbench resultarán ligeramente diferentes dependiendo del sistema específico. En nuestro caso, es muy probable que se presente la espera de una entrada de registro (IO / WALWrite), pero la mayoría de las veces el proceso no se detuvo, pero hizo algo presumiblemente útil.
Cerraduras de luz
Siempre debe recordar que la ausencia de expectativas cuando se realiza el muestreo no significa que no haya expectativas. Si fue más corto que el período de muestreo (la centésima de segundo en nuestro ejemplo), entonces simplemente no podría caer en la muestra.
Por lo tanto, los bloqueos de luz no aparecieron en el perfil, pero aparecerán si recopila datos durante mucho tiempo. Para garantizar un vistazo, puede ralentizar artificialmente el sistema de archivos, por ejemplo, utilice el proyecto
slowfs integrado en el sistema de archivos
FUSE .
Esto es lo que podemos ver en la misma prueba si cualquier operación de E / S tarda 1/10 de segundo.
=> SELECT pg_wait_sampling_reset_profile();
student$ pgbench -T 30 test
=> SELECT p.pid, a.backend_type, a.application_name AS app, p.event_type, p.event, p.count FROM pg_wait_sampling_profile p LEFT JOIN pg_stat_activity a ON p.pid = a.pid WHERE a.application_name = 'pgbench' ORDER BY p.pid, p.count DESC;
pid | backend_type | app | event_type | event | count -------+----------------+---------+------------+----------------+------- 29240 | client backend | pgbench | IO | WALWrite | 1445 29240 | client backend | pgbench | LWLock | WALWriteLock | 803 29240 | client backend | pgbench | IO | DataFileExtend | 20 (3 rows)
Ahora, la expectativa principal del proceso pgbench está relacionada con E / S, o más bien, una entrada de registro que se ejecuta en modo síncrono con cada confirmación. Dado que (como se muestra en el ejemplo anterior), escribir un registro en el disco está protegido por el bloqueo de luz WALWriteLock, este bloqueo también está presente en el perfil; queríamos verlo.
Clip buffer
Para ver la fijación del búfer, aprovechamos el hecho de que los cursores abiertos sostienen el pin para que la lectura de la siguiente línea sea más rápida.
Comenzamos la transacción, abrimos el cursor y seleccionamos una fila.
=> BEGIN; => DECLARE c CURSOR FOR SELECT * FROM pgbench_history; => FETCH c;
tid | bid | aid | delta | mtime | filler -----+-----+-------+-------+----------------------------+-------- 9 | 1 | 35092 | 477 | 2019-09-04 16:16:18.596564 | (1 row)
Compruebe que el búfer está anclado (pinning_backends):
=> SELECT * FROM pg_buffercache WHERE relfilenode = pg_relation_filenode('pgbench_history') AND relforknumber = 0 \gx
-[ RECORD 1 ]----+------ bufferid | 190 relfilenode | 47050 reltablespace | 1663 reldatabase | 16386 relforknumber | 0 relblocknumber | 0 isdirty | t usagecount | 1 pinning_backends | 1 <-- 1
Ahora
limpiaremos la tabla:
| => SELECT pg_backend_pid();
| pg_backend_pid | ---------------- | 29367 | (1 row)
| => VACUUM VERBOSE pgbench_history;
| INFO: vacuuming "public.pgbench_history" | INFO: "pgbench_history": found 0 removable, 0 nonremovable row versions in 1 out of 1 pages | DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 732651 | There were 0 unused item pointers.
| Skipped 1 page due to buffer pins, 0 frozen pages.
| 0 pages are entirely empty. | CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s. | VACUUM
Como podemos ver, la página se omitió (se omitió 1 página debido a los pines del búfer). De hecho, la limpieza no puede manejarlo, ya que está prohibido eliminar físicamente las versiones de fila de una página en un búfer anclado. Pero la limpieza no esperará: la página se procesará la próxima vez.
Y ahora realizaremos la
limpieza con congelación :
| => VACUUM FREEZE VERBOSE pgbench_history;
Con una congelación claramente solicitada, no puede omitir una sola página que no esté marcada en el mapa de congelación; de lo contrario, es imposible reducir la antigüedad máxima de las transacciones no congeladas en pg_class.relfrozenxid. Por lo tanto, la limpieza se bloquea hasta que se cierra el cursor.
=> SELECT age(relfrozenxid) FROM pg_class WHERE oid = 'pgbench_history'::regclass;
age ----- 27 (1 row)
=> COMMIT;
| INFO: aggressively vacuuming "public.pgbench_history" | INFO: "pgbench_history": found 0 removable, 26 nonremovable row versions in 1 out of 1 pages | DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 732651 | There were 0 unused item pointers.
| Skipped 0 pages due to buffer pins, 0 frozen pages.
| 0 pages are entirely empty. | CPU: user: 0.00 s, system: 0.00 s, elapsed: 3.01 s. | VACUUM
=> SELECT age(relfrozenxid) FROM pg_class WHERE oid = 'pgbench_history'::regclass;
age ----- 0 (1 row)
Bueno, veamos el perfil de expectativas de la segunda sesión psql en la que se ejecutaron los comandos VACUUM:
=> SELECT p.pid, a.backend_type, a.application_name AS app, p.event_type, p.event, p.count FROM pg_wait_sampling_profile p LEFT JOIN pg_stat_activity a ON p.pid = a.pid WHERE p.pid = 29367 ORDER BY p.pid, p.count DESC;
pid | backend_type | app | event_type | event | count -------+----------------+------+------------+------------+------- 29367 | client backend | psql | BufferPin | BufferPin | 294 29367 | client backend | psql | Client | ClientRead | 10 (2 rows)
El tipo de espera BufferPin indica que el vaciado estaba esperando que se liberara el búfer.
En esto asumiremos que hemos completado las cerraduras. ¡Gracias a todos por su atención y comentarios!