- Knock definió un procedimiento para migrar Postgres, el almacén central de su motor de flujos de trabajo de notificaciones, de AWS RDS Aurora 11.9 a 15.3 sin afectar a los clientes
- La empresa tenía que actuar antes del 29 de febrero de 2024, fecha de retiro de Postgres 11.9 en Amazon RDS, o enfrentarse a una actualización forzada con posible tiempo de inactividad
- Descataron la actualización in-place y
pg_dump/pg_restoreporque requerían una interrupción larga, y eligieron configurar replicación lógica basada enPUBLICATION/SUBSCRIPTIONhacia una nueva base de datos - Dividieron la estrategia de replicación según el tamaño de las tablas y el patrón de escritura: las tablas pequeñas se replicaron directamente y las grandes append-only combinaron
copy_data = falsecon backfill desde snapshots - El cambio final se completó en segundos manteniendo conexiones a ambas bases de datos, cambiando un flag, dando 500 ms a las consultas en ejecución y pausando durante 1 segundo las solicitudes a la nueva base para reducir el riesgo de lecturas obsoletas
Objetivos y restricciones de la actualización
- Knock depende de Postgres para su motor de flujos de trabajo de notificaciones y lo usa para configuraciones de workflows, plantillas de mensajes, recolección de millones de logs y encolado de trabajos en segundo plano
- Por la naturaleza de una base de datos relacional, Postgres requiere al menos un reinicio al actualizarse, y una actualización de versión mayor puede necesitar una detención completa de varios minutos o más debido a cambios en cómo se almacenan los datos e índices en disco
- Postgres 11.9, que usaban desde el inicio de la empresa, iba a quedar retirado en Amazon RDS, y si no hacían nada podían terminar con una actualización forzada y downtime forzado
- Las condiciones de la actualización se definieron para reducir el riesgo operativo
- Saltar hasta la versión más reciente posible para Aurora: Postgres 15.3
- No permitir más de 60 segundos de downtime y, de forma ideal, 0 downtime del sistema
- Completarlo antes de la fecha límite de Amazon en 2024
- Minimizar el impacto en clientes, por ejemplo, 0 respuestas de error en la API
- Convertir el procedimiento en un runbook reutilizable para la siguiente actualización
- Pasar de 11.9 a 15.3 implicaba una actualización de 4 versiones mayores, por lo que descartaron repetir 4 veces una actualización in-place
Preparación previa: reducir riesgos y mejorar la observabilidad
- El enfoque para actualizar Postgres fue primero hacer una lista de riesgos y luego reducir antes que nada los de mayor impacto y más fáciles de eliminar con anticipación
- downtime prolongado
- pérdida de datos
- cambios en el rendimiento de la base de datos bajo la carga de la aplicación
- cambios en la frecuencia o el comportamiento de
VACUUM - necesidad de migrar replication slots
- Revisaron las notas de lanzamiento de Postgres para identificar cambios entre versiones, como modificaciones en el comportamiento de
VACUUMo la necesidad de reindexar en ciertas actualizaciones - Durante la actualización había que monitorear continuamente métricas del sistema y de la base de datos
- Max TXN ID para prevenir transaction wraparound
- uso de CPU de la base de datos
- sesiones en espera en la instancia writer
- latencia de consultas
- latencia de respuesta de la API de la aplicación
- Knock también monitoreó métricas propias de la aplicación, como el tiempo que tarda una petición de API en convertirse en una notificación
- Sin métricas visibles a tiempo, el proceso de actualización se vuelve prácticamente a ciegas
Enfoques descartados: actualización in-place y dump/restore
- La actualización in-place de AWS RDS se ejecuta desde la consola de AWS: AWS detiene la base de datos, corre scripts de actualización y luego la vuelve a poner en línea
- Ese proceso puede tardar desde minutos hasta varias horas, según la cantidad de datos y el tamaño de los cambios entre versiones
- Incluso después de volver a estar en línea, la base de datos puede requerir tareas de mantenimiento como
VACUUMoREINDEX, por lo que no necesariamente queda totalmente utilizable de inmediato - El enfoque con
pg_dumpypg_restoreobliga a desconectar todas las aplicaciones de la base original para obtener un backup confiable, y en bases grandes tanto el dump como el restore pueden tardar mucho - Como ambos enfoques probablemente excedían por mucho el límite de downtime de Knock, fueron descartados
Enfoque elegido: actualización basada en replicación lógica
- La opción final fue usar replicación lógica de Postgres con
PUBLICATIONySUBSCRIPTION - El flujo general fue el siguiente
- levantar una nueva base de datos con la versión objetivo de Postgres
- mover configuración, extensiones, estructura de tablas, usuarios, etc.
- crear una publication en la base original y una subscription en la nueva
- agregar tablas a la publication
- cuando la replicación estuviera completa, ejecutar pruebas para validar los riesgos restantes
- cuando la configuración de la nueva base estuviera suficientemente validada, cambiar la aplicación a la nueva base
- eliminar la base original
- Esto permitió avanzar en etapas graduales en lugar de ejecutar una actualización grande de una sola vez, y probar la nueva base con datos y carga reales
- Una vez lista la nueva base, el cambio en sí tomaba solo segundos, lo que daba mucho más control sobre el momento y la forma del cutover
Puntos clave de la configuración de replicación
- La replicación lógica de Postgres usa parámetros necesarios para configurar replication slots, y en aplicaciones simples el cambio principal puede ser poner
wal_levelenlogical - Si ya se usan replication slots para réplicas de lectura, failover de base de datos o sincronización con un data warehouse, hay que ajustar parámetros relacionados como
max_replication_slotssegún la documentación - La estructura de tablas en la nueva base debe ser idéntica a la original, pero debe estar vacía
- Se puede generar un snapshot del esquema con
pg_dumpallusando--schema-onlyy--no-role-passwords, y luego comparar el SQL con el de la nueva base para corregir diferencias - Al crear la publication en la base original y la subscription en la nueva, configuraron opciones importantes
enabled = false: para no iniciar la sincronización desde el principiocreate_slot = true: para que Postgres administre el replication slotcopy_data = true: para copiar por defecto el contenido de las tablasdisable_on_error = true: para detener la subscription ante errores inesperados y poder corregir el problema antes de reanudarla
- Agregar todas las tablas de una vez con
FOR ALL TABLESpuede causar problemas de rendimiento en bases grandes, así que Knock usóALTER PUBLICATION ... ADD TABLEpara agregarlas una por una
Clasificación de tablas y estrategia de replicación
- Knock dividió las tablas según tamaño en disco y cantidad de tuplas
- tablas pequeñas que podían sincronizarse en unos minutos
- tablas grandes pero casi append-only
- tablas grandes donde la mayoría de las filas se actualizan con frecuencia
- Para Knock, una tabla “pequeña” era una de menos de 50 GB y menos de 10 millones de tuplas
- En Postgres, una tupla es la unidad en la que se guarda un insert o update, y aunque haya pocas filas, si hay muchas tuplas sin limpiar la replicación puede tardar bastante
- Ejecutar
VACUUMantes de replicar puede ayudar a reducir la cantidad de tuplas que la base origen necesita copiar hacia la base destino - El tiempo de sincronización de una tabla está directamente relacionado con su tamaño en disco y su número de tuplas, y una sincronización demasiado larga puede obstaculizar
VACUUMen la base primaria, degradar el rendimiento y aumentar el riesgo de transaction wraparound
Replicación de tablas pequeñas
- Las tablas pequeñas se manejaron agregándolas a la publication en la base original y haciendo refresh de la subscription en la nueva
- Postgres se encargó de copiar la tabla, sincronizarla y aplicar los cambios posteriores
- Las tablas muy pequeñas podían sincronizarse en menos de 1 segundo
Replicación de tablas grandes append-only
- Las tablas grandes sin updates, o donde solo se actualizan filas recientes, pueden replicarse con una publication/subscription separada usando
copy_data = false - Knock usó el sufijo
_nocopyen el nombre para distinguirlas de la replicación normal - Primero replicaron solo los cambios nuevos, y luego hicieron backfill de los datos históricos por separado desde backups o snapshots
- En AWS RDS Aurora, el procedimiento fue este
- crear un snapshot de la base de producción
- restaurar el snapshot en una nueva instancia de base de datos
- agregar un sufijo como
_snapshota los nombres de las tablas del snapshot que se iban a replicar - crear también en la base destino tablas para snapshot con el mismo esquema
- configurar publication/subscription desde la base del snapshot hacia la base destino
- monitorear el progreso de la replicación
- cuando la replicación alcanzara el estado esperado, fusionar hacia la tabla destino real con
INSERT ... ON CONFLICT DO NOTHING
- En tablas muy grandes este proceso puede tardar días, pero al ejecutarse en segundo plano no debería afectar producción
- Después de la fusión, compararon el número de filas para validar consistencia y eliminaron las tablas de snapshot en la base destino, la subscription del snapshot y la instancia de base del snapshot
Tablas grandes con updates frecuentes
- Las tablas grandes donde la mayoría de las filas se actualizan con frecuencia son las más difíciles, y una replicación prolongada puede bloquear la ejecución de
AUTOVACUUM - Algunas medidas posibles son estas
- revisar si tareas de housekeeping pueden reducir el tamaño de la tabla
- verificar si se ejecutó
VACUUMrecientemente - evaluar si la tabla puede particionarse en fragmentos más pequeños
- comprobar si después de cierto tiempo las filas dejan de actualizarse, para poder tratarlas como append-only
- Si la base origen está en una versión anterior a PG 15, las opciones son limitadas y hay que replicarla como una tabla pequeña y monitorear si hay degradación del servicio
- Si hace falta, se puede hacer rollback quitando la tabla de la publication y refrescando la subscription
- En tablas demasiado grandes, conviene iniciar la replicación en horas de poco tráfico para reducir el impacto de la carga y de la actividad de escritura
Replicación fragmentada de tablas grandes en PG 15 o superior
- Si la base origen ya está en PG 15 o superior, se puede dividir la replicación en varias publications para mover tablas grandes en trozos más pequeños
- Este enfoque funciona de manera parecida al particionado o al sharding, a cambio de usar más replication slots
- Como Knock estaba migrando de 11.9 a 15.3, no podía usar este método y no lo probó directamente
- El ejemplo consiste en dividir filas entre varias publications usando un hash de la primary key y una cláusula
WHERE - El tamaño de fragmento que Knock consideraba manejable era de alrededor de 100 GB de datos sin contar índices
Verificación del estado de replicación e interrupción
- Cuando se agrega una tabla a una subscription, el estado puede verse en
pg_subscription_rel.srsubstatede la base destinoi: inicializaciónd: copia del contenido de la tablaf: copia completa, esperando sincronización finals: finalización de la sincronización inicialr: replicación normal en ejecución
- La fase
dnecesita mantener transaction IDs antiguos, por lo que puede bloquear efectivamenteVACUUMy derivar en problemas de rendimiento o transaction ID wraparound - Si el sistema se acerca al wraparound, conviene abortar la migración y dividirla en fragmentos más pequeños
- Para detener la replicación de una tabla específica, hay que quitarla de la publication en la base original y refrescar la subscription en la nueva
- Deshabilitar solo la subscription puede no resolver el problema de rendimiento, porque la base origen puede seguir reteniendo transaction IDs viejos
- En emergencias, se puede borrar por completo la publication y la subscription y empezar de nuevo; Postgres limpiará los replication slots relacionados
Restricciones al migrar replication slots
- Los replication slots de Postgres guardan logs de actividad de la base que pueden consumir otras bases o aplicaciones
- Su progreso se rastrea con el Log Sequence Number, o LSN, y el LSN es único para la base Postgres primaria
- No es posible copiar directamente el LSN de un replication slot de la base original a la nueva
- Si una aplicación consume replication slots, como una herramienta de data warehouse, hay que definir la estrategia de migración según la documentación de cada herramienta
- Si una aplicación propia usa replication slots, contar con un mecanismo de idempotencia para deduplicar transacciones entre la base vieja y la nueva puede ayudar
Validación final
- Una vez agregadas todas las tablas a la publication y cuando la subscription se puso al día, había que validar que las tablas coincidieran
- Debido al retraso inherente de la replicación lógica, no siempre es posible que la base original y la nueva coincidan perfectamente en el mismo instante, pero comparar conteos de filas permite verificar si están suficientemente cerca
- Knock escribió un script para contar filas por tabla en la base original y la nueva
- En tablas con columna
inserted_at, compararon solo filas de más de 10 segundos de antigüedad, asumiendo que los últimos 10 segundos todavía podrían replicarse en breve - En algunas tablas también compararon muestras aleatorias de filas para validar con más detalle que el contenido coincidiera
Cómo hicieron el cambio de la aplicación
- Para el cutover final, la aplicación podía modificarse para conectarse a ambas bases de datos
- En bases con poco tráfico, la migración puede hacerse simplemente cambiando la configuración a la nueva base y reiniciando la aplicación
- En aplicaciones con mucha actividad concurrente, había que evitar writes en conflicto entre la base original y la nueva
- El script de cutover de Knock siguió este orden
- indicar a todas las instancias de la aplicación que envíen las nuevas consultas a la nueva base
- dar 500 ms a las consultas en ejecución para que terminen, y después cancelarlas por la fuerza
- durante el primer segundo tras cambiar el flag, pausar artificialmente las solicitudes a la nueva base para dar tiempo a que las transacciones pendientes se repliquen
- luego normalizar la actividad de base de datos, ya apuntando a la nueva base
- reiniciar algunas cargas de trabajo especiales de base de datos para que se reconecten a la nueva base después de detenerse
- Knock confirmó que 500 ms era mucho más que la duración de la mayoría de sus consultas y que no hubo errores por desconexión forzada
Manejo de sequences
- La replicación lógica de Postgres no sincroniza las
sequence - Aunque se usen valores de
sequenceen la base original, los de la nueva base no avanzan automáticamente - Justo antes del cambio por feature flag, Knock ejecutó un script que se conectaba a ambas bases
- en la base original, para cada
sequence, obtenía el siguiente valor conSELECT nextval('sequence_name') - en la nueva base, adelantaba la
sequenceconSELECT setval('sequence_name', value::int4 + 100000)
- en la base original, para cada
- Este método crea huecos en las
sequence, pero en Knock eran bigint, así que saltarse 100 mil valores representaba prácticamente 0% del espacio disponible - El tamaño del salto debe ajustarse según la cantidad de valores de
sequenceque podrían usarse durante el cutover real
Qué revisar antes del cutover
- La lista de verificación final cubría ampliamente la preparación operativa
- que el conteo de filas de todas las tablas coincida como se espera
- que todas las subscriptions estén habilitadas y corriendo sin errores
- que el esquema coincida y que puedan congelarse los releases de migración
- que la nueva base tenga el sizing adecuado para la carga
- si hacen falta read replicas para igualar la topología del clúster entre la base original y la nueva
- que se hayan ejecutado
REINDEXy el mantenimiento normal deVACUUMen la nueva base - que se hayan revisado otra vez las notas de lanzamiento de Postgres por posibles regresiones en la aplicación
- que se hayan hecho pruebas automáticas y manuales en una base de staging con la nueva versión
- que se haya hecho load testing con
pg_benchsobre las consultas más pesadas - si queda algún riesgo que todavía pueda reducirse
- que se haya practicado el procedimiento de cutover varias veces en staging o test
- que se haya creado un backup de la base justo antes del cutover
Resultado real del cambio
- Knock replicó las tablas una por una durante varias semanas, principalmente fuera del horario laboral y en las franjas de menor tráfico
- Practicaron el cutover varias veces en staging y fueron afinando el procedimiento para que funcionara con poca intervención operativa
- Una vez listas la réplica de PG 15 y la lógica de cambio en la aplicación, hicieron la validación final y activaron el flag
- El cutover real terminó en cuestión de segundos, y salvo una breve subida intencional de latencia mientras esperaban la replicación, la aplicación siguió funcionando
- Después revirtieron los cambios temporales de la aplicación, movieron de forma permanente todas las conexiones a la nueva base y eliminaron la subscription en la nueva base y la base original
- Knock completó una migración sin downtime de Postgres de 11.9 a 15.3
Conclusión
- Saltar 4 versiones mayores de Postgres de una sola vez es difícil, pero posible
- El enfoque de replicación lógica puede ser más seguro que un downtime programado porque permite practicar, probar y rehacer varias veces antes del cutover real
- Si algo falla durante el proceso, se puede eliminar la publication de la base original y empezar de nuevo sin degradar el servicio
- Aunque una disponibilidad perfecta del 100% no siempre es técnicamente posible, una migración sin downtime ayuda a mantener el sistema en operación sin interrupciones importantes
1 comentarios
Opiniones de Hacker News
El enfoque de copiar todo el contenido de las tablas una por una genera demasiada carga de E/S, y no funciona con tablas muy grandes.
Un mejor método es crear un slot de replicación, tomar un snapshot, restaurarlo en una nueva instancia, avanzar el LSN y luego replicar desde ahí. Así se obtiene una réplica lógica con todos los datos, y basta con actualizar esa réplica.
El artículo de Instacart explica el método: https://archive.ph/K5ZuJ
Si no recuerdo mal, el artículo tenía algunos errores pequeños, pero el procedimiento general funcionaba y actualicé varias veces instancias de nivel TB de esta forma.
Si se inicia primero la replicación lógica y luego se ejecuta pg_upgrade, hay riesgo de corrupción. La discusión relacionada está en pgsql-hackers: https://www.postgresql.org/message-id/flat/20230217075433.u5...
Para resolverlo, primero hay que crear el slot lógico, avanzar el nuevo clúster hasta la posición LSN del slot sin iniciar todavía la replicación lógica, luego ejecutar pg_upgrade y, una vez que el clúster levante con la nueva versión de PostgreSQL, iniciar la replicación lógica.
Postgres.ai usó recientemente exactamente este enfoque para actualizar sin downtime varios clústeres multi-TiB de GitLab bajo alta carga, y también utilizó PAUSE/RESUME de PgBouncer. Hay una charla de Alexander Sosna programada para más adelante esta semana: https://www.postgresql.eu/events/pgconfeu2023/schedule/sessi...
El avance tabla por tabla era mucho más engorroso, pero parecía más confiable.
Con este método hemos actualizado con éxito muchas bases de datos muy grandes y activas.
https://www.instacart.com/company/how-its-made/zero-downtime...
El enfoque es interesante y está bien documentado, pero la frase “los clientes modernos esperan 100% de disponibilidad” me hace ruido.
No es mi preferencia como cliente, ni mi experiencia como proveedor. En muchas cargas de trabajo, la consistencia es mucho más importante que la disponibilidad.
Cuando un proveedor anuncia una ventana de downtime, muchas veces me tranquiliza, porque lo veo como una señal de que está tratando mis datos con cuidado.
Quería generar confianza tanto en la confiabilidad del producto como en la consistencia de la carga de trabajo. Por supuesto, es mucho mejor gestionar las expectativas de los clientes y tomar downtime de forma deliberada para lograr mejor uptime a largo plazo, que fingir consistencia mientras todo es inestable.
Hacer que las ventanas periódicas de mantenimiento sean previsibles de antemano también podría llevar a una arquitectura más robusta en general. Si los clientes crean mecanismos de seguridad para tolerar downtime, aumenta la resiliencia, y el equipo también gana tiempo para invertir en un mejor producto cuando puede confiar en que los clientes lo entienden así.
Tal vez después de la próxima actualización de versión mayor escriba un artículo titulado “Establecer expectativas sobre el downtime como camino hacia un uptime muy alto”.
Como cliente de AWS, espero 100% de disponibilidad, porque mis propios clientes están en todo el mundo y no hay un horario en el que pueda tener downtime.
AWS ahora soporta despliegues blue/green: https://aws.amazon.com/about-aws/whats-new/2023/10/amazon-rd...
Después de varios intercambios con AWS, el experimento quedó detenido durante horas, y recién más tarde la UI de AWS admitió que el cambio no se había aplicado. Por suerte falló de forma segura, pero no tengo confianza en que se pueda coordinar el momento real del cambio con datasets de más de GB.
Tal vez la próxima vez sea posible.
Esto es excelente.
Creé una herramienta que automatiza la mayor parte de lo que pasaron; si les resulta útil o quieren ampliarla con feedback/ideas, son más que bienvenidos: https://github.com/shayonj/pg_easy_replicate
Los hallazgos en tablas grandes podrían ser interesantes para una herramienta así. Si facilitara aplicar la estrategia adecuada por tabla, podría convertirse en una herramienta imprescindible para los equipos que hagan este tipo de migraciones en el futuro.
Me parece dudosa la idea de que “para un servicio como Knock no se permite ningún downtime, esté programado o no”
Si es un sistema complejo, hay fallas y hay downtime. Un downtime de 15 minutos anunciado de antemano está bien para casi cualquier negocio SaaS. No es un hospital ni una central eléctrica
Se genera mucho trabajo ficticio por pensar que el servicio es más importante de lo que realmente es. Si el tiempo de ingeniería invertido aquí se hubiera usado para mejorar el producto o la productividad del equipo de desarrollo, es muy probable que los usuarios hubieran quedado más contentos. Sobre todo si las notificaciones se pueden poner en una cola y ponerse al día después del downtime
Si existe un SLA enterprise con condiciones de compensación por 15 minutos de downtime, podría justificarse, pero la mayoría no lo tiene. De hecho, es muy probable que ya hayan tenido algunas interrupciones similares o más largas
En migraciones de bases de datos, la diferencia de trabajo entre “downtime breve” y “sin downtime” suele ser considerable, así que esto importa más. En especial cuando, como en este caso, es algo de una sola vez y las versiones recientes de PostgreSQL en RDS vienen soportadas por defecto; me parece particularmente difícil de justificar
También discutimos definir una ventana de interrupción, pero lo que seguíamos pensando era cómo podíamos hacer un ensayo del upgrade con datos de producción. Una réplica de PG 15 sincronizada con los datos de producción fue muy importante para validar que las cargas de trabajo se comportaran como esperábamos
Usar una réplica en tiempo real permite hacer ensayos con impacto mínimo en el entorno de producción
Una gran lección de esta migración fue lo útil que resulta rastrear y mitigar todos los riesgos imaginables en este tipo de proyecto. Al final, el riesgo de un upgrade in-place nos pareció mayor que el riesgo del camino elegido, y esa evaluación era independiente de si había o no una ventana de interrupción
Como extra, si en el futuro necesitamos este enfoque, esta entrada de blog servirá como punto de partida y nos ahorrará semanas. Espero que también ayude a otros equipos en situaciones similares
Epic, uno de los mayores proveedores de historias clínicas electrónicas en EE. UU., también tiene downtime programado al menos una vez al mes para upgrades, normalmente de unos 30 a 60 minutos cada vez
No se puede controlar el momento del reinicio. Si inicias el proceso, el cambio puede empezar una hora después, dos horas después, tres horas después, y no puedes saber ni controlar cuándo se reinicia
Si tienes réplicas, se actualizan en paralelo y se reinician en momentos arbitrarios, lo que complica más las cosas
Por lo tanto, si no puedes tolerar indisponibilidad arbitraria durante una ventana que, según el tamaño de la base de datos, puede extenderse por varias horas, la replicación lógica es prácticamente la única opción para upgrades en RDS
Cuanto más grande es la instancia, más difícil se vuelve el problema
Si Jira cae 15 minutos al día, normalmente no tiene gran impacto. Hay otras cosas en la cola de trabajo y, en el peor de los casos, incluso si se combinan varias interrupciones, hay documentación prometida a alguien
Pero si toda la suite de Atlassian se cae al mismo tiempo, se vuelve mucho más difícil mantener trabajo de reserva para seguir avanzando. Si haces que todas las apps de una empresa usen el mismo arreglo de almacenamiento, la pérdida de productividad puede saltar de 5% a 95%
Ese competidor está poniendo mis necesidades por encima de su propia conveniencia
Tu interrupción también es mi interrupción
En hava.io estamos pasando por este proceso ahora mismo
Estamos subiendo de AWS RDS PostgreSQL 11.13 a 15.5
Al final elegimos un enfoque relativamente simple: replicación unidireccional usando pglogical. Ya habíamos hecho una migración sin downtime de Google Cloud SQL a AWS RDS con el mismo método, así que teníamos confianza en que funcionaría sin impacto visible para los clientes
pglogical hace que este tipo de migraciones sean bastante simples. No siempre es rápido, pero si puedes esperar unos días mientras toda la base de datos se replica gradualmente a la nueva instancia, está bien
Este método también nos dio más libertad para cambiar el tipo y el tamaño del almacenamiento. Como teníamos almacenamiento sobredimensionado para obtener IOPS, queríamos cambiar el tipo de almacenamiento y también reducir el tamaño. Por eso una simple restauración desde snapshot no servía
Me pregunto si se refieren a esa funcionalidad que AWS prometió durante la etapa de “ingeniería de ventas”
En la práctica, cuando tuvimos que hacer forzosamente un upgrade de versión mayor, no pudieron ofrecerla
Sorprende que no se pueda inicializar una réplica desde un backup
Si se pudiera, habría evitado el trabajo de hacer streaming del contenido estable de la base de datos existente hacia el servidor nuevo
Y esto no es “sin interrupciones”: hay unos segundos de downtime al cambiar el servicio al servidor nuevo
El artículo omitió cómo preservaron la consistencia. Por ejemplo, no se puede simplemente conectar la aplicación a ambos servidores durante un tiempo. Las lecturas quizá podrían servirse desde ambos, aunque eso tampoco es perfecto, y las escrituras necesariamente deben ir a un solo servidor
Por último, tampoco hay opción de rollback. En operaciones donde se mueve de una vez un volumen tan grande de datos, a veces las cosas salen mal tarde en la noche. Por eso siempre hace falta un plan que permita volver al paso anterior y dormir con la certeza de que el servicio seguirá vivo por la mañana
En particular, si ya enviaste transacciones de escritura al servidor nuevo y por alguna razón tienes que volver al servidor anterior, es difícil, y los datos ya quedaron inconsistentes
Si no hay algún medio de replicación, o si no se sube esa lógica a la capa de la aplicación, el sistema restaurado tendrá escrituras faltantes
Por ejemplo, se podría modificar la app para aplicar escritura doble. Entiendo que eso hicieron equipos que replatformearon toda su aplicación desde un RDBMS a una base de datos completamente distinta, como Apache Cassandra
En nuestro caso, la escritura doble parecía más riesgosa que configurar replicación por streaming con una funcionalidad nativa de PostgreSQL. Pero para algunos equipos puede ser una mejor opción
Sobre “no es sin interrupciones” y “faltan detalles de preservación de consistencia”, el artículo explicó en detalle cómo mantuvimos la consistencia y evitamos downtime de la API. La idea central es que la app estaba conectada a ambas bases de datos, pero todavía no usaba la base nueva como principal
Luego enviamos una señal de cambio a todas las instancias de la app con LaunchDarkly, que mantiene conexiones de baja latencia con todas las instancias
Durante el primer segundo después de la señal, los servidores pusieron en cola las solicitudes a la base de datos para que la replicación pudiera ponerse al día. Esto causó un breve pico de latencia, pero estaba dentro de un margen aceptable calculado intencionalmente. Después de esa pausa temporal, las solicitudes fluyeron como de costumbre, pero apuntando a la base de datos nueva, y el cambio quedó completado
Para el tráfico que seguía quedando hacia la base de datos anterior, también agregamos una desconexión forzada con timeout de 500 ms. Ese valor era mucho mayor que nuestro tiempo de consulta p99, así que no terminaba consultas en ejecución a la fuerza. Con esto se detuvo el tráfico hacia la base de datos anterior y la replicación tuvo tiempo suficiente para ponerse al día
La opción de rollback no quedó en el blog, pero también evaluamos crear una base de datos alternativa en PG 11.9 y replicar la base 15.3 hacia esa tercera base de datos. Si teníamos que interrumpir, podríamos haber hecho roll forward a esa base de datos de la misma versión
Después de practicar varias veces el procedimiento de upgrade en staging y confirmar las probabilidades de éxito, decidimos no usar esa opción. Como hicimos varios ensayos, teníamos confianza para el cambio real. En producción también validamos algunas cargas de trabajo solo de lectura contra la instancia 15.3 mediante despliegue canario, tratándola como una réplica de lectura
Para evitar problemas tarde en la noche, lo hicimos deliberadamente temprano en la tarde-noche de un fin de semana. El cambio fue cuidadosamente scriptado y ensayado para reducir el riesgo de error humano
Si ocurría una falla catastrófica, el sistema también estaba preparado para volver a la base de datos anterior. En ese caso habría habido pérdida de algunos datos que entraron en la base nueva, y teníamos preparadas conciliaciones para las partes críticas. Para reducir el riesgo de pérdida de datos, pausamos temporalmente algunas tareas en segundo plano durante el cambio y así redujimos la cantidad de escrituras
No incluimos estos detalles en el blog porque queríamos enfocarnos en los detalles relacionados con PostgreSQL más que en consideraciones específicas de Knock. Cualquier equipo que quiera aplicar este playbook siempre debe armar su propia lista de riesgos en su contexto y mitigarlos
La parte sobre secuencias definitivamente es interesante
Hace tiempo que casi no uso secuencias; principalmente uso UUID secuenciales, UUID v7 o enfoques como HiLo
https://en.wikipedia.org/wiki/Hi/Lo_algorithm
Basándose en el borrador de la especificación de IETF, crea una secuencia de 12 bits y construye el UUID combinando los milisegundos actuales desde el UNIX epoch con 62 bits aleatorios
La clave es tener
uuidv7_seqy hacer que la funcióngenerate_uuidv7()useclock_timestamp(),NEXTVALyRANDOM()para devolver un valor con formato UUID v7Usamos KSUID y UUID v4 en varios lugares. Esta “trampa” aplica a todas las secuencias, así que vale la pena señalarla como consejo general al hacer este tipo de migraciones
[1]: https://segment.com/blog/a-brief-history-of-the-uuid/
No quiero restarle mérito al enorme trabajo que hicieron con éxito, pero me pregunto por qué no hicieron upgrades pequeños cada vez que salía una versión nueva
Como lectura es excelente, pero se siente como una historia de marineros que, en vez de rodear una gran tormenta, decidieron atravesarla de frente aun sabiendo que podía terminar en tragedia
En este caso, ¿los upgrades pequeños no eran una opción? Me pregunto si era algo como “cada upgrade pequeño implicaba el mismo costo de downtime que un upgrade grande, así que lo postergamos lo máximo posible”. En la introducción parece haber indicios de eso, aunque quizá estoy leyendo demasiado
Más que “lo postergamos hasta quedar contra la pared”, fue algo más parecido a “si no está roto, no lo arregles”, aunque sabíamos que en algún momento tendríamos que dar el salto
Aunque la respuesta real sea menos de 60 segundos, en el camino hasta 15 habríamos pasado por ese downtime varias veces