标签档案:迁移

数据库迁移

MalcolmInTheMiddleGif

(总是多一件事……)

我们是谁?

Bazaarvoice的广告管理团队是从一个孵化器团队发展而来的。我们孵化器的目标是快速迭代想法,产生原型和“概念验证”项目,如果它们验证了客户的需求,就可以进行迭代。这里感兴趣的项目基于从公司其他几个团队收集的事件数据的聚合生成报告。随着我们项目的发展,它的规模和范围也在不断扩大,最终导致我们需要重新审视在原型阶段所做出的一些设计决策。具体来说,我们发现我们选择的原始数据库系统EmoDB随着需求的发展而不能满足我们的需求。

为什么迁移?

当这个项目开始时,它作为一个原型开始,旨在使项目尽可能快速和轻松地运行。最初的团队选择了EmoDB,因为他们熟悉其他项目的内部技术,它符合我们最初的需求。随着项目的发展,我们有了更多的数据来操作,我们遇到了可伸缩性问题,最初是通过缓存和一些重构解决的。我们发现,当我们查询EmoDB时,就好像它是一个典型的关系数据库,而实际上它并不是为那种用例而设计的。(Emodb是一个最终一致的json blob存储,具有跨越多个AWS az和区域的变更通知数据。EmoDB为我们在Bazaarvoice的许多解决方案提供了动力,现在它是开源的,可以在以下网站上获得:
https://github.com/bazaarvoice/emodb

我们选择切换到MySql,以利用关系数据模型来汇总我们收集和计算的数据。我们以前在检索整个文档以对数据执行聚合时遇到了一些问题,这导致我们决定针对关系模型进行优化的技术更适合这个项目。

如何移民?

因为在我们想要迁移数据库系统时,我们的项目已经有了训练有素的用户,所以我们需要使用无停机时间的方法来设计迁移;“无缝地”为我们的用户更改后端实现。我们还使这些转换成为可配置的,这样我们就不需要从新系统进行一次大型的主切换,但是我们可以选择准备将哪些服务切到新的数据后端。
下图是我们的设计文档,它描述了我们如何计划迁移。左边是我们命名为“legacy”的原始代码库。右边是我们迁移的新服务堆栈的建议设计。插入到中间的是“服务Facade”,我们打算在其中针对遗留技术堆栈和新技术堆栈之间的实时数据运行质量保证。

广告管理迁移到基于MySql的堆栈的副本-第1页

如何维护数据一致性?

根据在数据库之间进行差异和迁移的数据的大小,运行必要的迁移成本可能很高。我们的解决方案是编写回填数据的特定任务或直接将数据集迁移到新数据源。这使我们能够进行冒烟测试,以确定我们的服务是否正常工作,而无需花费大量时间或金钱在此过程中寻找漏洞。随着我们对自定义工具和服务信心的增长,我们将回填和迁移更大的数据块,直到我们从新服务中迁移了所需的一切。

什么是服务Facade?

服务facade层负责在遗留堆栈和新堆栈之外执行各自的操作。这是我们放置不同逻辑的地方,用于比较Emo和Mysql在相同操作中返回的结果。facade从预定义的配置堆栈返回数据。这意味着应用程序的某些部分可以从Mysql中获取资源,而其他部分,我们没有信心,继续从Emo中获取资源。例如,我们用Scala编写的CampaignRoiReportBuilderServiceFacade是这样的:

class CampaignRoiReportBuilderServiceFacade @Inject()(private val campaignRoiReportBuilderServiceLegacy: campaign roireportelegacy, private val campaignRoiReportService: campaignRoiReportService, private val campaignConfig: CampaignConfiguration, private val facadeDiffTool: facadeDiffTool){…def buildReport(…):Future[选项[CampaignRoiReport]] = {val roiReportFromEmoDbFuture: Future[选项[CampaignRoiReport]] = campaignRoiReportBuilderServiceLegacy.buildReport(…)val roiReportFromMySqlFuture:Future[Option[CampaignRoiReport]] = campaignRoiReportService.buildReport(…)//从scala Future中提取数据{roiReportFromEmoDbMaybe <- roiReportFromEmoDbFuture roiReportFromMySqlMaybe <- roiReportFromMySqlFuture}{//模式匹配从scala Option (roiReportFromEmoDbMaybe, roiReportFromMySqlMaybe)匹配{case (None, None) =>//这是一个不可能的情况,但列出以避免编译警告情况(Some(_), None) =>日志。warn("/*报告丢失/不匹配的数据*/")case (None, Some(_)) =>日志。warn("/*报告丢失/不匹配的数据*/")case (Some(roiReportFromEmo),Some(roireportfromysql)) =>if(mismatches. nonempty) LOG. val mismatches = facadeDiffTool.campaignROIReportLegacyDiff(roiReportFromEmo, roireportfromysql)warn("/*报告丢失/不匹配的数据*/")}}//这是我们如何配置我们返回给资源的源。campaignConfig。masteringFrom match { case EmoDb => roiReportFromMySqlFuture.onFailure{ case e:Throwable => LOG.warn("Failed to build an ROI report on the MySql side", e) } roiReportFromEmoDbFuture case MySql => roiReportFromEmoDbFuture.onFailure{ case e:Throwable => LOG.warn("Failed to build an ROI report on the EmoDb side", e) } roiReportFromMySqlFuture } } }

原始资源类将被修改为从新的facade层调用,但其他功能不应更改。如果构造正确,facade层将以与原始服务相同的方式工作,因为facade模仿原始服务类中可用的公共功能。这些重复的函数将调用来自遗留服务类和新服务的方法。有了来自旧服务和新服务的响应,facade层可以对两个服务栈之间的差异进行评估。为了报告我们的差异,以便在API使用过程中得到通知,我们将把它们记录到我们的日志管理和监控系统中。

我们如何捕捉不匹配?

伐木是我们的一大关注点。我们知道,在调试新的服务堆栈时,每次调用都会有很多不同之处。例如,在一次通话中,我们报告了2000多个差异。我们希望以一种有意义的方式将所有差异组合到每个调用的一个日志中。为此,我们编写了自定义差异工具,它将以MismatchedField类集的形式返回数据中的差异。

case类MismatchedField[T](name: String, legacyValue: T, newValue: T)


这个模板化类将保存从遗留服务(legacyValue)和新堆栈的服务(newValue)返回的值,以及一些有意义的标记,用于标识这种不匹配的来源(name)。然后,我们将通过自定义diff工具将任何给定调用的所有不匹配组合到单个日志中。自定义差异工具中的每个函数都会返回Set[MismatchedField[Any]]。然后,我们可以将每个集组合成一个差异集,这样我们就可以只使用一个日志调用,在一个日志条目中写出整个差异集。

一个有趣的发现:

通过这次迁移,我们发现的最有趣的发现之一并不是在为新数据库构建新服务堆栈时出现的错误,而是我们在原始数据库堆栈中发现了错误。从中得到的一个结论是,要确保对源数据中发现的任何不匹配进行调查。在代码迁移过程中,我们发现一些遗留功能写得不正确。例如,在我们的遗留代码中,我们将一些聚合数据存储在集合中,无意中屏蔽了重复的数据。当为我们的新服务堆栈重新实现这些相同的聚合时,它们被正确地实现为列表,从而在数据中产生不匹配。通过我们的调查,我们没有简单地将数据与遗留服务的工作方式相匹配,而是返回到原始数据,并通过Scala REPL手动运行计算。在这样做的过程中,我们发现新服务是正确的,而遗留代码是错误的。幸运的是,遗留代码中的错误很容易修复。我们在遗留代码中实现了修复,我们的不匹配消失了。

其他重要信息:

一个重要的团队收获是对迁移所需的工作非常坦率和声明性。我们对迁移的调查不仅涉及到为MySql建立一个新的技术堆栈,而且还改变了我们的构建工具MavenSBT,介绍再经+Jooq插件在整个迁移过程中强制执行类型安全,设计一个新的数据模型(这是最初执行迁移的最终驱动因素),以及将代码升级到最新的scala版本以利用之前的所有更改。最终,我们严重低估并低估了开始迁移所需的工作。

同样重要的是要记住,每个团队都是不同的,有不同的需求。在讨论数据库迁移时,要花时间对未来的工作进行适当的风险评估。在迁移过程中也要保持这些对话。作为一个团队,我们最终优先考虑新的特性请求和与迁移无关的bug,因为迁移感觉与我们的生产环境是正交的。

进一步的结论是,如果我们更现实地评估用户,我们本可以节省大量时间。回想起来,这些报告的用户是内部的,对于较小的服务中断会更宽容,这将允许我们利用可配置的服务更快地迁移。以稳定性为代价,我们相信我们可以通过强迫自己解决问题来实现更快的迁移,而不是像我们所做的那样长时间地维护我们的遗留代码。尽管如此,大多数场景都没有这种奢侈,我们希望基于façade的方法对您有所帮助。