Cambiar el esquema de tablas PostgreSQL sin bloqueos largos. Conferencia de Yandex

Si al mismo tiempo se realizan muchas operaciones para cambiar el esquema de la base de datos, el servicio no puede funcionar correctamente en la grabación. El desarrollador Vladimir Kolyasinsky explicó qué operaciones en PostgreSQL requieren bloqueos a largo plazo y cómo el equipo Yandex.Connect proporciona casi el 100% de acceso de escritura al servicio durante tales operaciones. Además, aprenderá acerca de la biblioteca para Django, que está diseñada para automatizar parte de los procesos descritos.


Tenemos cargas pesadas, miles de RPS y el tiempo de inactividad en unos minutos, sin mencionar más tiempo, es inaceptable. Es necesario que las migraciones pasen desapercibidas para el usuario. Y con tales cargas no será posible levantarse a las cuatro de la mañana, rodar algo cuando no hay carga y volver a la cama, porque la carga va las 24 horas.

- Buenas tardes a todos! Mi nombre es Vladimir, llevo cinco años trabajando en Yandex. Los últimos dos años he estado desarrollando servicios internos y servicios para organizaciones.

Un poco sobre cuáles son estos servicios para las organizaciones. Hemos estado utilizando una gran cantidad de servicios internos durante mucho tiempo: un wiki para almacenar e intercambiar datos, un mensajero para una comunicación rápida con colegas, un rastreador para organizar el proceso de trabajo, formularios para realizar encuestas por dentro y por fuera, así como muchos otros servicios.

Hace algún tiempo, decidimos que nuestros servicios son geniales y que pueden ser útiles no solo dentro de Yandex, sino también a personas externas. Comenzamos a llevarlos a una plataforma unificada Yandex.Connect, agregando servicios externos existentes allí, como Correo para un dominio.



Actualmente estoy desarrollando el Diseñador de formularios y Wiki. La pila utilizada es principalmente servicios escritos en Python de la segunda y tercera versión; Django 1.9-1.11. Como base de datos, la mayor parte es PostgreSQL. También es Apio con MongoDB y SQS como intermediarios. Todo esto funciona en Docker.

Pasemos al problema que enfrentamos. Los servicios son populares, son utilizados por cientos de miles de personas todos los días, los datos se acumulan, las tablas se vuelven cada vez más y, con el tiempo, muchas operaciones de cambio de esquemas de bases de datos, que los usuarios realizaron desapercibidas ayer, comienzan a impedir el funcionamiento normal de los servicios.

Hoy hablaremos sobre cómo hacemos frente a tales situaciones y cómo logramos una alta disponibilidad de servicios de lectura y escritura.

Primero, consideremos qué operaciones con PostgreSQL requieren bloqueos largos en la tabla. Por bloqueo, me refiero a cualquier tipo de bloqueo que impide el funcionamiento normal de la tabla, ya sea acceso exclusivo, que impide la escritura y la lectura, o niveles de bloqueo más débiles que impiden solo la escritura.

A continuación, veremos cómo evitar bloqueos durante tales operaciones. Luego hablaremos sobre qué operaciones con PostgreSQL son inicialmente rápidas y no requieren bloqueos largos. Y al final, hablemos de nuestra biblioteca zero_downtime_migrations, que usamos para automatizar algunas de las técnicas descritas anteriormente para evitar bloqueos largos.

Operaciones que requieren un bloqueo largo:



Creando un índice. De forma predeterminada, no bloquea las operaciones de lectura en la tabla, pero todas las operaciones de escritura se bloquearán durante todo el tiempo que se cree el índice; en consecuencia, el servicio será de solo lectura.

Además, tales operaciones incluyen agregar una nueva columna con un valor predeterminado, ya que debajo del capó PostgreSQL sobrescribirá toda la tabla, y por este tiempo estará bloqueado tanto para leer como para escribir. Además, se sobrescribirán todos sus índices.

Acerca de cambiar el tipo de columna: sucederá algo similar, la placa también se sobrescribirá nuevamente. Cabe señalar que esto no solo lleva mucho tiempo en tablas grandes, sino que también por un corto tiempo requiere hasta el doble de la cantidad de memoria libre ocupada por la tabla.

Además, la operación VACUUM FULL requiere el mismo nivel de bloqueo que las operaciones anteriores; esto es acceso exclusivo. VACUUM FULL también bloqueará todas las operaciones de lectura y escritura en la tabla.

Las dos últimas operaciones son agregar propiedades únicas a la columna y, en general, agregar CONSTRAINT. También requieren bloqueo durante la verificación de datos, aunque tardan mucho menos tiempo que los considerados anteriormente, ya que no sobrescriben las tablas debajo del capó.





Creando un índice. Aquí es bastante simple, se puede crear usando la palabra clave CONCURRENTEMENTE. Cual es la diferencia Esta operación llevará más tiempo, ya que no se realizarán una, sino varias pasadas a través de la tabla, y también esperará la finalización de todas las operaciones actuales que potencialmente pueden cambiar el índice. Y también puede fallar, por ejemplo, si se viola un índice único al crear un índice único. Luego, el índice se marcará como no válido y deberá ser eliminado y recreado. No se recomienda el comando REINDEX, ya que funciona igual que el CREATE INDEX normal, es decir, bloqueará la tabla para escribir.

Con respecto a la eliminación del índice, a partir de la versión 9.3, también puede eliminar el índice CONCURRENTEMENTE para evitar el bloqueo durante su eliminación, aunque en general es una operación tan rápida.



Veamos cómo agregar una nueva columna con un valor predeterminado. Aquí hay una operación estándar que se realiza cuando queremos ejecutar dicho comando, incluso Django realiza dicha operación.

¿Cómo puedo reescribirlo para evitar sobrescribir la tabla? Primero, en una transacción, agregue una nueva columna sin un valor predeterminado y agregue un valor predeterminado en una solicitud separada. ¿Cuál es la diferencia aquí? Cuando agregamos un valor predeterminado a una columna existente, esto no cambia los datos existentes en la tabla. Solo los metadatos cambian. Es decir, para todas las líneas nuevas, este valor predeterminado ya estará garantizado. Nos queda por actualizar todas las filas existentes que estaban en la tabla en el momento en que se ejecutó este comando. Lo que haremos en lotes de varios miles de copias para no bloquear durante mucho tiempo una gran cantidad de datos.

Después de actualizar todos los datos, solo queda ejecutar SET NOT NULL si creamos una columna NOT NULL. Si no creamos, entonces no. De esta forma, puede evitar sobrescribir la tabla al hacer este tipo de cambio.

Dicha secuencia de comandos lleva más tiempo que la ejecución de un comando regular, ya que depende del tamaño de la tabla y del número de índices que contiene, y el comando habitual simplemente bloquea todas las operaciones y sobrescribe la tabla independientemente de la carga, ya que no hay carga en este momento. Pero esto no importa tanto, porque durante la operación la tabla está disponible para leer y escribir. Lleva mucho tiempo, solo necesitas seguir esto y eso es todo.



Acerca de cambiar el tipo de columna. El enfoque es similar a agregar una columna con un valor predeterminado. Primero agregamos una columna separada del tipo que necesitamos, luego agregamos disparadores para cambiar los datos en la columna original para escribir en ambas columnas a la vez, a una nueva con el tipo de datos que necesitamos. Para todas las entradas nuevas, irán inmediatamente a ambas columnas. Necesitamos actualizar todos los existentes. Lo que hacemos en porciones, como en la diapositiva anterior, fue similar.

Después de eso, permanece en una transacción para eliminar el activador, eliminar la columna anterior y cambiar el nombre de la columna anterior a una nueva. Por lo tanto, logramos el mismo resultado: cambiamos el tipo de columna, mientras que el bloqueo de la tabla no fue largo.



Acerca de agregar una columna única. Se toma una cerradura en el momento de la creación. Se puede evitar si sabe que la unicidad en PostgreSQL está garantizada mediante la creación de un índice único. Nosotros mismos podemos construir el índice único requerido usando CONCURRENTEMENTE. Y después de construir este índice, cree CONSTRAINT usando este índice. Después de esto, la definición del índice inicial de la tabla desaparecerá, y el resultado que la definición de la tabla nos mostrará no será diferente después de realizar estas dos operaciones.



Y en general, al agregar CONSTRAINT. Puede usar esta técnica para evitar el bloqueo mientras verifica los datos. Primero agregamos CONSTRAINT con la palabra clave NOT VALID. Esto significa que no se garantiza que esta CONSTRAINT se ejecute para todas las filas de la tabla. Pero al mismo tiempo, para todas las líneas nuevas, esta CONSTRAINT ya se aplicará, y se lanzarán las excepciones correspondientes si no se ejecuta.

Solo podemos validar todos los valores existentes, lo que se puede hacer con un comando VALIDATE CONSTRAINT separado, y al mismo tiempo este comando ya no interfiere con la lectura o la escritura en la tabla. Una mesa para este tiempo estará disponible.

Operaciones que inicialmente funcionan rápidamente en PostgreSQL y no requieren bloqueos largos:



Una de estas operaciones es agregar una columna sin valores predeterminados y sin restricciones. Como no se realizan cambios en la tabla en sí, solo cambian sus metadatos. Y todos los valores NULL que vemos como resultado de SELECT se mezclan simplemente en la salida.

Además, agregar valores predeterminados a una etiqueta existente es una operación rápida porque solo cambian los metadatos. La tabla y el bloqueo se toman literalmente durante los pocos milisegundos necesarios para ingresar esta información.

Además, la operación rápida de establecer SET NOT NULL, aquí lleva un poco más de tiempo que lo descrito anteriormente, unos pocos segundos por tabla de 30 millones de registros. Este tiempo también se puede evitar si es importante.

Cambiar el nombre de una columna, cambiar la longitud de una columna tampoco conduce a sobrescribir una columna. Eliminar una columna y, en general, muchas entidades en PostgreSQL también es una operación rápida.



En cuanto a la adición de una columna NOT NULL. Para evitar el bloqueo durante la validación, puede realizar el método mencionado anteriormente: agregue CONSTRAINT correspondiente a CHECK (la columna NO ES NULO) NO ES VÁLIDO y valide con un comando por separado.

La diferencia en general es que esta restricción existirá a nivel de tabla, y no a nivel de columna en la definición de la tabla. Otra diferencia es que puede afectar el rendimiento, aproximadamente el uno por ciento. En este caso, no habrá bloqueo, si el servicio está muy cargado, incluso unos pocos segundos de bloqueo pueden generar una enorme cola de transacciones y habrá un problema en el servicio.



La eliminación de datos en PostgreSQL generalmente es una operación rápida, ya que los datos no se eliminan de inmediato, solo la columna está marcada como obsoleta en los atributos de la tabla, y los datos se eliminarán solo después del inicio del próximo vacío.



Hablemos de la biblioteca . Estoy hablando de Django, migración. En general, Django es una biblioteca para Python, un marco web, originalmente fue creado para crear rápidamente sitios web como noticias, desde entonces se ha actualizado significativamente. Hay un sistema ORM que le permite comunicarse con registros en la base de datos, con tablas, como si fueran objetos o clases de Python. Es decir, cada tabla tiene su propia clase en Python. Y cuando hacemos cambios en nuestro código de Python, es decir, agregamos nuevos atributos como columnas a la tabla, Django durante el proceso de creación de la migración notifica estos cambios y crea los archivos de migración para hacer cambios espejo en la base de datos para que no diverjan.

La biblioteca fue escrita para automatizar algunas de las técnicas discutidas anteriormente para evitar bloqueos largos en la mesa durante tales migraciones. Ha estado trabajando con Django desde la versión 1.8 a 2.1 inclusive, y Python desde 2.7 a 3.7 inclusive.

Con respecto a las características actuales de la biblioteca, esto está agregando una columna con un valor predeterminado sin bloqueos, anulables o no, esto está creando un índice CONCURRENTEMENTE, así como la capacidad de reiniciarse cuando se bloquea. En la implementación estándar de Django, si agregamos una columna con un valor predeterminado, la tabla está bloqueada, y si es grande, podría ser 40 minutos de bloqueo en mi experiencia. La tabla está bloqueada, y eso es todo, espere hasta que los cambios se copien y se realicen. Pasaron 30 minutos: detectaron el error de conexión a la base de datos, la migración se cae, los cambios no se confirman y hay que comenzar de nuevo, esperar 40 minutos nuevamente y volver a bloquear la tabla esta vez.


Enlace GitHub

La biblioteca le permite reanudar la migración desde el lugar donde fue interrumpida. Cuando se bloquea y reinicia, se muestra un cuadro de diálogo donde hay varias opciones de acción, es decir, puede decir que continúe actualizando los datos. Esta suele ser una actualización de datos porque es el proceso más largo. La migración simplemente continuará desde donde se quedó. Tal operación también lleva más tiempo que una operación estándar con bloqueo de mesa, pero al mismo tiempo, el servicio permanece operativo en este momento.



Sobre la conexión en su conjunto. Hay documentación; en resumen, debe reemplazar el motor en la configuración de la base de datos de Django con el motor de la biblioteca. También hay varios mixins si usa sus motores para conectarse.



Un ejemplo de trabajo consiste en agregar una columna con un valor predeterminado. Aquí agregamos columnas con un valor booleano, True por defecto. ¿Qué operaciones realiza el SchemaEditor estándar? Las operaciones que puede ver si ejecuta SQL migrate. Esto es bastante útil, por el mismo tipo de migración, no siempre está claro qué puede cambiar Django allí. Y es útil comenzar y ver si las operaciones esperadas por nosotros se han completado y si algo superfluo e innecesario ha llegado allí.

¿Qué comandos ejecuta SchemaEditor? Primero, se agrega una nueva columna a una transacción, se agrega el valor predeterminado. Luego, hasta que dicha actualización regrese que haya actualizado cero, los datos se actualizarán.

Luego, SET NOT NULL se establece en la columna, y el valor predeterminado se eliminará, repitiendo el comportamiento de Django, que almacena el valor predeterminado no en la base de datos, sino en su propio nivel lógico en el código.

Aquí, en general, también hay espacio para crecer. Por ejemplo, puede crear un índice auxiliar para encontrar rápidamente esas filas con un valor NULL a medida que se acerca a actualizar toda la tabla.



También puede corregir la identificación máxima para el tiempo de actualización cuando comenzamos la migración, de modo que por identificación pueda encontrar rápidamente valores que aún no hemos actualizado.

En general, la biblioteca se está desarrollando, aceptamos solicitudes de grupo. A quién le importa, únete.

Vale la pena prestar atención a que con el crecimiento de las bases de datos, las migraciones tienen una propiedad inevitable para frenar. Debe realizar un seguimiento de los bloqueos que toma la tabla, ejecutar migraciones de SQL para ver qué operaciones se aplican. Por nuestra parte, en Yandex.Connect usamos esta biblioteca donde sus capacidades lo permiten. Y donde no lo permiten, nosotros mismos, con nuestras propias manos, migraciones falsas de Django, ejecutamos nuestras consultas SQL.

Por lo tanto, logramos alta disponibilidad de servicios de lectura y escritura. Tenemos cargas pesadas, miles de RPS y el tiempo de inactividad en unos minutos, sin mencionar más tiempo, es inaceptable. Es necesario que las migraciones pasen desapercibidas para el usuario. Y con tales cargas, no será posible levantarse a las cuatro de la mañana, rodar algo cuando no hay carga y volver a la cama, porque la carga va las 24 horas.

Vale la pena señalar que incluso las operaciones rápidas en PostgreSQL pueden causar una desaceleración del servicio y errores debido a la forma en que funciona la cola de bloqueo en PostgreSQL.

Imagine que se lanza una operación que, incluso por unos pocos milisegundos, requiere acceso exclusivo. Un ejemplo de tal operación es agregar una columna sin un valor predeterminado. Imagine que en el momento de su lanzamiento en otra transacción, hay otra operación larga, por ejemplo, SELECCIONAR con agregación. En este caso, nuestra operación hará cola para ella. Esto sucederá porque el acceso exclusivo entra en conflicto con todos los demás tipos de bloqueos.

Mientras nuestra operación de agregar una columna está esperando un bloqueo, todos los demás lo pondrán en cola y no se ejecutarán hasta que se complete. Al mismo tiempo, la operación que se está realizando (SELECCIONAR con agregación) puede no entrar en conflicto con las demás, y si no fuera por nuestra creación de la columna, no se habrían puesto en la cola, sino que se habrían ejecutado en paralelo.

Esta situación puede crear grandes problemas en el servicio. Por lo tanto, antes de iniciar ALTER TABLE o cualquier otra operación que requiera bloqueo exclusivo de acceso, debe buscar para que las consultas largas no vayan a la base de datos en este momento. O simplemente puede insertar un tiempo de espera de registro muy pequeño. Entonces, si no fuera posible tomar rápidamente la cerradura, la operación se caería. Podríamos reiniciarlo y no bloquear la tabla durante mucho tiempo, mientras que la operación esperará la concesión de una concesión para los bloqueos. Eso es todo, gracias.

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


All Articles