Migrar un esquema de base de datos sin tiempo de inactividad para postgresql usando django como ejemplo

Introduccion


Hola Habr!


Quiero compartir la experiencia de escribir migraciones para postgres y django. Esto será principalmente sobre postgres, django es una buena adición aquí, ya que tiene una migración automática del esquema de datos para cambios de modelo listos para usar, es decir, tiene una lista bastante completa de operaciones de trabajo para cambiar el esquema. Django se puede reemplazar con cualquier marco / biblioteca favorito: los enfoques probablemente serán similares.


No describiré cómo llegué a esto, pero ahora que leo la documentación me doy cuenta de que era necesario hacer esto antes con más cuidado y conciencia, por lo que lo recomiendo encarecidamente.


Antes de continuar, déjame hacer las siguientes suposiciones.


Puede dividir la lógica de trabajar con la base de datos de la mayoría de las aplicaciones en 3 partes:


  1. Migraciones: al cambiar el esquema de la base de datos (tablas), supongamos que siempre las ejecutamos en un hilo.
  2. Lógica empresarial: el trabajo directo con datos (en tablas de usuario), trabaja con los mismos datos de forma constante y competitiva.
  3. Migraciones de datos: no cambien los esquemas de datos, funcionan esencialmente como la lógica de negocios, de forma predeterminada, cuando hablamos de lógica de negocios, también nos referiremos a las migraciones de datos.

El tiempo de inactividad es un estado en el que parte de nuestra lógica empresarial no está disponible / cae / se carga durante un tiempo notable para el usuario, supongamos que esto es un par de segundos.


La ausencia de tiempo de inactividad puede ser una condición crítica para un negocio, que debe ser respetada por cualquier esfuerzo.


Proceso de despliegue


Los principales requisitos al implementar:


  1. Tenemos una base de trabajo.
  2. Tenemos varias máquinas donde la lógica de negocios gira.
  3. Los autos con lógica de negocios están ocultos detrás del balanceador.
  4. nuestra aplicación funciona bien antes, durante y después de la migración continua (el código antiguo funciona correctamente con el esquema de base de datos antiguo y nuevo).
  5. Nuestra aplicación funciona bien antes, durante y después de actualizar el código en los automóviles (el código antiguo y el nuevo funcionan correctamente con el esquema de base de datos actual).

Si hay una gran cantidad de cambios y el despliegue deja de satisfacer estas condiciones, entonces se divide en el número requerido de despliegues más pequeños que satisfacen estas condiciones, de lo contrario, tenemos un tiempo de inactividad.


Orden de despliegue directo:


  1. inundó la migración;
  2. quitó una máquina del equilibrador, actualizó la máquina y reinició, devolvió la máquina al equilibrador;
  3. repitió el paso anterior para actualizar todos los autos.

El orden de despliegue inverso es relevante para eliminar tablas y columnas en una tabla, cuando creamos automáticamente migraciones de acuerdo con el esquema modificado y validamos la presencia de todas las migraciones a CI:


  1. quitó una máquina del equilibrador, actualizó la máquina y reinició, devolvió la máquina al equilibrador;
  2. repitió el paso anterior para actualizar todos los autos;
  3. inundó la migración.

Teoría


Postgres es una excelente base de datos, podemos escribir una aplicación que escribirá y leerá los mismos datos en cientos y miles de flujos, y con una alta probabilidad podemos estar seguros de que nuestros datos seguirán siendo válidos y no se dañarán, en general, ACID completo. Postgres implementa varios mecanismos para lograr esto; uno de ellos es el bloqueo.


Postgres tiene varios tipos de bloqueos, puede ver más detalles aquí , como parte del tema, solo cubriré los bloqueos de tabla y escritura.


Cerraduras a nivel de mesa


A nivel de tabla, postgres tiene varios tipos de bloqueos , la característica principal es que tienen conflictos, es decir, dos operaciones con bloqueos en conflicto no se pueden realizar simultáneamente:


ACCESS SHAREROW SHAREROW EXCLUSIVESHARE UPDATE EXCLUSIVESHARESHARE ROW EXCLUSIVEEXCLUSIVEACCESS EXCLUSIVE
ACCESS SHAREX
ROW SHAREXX
ROW EXCLUSIVEXXXX
SHARE UPDATE EXCLUSIVEXXXXX
SHAREXXXXX
SHARE ROW EXCLUSIVEXXXXXX
EXCLUSIVEXXXXXXX
ACCESS EXCLUSIVEXXXXXXXX

Por ejemplo, ALTER TABLE tablename ADD COLUMN newcolumn integer y SELECT COUNT(*) FROM tablename deben ejecutarse estrictamente uno por uno, de lo contrario no podemos averiguar qué columnas volver a COUNT(*) .


En las migraciones de django (lista completa a continuación), existen las siguientes operaciones y sus bloqueos correspondientes:


bloqueooperaciones
ACCESS EXCLUSIVECREATE SEQUENCE , DROP SEQUENCE , CREATE TABLE , DROP TABLE , ALTER TABLE , DROP INDEX
SHARECREATE INDEX
SHARE UPDATE EXCLUSIVECREATE INDEX CONCURRENTLY , DROP INDEX CONCURRENTLY , ALTER TABLE VALIDATE CONSTRAINT

De los comentarios, no todas las ALTER TABLE tienen un ACCESS EXCLUSIVE bloqueo, tampoco las migraciones de django no tienen CREATE INDEX CONCURRENTLY y ALTER TABLE VALIDATE CONSTRAINT , pero serán necesarias para una alternativa más segura a las operaciones estándar un poco más tarde.


Si las migraciones se realizan en un hilo secuencialmente, entonces todo se ve bien, ya que la migración no entrará en conflicto con otra migración, pero nuestra lógica de negocios funcionará solo durante la migración y el conflicto.


bloqueooperacionesconflictos con cerradurasconflictos con operaciones
ACCESS SHARESELECTACCESS EXCLUSIVEALTER TABLE , DROP INDEX ALTER TABLE DROP INDEX
ROW SHARESELECT FOR UPDATEACCESS EXCLUSIVE , EXCLUSIVEALTER TABLE , DROP INDEX ALTER TABLE DROP INDEX
ROW EXCLUSIVEINSERT , UPDATE , DELETEACCESS EXCLUSIVE , EXCLUSIVE , SHARE ROW EXCLUSIVE , SHAREALTER TABLE , DROP INDEX , CREATE INDEX

Aquí se pueden resumir dos puntos:


  1. Si hay una alternativa con un bloqueo más fácil, puede usarla como CREATE INDEX y CREATE INDEX CONCURRENTLY .
  2. la mayoría de las migraciones para cambiar el esquema de datos entran en conflicto con la lógica comercial, además, entran en conflicto con ACCESS EXCLUSIVE , es decir, ni siquiera podremos SELECT mientras mantenemos este bloqueo y posiblemente esperamos un tiempo de inactividad aquí, excepto en el caso en que esta operación no funcione instantáneamente y nuestro tiempo de inactividad será Un par de segundos.

Debe haber una opción, o siempre evitamos ACCESS EXCLUSIVE , es decir, creamos nuevas placas y copiamos los datos allí, de manera confiable, pero durante mucho tiempo para una gran cantidad de datos, o hacemos ACCESS EXCLUSIVE más rápido posible y hacemos advertencias adicionales contra el tiempo de inactividad: es potencialmente peligroso, pero rápido.


Grabar cerraduras


En el nivel de grabación, también hay bloqueos https://www.postgresql.org/docs/current/static/explicit-locking.html#LOCKING-ROWS , también entran en conflicto, pero solo afectan nuestra lógica comercial:


FOR KEY SHAREFOR SHAREFOR NO KEY UPDATEFOR UPDATE
FOR KEY SHAREX
FOR SHAREXX
FOR NO KEY UPDATEXXX
FOR UPDATEXXXX

Este es el punto principal en las migraciones de datos, es decir, si UPDATE la migración de datos en toda la placa, entonces el resto de la lógica de negocios, que actualiza los datos, esperará a que se libere el bloqueo y puede exceder nuestro umbral de tiempo de inactividad, por lo tanto, es mejor hacer actualizaciones en partes para migraciones de datos. También vale la pena señalar que cuando se usan consultas sql más complejas para migraciones de datos, la división en partes puede funcionar más rápido, ya que puede usar un plan e índices más óptimos.


El orden de las operaciones


Otro conocimiento importante es cómo se realizarán las operaciones, cuándo y cómo toman y liberan bloqueos:


imagen


Aquí puede resaltar los siguientes elementos:


  1. tiempo de ejecución de la operación: para la migración, es el momento de mantener el bloqueo, si el bloqueo pesado se mantiene durante mucho tiempo, tendremos un tiempo de inactividad, por ejemplo, puede ser con CREATE INDEX o ALTER TABLE ADD COLUMN SET DEFAULT (en postgres 11 esto es mejor).
  2. el tiempo de espera para bloqueos en conflicto, es decir, la migración espera hasta que todas las solicitudes en conflicto funcionen, y en este momento las nuevas solicitudes esperarán nuestra migración, las solicitudes lentas pueden ser muy peligrosas aquí, ya sea simplemente no óptimas o analíticas, por lo que no debería haber solicitudes lentas durante migración
  3. la cantidad de solicitudes por segundo: si tenemos muchas solicitudes funcionando durante mucho tiempo, las conexiones gratuitas pueden finalizar rápidamente y, en lugar de un lugar problemático, toda la base de datos puede entrar en tiempo de inactividad (solo habrá un límite de conexión para el superusuario), aquí debe evitar solicitudes lentas, reducir la cantidad de solicitudes por ejemplo, inicie las migraciones durante la carga mínima, separe los componentes críticos en diferentes servicios con sus propias bases de datos.
  4. hay muchas operaciones de migraciones en una transacción: cuantas más operaciones se realicen en una transacción, más tiempo se mantendrá el bloqueo pesado, por lo tanto, es mejor separar las operaciones pesadas, sin ALTER TABLE VALIDATE CONSTRAINT o migraciones de datos en una transacción con un bloqueo pesado.

Tiempos de espera


lock_timeout tiene configuraciones como lock_timeout y statement_timeout , que pueden proteger el inicio de las migraciones, tanto de la migración mal escrita como de las malas condiciones en las que se puede desencadenar la migración. Se pueden instalar tanto globalmente como para la conexión actual.


SET lock_timeout TO '2s' evitará el tiempo de inactividad mientras espera solicitudes / transacciones lentas antes de la migración: https://www.postgresql.org/docs/current/static/runtime-config-client.html#GUC-LOCK-TIMEOUT .


SET statement_timeout TO '2s' evitará el tiempo de inactividad al comenzar una migración pesada con un bloqueo pesado: https://www.postgresql.org/docs/current/static/runtime-config-client.html#GUC-STATEMENT-TIMEOUT .


Puntos muertos


Los puntos muertos en las migraciones no tienen que ver con el tiempo de inactividad, pero no es agradable cuando se escribe la migración, funciona bien en un entorno de prueba, pero atrapa los puntos muertos al rodar en el producto. Las principales fuentes de problemas pueden ser una gran cantidad de operaciones en una transacción y una clave externa, ya que crea bloqueos en ambas tablas, por lo que es mejor separar las operaciones de migración, cuanto más atómicas, mejor.


Almacenamiento de registros


Postgres almacena valores de diferentes tipos de diferentes maneras : si los tipos se almacenan de diferentes maneras, la conversión entre ellos requerirá una reescritura completa de todos los valores, afortunadamente, algunos tipos se almacenan de la misma manera y no necesitan reescribirse cuando se cambian. Por ejemplo, las filas se almacenan de la misma manera, independientemente del tamaño, y la disminución / aumento de la dimensión de una fila no requerirá reescritura, pero la disminución requiere verificar que todas las filas no excedan un tamaño menor. Otros tipos también pueden almacenarse de manera similar y tener características similares.


Control de concurrencia multiversional (MVCC)


Según la documentación , la consistencia de postgres se basa en la multiversion de datos, es decir, cada transacción y operación ve su propia versión de los datos. Esta característica hace frente al acceso competitivo y también tiene un efecto interesante cuando el cambio de un esquema como agregar y eliminar columnas solo cambia el esquema, si no hay operaciones adicionales para cambiar datos, índices o constantes, después de lo cual las operaciones de inserción y actualización a un nivel bajo crearán nuevas registros con todos los valores necesarios, la eliminación marcará el registro correspondiente eliminado. VACUUM o AUTO VACUUM es responsable de limpiar los restos restantes.


Ejemplo de Django


Ahora tenemos una idea de qué tiempo de inactividad puede depender y cómo evitarlo, pero antes de aplicar el conocimiento, puede ver lo que django ofrece de inmediato ( https://github.com/django/django/blob/2.1.2/django /db/backends/base/schema.py y https://github.com/django/django/blob/2.1.2/django/db/backends/postgresql/schema.py ):


operación
1CREATE SEQUENCE
2DROP SEQUENCE
3CREATE TABLE
4 4DROP TABLE
5 5ALTER TABLE RENAME TO
6 6ALTER TABLE SET TABLESPACE
7 7ALTER TABLE ADD COLUMN [SET DEFAULT] [SET NOT NULL] [PRIMARY KEY] [UNIQUE]
8ALTER TABLE ALTER COLUMN [TYPE] [SET NOT NULL|DROP NOT NULL] [SET DEFAULT|DROP DEFAULT]
9 9ALTER TABLE DROP COLUMN
10ALTER TABLE RENAME COLUMN
11ALTER TABLE ADD CONSTRAINT CHECK
12ALTER TABLE DROP CONSTRAINT CHECK
13ALTER TABLE ADD CONSTRAINT FOREIGN KEY
14ALTER TABLE DROP CONSTRAINT FOREIGN KEY
15ALTER TABLE ADD CONSTRAINT PRIMARY KEY
16ALTER TABLE DROP CONSTRAINT PRIMARY KEY
17ALTER TABLE ADD CONSTRAINT UNIQUE
18 añosALTER TABLE DROP CONSTRAINT UNIQUE
19CREATE INDEX
20DROP INDEX

Django cubre mis necesidades de migración muy bien, ahora podemos discutir operaciones seguras y peligrosas para migraciones sin tiempo de inactividad con nuestro conocimiento.


Llamaremos migraciones más seguras con el bloqueo SHARE UPDATE EXCLUSIVE o ACCESS EXCLUSIVE , que funciona al instante.
Llamaremos migraciones peligrosas con los bloqueos SHARE y ACCESS EXCLUSIVE , lo que lleva un tiempo considerable.


Dejaré un enlace útil a la documentación por adelantado con excelentes ejemplos.


Crear y eliminar una tabla


CREATE SEQUENCE , DROP SEQUENCE , CREATE TABLE , DROP TABLE puede llamarse seguro, ya que la lógica de negocios ya no funciona con la tabla migrada, el comportamiento de eliminar una tabla con FOREIGN KEY será un poco más tarde.


Operaciones de hoja de trabajo muy compatibles


ALTER TABLE RENAME TO - No puedo llamarlo seguro, ya que es difícil escribir una lógica que funcione con una tabla así antes y después de la migración.


ALTER TABLE SET TABLESPACE - inseguro, ya que mueve físicamente la placa, y esto puede llevar mucho tiempo en un gran volumen.


Por otro lado, estas operaciones son bastante raras, como alternativa, puede ofrecer la creación de una nueva tabla y copiar datos en ella.


Crear y eliminar columnas


ALTER TABLE ADD COLUMN , ALTER TABLE DROP COLUMN : se puede llamar seguro (creación sin DEFAULT / NOT NULL / PRIMARY KEY / UNIQUE), porque la lógica de negocios ya no funciona con una columna migrada, el comportamiento de eliminar una columna con FOREIGN KEY, otras constantes e índices vendrán después.


ALTER TABLE ADD COLUMN SET DEFAULT , ALTER TABLE ADD COLUMN SET NOT NULL , ALTER TABLE ADD COLUMN PRIMARY KEY , ALTER TABLE ADD COLUMN UNIQUE - operaciones inseguras, porque agregan una columna y, sin liberar bloqueos, actualizan datos con valores predeterminados o crean construcciones como alternativas, columnas anulables y más cambios.


Vale la pena mencionar el SET DEFAULT más rápido en postgres 11, puede considerarse seguro, pero no es muy útil en django, ya que django usa SET DEFAULT solo para llenar la columna y luego hace DROP DEFAULT , y en el intervalo entre la migración y la actualización de las máquinas con lógica empresarial, se pueden crear registros en los que el valor predeterminado estará ausente, es decir, de todos modos, realizar la migración de datos.


Operaciones fuertemente soportadas en una hoja de trabajo


ALTER TABLE RENAME COLUMN : tampoco puedo llamarlo seguro, ya que es difícil escribir una lógica que funcione con una columna de este tipo antes y después de la migración. Por el contrario, esta operación tampoco será frecuente, ya que se puede proponer una alternativa para crear una nueva columna y copiarle datos.


Cambio de columna


ALTER TABLE ALTER COLUMN TYPE : la operación puede ser peligrosa y segura. Seguro si postgres cambia solo el esquema, y ​​los datos ya están almacenados en el formato requerido y no se necesitan verificaciones de tipo adicionales, por ejemplo:


  • cambio de tipo de varchar(LESS) a varchar(MORE) ;
  • cambio de tipo de varchar(ANY) a text ;
  • escriba cambio de numeric(LESS, SAME) a numeric(MORE, SAME) .

ALTER TABLE ALTER COLUMN SET NOT NULL es peligroso porque pasa a través de los datos en el interior y verifica NULL, afortunadamente esta construcción puede ser reemplazada por otra CHECK IS NOT NULL . Vale la pena señalar que este reemplazo conducirá a un esquema diferente, pero con propiedades idénticas.


ALTER TABLE ALTER COLUMN DROP NOT NULL , ALTER TABLE ALTER COLUMN SET DEFAULT , ALTER TABLE ALTER COLUMN DROP DEFAULT


Crear y eliminar índices y constantes


ALTER TABLE ADD CONSTRAINT CHECK y ALTER TABLE ADD CONSTRAINT FOREIGN KEY son operaciones inseguras, pero se pueden declarar como NOT VALID y luego ALTER TABLE VALIDATE CONSTRAINT .


ALTER TABLE ADD CONSTRAINT PRIMARY KEY y ALTER TABLE ADD CONSTRAINT UNIQUE no ALTER TABLE ADD CONSTRAINT UNIQUE seguros, ya que crean un índice único en su interior, pero puede crear un índice único como CONCURRENTLY , luego cree la constante correspondiente utilizando un índice listo a través de USING INDEX .


CREATE INDEX es una operación insegura, pero se puede crear un índice como CONCURRENTLY .


ALTER TABLE DROP CONSTRAINT CHECK ALTER TABLE DROP CONSTRAINT FOREIGN KEY , ALTER TABLE DROP CONSTRAINT FOREIGN KEY ALTER TABLE DROP CONSTRAINT PRIMARY KEY , ALTER TABLE DROP CONSTRAINT PRIMARY KEY ALTER TABLE DROP CONSTRAINT UNIQUE , ALTER TABLE DROP CONSTRAINT UNIQUE , DROP INDEX DE DROP INDEX - operaciones seguras


Vale la pena señalar que ALTER TABLE ADD CONSTRAINT FOREIGN KEY y ALTER TABLE DROP CONSTRAINT FOREIGN KEY bloquean dos tablas a la vez.


Aplicando conocimiento en django


Django tiene una operación en migraciones para ejecutar cualquier SQL: https://docs.djangoproject.com/en/2.1/ref/migration-operations/#django.db.migrations.operations.RunSQL . A través de él, puede establecer los tiempos de espera necesarios y aplicar operaciones alternativas para las migraciones, indicando state_operations , la migración que estamos reemplazando.


Esto funciona bien para su código, aunque requiere escritura adicional, pero puede dejar el trabajo sucio en el backend de db, por ejemplo, https://github.com/tbicr/django-pg-zero-downtime-migrations/blob/master/django_zero_downtime_migrations_postgres_backend/schema .py recopila las prácticas descritas y reemplaza las operaciones inseguras con contrapartes seguras, y esto funcionará para bibliotecas de terceros.


Al final


Estas prácticas me permitieron obtener un esquema idéntico creado por django fuera de la caja, con la excepción de reemplazar la construcción CHECK IS NOT NULL lugar de NOT NULL y algunos nombres de construcción (por ejemplo, para ALTER TABLE ADD COLUMN UNIQUE y una alternativa). Otra compensación puede ser la falta de transaccionalidad para las operaciones de migración alternativas, especialmente donde ALTER TABLE VALIDATE CONSTRAINT CREATE INDEX CONCURRENTLY y ALTER TABLE VALIDATE CONSTRAINT .


Si no va más allá de postgres, puede haber muchas opciones para cambiar el esquema de datos, y se pueden variar en combinación bajo condiciones específicas:


  • usando jsonb como solución schamaless
  • la oportunidad de ir al tiempo de inactividad
  • requisito para realizar migraciones sin tiempo de inactividad

En cualquier caso, espero que el material haya resultado útil para aumentar el tiempo de actividad o para expandir la conciencia.

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


All Articles