以客户为中心的微服务转型:从GraphQL单体应用开始的旅程
微服务已经成为创建可扩展且可独立部署的系统的首选方案。然而,从单块架构迁移,特别是涉及复杂 GraphQL API 的情况时,会带来独特的挑战,这需要有策略性的规划。通常从拆分服务器端组件开始,然后再调整客户端,但是否有一种更高效的方法呢?一种方案是在拆分服务器端的同时,同时调整客户端!
这篇博客文章探讨了我们分解一个复杂的GraphQL单体API服务的过程,首先从调整客户端应用程序开始(或者同时拆分服务器端)。在文章结束时,您将理解我们为何选择这种以客户端为主的策略、迁移过程中涉及的步骤以及这种方法的优点和缺点。
GraphQL 单体 API 简介我们的旅程开始于一个自2016年以来一直使用的单体式的GraphQL API服务,支持70个客户端。最初,这种集中式架构由于其简单性和易于开发而显得有优势。然而,随着我们的发展、增加了更多的客户端以及新功能,其局限性逐渐显现出来。作为Agoda酒店管理(YCS)应用的主要后端,随着YCS扩展到新的领域,该服务逐渐积累了各种后端API,这导致了不同领域在单一服务中的混合。
性能瓶颈、漫长的部署周期、混杂的代码库所有权和对单一代码库的依赖是影响我们阿果达开发者体验的一些关键问题——开发者体验是我们的一项关键指标。意识到我们需要一个更强大和灵活的架构,我们开始探索微服务架构。
从单一代码库转向微服务架构可以带来许多好处,但也带来了独特的挑战,特别是在使用 GraphQL 时。其中一些主要挑战包括:
- 拼接多域查询/突变(参见下图1)
- 模式管理和服务发现
- 数据一致性。
图1:单体拆分前后
在这篇文章里,我们将探讨如何解决这些挑战。但首先,让我们深入探讨一下我们如何开始拆分单体。
拆解单体把单体拆分成微服务的过程经过了周密的规划,并且分阶段进行。
定义微服务的界限我们开始定义微服务(也称为领域)的逻辑边界,基于业务能力和数据关注点。这一步对于确保每个微服务的内聚性至关重要。它处理特定的业务逻辑和数据,并通过松散耦合来最小化服务间的依赖。
- 业务能力:我们首先分析了单体架构应用提供的各种业务功能。
- 数据考虑:当我们明确了业务能力后,我们检查了每个功能的数据访问模式和存储需求。确保每个微服务只能访问它需要的数据,并且最好能够自主管理自己的数据这一点至关重要。
- 可伸缩性和性能:可伸缩性需求是边界定义的另一个关键因素。具有不同负载模式的服务会被隔离以实现各自独立的扩展。例如,处理价格变更功能的微服务可能需要与处理内容变更的微服务有不同的缩放方式。
- 所有权和可维护性:我们也考虑了每个微服务的所有权和可维护性。特定开发团队被明确指派了管理职责,促进了对服务的责任感和专长。
同时,我们更新了客户端应用以支持现有的单体架构以及未来的微服务架构。
这涉及了:
- 客户分析:我们采用数据导向的方法分析了全部70个客户。
- 简化了复杂的查询(见图1)。
- 集成自定义库(智能编排库):此库帮助管理查询和变体,在大多数情况下无需更改客户端查询;但在某些特定情况下,由于查询的特性,仍需进行相应的修改。
实施阶段是逐步执行的,微服务逐一被开发、测试并部署。在此阶段,采用了严格的测试手段来验证数据的正确性,其中包括一个准确性测试系统,确保单体应用和微服务在投入生产前生成相同的结果。
监控与调整单一后台逐渐分解以及充分的客户端准备确保了平滑过渡。这种方法保持了最佳的操作效率,并减少了服务中断。在整个过程中,服务中断被最小化。
我们已经成功地根据这个详细的计划迁移了七个微服务中的三个。
图2:拼接查询的前/后状态
客户优先的非传统方式采取“服务器优先”的方法存在一些问题:
- 在迁移客户端前,我们需要完成服务器端拆分为领域特定的微服务,这将花费大量时间。
- 与多个负责实现这些客户端的外部团队协调,并确保他们承诺预计的到达时间(ETA)可能会很有挑战性。
采用“客户优先”的方法减少了与外部团队的大量沟通需求,让他们可以根据自己的安排进行迁移。同时,后端工作也在并行进行。这种策略还确保客户从一开始就准备好应对单体应用的拆分,从而降低了将单体拆分到多个独立服务(微服务)中的风险,并保持了用户无缝的体验。
客户就绪指的是什么?一旦单体应用被拆分成微服务,我们就能够将客户端的请求转发到合适的服务,而无需修改客户端应用程序的代码。
图3:客户的准备情况
以下是我们在准备拆分过程中需要解决的客户端挑战,以及我们是如何解决这些问题的:
- 模式管理/服务发现:原来的单体应用需要拆分成六个微服务,这在逐步拆分的过程中增加了GraphQL架构管理和客户端服务发现的复杂性。
- 跨域查询/跨域变异拼接:随着单体服务被拆分,现有查询可能需要根据架构变化及其重组方式进行调整。
我们有几个有希望的方案来解决模式管理和服务发现的问题。
- 阿波罗联盟(第三方),
- 智能编排库软件。
阿波罗联盟:简单介绍
Apollo Federation 可以把多个 GraphQL API 整合成一个统一的联邦图。这种方法让客户端可以通过一个请求与多个 GraphQL API 交互。
在联邦图设置中,客户端将其 GraphQL 请求发送到一个被称为路由器的统一的入口点。路由器然后智能管理和分配这些请求到各个 GraphQL API,并合并一个统一的响应。从客户端的角度来看,通过路由器查询联邦图与直接查询任何独立的 GraphQL 服务器没有任何区别。
图4:如图所示,阿波罗联邦路由器的流程
Apollo联邦的局限性虽然 Apollo 联邦是一个将现有的微服务整合成统一的图谱的强大工具,但它并不适合阿果达项目的需要。主要原因有:
- 现有微服务的需求:
Apollo Federation 最适合与现有的微服务一起工作,将它们统一成一个无缝的系统整体。然而,我们的项目需要拆分一个单体应用,这Apollo Federation 并不能高效地支持这一点,因为它需要对整个单体应用的代码库进行修改以实现兼容性,这与我们逐步拆分的目标相悖。
2. 项目重点:
我们的策略是先准备客户端应用,再逐步过渡到服务器端组件。Apollo Federation 不适合这种逐步迁移,因为它要求在集成前进行直接拆分。
3. 逐步过渡法:
我们需要一个逐步的方法来独立管理和测试每个新的微服务。每个新的微服务。Apollo Federation 是为现有的服务设计的,并不能很好地支持这种逐步推进的方法。
鉴于将单体架构拆分成微服务的复杂性和独特挑战,我们寻求了一个具有灵活性、支持逐步迁移并无缝管理模式更新的解决方案。这使我们选择了智能编排客户端库(Smart Orchestrator Client Library),而不是其他选项,如 Apollo Federation。
图5:智能调度器的工作流程
这就为什么了
1. 与现有客户端应用程序的无缝集成: 我们的客户端应用程序当前通过自定义客户端请求单体系统。智能编排器被设计放置在客户端应用程序和领域客户端库之间,提供与现有接口相同的功能。这确保现有客户端应用无需重大改动即可继续运行,使集成变得顺畅无虞。
2. 高效请求路由,不涉及业务逻辑: 智能调度器不包含任何业务逻辑;其主要功能是将请求路由到相应的微服务(也称为领域)。这种解耦的方式保持编排器轻量高效,仅作为客户端应用程序和相应微服务之间请求的调度。
3. 渐进且受控的过渡: 与需要预先存在的微服务架构的解决方案不同,智能编排器是一种设计来支持渐进且受控迁移的工具。从单体架构开始,我们可以逐步提取并部署每个查询与变更操作到新的微服务中。这种逐步推进的方法可以最小化风险,避免潜在的中断,并确保系统在整个过渡期间稳定运行。
4. 自动化架构映射更新: 智能编排器的一个突出特点在于它能够自动更新架构映射,当我们从单体架构迁移到微服务时。这种自动化大大减少了手动工作量,降低了错误风险,并加速了迁移过程。智能编排器通过高效管理所有架构变更,确保了更平滑和可靠的过渡路径。
5. 动态配置和初始化: 当客户端应用代码开始执行时,智能编排器(Smart Orchestrator)接受定义微服务(也称为域)及其对应的Mesh URL的配置详情。然后为每个微服务实例化一个GraphQL客户端对象。此功能支持动态和灵活的配置,确保编排器能够适应新开发的服务和不断变化的架构。
智能调度器的示例设置如下:
智能调度客户端配置
6. 模式管理和内省: 智能编排器负责通过自我检查查询为所有领域维护最新的模式。在初始化时,它从CEPH获取模式映射,然后持续对每个微服务执行自我检查查询,以保持这些映射的更新。这种模式管理能力确保编排器始终根据最新的模式信息路由请求。
图6:智能调度器模块
7. 内存中的微服务映射表: 自我检查响应被转换为内存中的微服务映射表。这确保了对适当微服务的快速引用和有效路由请求。从所有模式指向单体开始,编排器会自动更新映射表,随着微服务从单体中提取进行相应调整,从而确保平滑过渡并减少客户端干扰。
8. Schema Deprecation Handling: 为了在单体中弃用旧Schema,我们将已迁移到各自领域中的字段标记为 IsDeprecated = true
。此标签作为提示用于智能编排器(Smart Orchestrator),表明该字段已移至另一个微服务中。如果智能编排器在对某个领域的查询中发现此字段,该领域的Schema将优先于现有单体Schema在内存中的领域映射表。这种方法确保编排器始终基于最新且最准确的Schema来处理请求。
应对第二个客户准备挑战时,我们遇到了跨域拼接查询/变异的问题。
随着单体服务被拆分后,之前工作的查询可能需要根据模式(schema)如何拆分和重组来进行调整。
数据导向的拼接查询/变异查找方式作为客户准备阶段的一部分,我们需要在大约100个代码库中找到拼接查询修改。手动检查每个仓库并不现实,因此我们选择了一种更高效的数据驱动方法。
我们需要一个能利用现有生产数据的自动化方案来帮助我们更高效地找到这些查询。
我们开发了一套 ETL 任务来分析生产环境下的 GraphQL 请求。这些任务提取了请求数据和元数据,包括在拆分单体应用后,哪些查询和变更属于哪个领域的信息。
使用这种以数据为中心的方法来查找拼接查询使我们的流程更高效且准确。它突显了使用生产数据指导架构选择的重要性,使我们从单体应用到微服务的转换更加结构化且有效。
查询和突变被分为三种类型。
- 简单的查询或变异 不需要任何更改,仅涉及一个领域(可嵌套),无需修改。
- 中等的查询或变异 涉及在同一层次上松散连接的多个领域,尽管它们被批处理在一起,但仍需要简单的拆分(见下图)
- 复杂的查询或变异 涉及紧密相连的嵌套领域,由于它们的高度耦合以及作为主要查询的嵌套查询,需要在服务器端和客户端进行大量的拆分工作。
图7:简单/一般/复杂查询示例。
拆解查询/变异如何解构跨域数据查询
这里是我们如何处理或分解中等或复杂的查询。
中等查询或变异
对于中等的查询或变异,需要将它们分解,为每个领域创建单独的查询。
以下是一个取消中等复杂度查询的操作示例
图8:怎样取消中等难度的查询
复杂查询/变异请求
在处理复杂的查询或变异请求时,我们需要提取跨域嵌套查询,并确定它们能否独立运行。如果第二个查询依赖于第一个,则必须等待第一个完成后执行第二个。
图9:怎样解开复杂的查询语句:
以客户为中心的方法在我们向微服务转型的过程中发挥了重要作用。
这里是我们观察到的一些关键好处,包括:
- 减少协调努力
从客户端应用程序开始,减少了与外部团队在迁移过程中所需的大量协调工作。这些团队因此有了更多的时间和灵活性,可以根据自己的时间表来管理迁移。 - 迁移期间客户端代码无变更
我们首先准备客户端以处理单体架构和微服务,无需在将流量迁移到微服务(领域)时更改客户端代码,这将开发工作量降至最低。如下图所示,流量平滑迁移至新领域的一个服务,而客户端代码无需变更。
图10:迁移到新域的服务流量
- 风险缓解
首先准备客户端应用程序,这降低了拆分服务器的风险。 我们可以逐步提取领域部分,而不影响前端操作或用户体验。 如下面的图表所示,你可以看到我们在每次将模式迁移到新微服务时连续运行的数据正确性,这使我们对迁移过程充满信心。
图11:数据准确性
- 明确长尾客户
采取客户优先的方式确保在全面过渡后没有遗漏任何长尾客户。过去,我们曾为几个无法迁移到新微服务的客户继续运行单体架构而经历困难。
《实施以客户为中心的方法时学到的经验》
这里有几个重要的教训:
不要在客户端准备好后允许跨域请求
在使用客户端优先方法时,在项目的早期阶段,我们在将某个客户端标记为准备就绪后,允许开发人员继续添加拼接的查询和突变。因此,不断添加新的拼接查询,我们不得不重新进行解除拼接的工作。为了解决这个问题,我们在智能编排器中增加了一个功能,拦截这些拼接的查询和突变,一旦某个客户端准备好了,就不再允许新的跨域依赖。
避免旧技术问题在拆解单体应用时,我们只是把现有代码原封不动地移过去。这导致旧的技术问题也被带到了新的微服务里。这并没有给我们一个全新的开始,反而在新的微服务架构中遇到了同样的问题。意识到这一点后,我们不得不重构这些新的微服务来解决问题。这让我们明白,仅仅迁移代码是不够的,还需要对它进行改进,以避免把旧问题带到新系统里。
结论部分从 GraphQL 单体应用转向微服务,并且重点关注客户端准备,对我们 Agoda 来说证明是既艰难又值得的过程。这种不寻常的客户端优先策略使我们解决了我们单体架构中的痛点,同时最大限度利用微服务的潜力。我们通过细心规划每一步骤,确保客户端准备充分,并实现无缝切换,从而减少干扰。
共同学习,写下你的评论
评论加载中...
作者其他优质文章