以django为例,为PostgreSQL迁移数据库模式而无需停机

引言


哈Ha!


我想分享为postgres和django编写迁移的经验。 这主要是关于postgres的,django是一个很好的补充,因为它具有开箱即用的模型更改自动迁移数据方案的功能,也就是说,它具有用于更改方案的相当完整的工作操作列表。 Django可以用任何喜欢的框架/库替换-方法很可能是相似的。


我不会描述我是如何做到这一点的,但是现在阅读文档时,我发现有必要在更早的时候加倍小心和意识地做到这一点,因此,我强烈建议这样做。


在继续之前,让我做以下假设。


您可以将使用大多数应用程序的数据库的逻辑分为三部分:


  1. 迁移-更改数据库架构(表),假设我们始终在一个线程中运行它们。
  2. 业务逻辑-直接处理数据(在用户表中),持续且具有竞争力地处理相同的数据。
  3. 数据迁移-不更改数据模式,它们基本上像业务逻辑一样工作,默认情况下,当我们谈论业务逻辑时,我们也指数据迁移。

停机是指当我们的业务逻辑的一部分在用户明显的时间内无法使用/掉落/加载的状态,假设这是几秒钟。


停机时间的缺乏可能是企业的关键条件,任何努力都必须遵守。


推出过程


推出时的主要要求:


  1. 我们有一个工作基地。
  2. 我们有几台业务逻辑在其中发展的机器。
  3. 具有业务逻辑的汽车隐藏在平衡器的后面。
  4. 我们的应用程序在滚动迁移之前,期间和之后都能很好地工作(旧代码可以在新旧数据库模式下正常工作)。
  5. 我们的应用程序在更新汽车上的代码之前,之中和之后都能很好地工作(旧代码和新代码在当前数据库方案下均能正常工作)。

如果存在大量更改,并且推出不再满足这些条件,则将其划分为所需数量的满足这些条件的较小推出,否则我们将停机。


直接推出订单:


  1. 大量移民
  2. 从平衡器中删除一台机器,更新机器并重新启动,然后将机器退还给平衡器;
  3. 重复上一步以更新所有汽车。

当我们根据更改的方案自动创建迁移并验证是否存在所有向CI迁移时,反向推出顺序与删除表中的表和列有关。


  1. 从平衡器中删除一台机器,更新机器并重新启动,然后将机器退还给平衡器;
  2. 重复上一步以更新所有汽车;
  3. 大量移民。

理论


Postgres是一个出色的数据库,我们可以编写一个应用程序,以成千上万个流来写入和读取相同的数据,而且很有可能确保我们的数据将保持有效并且不会被破坏(通常是完整的ACID)。 Postgres实现了多种机制来实现这一目标;其中之一是阻塞。


Postgres有几种类型的锁,在这里可以找到更多详细信息,作为主题的一部分,我将仅涉及表和记录级别的锁。


表级锁


在表级别,postgres有几种类型的锁 ,主要特征是它们有冲突,即不能同时执行两个有冲突锁的操作:


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

例如,必须严格一对一地执行ALTER TABLE tablename ADD COLUMN newcolumn integerSELECT COUNT(*) FROM tablename ,否则,我们将无法找出要返回COUNT(*)


在django迁移(下面的完整列表)中,有以下操作及其相应的锁:


阻塞运作
ACCESS EXCLUSIVECREATE SEQUENCEDROP SEQUENCECREATE TABLEDROP TABLEALTER TABLEDROP INDEX
SHARECREATE INDEX
SHARE UPDATE EXCLUSIVECREATE INDEX CONCURRENTLY ,同时DROP INDEX CONCURRENTLYALTER TABLE VALIDATE CONSTRAINT

在注释中,并非所有ALTER TABLE都具有ACCESS EXCLUSIVE锁定,Django迁移也没有CREATE INDEX CONCURRENTLYALTER TABLE VALIDATE CONSTRAINT ,但是稍后将需要它们来作为标准操作的更安全替代方法。


如果在一个线程中按顺序执行迁移,那么一切看起来都不错,因为迁移不会与另一迁移冲突,但是我们的业务逻辑仅在迁移和冲突期间起作用。


阻塞运作与锁冲突与运营冲突
ACCESS SHARESELECTACCESS EXCLUSIVEALTER TABLEDROP INDEX
ROW SHARESELECT FOR UPDATEACCESS EXCLUSIVEALTER TABLEDROP INDEX
ROW EXCLUSIVEINSERTUPDATEDELETEACCESS EXCLUSIVEEXCLUSIVESHARE ROW EXCLUSIVESHAREALTER TABLEDROP INDEXCREATE INDEX

这里可以总结两点:


  1. 如果有更容易锁定的替代方法,则可以将其用作CREATE INDEXCREATE INDEX CONCURRENTLY
  2. 大多数更改数据架构的迁移都与业务逻辑发生冲突,此外,它们与ACCESS EXCLUSIVE发生冲突,也就是说,我们甚至无法在保持此锁定的情况下进行SELECT并可能期望在此处停机,除非这种操作无法立即生效且停机时间为几秒钟。

必须做出选择,或者我们始终避免ACCESS EXCLUSIVE ,也就是说,我们创建新的板并在那里可靠地复制数据,但是长时间复制大量数据,或者使ACCESS EXCLUSIVE尽可能快并针对停机提出其他警告-这是潜在的危险,但是速度很快。


记录锁


在记录级别,还存在锁https://www.postgresql.org/docs/current/static/explicit-locking.html#LOCKING-ROWS ,它们也发生冲突,但它们只会影响我们的业务逻辑:


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

这是数据迁移的重点,也就是说,如果我们在整个板上进行UPDATE数据迁移,则其余更新数据的业务逻辑将等待锁释放并可能超过我们的停机时间阈值,因此最好进行部分更新以进行数据迁移。 还值得注意的是,当使用更复杂的sql查询进行数据迁移时,拆分成多个部分可以更快地进行,因为它可以使用更优化的计划和索引。


操作顺序


另一个重要的知识是如何执行操作,何时以及如何进行和释放锁:


图片


您可以在此处突出显示以下项目:


  1. 操作执行时间-对于迁移来说,这是持有锁的时间,如果长时间保持沉重的锁,我们将有一个停机时间,例如,可以使用CREATE INDEXALTER TABLE ADD COLUMN SET DEFAULT (在postgres 11中更好)。
  2. 冲突锁的等待时间-也就是说,迁移会一直等到所有冲突的请求都解决了,此时新的请求将等待我们的迁移,在这里缓慢的请求可能非常危险,无论是不是最佳的还是分析性的,因此在此期间不应存在缓慢的请求迁移。
  3. 每秒的请求数-如果长时间有很多请求在处理,那么免费连接可以迅速结束,而不是一个有问题的地方,整个数据库都可以进入停机状态(超级用户将只有一个连接限制),在这里您需要避免缓慢的请求,减少请求数量例如,在最小负载下开始迁移,使用其自己的数据库将关键组件分为不同的服务。
  4. 一个事务中有许多迁移操作-一个事务中的操作越多,重锁的持有时间就越长,因此最好分离重操作,而ALTER TABLE VALIDATE CONSTRAINT或具有重锁的一个事务中的数据迁移则更好。

超时时间


lock_timeout具有诸如lock_timeoutstatement_timeout设置,可以保护迁移的开始,既防止写得不好的迁移,也可以防止可能触发迁移的恶劣条件。 它们既可以全局安装,也可以当前连接安装。


在迁移之前等待缓慢的请求/事务之前,将SET lock_timeout TO '2s'可以避免停机: https : //www.postgresql.org/docs/current/static/runtime-config-client.html#GUC-LOCK-TIMEOUT


SET statement_timeout TO '2s'可以避免在使用重锁启动重迁移时避免停机: https : //www.postgresql.org/docs/current/static/runtime-config-client.html#GUC-STATEMENT-TIMEOUT


死锁


迁移中的死锁与停机时间无关,但是在编写迁移时并不令人满意,它在测试环境中可以正常工作,但在产品上滚动时却可以捕获死锁。 问题的主要根源可能是在一个事务和外键中进行大量操作,因为它在两个表中都创建了锁,所以最好将迁移操作分开,原子化越好。


记录存储


Postgres 以不同的方式存储不同类型的值 :如果类型以不同的方式存储,则在它们之间进行转换将需要完全重写所有值,幸运的是,某些类型以相同的方式存储,并且在更改时不需要重写。 例如,无论大小如何,行都存储相同,减小/增大行的大小将不需要重写,但是减小行需要验证所有行均不超过较小的大小。 其他类型也可以以相似的方式存储并具有相似的特性。


多版本并发控制(MVCC)


根据文档 ,postgres一致性基于数据多版本,也就是说,每个事务和操作都可以看到自己的数据版本。 此功能完美地解决了竞争访问问题,并且在更改方案(例如添加和删除列)仅更改方案时,如果没有用于更改数据,索引或常量的附加操作,那么在较低级别的插入和更新操作将创建新的方案时,也会产生有趣的效果。具有所有必要值的记录,删除将标记相应的记录已删除。 VACUUM或AUTO VACUUM负责清洁剩余的碎屑。


Django范例


现在,我们已经了解了停机时间可能取决于什么以及如何避免停机,但是在应用知识之前,您可以先看看django给出的内容( https://github.com/django/django/blob/2.1.2/django /db/backends/base/schema.pyhttps://github.com/django/django/blob/2.1.2/django/db/backends/postgresql/schema.py ):


操作
1个CREATE SEQUENCE
2DROP SEQUENCE
3CREATE TABLE
4DROP TABLE
5ALTER TABLE RENAME TO
6ALTER TABLE SET TABLESPACE
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]
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岁ALTER TABLE DROP CONSTRAINT UNIQUE
19CREATE INDEX
20DROP INDEX

Django很好地满足了我的迁移需求,现在我们可以在不停机的情况下讨论安全和危险的迁移操作。


我们将使用SHARE UPDATE EXCLUSIVE锁定或ACCESS EXCLUSIVE来调用更安全的迁移,该迁移可立即生效。
我们将使用SHAREACCESS EXCLUSIVE锁来进行危险的迁移,这将花费大量时间。


我将通过大量示例预先留下有用的文档链接


创建和删除表


CREATE SEQUENCEDROP SEQUENCECREATE TABLEDROP TABLE称为安全的,因为业务逻辑要么不再与已迁移的表一起工作,否则使用FOREIGN KEY删除表的行为将在以后出现。


大量支持的工作表操作


ALTER TABLE RENAME TO我很难称呼它为安全,因为在迁移前后很难编写适用于此类表的逻辑。


ALTER TABLE SET TABLESPACE不安全,因为它会物理移动板,并且在大体积上可能要花费很长时间。


另一方面,这些操作非常少见,作为替代,您可以提供创建新表并将数据复制到其中的功能。


创建和删除列


ALTER TABLE ADD COLUMNALTER TABLE DROP COLUMN -可以称为安全(创建时没有DEFAULT / NOT NULL / PRIMARY KEY / UNIQUE),因为业务逻辑不再适用于已迁移的列,使用FOREIGN KEY删除列的行为,其他常量和索引将在以后出现。


ALTER TABLE ADD COLUMN SET DEFAULTALTER TABLE ADD COLUMN SET NOT NULLALTER TABLE ADD COLUMN PRIMARY KEYALTER TABLE ADD COLUMN UNIQUE不安全的操作,因为它们添加列并且在不释放锁的情况下使用默认值更新数据或创建构造作为替代,可为空的列,并作进一步的更改。


值得一提的是postgres 11中更快的SET DEFAULT ,它可以被认为是安全的,但是在django中并没有什么用,因为django仅使用SET DEFAULT来填充列,然后使DROP DEFAULT ,以及在迁移和更新机器之间的间隔在业务逻辑中,可以创建不存在默认值的记录,也就是说,同样地,执行数据迁移。


工作表上大量支持的操作


ALTER TABLE RENAME COLUMN我也不能称之为安全,因为在迁移前后很难编写与该列兼容的逻辑。 而是,该操作也不会很频繁,因为可以建议创建新列并将数据复制到该列。


列变更


ALTER TABLE ALTER COLUMN TYPE -操作既危险又安全。 如果postgres仅更改方案,并且数据已经以所需格式存储并且不需要其他类型检查,则是安全的,例如:


  • varchar(LESS)varchar(MORE)类型更改;
  • 将类型从varchar(ANY)更改为text
  • 类型从numeric(LESS, SAME)更改为numeric(MORE, SAME)

ALTER TABLE ALTER COLUMN SET NOT NULL是危险的,因为它会传递内部数据并检查NULL,幸运的是,此结构可以用另一个CHECK IS NOT NULL代替。 值得注意的是,这种替换将导致不同的方案,但具有相同的属性。


ALTER TABLE ALTER COLUMN DROP NOT NULLALTER TABLE ALTER COLUMN SET DEFAULTALTER TABLE ALTER COLUMN DROP DEFAULT安全操作。


创建和删除索引和常量


ALTER TABLE ADD CONSTRAINT CHECKALTER TABLE ADD CONSTRAINT FOREIGN KEY是不安全的操作,但是可以将它们声明为NOT VALID ,然后再声明ALTER TABLE VALIDATE CONSTRAINT


ALTER TABLE ADD CONSTRAINT PRIMARY KEYALTER TABLE ADD CONSTRAINT UNIQUE不安全的,因为它们在内部创建了唯一索引,但是您可以创建唯一索引为CONCURRENTLY ,然后使用现成索引通过USING INDEX创建相应的常量。


CREATE INDEX是不安全的操作,但是可以将CONCURRENTLY创建为索引。


ALTER TABLE DROP CONSTRAINT CHECKALTER TABLE DROP CONSTRAINT FOREIGN KEYALTER TABLE DROP CONSTRAINT PRIMARY KEYALTER TABLE DROP CONSTRAINT UNIQUEDROP INDEX安全操作。


值得注意的是, ALTER TABLE ADD CONSTRAINT FOREIGN KEYALTER TABLE DROP CONSTRAINT FOREIGN KEY一次锁定两个表。


在django中应用知识


Django在迁移中具有执行任何SQL的操作: https : //docs.djangoproject.com/en/2.1/ref/migration-operations/#django.db.migrations.operations.RunSQL 。 通过它,您可以设置必要的超时并为迁移应用替代操作,指示state_operations我们正在替换的迁移。


尽管需要额外的编写,但是这对于其代码而言效果很好,但是您可以将繁琐的工作留在数据库后端,例如, https://github.com/tbicr/django-pg-zero-downtime-migrations/blob/master/django_zero_downtime_migrations_postgres_backend/schema .py收集描述的做法,并用安全的对等替换不安全的操作,这将适用于第三方库。


最后


这些实践使我可以直接使用django创建的相同方案,只是替换了CHECK IS NOT NULL构造而不是NOT NULL和一些构造名称(例如,对于ALTER TABLE ADD COLUMN UNIQUE和其他方式)。 另一个权衡可能是替代迁移操作缺乏事务性,尤其是在出现CREATE INDEX CONCURRENTLYALTER TABLE VALIDATE CONSTRAINT


如果您没有超出postgres的范围,那么可以使用许多选项来更改数据方案,并且可以在特定条件下组合使用它们:


  • 使用jsonb作为schamaless解决方案
  • 停机的机会
  • 无需停机即可进行迁移的要求

无论如何,我希望该材料对增加正常运行时间或扩大意识很有用。

Source: https://habr.com/ru/post/zh-CN425063/


All Articles