策划 | 万佳
在 Botify,我们工程团队的核心价值观之一是 ownership。我们赋予工程和产品团队自主权以及灵活性,让它们拥有自己的项目并完成这些项目。然而,随着我们在更大的技术栈上工作,团队规模也越来越大,我们开始遇到一些关于如何共享工作成果的问题。
2016 年年底,我们想赋予工程师和产品经理更多的 local ownership,从而能快速轻松地将他们的产品和技术栈投入使用。为此,我们决定将 Django 应用程序拆分为微服务。
这个故事阐述了我们是怎样失败,以及如今我们为何又将这些服务迁回到单体应用。同时,我们还会花些时间来分享我们的经验教训。
在深入讨论前,我想强调下:本文的目的并非指责微服务。在 Botify,我们的理念是“使用最合适的工具”——我们认为,微服务并非此刻解决我们问题的适当工具。
首先,简单介绍下 Botify 的历史。Botify 于 2012 年在 Python/Django 技术栈上创建。到 2016 年初,整个 Botify 平台都是通过我们 Django 应用程序的负载均衡集群提供服务。当时,我们有一个大约 15 人的产品和工程团队。
以下两个目标促使我们评估在技术栈中使用微服务的可能性:
提高开发速度:我们希望减少前端和后端之间的依赖关系,并增加 local ownership,从而将开发主体限定为一个小团队;
统一技术:我们在巴黎招聘 Python/Django 工程师越来越多困难,所以我们就想,前后端都统一使用 JavaScript 会让招聘工作更容易,因为我们在转向“全栈”JavaScript 这样一个单一的角色。
我们决定从身份验证和授权栈入手实现第一个微服务。我们认为,应该从小处着手,将 Django 应用程序当前的一部分功能转移到微服务中。我们创建了一个小型的 JavaScript 团队,负责实现 NodeJS 后端,用于处理用户及其帐户和权限。我们称之为 Customer Success。
使用微服务的 Botify API 架构概要
当你像我们一样,在人为因素的驱动下做出技术决策时,你很快就会遇到问题。新团队的组织和流程很难与现有的团队并行不悖。由于特性是独立开发的,很快就出现了知识分化。微服务内部的东西没法共享,而跨栈代码审查的缺失让我们觉得失去了 ownership。
你可能已经注意到,在上面的架构中,微服务 Customer Success 并非完全隔离。从单体到微服务存在后端到后端的通信,反之亦然。这并不完全是坏事(似乎还很常见),但这还是会降低性能,并在两个服务之间创建依赖关系。从长远来看,这还意味着更复杂的机制,如 circuit breakers 和优雅的服务降级(为保证正常运行时间和可用性)。
我们还看到,在该模式下,我们主要的关系数据库是共享的。在数据库内部,一些表被映射到 Django 和 Express/Sequelize 的模型。换句话说,修改共享表的模式需要对微服务和单体进行同步修改。这是由糟糕的领域隔离所导致的。
随着时间的推移,我们学会了如何健壮地构建和部署我们的 Django 应用程序,但是,对于每个新的技术栈,我们必须重新掌握这个过程。虽然在微服务环境中,独立部署是一个核心优势,但与部署微服务相比,我们还是对部署单体更有信心。不过,我们花了比较少的时间就为我们的微服务实现了健壮、流畅的自动化部署。
在日常工作中,我们遇到的问题越来越多,为了让架构更有效,我们需要不断地修改解决方案。期间,我们针对 Django 技术栈做了一些工作,改进代码、覆盖率、可测试性、依赖关系和性能。
我们颠覆了最初驱使我们采用微服务的模式。
微服务技术栈是从当前流行的框架和开发团队最熟悉的框架中选取的。然而,在 Botify,我们坚信,要为合适的工作选择合适的工具:合适的工具不一定是最著名的工具,也不一定是我们已经知道的工具。
我们确定,是时候认真地(重新)考虑我们的微服务架构和 Customer Success 后端了。
鉴于每天都要在 JavaScript 身份验证后端和 Django 模块之间频繁地来回切换,我们把工程师们召集在一起,权衡该架构的优缺点以及潜在的迁移成本。
我们做出大胆选择,将身份验证后端重新加入到 Django 单体中,并重新生成了我们在 2016 年从 Django 转换到 Node 的 API 端点。为什么要重新采用以前的做法?下面是其中一些原因。
重要的是要记住我们的用例以及 Botify 为谁服务。我们的平台是一个企业级 B2B 服务,帮助 Web 上最大的网站改善他们的 SEO。我们的主要挑战在于对客户数据的分析,而不是我们的流量:我们每个月要处理 PB 级的数据,在一些长期运行的任务上,比如爬取或处理数据。
不过,我们不需要巨大的面向用户的流量,因为在撰写本文时,我们的用户主要是 Web 上大型站点的 SEO 经理。处理用户相关数据的微服务架构旨在服务于高流量的 B2C 平台,而 Botify 的挑战在于动态地聚合数以 GB 的 SEO 数据,让其在几秒钟内可用。对大约一万名客户的元数据以毫秒为单位进行响应,这项任务无需高度可伸缩的微服务架构。恰恰相反,我们的后端到后端通信减慢了这些简单的检索过程,花费了更多时间。
举个例子,最近的一个项目是删除一个无人维护的依赖项,该依赖项会将 JSON 数据库列作为文本处理,以便利用 PostgreSQL 中内置的列类型 jsonb。
然而,这些文本列是在两个代码库之间共享的,逐步迁移非常麻烦。同步部署多个后端很容易出错,尤其是在规模大并存在负载均衡的情况下,这与微服务最初的好处背道而驰。在 Botify,我们总是喜欢用这个警示性的故事来说明我们不喜欢同步部署。这是个有趣的故事,如果你有时间的话,可以一读。
微服务最初由少数人实现,是为了保护新来者不受无人维护的代码库的影响,同样的人处理了所有的演进。这种现象违背了我们的代码评审理念:任何人都能够评审并理解正在发生的事情。在 Botify,我们喜欢开放式的工作方法,这样,团队中的任何人都可以查看所有代码并发表评论或提出建议。使用单独的技术栈、团队和语言,这让我们失去了团队合作和严格评审给开发团队带来的许多好处。
以上这些促使我们停用身份验证微服务并将其端点迁回 Django 单体的主要原因。
停用微服务非常简单,有许多方法可供选择。
我们希望在终止 Customer Success 后端时,所作的更改尽可能少,这意味着要模拟微服务的行为,在单体中逐个重写 API 端点并切换路由。这样,更改只会影响 API,而不影响前端,从而避免可怕的同步部署或 API 版本控制。
以下是我们停用微服务所采取的步骤:
识别路由:列出微服务提供的所有 API 路由,确定它们的用途和目标。先处理简单的情况:有些路由可能没用,或者没有必要。
在目标后端创建路由:最麻烦的工作是在目标代码库中编写相同的逻辑。相同的输入,相同的输出,还要最小化切换风险。
Y 分支(Y-branch)调用,比较结果:设置一个简单的分支系统,使用两个后端并比较响应,以发现不一致的地方。
根据 QA 反馈,进行修复:QA 大量而广泛地审查逻辑,以发现任何遗留的 Bug。
逐步扩大 Y 分支范围,以使用新迁移的逻辑。
待所有逻辑都迁移到单体后,删除微服务,并完成清理工作。移除正在运行的实例和计算机。
我们可以很自豪地说,我们在 2 月 4 日 10:34 成功地停用了 NodeJS 身份验证后端 Customer Success,没有任何同步部署或 API 版本控制。
停止时间:10:34
如前所述,停用后端非常简单,使用这套方法,我们没有遇到大的麻烦。
实际上,我们遇到的最令人沮丧的问题是我们自己找出的:在构建 Customer Success 后端时,我们选择的策略是,不管 REST 调用的结果是什么,总是返回 200 响应,并使用布尔型的 success 和响应关键字 statusCode 提供更具体的错误信息:
{
"errors": [
{
"message": "Organization was not found",
"code": 404,
"name": "NotFoundError"
},
],
"success": false,
"statusCode": 404
}
虽然这是一种常见的 REST 实践,但它不适用于 Django REST 框架。在不更改契约的情况下,将这些端点从 Express 迁移到 Django,需要大量重写 Django REST 框架的行为,才能匹配我们的 Express 实现。我们没有利用 HTTP 状态码来处理错误,而是对其进行了修改,这样,我们就可以用相同的响应关键字返回 200 状态码。令人失望,但也不是太糟糕。这种差异让人心里发痒,如果我们认为有必要,就会把它作为技术债务的一部分加以修正。好的一面是,我们的微服务定义了一种智能的跨源资源共享策略,这个必须要复制到我们的单体系统中。虽然一开始做正确的事情很麻烦,但它使我们能更好地保护应用程序免受恶意攻击。
本文不是要批评微服务,也不是要对 Django 和 Express REST 后端评短论长。每一种技术都自有其用途,但我们相信,要在正确的时间选择正确的解决方案来构建我们的产品。如今,我们不必来回切换,也不用维护两个后端,显然,我们已经从中获益。
顺便提一下,当我们构建微服务后端时,我们实际上还同时构建了另一个后端,这个后端直到今天都还用着。它的功能与用户以及我们的单体应用联系比较少,所以它运行地很好,依赖和痛点也都更少。如果我们必须再次实现一个类似的特性,我们可能会在单体应用中实现,但是现在,既然它还没出问题,我们就先不修复了。
对于某些用例和情况,我们仍然相信微服务。我们并不排除未来创建新的微服务的可能性。不管怎样,我们已经从错误中吸取了教训。
我们的团队在组织结构上也有了变化: 现在实行小组制,有单独的小组负责产品特性以及前端和后端工程师面临的技术挑战,还有一名产品经理和一名 QA 工程师。我们吸取教训,每个小组都几乎是独立地设计和实现其特性,但代码库在小组间是共享的,代码审查则是由 Botify 的所有工程师负责。这让我们可以共享 ownership、知识和技术栈维护,并达到我们作为一个团队希望达到的卓越技术水平。
参考阅读:
https://medium.com/botify-labs/to-kill-a-microservice-d6c9e7ad444c
「参与创作,免费领取 InfoQ 编辑训练营内训课程」在写作平台上仅需上传 3+ 篇文章,即可免费领取 InfoQ 内训课程【教你如何写作——编辑训练营】~投稿越多、文章越走心、阅读量越高还可挑战更高奖项!扫码了解活动详情:
点个在看少个 bug 👇