“想要零停机地迁移数据表,请给三倍时间”
迁移完工!
作者 | Kiran Ra 译者 | 弯月
出品 | CSDN(ID:CSDNnews)
作为移动应用公司的一名后端开发人员,我最常遇到的一项任务就是迁移数据表结构。有时是为了提高查询性能,而有时则是更改列名/类型,或者是让数据适应新的用例。虽然任务本身看似只是一组简单的 SQL 命令,但想实现零停机还是相当不容易的。
具体步骤如下:
1. 创建新的空表;
2. 同时写入新旧表;
3. 将数据(以块的形式)从旧表复制到新表;
4. 验证一致性;
5. 将读取切换到新表;
6. 停止写入旧表;
7. 清理旧表。
在这篇指南中,我们来看看迁移PostgreSQL数据表的全过程。虽然这些示例介绍的是PostgreSQL的表迁移,但文中介绍的步骤同样适用于其他类型的数据迁移。
1、背景介绍
现有的表结构
假设我们有如下表结构式,其中包含一个名为 old 的表,还有一个包含该表CRUD(Create、Read、Update和Delete,即创建、读取、更新和删除)操作的API。
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE IF NOT EXISTS old (
old_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
data TEXT NOT NULL
);
-- Create
INSERT INTO old (data)
VALUES (?)
RETURNING *;
-- Read
SELECT *
FROM old
WHERE old_id = ?;
-- Update
UPDATE old
SET data = ?
WHERE old_id = ?;
-- Delete
DELETE
FROM old
WHERE old_id = ?;
新的表结构
列data的类型原本为TEXT,是为了方便操作,但现在该列只用于保存时间戳。假设,现在我们收到了一个请求,要求统计两个时间戳之间的所有数据项。尽管用旧的表结构也能实现,但我们认为,将data的类型改为TIMESTAMP,可以更好地实现需求。同时,表名old也不是一个准确的名称,我们决定改为new。
我们想要的表结构如下:
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE IF NOT EXISTS new (
new_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
created_date TIMESTAMP WITH TIME ZONE NOT NULL
);
迁移要求
此次数据迁移还有更进一步地要求:
● 在整个迁移过程中,系统必须能够正常响应请求。
● 任何操作都不能锁定大量数据(写锁定)。
● 不能进行不安全的操作;
● 如果遇到问题,必须能够撤销更改,回滚到前一个版本。
2、具体步骤
创建一个新的空表
CREATE TABLE IF NOT EXISTS new (
new_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
created_date TIMESTAMP WITH TIME ZONE NOT NULL
);
同时写入新旧表
现在,我们有两个表,我们可以同时写入两个表。虽然此时旧表的数据才是真实的来源,但这里我们采用最终一致性。
-- Create
WITH new_rows AS (
INSERT INTO new (created_date)
VALUES (?)
RETURNING *
)
INSERT
INTO old (old_id, data)
SELECT new_id, CAST(created_date AS TEXT)
FROM new_rows
RETURNING *;
-- Update
UPDATE old
SET data = ?
WHERE old_id = ?;
UPDATE new
SET created_date = ?
WHERE new_id = ?;
-- Delete
DELETE
FROM old
WHERE old_id = ?;
DELETE
FROM new
WHERE new_id = ?;
请注意,我们的操作看起来比之前更为复杂,因为我们需要在新表中创建一行数据,然后再插入到旧表中。为了确保随机生成的 UUID 是一致的,这两个操作在一个事务中完成。
将数据复制到新表
在处理好新记录后,下一步我们需要复制旧记录。
INSERT INTO new(new_id, created_date)
SELECT old_id, CAST(data AS TIMESTAMP)
FROM OLD
WHERE NOT EXISTS(SELECT *
FROM new
WHERE new_id = OLD.old_id)
LIMIT 1000
RETURNING *;
我们将新表中不存在的数据,从旧表复制到新表。为了保持数据库正常响应,我们通过limit 1000,分批完成这项操作。虽然块的大小可以根据表进行调整,但我更喜欢小块,因为可以避免锁定大量数据。
验证一致性
这一步的工作经常被忽视。在切换读取之前,我们必须确保新旧表之间的数据完全同步。我们可以通过一些查询来验证新旧表之间的一致性。
检查项1:是否缺少任何记录?
SELECT *
FROM old
FULL OUTER JOIN new ON old_id = new_id
WHERE new_id IS NULL
OR old_id IS NULL
检查项2:数据是否一致?
SELECT *
FROM old
INNER JOIN new ON old_id = new_id
WHERE CAST(data AS TIMESTAMP) <> created_date
切换读取
这一步的工作量往往最繁重。经常有几十处不同的代码会从表中读取。由于现在两个表之间的数据是同步的,因此我们可以花点时间慢慢地完成这部分迁移工作。
SELECT *
FROM new
WHERE new_id = ?;
在这个阶段,我们还需要更新视图、外键、触发器等,让它们都引用新表。
停止写入旧表
到这里,所有读取已切换到新表,我们不再需要更新旧数据库。
-- Create
INSERT INTO new (created_date)
VALUES (?)
RETURNING *
-- Update
UPDATE new
SET created_date = ?
WHERE new_id = ?;
-- Delete
DELETE
FROM new
WHERE new_id = ?;
清理旧表
在确认系统不再引用旧表时,旧表就可以删除了。
DROP TABLE IF EXISTS old;
3、完工!
迁移完工!
最困难的工作是向产品小组解释,为什么看似如此小的一个请求,花费的时间却是他们预想的三倍。希望在阅读本文之后,他们能够理解。
原文链接:https://kiranrao.ca/2022/05/04/zero-downtime-migrations.html
声明:本文为 CSDN 翻译,转载请注明来源。
更多推荐
所有评论(0)