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:
- Migraciones: al cambiar el esquema de la base de datos (tablas), supongamos que siempre las ejecutamos en un hilo.
- Lógica empresarial: el trabajo directo con datos (en tablas de usuario), trabaja con los mismos datos de forma constante y competitiva.
- 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:
- Tenemos una base de trabajo.
- Tenemos varias máquinas donde la lógica de negocios gira.
- Los autos con lógica de negocios están ocultos detrás del balanceador.
- 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).
- 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:
- inundó la migración;
- quitó una máquina del equilibrador, actualizó la máquina y reinició, devolvió la máquina al equilibrador;
- 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:
- quitó una máquina del equilibrador, actualizó la máquina y reinició, devolvió la máquina al equilibrador;
- repitió el paso anterior para actualizar todos los autos;
- 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 SHARE | ROW SHARE | ROW EXCLUSIVE | SHARE UPDATE EXCLUSIVE | SHARE | SHARE ROW EXCLUSIVE | EXCLUSIVE | ACCESS EXCLUSIVE |
---|
ACCESS SHARE | | | | | | | | X |
ROW SHARE | | | | | | | X | X |
ROW EXCLUSIVE | | | | | X | X | X | X |
SHARE UPDATE EXCLUSIVE | | | | X | X | X | X | X |
SHARE | | | X | X | | X | X | X |
SHARE ROW EXCLUSIVE | | | X | X | X | X | X | X |
EXCLUSIVE | | X | X | X | X | X | X | X |
ACCESS EXCLUSIVE | X | X | X | X | X | X | X | X |
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:
bloqueo | operaciones |
---|
ACCESS EXCLUSIVE | CREATE SEQUENCE , DROP SEQUENCE , CREATE TABLE , DROP TABLE , ALTER TABLE , DROP INDEX |
SHARE | CREATE INDEX |
SHARE UPDATE EXCLUSIVE | CREATE 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.
bloqueo | operaciones | conflictos con cerraduras | conflictos con operaciones |
---|
ACCESS SHARE | SELECT | ACCESS EXCLUSIVE | ALTER TABLE , DROP INDEX ALTER TABLE DROP INDEX |
ROW SHARE | SELECT FOR UPDATE | ACCESS EXCLUSIVE , EXCLUSIVE | ALTER TABLE , DROP INDEX ALTER TABLE DROP INDEX |
ROW EXCLUSIVE | INSERT , UPDATE , DELETE | ACCESS EXCLUSIVE , EXCLUSIVE , SHARE ROW EXCLUSIVE , SHARE | ALTER TABLE , DROP INDEX , CREATE INDEX |
Aquí se pueden resumir dos puntos:
- Si hay una alternativa con un bloqueo más fácil, puede usarla como
CREATE INDEX
y CREATE INDEX CONCURRENTLY
. - 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 SHARE | FOR SHARE | FOR NO KEY UPDATE | FOR UPDATE |
---|
FOR KEY SHARE | | | | X |
FOR SHARE | | | X | X |
FOR NO KEY UPDATE | | X | X | X |
FOR UPDATE | X | X | X | X |
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:

Aquí puede resaltar los siguientes elementos:
- 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). - 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
- 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.
- 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 |
---|
1 | CREATE SEQUENCE |
2 | DROP SEQUENCE |
3 | CREATE TABLE |
4 4 | DROP TABLE |
5 5 | ALTER TABLE RENAME TO |
6 6 | ALTER TABLE SET TABLESPACE |
7 7 | ALTER TABLE ADD COLUMN [SET DEFAULT] [SET NOT NULL] [PRIMARY KEY] [UNIQUE] |
8 | ALTER TABLE ALTER COLUMN [TYPE] [SET NOT NULL|DROP NOT NULL] [SET DEFAULT|DROP DEFAULT] |
9 9 | ALTER TABLE DROP COLUMN |
10 | ALTER TABLE RENAME COLUMN |
11 | ALTER TABLE ADD CONSTRAINT CHECK |
12 | ALTER TABLE DROP CONSTRAINT CHECK |
13 | ALTER TABLE ADD CONSTRAINT FOREIGN KEY |
14 | ALTER TABLE DROP CONSTRAINT FOREIGN KEY |
15 | ALTER TABLE ADD CONSTRAINT PRIMARY KEY |
16 | ALTER TABLE DROP CONSTRAINT PRIMARY KEY |
17 | ALTER TABLE ADD CONSTRAINT UNIQUE |
18 años | ALTER TABLE DROP CONSTRAINT UNIQUE |
19 | CREATE INDEX |
20 | DROP 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.