引言
哈Ha!
我想分享为postgres和django编写迁移的经验。 这主要是关于postgres的,django是一个很好的补充,因为它具有开箱即用的模型更改自动迁移数据方案的功能,也就是说,它具有用于更改方案的相当完整的工作操作列表。 Django可以用任何喜欢的框架/库替换-方法很可能是相似的。
我不会描述我是如何做到这一点的,但是现在阅读文档时,我发现有必要在更早的时候加倍小心和意识地做到这一点,因此,我强烈建议这样做。
在继续之前,让我做以下假设。
您可以将使用大多数应用程序的数据库的逻辑分为三部分:
- 迁移-更改数据库架构(表),假设我们始终在一个线程中运行它们。
- 业务逻辑-直接处理数据(在用户表中),持续且具有竞争力地处理相同的数据。
- 数据迁移-不更改数据模式,它们基本上像业务逻辑一样工作,默认情况下,当我们谈论业务逻辑时,我们也指数据迁移。
停机是指当我们的业务逻辑的一部分在用户明显的时间内无法使用/掉落/加载的状态,假设这是几秒钟。
停机时间的缺乏可能是企业的关键条件,任何努力都必须遵守。
推出过程
推出时的主要要求:
- 我们有一个工作基地。
- 我们有几台业务逻辑在其中发展的机器。
- 具有业务逻辑的汽车隐藏在平衡器的后面。
- 我们的应用程序在滚动迁移之前,期间和之后都能很好地工作(旧代码可以在新旧数据库模式下正常工作)。
- 我们的应用程序在更新汽车上的代码之前,之中和之后都能很好地工作(旧代码和新代码在当前数据库方案下均能正常工作)。
如果存在大量更改,并且推出不再满足这些条件,则将其划分为所需数量的满足这些条件的较小推出,否则我们将停机。
直接推出订单:
- 大量移民
- 从平衡器中删除一台机器,更新机器并重新启动,然后将机器退还给平衡器;
- 重复上一步以更新所有汽车。
当我们根据更改的方案自动创建迁移并验证是否存在所有向CI迁移时,反向推出顺序与删除表中的表和列有关。
- 从平衡器中删除一台机器,更新机器并重新启动,然后将机器退还给平衡器;
- 重复上一步以更新所有汽车;
- 大量移民。
理论
Postgres是一个出色的数据库,我们可以编写一个应用程序,以成千上万个流来写入和读取相同的数据,而且很有可能确保我们的数据将保持有效并且不会被破坏(通常是完整的ACID)。 Postgres实现了多种机制来实现这一目标;其中之一是阻塞。
Postgres有几种类型的锁,在这里可以找到更多详细信息,作为主题的一部分,我将仅涉及表和记录级别的锁。
表级锁
在表级别,postgres有几种类型的锁 ,主要特征是它们有冲突,即不能同时执行两个有冲突锁的操作:
| 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 |
例如,必须严格一对一地执行ALTER TABLE tablename ADD COLUMN newcolumn integer
和SELECT COUNT(*) FROM tablename
,否则,我们将无法找出要返回COUNT(*)
。
在django迁移(下面的完整列表)中,有以下操作及其相应的锁:
阻塞 | 运作 |
---|
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 |
在注释中,并非所有ALTER TABLE
都具有ACCESS EXCLUSIVE
锁定,Django迁移也没有CREATE INDEX CONCURRENTLY
和ALTER TABLE VALIDATE CONSTRAINT
,但是稍后将需要它们来作为标准操作的更安全替代方法。
如果在一个线程中按顺序执行迁移,那么一切看起来都不错,因为迁移不会与另一迁移冲突,但是我们的业务逻辑仅在迁移和冲突期间起作用。
阻塞 | 运作 | 与锁冲突 | 与运营冲突 |
---|
ACCESS SHARE | SELECT | ACCESS EXCLUSIVE | ALTER TABLE , DROP INDEX |
ROW SHARE | SELECT FOR UPDATE | ACCESS EXCLUSIVE | ALTER TABLE , DROP INDEX |
ROW EXCLUSIVE | INSERT , UPDATE , DELETE | ACCESS EXCLUSIVE , EXCLUSIVE , SHARE ROW EXCLUSIVE , SHARE | ALTER TABLE , DROP INDEX , CREATE INDEX |
这里可以总结两点:
- 如果有更容易锁定的替代方法,则可以将其用作
CREATE INDEX
和CREATE INDEX CONCURRENTLY
。 - 大多数更改数据架构的迁移都与业务逻辑发生冲突,此外,它们与
ACCESS EXCLUSIVE
发生冲突,也就是说,我们甚至无法在保持此锁定的情况下进行SELECT
并可能期望在此处停机,除非这种操作无法立即生效且停机时间为几秒钟。
必须做出选择,或者我们始终避免ACCESS EXCLUSIVE
,也就是说,我们创建新的板并在那里可靠地复制数据,但是长时间复制大量数据,或者使ACCESS EXCLUSIVE
尽可能快并针对停机提出其他警告-这是潜在的危险,但是速度很快。
记录锁
在记录级别,还存在锁https://www.postgresql.org/docs/current/static/explicit-locking.html#LOCKING-ROWS ,它们也发生冲突,但它们只会影响我们的业务逻辑:
| 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 |
这是数据迁移的重点,也就是说,如果我们在整个板上进行UPDATE
数据迁移,则其余更新数据的业务逻辑将等待锁释放并可能超过我们的停机时间阈值,因此最好进行部分更新以进行数据迁移。 还值得注意的是,当使用更复杂的sql查询进行数据迁移时,拆分成多个部分可以更快地进行,因为它可以使用更优化的计划和索引。
操作顺序
另一个重要的知识是如何执行操作,何时以及如何进行和释放锁:

您可以在此处突出显示以下项目:
- 操作执行时间-对于迁移来说,这是持有锁的时间,如果长时间保持沉重的锁,我们将有一个停机时间,例如,可以使用
CREATE INDEX
或ALTER TABLE ADD COLUMN SET DEFAULT
(在postgres 11中更好)。 - 冲突锁的等待时间-也就是说,迁移会一直等到所有冲突的请求都解决了,此时新的请求将等待我们的迁移,在这里缓慢的请求可能非常危险,无论是不是最佳的还是分析性的,因此在此期间不应存在缓慢的请求迁移。
- 每秒的请求数-如果长时间有很多请求在处理,那么免费连接可以迅速结束,而不是一个有问题的地方,整个数据库都可以进入停机状态(超级用户将只有一个连接限制),在这里您需要避免缓慢的请求,减少请求数量例如,在最小负载下开始迁移,使用其自己的数据库将关键组件分为不同的服务。
- 一个事务中有许多迁移操作-一个事务中的操作越多,重锁的持有时间就越长,因此最好分离重操作,而
ALTER TABLE VALIDATE CONSTRAINT
或具有重锁的一个事务中的数据迁移则更好。
超时时间
lock_timeout
具有诸如lock_timeout
和statement_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.py和https://github.com/django/django/blob/2.1.2/django/db/backends/postgresql/schema.py ):
| 操作 |
---|
1个 | CREATE SEQUENCE |
2 | DROP SEQUENCE |
3 | CREATE TABLE |
4 | DROP TABLE |
5 | ALTER TABLE RENAME TO |
6 | ALTER TABLE SET TABLESPACE |
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 | 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岁 | ALTER TABLE DROP CONSTRAINT UNIQUE |
19 | CREATE INDEX |
20 | DROP INDEX |
Django很好地满足了我的迁移需求,现在我们可以在不停机的情况下讨论安全和危险的迁移操作。
我们将使用SHARE UPDATE EXCLUSIVE
锁定或ACCESS EXCLUSIVE
来调用更安全的迁移,该迁移可立即生效。
我们将使用SHARE
和ACCESS EXCLUSIVE
锁来进行危险的迁移,这将花费大量时间。
我将通过大量示例预先留下有用的文档链接 。
创建和删除表
CREATE SEQUENCE
, DROP SEQUENCE
, CREATE TABLE
, DROP TABLE
称为安全的,因为业务逻辑要么不再与已迁移的表一起工作,否则使用FOREIGN KEY删除表的行为将在以后出现。
大量支持的工作表操作
ALTER TABLE RENAME TO
我很难称呼它为安全,因为在迁移前后很难编写适用于此类表的逻辑。
ALTER TABLE SET TABLESPACE
不安全,因为它会物理移动板,并且在大体积上可能要花费很长时间。
另一方面,这些操作非常少见,作为替代,您可以提供创建新表并将数据复制到其中的功能。
创建和删除列
ALTER TABLE ADD COLUMN
, ALTER TABLE DROP COLUMN
-可以称为安全(创建时没有DEFAULT / NOT NULL / PRIMARY KEY / UNIQUE),因为业务逻辑不再适用于已迁移的列,使用FOREIGN KEY删除列的行为,其他常量和索引将在以后出现。
ALTER TABLE ADD COLUMN SET DEFAULT
, ALTER TABLE ADD COLUMN SET NOT NULL
, ALTER TABLE ADD COLUMN PRIMARY KEY
, ALTER 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 NULL
, ALTER TABLE ALTER COLUMN SET DEFAULT
, ALTER TABLE ALTER COLUMN DROP DEFAULT
安全操作。
创建和删除索引和常量
ALTER TABLE ADD CONSTRAINT CHECK
和ALTER TABLE ADD CONSTRAINT FOREIGN KEY
是不安全的操作,但是可以将它们声明为NOT VALID
,然后再声明ALTER TABLE VALIDATE CONSTRAINT
。
ALTER TABLE ADD CONSTRAINT PRIMARY KEY
和ALTER TABLE ADD CONSTRAINT UNIQUE
不安全的,因为它们在内部创建了唯一索引,但是您可以创建唯一索引为CONCURRENTLY
,然后使用现成索引通过USING INDEX
创建相应的常量。
CREATE INDEX
是不安全的操作,但是可以将CONCURRENTLY
创建为索引。
ALTER TABLE DROP CONSTRAINT CHECK
, ALTER TABLE DROP CONSTRAINT FOREIGN KEY
, ALTER TABLE DROP CONSTRAINT PRIMARY KEY
, ALTER TABLE DROP CONSTRAINT UNIQUE
, DROP INDEX
安全操作。
值得注意的是, ALTER TABLE ADD CONSTRAINT FOREIGN KEY
和ALTER 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 CONCURRENTLY
和ALTER TABLE VALIDATE CONSTRAINT
。
如果您没有超出postgres的范围,那么可以使用许多选项来更改数据方案,并且可以在特定条件下组合使用它们:
- 使用jsonb作为schamaless解决方案
- 停机的机会
- 无需停机即可进行迁移的要求
无论如何,我希望该材料对增加正常运行时间或扩大意识很有用。