原文地址 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 版)