Comenzamos con problemas relacionados con el
aislamiento , hicimos una digresión sobre la
organización de datos en un nivel bajo y hablamos en detalle
sobre las versiones de fila y cómo se obtienen
instantáneas de las versiones.
Luego examinamos diferentes tipos de limpieza:
intra-página (junto con actualizaciones HOT),
regular y
automática .
Y llegué al último tema de este ciclo. Hoy hablaremos sobre el problema de la identificación y el congelamiento de la transacción.
Desbordamiento de contador de transacciones
PostgreSQL tiene 32 bits asignados para el número de transacción. Este es un número bastante grande (alrededor de 4 mil millones), pero con la operación activa del servidor, puede agotarse. Por ejemplo, con una carga de 1000 transacciones por segundo, esto sucederá después de solo un mes y medio de operación continua.
Pero hablamos sobre el hecho de que el mecanismo de versiones múltiples se basa en la secuencia de numeración; luego, de dos transacciones, se puede considerar que una transacción con un número más bajo comenzó antes. Por lo tanto, está claro que no puede simplemente reiniciar el contador y continuar numerando nuevamente.

¿Por qué no se asignan 64 bits para el número de transacción, ya que esto eliminaría por completo el problema? El hecho es que (como se discutió
anteriormente ) en el encabezado de cada versión de la línea se almacenan dos números de transacción: xmin y xmax. El encabezado ya es bastante grande, al menos 23 bytes, y un aumento en la profundidad de bits conduciría a su aumento en otros 8 bytes. Esto es absolutamente de ninguna manera.
Los números de transacción de 64 bits se implementan en el producto de nuestra empresa, Postgres Pro Enterprise, pero incluso allí no son del todo honestos: xmin y xmax siguen siendo de 32 bits, y el encabezado de la página contiene el "comienzo de una era" común para toda la página.
Que hacer En lugar de un diagrama lineal, todos los números de transacción están en bucle. Para cualquier transacción, se considera que la mitad de los números "en sentido antihorario" pertenecen al pasado y la mitad "en sentido horario" al futuro.
La antigüedad de una transacción es el número de transacciones que han pasado desde que apareció en el sistema (independientemente de si el contador pasó por cero o no). Cuando queremos entender si una transacción es más antigua que otra o no, comparamos su edad, no los números. (Por lo tanto, por cierto, las operaciones "mayor" y "menor" no están definidas para el tipo de datos xid).

Pero en un circuito tan cerrado, surge una situación desagradable. Una transacción que estaba en el pasado distante (transacción 1 en la figura), después de un tiempo estará en esa mitad del círculo que se relaciona con el futuro. Esto, por supuesto, viola las reglas de visibilidad y generaría problemas: los cambios realizados por la transacción 1 simplemente desaparecerían de la vista.

Congelación de versiones y reglas de visibilidad
Para evitar tales "viajes" del pasado al futuro, el proceso de limpieza (además de liberar espacio en las páginas) realiza otra tarea. Encuentra versiones bastante antiguas y "frías" de las líneas (que son visibles en todas las imágenes y cuyo cambio ya es poco probable) y de una manera especial las marca: las "congela". La versión congelada de la fila se considera más antigua que cualquier dato normal y siempre está visible en todas las instantáneas de datos. Además, ya no es necesario mirar el número de transacción xmin, y este número puede reutilizarse de forma segura. Por lo tanto, las versiones congeladas de cadenas siempre permanecen en el pasado.

Para marcar el número de transacción xmin como congelado, ambos bits de sugerencia se establecen al mismo tiempo: el bit de confirmación y el bit de cancelación.
Tenga en cuenta que la transacción xmax no necesita ser congelada. Su presencia significa que esta versión de la cadena ya no es relevante. Una vez que deja de ser visible en las instantáneas de datos, esta versión de la fila se borrará.
Para experimentos, crea una tabla. Establecemos el factor de relleno mínimo para que solo quepan dos líneas en cada página, por lo que será más conveniente para nosotros observar lo que está sucediendo. Y apague la automatización para controlar el tiempo de limpieza usted mismo.
=> CREATE TABLE tfreeze( id integer, s char(300) ) WITH (fillfactor = 10, autovacuum_enabled = off);
Ya hemos creado varias variantes de la función, que, utilizando la extensión pageinspect, mostró la versión de las líneas que están en la página. Ahora crearemos otra variante de la misma función: ahora mostrará varias páginas a la vez y mostrará la antigüedad de la transacción xmin (para esto se utiliza la antigüedad de la función del sistema):
=> CREATE FUNCTION heap_page(relname text, pageno_from integer, pageno_to integer) RETURNS TABLE(ctid tid, state text, xmin text, xmin_age integer, xmax text, t_ctid tid) AS $$ SELECT (pageno,lp)::text::tid AS ctid, CASE lp_flags WHEN 0 THEN 'unused' WHEN 1 THEN 'normal' WHEN 2 THEN 'redirect to '||lp_off WHEN 3 THEN 'dead' END AS state, t_xmin || CASE WHEN (t_infomask & 256+512) = 256+512 THEN ' (f)' WHEN (t_infomask & 256) > 0 THEN ' (c)' WHEN (t_infomask & 512) > 0 THEN ' (a)' ELSE '' END AS xmin, age(t_xmin) xmin_age, t_xmax || CASE WHEN (t_infomask & 1024) > 0 THEN ' (c)' WHEN (t_infomask & 2048) > 0 THEN ' (a)' ELSE '' END AS xmax, t_ctid FROM generate_series(pageno_from, pageno_to) p(pageno), heap_page_items(get_raw_page(relname, pageno)) ORDER BY pageno, lp; $$ LANGUAGE SQL;
Tenga en cuenta que el signo de congelación (que mostramos con la letra f entre paréntesis) está determinado por la instalación simultánea de mensajes confirmados y cancelados. Muchas fuentes (incluida la documentación) mencionan el número especial FrozenTransactionId = 2, que marca las transacciones congeladas. Tal sistema funcionó hasta la versión 9.4, pero ahora ha sido reemplazado por bits de información sobre herramientas; esto le permite guardar el número de transacción original en la versión de línea, lo cual es conveniente para fines de soporte y depuración. Sin embargo, las transacciones con el número 2 todavía pueden ocurrir en sistemas más antiguos, incluso actualizadas a las últimas versiones.
También necesitamos la extensión pg_visibility, que le permite mirar en el mapa de visibilidad:
=> CREATE EXTENSION pg_visibility;
Antes de PostgreSQL 9.6, el mapa de visibilidad contenía un bit por página; marcó páginas que solo contenían versiones "bastante antiguas" de cadenas que ya están garantizadas para ser visibles en todas las imágenes. La idea aquí es que si la página está marcada en el mapa de visibilidad, entonces para su versión de las líneas no necesita verificar las reglas de visibilidad.
A partir de la versión 9.6, se agregó un mapa congelado a la misma capa, un bit más por página. El mapa de congelación marca las páginas en las que se congelan todas las versiones de las filas.
Insertamos varias filas en la tabla e inmediatamente realizamos la limpieza para crear un mapa de visibilidad:
=> INSERT INTO tfreeze(id, s) SELECT g.id, 'FOO' FROM generate_series(1,100) g(id); => VACUUM tfreeze;
Y vemos que ambas páginas ahora están marcadas en el mapa de visibilidad (all_visible), pero aún no están congeladas (all_frozen):
=> SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('tfreeze',g.blkno) ORDER BY g.blkno;
blkno | all_visible | all_frozen -------+-------------+------------ 0 | t | f 1 | t | f (2 rows)
La antigüedad de la transacción que creó las filas (xmin_age) es 1: esta es la última transacción que se realizó en el sistema:
=> SELECT * FROM heap_page('tfreeze',0,1);
ctid | state | xmin | xmin_age | xmax | t_ctid -------+--------+---------+----------+-------+-------- (0,1) | normal | 697 (c) | 1 | 0 (a) | (0,1) (0,2) | normal | 697 (c) | 1 | 0 (a) | (0,2) (1,1) | normal | 697 (c) | 1 | 0 (a) | (1,1) (1,2) | normal | 697 (c) | 1 | 0 (a) | (1,2) (4 rows)
Edad mínima para congelar
Tres parámetros principales controlan la congelación, y los consideraremos a su vez.
Comencemos con
vacuum_freeze_min_age , que define la edad mínima de transacción xmin a la que se puede congelar la versión de cadena. Cuanto menor sea este valor, más costos innecesarios pueden resultar: si estamos lidiando con datos "activos" que cambian activamente, entonces la congelación de más y más versiones nuevas desaparecerá sin ningún beneficio. En este caso, es mejor esperar.
El valor predeterminado para este parámetro establece que las transacciones comienzan a congelarse después de que hayan pasado 50 millones de otras transacciones desde que aparecieron:
=> SHOW vacuum_freeze_min_age;
vacuum_freeze_min_age ----------------------- 50000000 (1 row)
Para ver cómo ocurre la congelación, reducimos el valor de este parámetro a la unidad.
=> ALTER SYSTEM SET vacuum_freeze_min_age = 1; => SELECT pg_reload_conf();
Y actualizaremos una línea en la página cero. La nueva versión llegará a la misma página debido al pequeño valor del factor de relleno.
=> UPDATE tfreeze SET s = 'BAR' WHERE id = 1;
Esto es lo que vemos ahora en las páginas de datos:
=> SELECT * FROM heap_page('tfreeze',0,1);
ctid | state | xmin | xmin_age | xmax | t_ctid -------+--------+---------+----------+-------+-------- (0,1) | normal | 697 (c) | 2 | 698 | (0,3) (0,2) | normal | 697 (c) | 2 | 0 (a) | (0,2) (0,3) | normal | 698 | 1 | 0 (a) | (0,3) (1,1) | normal | 697 (c) | 2 | 0 (a) | (1,1) (1,2) | normal | 697 (c) | 2 | 0 (a) | (1,2) (5 rows)
Ahora las líneas anteriores a
vacuum_freeze_min_age = 1 deben congelarse. Pero tenga en cuenta que la línea cero no está marcada en el mapa de visibilidad (el comando UPDATE restableció el bit, que cambió la página), y la primera permanece marcada:
=> SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('tfreeze',g.blkno) ORDER BY g.blkno;
blkno | all_visible | all_frozen -------+-------------+------------ 0 | f | f 1 | t | f (2 rows)
Ya
hemos dicho que la limpieza escanea solo las páginas que no están marcadas en el mapa de visibilidad. Y así resulta:
=> VACUUM tfreeze; => SELECT * FROM heap_page('tfreeze',0,1);
ctid | state | xmin | xmin_age | xmax | t_ctid -------+---------------+---------+----------+-------+-------- (0,1) | redirect to 3 | | | | (0,2) | normal | 697 (f) | 2 | 0 (a) | (0,2) (0,3) | normal | 698 (c) | 1 | 0 (a) | (0,3) (1,1) | normal | 697 (c) | 2 | 0 (a) | (1,1) (1,2) | normal | 697 (c) | 2 | 0 (a) | (1,2) (5 rows)
En la página cero, una versión está congelada, pero la primera página no consideró la limpieza en absoluto. Por lo tanto, si solo quedan versiones actuales en la página, la limpieza no llegará a dicha página y no las congelará.
=> SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('tfreeze',g.blkno) ORDER BY g.blkno;
blkno | all_visible | all_frozen -------+-------------+------------ 0 | t | f 1 | t | f (2 rows)
Edad para congelar toda la mesa
Para congelar la versión de las líneas que quedan en las páginas que la limpieza simplemente no ve, se proporciona un segundo parámetro:
vacuum_freeze_table_age . Determina la antigüedad de la transacción, en la cual la limpieza ignora el mapa de visibilidad y pasa por todas las páginas de la tabla para congelarse.
Cada tabla almacena un número de transacción, por lo que se sabe que todas las transacciones anteriores están garantizadas para congelarse (pg_class.relfrozenxid). Con la antigüedad de esta transacción recordada, se
compara el valor del parámetro
vacuum_freeze_table_age .
=> SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 'tfreeze';
relfrozenxid | age --------------+----- 694 | 5 (1 row)
Antes de PostgreSQL 9.6, la limpieza realizó un escaneo completo de la tabla para asegurar que todas las páginas fueran rastreadas. Para mesas grandes, esta operación fue larga y triste. El asunto se agravó por el hecho de que si la limpieza no llegaba al final (por ejemplo, un administrador impaciente interrumpía la ejecución de un comando), era necesario comenzar desde el principio.
A partir de la versión 9.6, gracias al mapa congelado (que vemos en la columna all_frozen en la salida pg_visibility_map), al borrar solo se omiten aquellas páginas que no están marcadas en el mapa. Esto no es solo una cantidad de trabajo mucho menor, sino también una resistencia a las interrupciones: si el proceso de limpieza se detiene y comienza de nuevo, no tendrá que volver a mirar las páginas que ya logró marcar en el mapa de congelación la última vez.
De una forma u otra, todas las páginas de la tabla se congelan una vez en las
transacciones (
vacuum_freeze_table_age -
vacuum_freeze_min_age ). Con los valores predeterminados, esto ocurre una vez por millón de transacciones:
=> SHOW vacuum_freeze_table_age;
vacuum_freeze_table_age ------------------------- 150000000 (1 row)
Por lo tanto, está claro que no se debe establecer demasiada
aspiración_freeze_min_age , porque en lugar de reducir la sobrecarga, esto comenzará a aumentarlos.
Veamos cómo se congela toda la tabla, y para hacer esto, reduzca
vacuum_freeze_table_age a 5 para que se cumpla la condición de congelación.
=> ALTER SYSTEM SET vacuum_freeze_table_age = 5; => SELECT pg_reload_conf();
Vamos a limpiar:
=> VACUUM tfreeze;
Ahora, dado que se ha garantizado la verificación de toda la tabla, se puede aumentar el número de la transacción congelada; estamos seguros de que las páginas no tienen una transacción anterior no congelada.
=> SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 'tfreeze';
relfrozenxid | age --------------+----- 698 | 1 (1 row)
Ahora todas las versiones de las líneas en la primera página están congeladas:
=> SELECT * FROM heap_page('tfreeze',0,1);
ctid | state | xmin | xmin_age | xmax | t_ctid -------+---------------+---------+----------+-------+-------- (0,1) | redirect to 3 | | | | (0,2) | normal | 697 (f) | 2 | 0 (a) | (0,2) (0,3) | normal | 698 (c) | 1 | 0 (a) | (0,3) (1,1) | normal | 697 (f) | 2 | 0 (a) | (1,1) (1,2) | normal | 697 (f) | 2 | 0 (a) | (1,2) (5 rows)
Además, la primera página está marcada en el mapa de congelación:
=> SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('tfreeze',g.blkno) ORDER BY g.blkno;
blkno | all_visible | all_frozen -------+-------------+------------ 0 | t | f 1 | t | t (2 rows)
Edad para la respuesta "agresiva"
Es importante que las versiones de fila se congelen a tiempo. Si surge una situación en la que una transacción que aún no se ha congelado corre el riesgo de entrar en el futuro, PostgreSQL se bloqueará para evitar posibles problemas.
¿Cuál podría ser la razón de esto? Hay varias razones
- La limpieza automática se puede desactivar, y la limpieza regular tampoco comienza. Ya hemos dicho que esto no es necesario, pero técnicamente es posible.
- Incluso la limpieza automática incluida no llega a las bases de datos que no se utilizan (recuerde el parámetro track_counts y la base de datos template0).
- Como vimos la última vez , la limpieza omite las tablas en las que solo se agregan datos, pero no se eliminan o cambian.
En tales casos, se proporciona una operación de
limpieza automática "agresiva", y está regulada por el parámetro
autovacuum_freeze_max_age . Si en cualquier tabla de cualquier base de datos puede haber una transacción no congelada anterior a la edad especificada en el parámetro, la limpieza automática comienza a la fuerza (incluso si está desactivada) y tarde o temprano llegará a la tabla de problemas (independientemente de los criterios habituales).
El valor predeterminado es bastante conservador:
=> SHOW autovacuum_freeze_max_age;
autovacuum_freeze_max_age --------------------------- 200000000 (1 row)
El límite para
autovacuum_freeze_max_age es de 2 mil millones de transacciones, y se utiliza un valor 10 veces menor. Esto tiene sentido: al aumentar el valor, aumentamos el riesgo de que durante el tiempo restante, la limpieza automática simplemente no tenga tiempo de congelar todas las versiones necesarias de las líneas.
Además, el valor de este parámetro determina el tamaño de la estructura XACT: dado que no debería haber ninguna transacción anterior en el sistema para la que deba averiguar el estado, la limpieza automática elimina los archivos innecesarios del segmento XACT, liberando espacio.
Veamos cómo la limpieza maneja las tablas de solo agregar, usando tfreeze como ejemplo. Para esta tabla, la limpieza automática generalmente está deshabilitada, pero esto no será un obstáculo.
Cambiar el parámetro
autovacuum_freeze_max_age requiere reiniciar el servidor. Pero todos los parámetros discutidos anteriormente también se pueden establecer a nivel de tablas individuales usando parámetros de almacenamiento. Por lo general, solo tiene sentido hacer esto en casos especiales, cuando la mesa realmente requiere un cuidado especial.
Entonces, estableceremos
autovacuum_freeze_max_age en el nivel de la tabla (y al mismo tiempo devolveremos el factor de relleno normal también). Desafortunadamente, el valor mínimo posible es 100,000:
=> ALTER TABLE tfreeze SET (autovacuum_freeze_max_age = 100000, fillfactor = 100);
Desafortunadamente, porque tenemos que completar 100,000 transacciones para reproducir la situación que nos interesa. Pero, por supuesto, para fines prácticos, este es un valor muy, muy bajo.
Como vamos a agregar datos, insertaremos 100,000 filas en la tabla, cada una en nuestra transacción. Y nuevamente tengo que hacer una reserva de que en la práctica esto no debería hacerse. Pero ahora solo estamos explorando, podemos.
=> CREATE PROCEDURE foo(id integer) AS $$ BEGIN INSERT INTO tfreeze VALUES (id, 'FOO'); COMMIT; END; $$ LANGUAGE plpgsql; => DO $$ BEGIN FOR i IN 101 .. 100100 LOOP CALL foo(i); END LOOP; END; $$;
Como podemos ver, la antigüedad de la última transacción congelada en la tabla ha excedido el valor umbral:
=> SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 'tfreeze';
relfrozenxid | age --------------+-------- 698 | 100006 (1 row)
Pero si espera un poco ahora, en el registro de mensajes del servidor habrá una entrada sobre el vacío agresivo automático de la tabla "test.public.tfreeze", el número de la transacción congelada cambiará y su antigüedad volverá a la decencia:
=> SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 'tfreeze';
relfrozenxid | age --------------+----- 100703 | 3 (1 row)
También existe la posibilidad de congelar las transacciones múltiples, pero aún no hablaremos de eso, lo pospondremos hasta que hablemos de bloqueos para no adelantarnos.
Congelación manual
A veces es conveniente controlar manualmente la congelación en lugar de esperar la llegada de la limpieza automática.
Puede congelar manualmente un comando con el comando VACUUM FREEZE: todas las versiones de fila se congelarán, independientemente de la antigüedad de las transacciones (como si el parámetro
autovacuum_freeze_min_age = 0). Cuando se reconstruye una tabla con los comandos VACUUM FULL o CLUSTER, todas las filas también se congelan.
Para congelar todas las bases de datos, puede usar la utilidad:
vacuumdb --all --freeze
Los datos también se pueden congelar durante la carga inicial utilizando el comando COPIAR especificando el parámetro FREEZE. Para hacer esto, la tabla debe crearse (o vaciarse con el comando TRUNCATE) en el mismo
transacciones como COPIA.
Dado que existen reglas de visibilidad separadas para las filas congeladas, dichas filas serán visibles en instantáneas de datos de otras transacciones en violación de las reglas de aislamiento habituales (esto se aplica a las transacciones con el nivel de lectura repetible o serializable).
Para verificar esto, en otra sesión, inicie una transacción con el nivel de aislamiento de lectura repetible:
| => BEGIN ISOLATION LEVEL REPEATABLE READ; | => SELECT txid_current();
Tenga en cuenta que esta transacción generó una instantánea de los datos, pero no accedió a la tabla tfreeze. Ahora vaciaremos la tabla tfreeze y cargaremos nuevas filas en ella en una transacción. Si una transacción paralela lee el contenido de tfreeze, el comando TRUNCATE se bloqueará hasta el final de la transacción.
=> BEGIN; => TRUNCATE tfreeze; => COPY tfreeze FROM stdin WITH FREEZE;
1 FOO 2 BAR 3 BAZ \.
=> COMMIT;
Ahora una transacción paralela ve datos nuevos, aunque esto rompe el aislamiento:
| => SELECT count(*) FROM tfreeze;
| count | ------- | 3 | (1 row)
| => COMMIT;
Pero, dado que es poco probable que dicha carga de datos ocurra regularmente, esto generalmente no es un problema.
Significativamente peor, COPIAR CON CONGELAR no funciona con el mapa de visibilidad: las páginas cargadas no están marcadas como que solo contienen versiones de las líneas que son visibles para todos. Por lo tanto, cuando accede por primera vez a la tabla, la limpieza se ve obligada a volver a procesar todo y crear un mapa de visibilidad. Para empeorar las cosas, las páginas de datos tienen un signo de visibilidad completa en su propio encabezado, por lo que la limpieza no solo lee toda la tabla, sino que también la reescribe por completo, colocando el bit deseado. Desafortunadamente, la solución a este problema no tiene que esperar antes de la versión 13 (
discusión ).
Conclusión
Esto concluye mi serie de artículos sobre aislamiento PostgreSQL y multiversion. Gracias por su atención y especialmente por los comentarios: mejoran el material y a menudo señalan áreas que requieren una atención más cuidadosa de mi parte.
Quédate con nosotros, para continuar!