通过模式拼接释放 GraphQL 联邦的强大功能。学习如何从多个服务构建统一的 GraphQL API,从而提高可扩展性和可维护性。
GraphQL 联邦:模式拼接 (Schema Stitching) - 全面指南
在不断发展的现代应用程序开发领域,对可扩展和可维护架构的需求已变得至关重要。微服务以其固有的模块化和独立可部署性,已成为一种流行的解决方案。然而,管理众多微服务可能会引入复杂性,尤其是在向客户端应用程序暴露统一 API 方面。这正是 GraphQL 联邦,特别是模式拼接 (Schema Stitching) 发挥作用的地方。
什么是 GraphQL 联邦?
GraphQL 联邦是一种强大的架构,允许您从多个底层的 GraphQL 服务(通常代表微服务)构建一个单一、统一的 GraphQL API。它使开发人员能够跨不同服务查询数据,就好像它们是一个单一的图谱,从而简化了客户端体验,并减少了客户端进行复杂编排逻辑的需求。
实现 GraphQL 联邦主要有两种方法:
- 模式拼接 (Schema Stitching): 这涉及到在网关层将多个 GraphQL 模式组合成一个单一、统一的模式。这是一种较早的方法,依赖于库来管理模式组合和查询委托。
- Apollo 联邦 (Apollo Federation): 这是一种更新、更强大的方法,它使用声明式模式语言和专用的查询计划器来管理联邦过程。它提供了类型扩展、键指令和分布式追踪等高级功能。
本文将重点介绍 模式拼接 (Schema Stitching),探讨其概念、优点、局限性和实际实现。
理解模式拼接
模式拼接是将多个 GraphQL 模式合并为一个单一、内聚的模式的过程。这个统一的模式充当一个外观(facade),向客户端隐藏了底层服务的复杂性。当客户端向拼接后的模式发出请求时,网关会智能地将请求路由到适当的底层服务,检索数据,并在将结果返回给客户端之前将其组合起来。
可以这样理解:您有多家专门从事不同菜系的餐厅(服务)。模式拼接就像一个万能菜单,它结合了每家餐厅的所有菜肴。当顾客(客户端)从这个万能菜单点餐时,订单被智能地路由到相应的餐厅厨房,食物被准备好,然后合并成一次单独的配送交付给顾客。
模式拼接中的关键概念
- 远程模式 (Remote Schemas): 这些是每个底层服务的独立 GraphQL 模式。每个服务都公开自己的模式,该模式定义了它提供的数据和操作。
- 网关 (Gateway): 网关是负责将远程模式拼接在一起并向客户端公开统一模式的中心组件。它接收客户端请求,将其路由到适当的服务,并组合结果。
- 模式合并 (Schema Merging): 这是将远程模式组合成单一模式的过程。这通常涉及重命名类型和字段以避免冲突,并定义跨不同模式的类型之间的关系。
- 查询委托 (Query Delegation): 当客户端向拼接后的模式发出请求时,网关需要将请求委托给适当的底层服务以检索数据。这涉及将客户端的查询转换为远程服务可以理解的查询。
- 结果聚合 (Result Aggregation): 在网关从底层服务检索数据后,它需要将结果组合成一个单一的响应,然后返回给客户端。这通常涉及转换数据以匹配拼接后模式的结构。
模式拼接的优势
对于采用微服务架构的组织来说,模式拼接提供了几个引人注目的优势:
- 统一 API: 为客户端提供单一、一致的 API,简化了数据访问,并减少了客户端直接与多个服务交互的需求。这带来了更清晰、更直观的开发人员体验。
- 降低客户端复杂性: 客户端只需与统一模式交互,从而将他们与底层微服务架构的复杂性隔离开来。这简化了客户端开发,并减少了客户端所需的代码量。
- 提高可扩展性: 允许您根据特定需求独立扩展单个服务。这提高了系统的整体可扩展性和弹性。例如,一个经历高负载的用户服务可以被扩展,而不会影响像产品目录这样的其他服务。
- 改善可维护性: 促进模块化和关注点分离,使得维护和演进单个服务变得更加容易。对一个服务的更改不太可能影响其他服务。
- 逐步采用: 可以增量实现,允许您从单体架构逐渐迁移到微服务架构。您可以从拼接现有 API 开始,然后逐步将单体分解为更小的服务。
模式拼接的局限性
虽然模式拼接提供了许多优点,但了解其局限性也很重要:
- 复杂性: 实现和管理模式拼接可能很复杂,尤其是在大型和复杂的系统中。仔细的规划和设计至关重要。
- 性能开销: 网关引入了一些性能开销,因为额外的间接层以及委托查询和聚合结果的需求。仔细的优化对于最小化这种开销至关重要。
- 模式冲突: 在合并来自不同服务的模式时可能会出现冲突,特别是当它们使用相同的类型名称或字段名称时。这需要仔细的模式设计,并可能需要重命名类型和字段。
- 高级功能有限: 与 Apollo 联邦相比,模式拼接缺少一些高级功能,如类型扩展和键指令,这可能使得管理跨不同模式的类型之间的关系更具挑战性。
- 工具成熟度: 围绕模式拼接的工具和生态系统不像围绕 Apollo 联邦的那么成熟。这可能使得调试和排查问题更具挑战性。
模式拼接的实践
让我们通过一个简化的例子,看看如何使用 Node.js 和 graphql-tools
库(一个流行的模式拼接选择)来实现模式拼接。这个例子涉及两个微服务:一个用户服务和一个产品服务。
1. 定义远程模式
首先,为每个远程服务定义 GraphQL 模式。
用户服务 (user-service.js
):
const { buildSchema } = require('graphql');
const userSchema = buildSchema(`
type User {
id: ID!
name: String
email: String
}
type Query {
user(id: ID!): User
}
`);
const users = [
{ id: '1', name: 'Alice Smith', email: 'alice@example.com' },
{ id: '2', name: 'Bob Johnson', email: 'bob@example.com' },
];
const userRoot = {
user: (args) => users.find(user => user.id === args.id),
};
module.exports = {
schema: userSchema,
rootValue: userRoot,
};
产品服务 (product-service.js
):
const { buildSchema } = require('graphql');
const productSchema = buildSchema(`
type Product {
id: ID!
name: String
price: Float
userId: ID! # Foreign key to User Service
}
type Query {
product(id: ID!): Product
}
`);
const products = [
{ id: '101', name: 'Laptop', price: 1200, userId: '1' },
{ id: '102', name: 'Smartphone', price: 800, userId: '2' },
];
const productRoot = {
product: (args) => products.find(product => product.id === args.id),
};
module.exports = {
schema: productSchema,
rootValue: productRoot,
};
2. 创建网关服务
现在,创建将两个模式拼接在一起的网关服务。
网关服务 (gateway.js
):
const { stitchSchemas } = require('@graphql-tools/stitch');
const { makeRemoteExecutableSchema } = require('@graphql-tools/wrap');
const { graphqlHTTP } = require('express-graphql');
const express = require('express');
const { introspectSchema } = require('@graphql-tools/wrap');
const { printSchema } = require('graphql');
const fetch = require('node-fetch');
async function createRemoteSchema(uri) {
const fetcher = async (params) => {
const response = await fetch(uri, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(params),
});
return response.json();
};
const schema = await introspectSchema(fetcher);
return makeRemoteExecutableSchema({
schema,
fetcher,
});
}
async function main() {
const userSchema = await createRemoteSchema('http://localhost:4001/graphql');
const productSchema = await createRemoteSchema('http://localhost:4002/graphql');
const stitchedSchema = stitchSchemas({
subschemas: [
{ schema: userSchema },
{ schema: productSchema },
],
typeDefs: `
extend type Product {
user: User
}
`,
resolvers: {
Product: {
user: {
selectionSet: `{ userId }`,
resolve(product, args, context, info) {
return info.mergeInfo.delegateToSchema({
schema: userSchema,
operation: 'query',
fieldName: 'user',
args: {
id: product.userId,
},
context,
info,
});
},
},
},
},
});
const app = express();
app.use('/graphql', graphqlHTTP({
schema: stitchedSchema,
graphiql: true,
}));
app.listen(4000, () => console.log('网关服务器运行在 http://localhost:4000/graphql'));
}
main().catch(console.error);
3. 运行服务
您需要在不同的端口上运行用户服务和产品服务。例如:
用户服务 (端口 4001):
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { schema, rootValue } = require('./user-service');
const app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: rootValue,
graphiql: true,
}));
app.listen(4001, () => console.log('用户服务运行在 http://localhost:4001/graphql'));
产品服务 (端口 4002):
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { schema, rootValue } = require('./product-service');
const app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: rootValue,
graphiql: true,
}));
app.listen(4002, () => console.log('产品服务运行在 http://localhost:4002/graphql'));
4. 查询拼接后的模式
现在您可以通过网关(运行在 4000 端口)查询拼接后的模式。您可以运行如下查询:
query {
product(id: "101") {
id
name
price
user {
id
name
email
}
}
}
此查询检索 ID 为 “101” 的产品,并从用户服务中获取关联的用户,展示了模式拼接如何让您在单个请求中跨多个服务查询数据。
高级模式拼接技术
除了基本示例外,这里还有一些可以用来增强模式拼接实现的高级技术:
- 模式委托 (Schema Delegation): 这允许您根据所请求的数据将查询的某些部分委托给不同的服务。例如,您可能将 `User` 类型的解析委托给用户服务,将 `Product` 类型的解析委托给产品服务。
- 模式转换 (Schema Transformation): 这涉及到在将远程服务的模式拼接到统一模式之前对其进行修改。这对于重命名类型和字段、添加新字段或删除现有字段非常有用。
- 自定义解析器 (Custom Resolvers): 您可以在网关中定义自定义解析器,以处理复杂的数据转换,或从多个服务中获取数据并将其组合成单个结果。
- 上下文共享 (Context Sharing): 在网关和远程服务之间共享上下文信息通常是必要的,例如身份验证令牌或用户 ID。这可以通过在查询委托过程中传递上下文信息来实现。
- 错误处理 (Error Handling): 实施强大的错误处理机制,以优雅地处理远程服务中发生的错误。这可能包括记录错误、返回用户友好的错误消息或重试失败的请求。
在模式拼接和 Apollo 联邦之间进行选择
虽然模式拼接是 GraphQL 联邦的一个可行选项,但由于其更高级的功能和改进的开发人员体验,Apollo 联邦已成为更受欢迎的选择。以下是两种方法的比较:
特性 | 模式拼接 | Apollo 联邦 |
---|---|---|
模式定义 | 使用现有的 GraphQL 模式语言 | 使用带指令的声明式模式语言 |
查询规划 | 需要手动进行查询委托 | 由 Apollo Gateway 自动进行查询规划 |
类型扩展 | 有限的支持 | 内置对类型扩展的支持 |
键指令 | 不支持 | 使用 @key 指令识别实体 |
分布式追踪 | 需要手动实现 | 内置对分布式追踪的支持 |
工具和生态系统 | 工具不太成熟 | 更成熟的工具和庞大的社区 |
复杂性 | 在大型系统中管理可能很复杂 | 为大型复杂系统设计 |
何时选择模式拼接:
- 您有现成的 GraphQL 服务,并希望快速将它们组合起来。
- 您需要一个简单的联邦解决方案,并且不需要高级功能。
- 您的资源有限,希望避免设置 Apollo 联邦的开销。
何时选择 Apollo 联邦:
- 您正在构建一个拥有多个团队和服务的大型复杂系统。
- 您需要类型扩展、键指令和分布式追踪等高级功能。
- 您想要一个更强大、可扩展的联邦解决方案。
- 您偏爱更具声明性和自动化的联邦方法。
真实世界示例和用例
以下是一些 GraphQL 联邦(包括模式拼接)的真实世界应用示例:
- 电子商务平台: 电子商务平台可能使用 GraphQL 联邦来组合来自多个服务的数据,例如产品目录服务、用户服务、订单服务和支付服务。这使得客户端可以轻松地检索显示产品详情、用户资料、订单历史和支付信息所需的所有信息。
- 社交媒体平台: 社交媒体平台可以使用 GraphQL 联邦来组合管理用户资料、帖子、评论和点赞的服务数据。这使得客户端能够高效地获取显示用户个人资料、其帖子以及与这些帖子相关的评论和点赞所需的所有信息。
- 金融服务应用: 金融服务应用可能使用 GraphQL 联邦来组合管理账户、交易和投资的服务数据。这使得客户端可以轻松地检索显示账户余额、交易历史和投资组合所需的所有信息。
- 内容管理系统 (CMS): CMS 可以利用 GraphQL 联邦来集成来自各种来源的数据,如文章、图片、视频和用户生成的内容。这为获取与特定主题或作者相关的所有内容提供了一个统一的 API。
- 医疗保健应用: 集成来自不同系统的患者数据,如电子健康记录 (EHR)、实验室结果和预约安排。这为医生提供了访问全面患者信息的单一入口点。
模式拼接的最佳实践
为确保成功实施模式拼接,请遵循以下最佳实践:
- 仔细规划您的模式: 在开始拼接模式之前,请仔细规划统一模式的结构。这包括定义跨不同模式的类型之间的关系,重命名类型和字段以避免冲突,以及考虑整体数据访问模式。
- 使用一致的命名约定: 在所有服务中为类型、字段和操作采用一致的命名约定。这将有助于避免冲突,并使统一模式更容易理解。
- 为您的模式编写文档: 详尽地记录统一模式,包括类型、字段和操作的描述。这将使开发人员更容易理解和使用该模式。
- 监控性能: 监控网关和远程服务的性能,以识别和解决任何性能瓶颈。使用分布式追踪等工具来跟踪跨多个服务的请求。
- 实施安全措施: 实施适当的安全措施,以保护网关和远程服务免受未经授权的访问。这可能涉及使用身份验证和授权机制,以及输入验证和输出编码。
- 对您的模式进行版本控制: 随着模式的演进,适当地对其进行版本控制,以确保客户端可以继续使用旧版本的模式而不会中断。这将有助于避免破坏性更改并确保向后兼容。
- 自动化部署: 自动化网关和远程服务的部署,以确保可以快速可靠地部署更改。这将有助于降低出错风险并提高系统的整体敏捷性。
结论
通过模式拼接实现的 GraphQL 联邦为在微服务架构中从多个服务构建统一 API 提供了一种强大的方法。通过理解其核心概念、优点、局限性和实现技术,您可以利用模式拼接来简化数据访问、提高可扩展性并增强可维护性。虽然 Apollo 联邦已成为一种更先进的解决方案,但对于更简单的场景或在集成现有 GraphQL 服务时,模式拼接仍然是一个可行的选择。仔细考虑您的具体需求和要求,为您的组织选择最佳方法。