不停机从 PostgreSQL 迁移到 MySQL
原文地址 https://engineeringblog.yelp.com/2024/10/migrating-from-postgres-to-mysql.html
Yelp Reservations 服务 (yelp_res) 是为 Yelp 上的预订提供支持的服务。它与 Seatme 一起于 2013 年被收购,是一个 Django 服务和网络应用程序。它为 Yelp Guest Manager(Yelp 客户管理)的预订后台和逻辑提供动力,并处理创建预订的用餐者和合作伙伴流程。此外,它还为我们的 Yelp Reservations 应用程序提供用户界面和后台 API,该应用程序已被 Yelp Guest Manager 取代,但我们的许多餐厅客户仍在使用。
代码
save()函数中添加了逻辑,以添加一个提交后触发器来发布到一个新的 AMQP 主题,并重构了代码以消除「批量」操作,这种操作会在不调用 save() 的情况下写入数据库。这意味着,即使在更新 MySQL 表时,我们现有的观察者也可以监听这个新的 AMQP 主题。我们还引入了事务分组,在事务块开始时为每个事务生成一个通用唯一标识符(UUID)。该标识符与变更数据一起写入,以便按事务对变更进行分组。然后,我们对现有主题和新主题进行监控,确保数据匹配,然后再切换到使用新主题。TableTimeSlotBlock的新表,其中包含以表 id 和每个现有阻塞时段的 15 分钟时段为键的行。然后,应用代码通过执行 SELECT ... FOR UPDATE查询来检查冲突并锁定行(即使它们还不存在)。我们将这一逻辑放在代码中比现有数据库触发器更早的位置,因此通过检查日志,我们可以确保现有触发器不再被使用,这意味着新解决方案的限制性至少与现状相同。在代码中所有模型的继承层次中添加一个名为 AlsoWriteToMysqlModel的新模型。该模型重新定义了 save() 和其他对象级 DB 写入函数,以便首先写入「主」DB,然后将对象保存到「次」DB对代码中所有查询集的 AlsoWriteToMysqlQuerySet和 queryset 操作也做了同样的处理在 Django 中,并非所有操作都是在对象上执行的;例如,您可以拥有一个带有过滤器的 queryset,然后将其删除,这样就可以使用该过滤器执行一次 DB 查询,而无需加载实际对象。 为我们无法控制的模型(如 User模型或第三方模型)添加了 post_save 和 pre_delete 信号处理器,它们也会执行同样的操作。我们本可以对所有模型都使用这种技术,但我们认为在模型内部设置逻辑更易于理解,同时也让数据库写入尽量集中在一起。 用在 MySQL 事务中嵌套 PostgreSQL 事务的装饰器取代了 Django 的默认事务装饰器。这意味着只要我们还在事务中,任何数据库故障都会导致两个数据库的回滚。 但在 MySQL 提交时出现的故障除外;这里的逻辑是,数据库触发器有时会导致 PostgreSQL 提交失败,而 MySQL 提交应该始终成功,除非有内核问题。我们最初为了降低在上线期间引入新的 PostgreSQL 写入失败的风险而颠倒了提交顺序,结果在向 MySQL 提交写入后,一些事务因 DB 触发器而失败,导致整个数据库的数据不一致。这是一个有趣的例子,在一个维度上的「稳妥行事」实际上导致了错误。
为路由器(Django 类,决定我们读取/写入哪个数据库)添加逻辑,以分离「读取数据库」和 「写入数据库」。 添加了中间件,以在我们希望从 MySQL 读取请求时设置一个标志,路由器会遵守该标志 在中间件堆栈中进行任何数据库读/写操作前都会设置该标记,以确保每个请求只从一个数据库读取数据
最初,我们计划了一个过渡期,在这个过渡期内,「主」数据库可以根据每次请求而有所不同。但是,这会导致自动递增主键的问题。具体来说,PostgreSQL 维护一个序列,只有在没有设置主键的情况下插入记录时才会递增。这意味着,要么始终设置主键,要么永远不设置主键。否则,每次在设置了主键的情况下写入(比如写入 MySQL,然后将对象保存到 PostgreSQL),都会导致 PostgreSQL今后在未设置主键的情况下写入失败。在推广过程中,我们花了一些时间才弄明白这一点,因为症状是现状流程中出现少量错误,但 在固定使用 MySQL 的请求中没有错误。 Django 用随机字符串命名数据库保存点。在我们的基础架构中,ProxySQL 位于客户端和数据库之间,存储查询摘要以用于度量。这些摘要是查询的通用表示,不依赖于写入或读取的实际数据,但摘要中包含了保存点名称,导致使用保存点的每个查询都有一个唯一的摘要。这导致 ProxySQL 内存使用量不断攀升,并出现了一些生产问题,直到我们解决了这个问题。我们通过更改 ProxySQL 实例中的设置解决了这个问题。 从「批量」操作转换到单个对象级操作意义重大,可能会导致逻辑问题(因为批量操作中不会调用 save()等操作)和性能问题(因为可能会执行更多数量级的数据库查询)。在一个实例中,这意味着要重写存档批处理作业以使用原始 SQL,但除此之外,MySQL 可以轻松处理我们的写入量。 尽早执行初始数据加载(回填)对于测试非常有用,但我们本应在修复所有错误后再执行一次完整、干净的第二次回填。相反,我们专门修复了我们发现的破损数据,但这导致了由已修复代码引起的错误,而这些错误本来是可以避免的。 不要忽视数据库的其他用户。我们的分析流水线原本直接从 PostgreSQL 获取数据,而将其转移到使用 MySQL 耗费了大量时间。这是淘汰旧数据库的最后障碍。 使用 Yelp 标准栈提高了性能。这并不是因为 MySQL 本身更具性能优势,而是通过使用公司内部标准的技术栈,我们能够从多个同事监控和优化数据库性能的努力中受益。
Bytebase 3.0.1 - 可配置在 SQL 编辑器执行 DDL/DML
全方位对比 Postgres 和 MySQL (2023 版)