联邦并不是一个编排器
关于排序突变和分布式编排的注意事项
Apollo Federation是一个强大的分布式系统跨查询编排系统。例如,您可以使用联邦来完成以下任务:
- 查询来自多个系统的数据并行处理
- 当之间存在依赖关系时,通过
@key
和@requires
指令对多个系统的调用进行排序 - 编排多个系统之间的并行和顺序调用
- 将单个调用批量调用以解决知名的N+1问题
- 使用
@defer
将单个调用拆分为多个具有不同优先级的调用
联邦将这些相同的编排功能应用到突变字段(及其选择集),但管理分布式系统中的状态变化通常涉及额外的需求。联邦不提供补偿事务或子字段间的数据传播等常见功能。
您需要在单个字段内实现自己的编排逻辑解析器以实现这些需求。这是由于GraphQL本身的设计所致。
在GraphQL中排序mutations
根查询
和变异
类型之间的唯一区别是:变异字段是按顺序执行而不是并行执行。变异字段是按顺序代替并行执行的。乍一看,这似乎是一种对一组相关变异进行排序的方式。
mutation DoMultipleThings {createAccount {successaccount {id}}validateAccount {success}setAccountStatus {success}}
GraphQL规范保证,如果在GraphQL规范中validateAccount
或setAccountStatus
失败,则不会执行。乍一看,这似乎是一种对一组mutations进行事务性执行的方式,但它有一些显著的缺点:
- 我们无法回滚
createAccount
所做的更改,如果validateAccount
或setAccountStatus
失败。 - 我们无法在mutations之间传播数据。例如,如果
Mutation.createAccount
返回一个带有id
的Account
,我们无法在validateAccount
或setAccountStatus
中使用那个id
。 - 客户端需要通过检查每个变异的
成功
字段并确定适当的行动方案来应对多种故障场景。
即使这三个变异在同一个服务中实现,在GraphQL执行引擎中运行时也很难实现适当的事务语义。相反,我们应该将这个 操作 作为单个变异解析器来实现,这样我们就可以在一个函数中处理故障和回滚。
在下面的JavaScript示例中,我们实现了一个简单的 "saga",它协调跨多个系统的状态变化。
const resolvers = {Mutation: {async createAndValidateAccount() {const account = createAccount();try {validate(account);setAccountStatus(account, 'ACTIVE');} catch (e) {deleteAccount(account);return {success: false};}return {success: true, account};}}};
通过在一个单独的字段中表示这个 操作,我们可以在一个函数中正确处理故障、回滚和数据传播。我们还从我们的客户端应用程序和模式中移除了复杂的错误管理,从而使客户端意图表达得更清晰。
分布式编排
如果状态变化发生在不同的服务中怎么办?由于 GraphQL 和 Federation 没有提供分布式事务的语义,我们仍然需要在单个 字段 内实现编排逻辑。
这个要求引发了一些挑战性的问题
- 哪个团队或领域拥有这个 变异 字段?
- 在哪个 子图 服务中我们应该实现解析器?
- 解析器如何与数据服务进行通信?
对这些问题的答案没有普遍正确的答案。您的答案将取决于您的组织、产品需求和系统的能力。
一种可能的策略是创建一个或多个专门用于编排 变异 的 子图。这些子图可以使用REST或RPC与数据服务进行通信。经验或产品团队往往是这些子图最合适的所有者。
有诱惑让协调 子图 解析器 回调到 GraphOS Router 来执行域特定的 变异。这种方法是可行的,但它需要仔细关注许多细节:
- 所有调用应通过 路由器 进行,而不是直接到 子图,因为路由器负责跟踪 GraphQL 使用情况(以及 子图不应直接接受流量。)
- 对特定领域的 突变的调用不再源自客户端应用程序,因此您必须传播客户端标识和其他请求元数据。
- GraphOS 将这些调用视为独立的 操作。
- 如果使用云原生遥测,您必须确保 路由器接收到跟踪上下文以关联追踪点。
- 注意循环依赖关系。考虑使用 合约 来创建 变体,这些变体仅公开领域特定的 突变,并消除操作循环的可能性。
💡 提示
在本技术笔记中了解关于 合约使用模式的更多信息。