> 技术文档 > ABP VNext + GraphQL Federation:跨微服务联合 Schema 分层

ABP VNext + GraphQL Federation:跨微服务联合 Schema 分层


ABP VNext + GraphQL Federation:跨微服务联合 Schema 分层 🚀

在微服务架构下,服务之间往往需要相互通信,而 GraphQL Federation 提供了一个有效的解决方案,帮助我们将多个微服务的 GraphQL API 聚合成一个统一的入口。在这篇文章中,我们将展示如何使用 ABP VNextGraphQL Federation 实现跨微服务联合 Schema 分层,从而解耦服务,提高可维护性和扩展性。


📚 目录

  • ABP VNext + GraphQL Federation:跨微服务联合 Schema 分层 🚀
    • 1. 引言 ✨
      • TL;DR
    • 2. 环境与依赖 ⚙️
      • 🛠️ 平台版本
      • 🔗 NuGet 包
      • 🔧 可选组件
    • 3. GraphQL Federation 基础 🔎
      • 3.1 什么是 GraphQL Federation?
      • 3.2 典型服务场景 🏗️
    • 4. 配置 ABP 服务的 GraphQL Schema 🔧
      • 4.1 启用 GraphQL
      • 4.2 定义 `@key` 和 `@external`
        • 4.2.1 定义 `@key`(联合查询主字段)
        • 4.2.2 定义 `@external`(跨服务引用)
      • 4.3 微服务的 Query 类型定义
        • 4.3.1 Order Service Schema
    • 5. GraphQL 联合查询与服务解耦 🔄
      • 5.1 跨微服务查询
      • 5.2 实现分布式查询与联接
      • 5.3 示例查询
    • 6. 微服务间数据扩展与版本控制 🔧
      • 6.1 扩展类型
      • 6.2 版本管理
        • Schema 合并与版本控制
    • 7. 安全性与权限管理 🔐
      • 7.1 服务级授权
      • 7.2 API 网关与流量控制
    • 8. Kibana 监控与性能优化 📊
      • 8.1 结合 Elastic APM
      • 8.2 性能优化
    • 9. 实践演示 🎯
      • 9.1 准备项目
      • 9.2 启动微服务
      • 9.3 在 Gateway 中配置 Federation
      • 9.4 执行联合查询
      • 9.5 Kibana & Elastic APM 监控
      • 9.6 性能优化建议

1. 引言 ✨

TL;DR

  • 基于 HotChocolate Federation,将多个 ABP 微服务的 GraphQL API 组合成统一入口 🌐
  • 服务间通过跨服务 Schema 联合,避免紧耦合与多端 API 重复 🚀
  • 演示如何在多服务架构中,使用 @key@external 实现跨服务查询和扩展 🔗
  • 解决微服务之间数据传递问题,支持服务解耦与动态扩展 🌱

在微服务架构中,前端往往需要从多个微服务获取数据,这导致了前端需要处理多个 API 请求并进行复杂的聚合。而 GraphQL Federation 为这一问题提供了解决方案。通过 GraphQL Federation,我们可以将多个微服务的 GraphQL API 聚合成一个统一的入口,从而简化前端的请求和聚合逻辑,同时保持微服务的解耦和独立性。

2. 环境与依赖 ⚙️

在开始之前,我们需要配置一些基本环境和依赖项:

🛠️ 平台版本

  • .NET 7/8
  • ABP VNext 7.x/8.x

🔗 NuGet 包

  • HotChocolate.AspNetCore
  • HotChocolate.AspNetCore.Federation
  • Volo.Abp.AspNetCore.Mvc(ABP WebAPI 集成)

🔧 可选组件

  • Redis:用于共享缓存或跨服务会话管理(可选)。

3. GraphQL Federation 基础 🔎

3.1 什么是 GraphQL Federation?

GraphQL Federation 是一种通过跨服务联合模式,将多个 GraphQL 服务组合成统一的 API 图。每个微服务负责自己的部分 Schema,它们通过指定的标注如 @key@external 来共享和扩展数据,从而实现跨服务的数据查询。

  • @key:用于标识联合查询的主字段。
  • @external:用于引用其他服务的数据字段。

3.2 典型服务场景 🏗️

假设我们有三个微服务:订单服务客户服务产品服务。在这些服务中,我们需要联合查询客户和产品信息,同时确保各个服务之间保持独立。

#mermaid-svg-5iclQqqR7re86cRU {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-5iclQqqR7re86cRU .error-icon{fill:#552222;}#mermaid-svg-5iclQqqR7re86cRU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5iclQqqR7re86cRU .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-5iclQqqR7re86cRU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5iclQqqR7re86cRU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5iclQqqR7re86cRU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5iclQqqR7re86cRU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5iclQqqR7re86cRU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5iclQqqR7re86cRU .marker.cross{stroke:#333333;}#mermaid-svg-5iclQqqR7re86cRU svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5iclQqqR7re86cRU .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5iclQqqR7re86cRU .cluster-label text{fill:#333;}#mermaid-svg-5iclQqqR7re86cRU .cluster-label span{color:#333;}#mermaid-svg-5iclQqqR7re86cRU .label text,#mermaid-svg-5iclQqqR7re86cRU span{fill:#333;color:#333;}#mermaid-svg-5iclQqqR7re86cRU .node rect,#mermaid-svg-5iclQqqR7re86cRU .node circle,#mermaid-svg-5iclQqqR7re86cRU .node ellipse,#mermaid-svg-5iclQqqR7re86cRU .node polygon,#mermaid-svg-5iclQqqR7re86cRU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5iclQqqR7re86cRU .node .label{text-align:center;}#mermaid-svg-5iclQqqR7re86cRU .node.clickable{cursor:pointer;}#mermaid-svg-5iclQqqR7re86cRU .arrowheadPath{fill:#333333;}#mermaid-svg-5iclQqqR7re86cRU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5iclQqqR7re86cRU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5iclQqqR7re86cRU .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-5iclQqqR7re86cRU .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-5iclQqqR7re86cRU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5iclQqqR7re86cRU .cluster text{fill:#333;}#mermaid-svg-5iclQqqR7re86cRU .cluster span{color:#333;}#mermaid-svg-5iclQqqR7re86cRU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5iclQqqR7re86cRU :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 订单服务 GraphQL 联合查询 客户服务 产品服务 客户信息 产品信息 前端聚合 跨微服务查询


4. 配置 ABP 服务的 GraphQL Schema 🔧

4.1 启用 GraphQL

首先,我们需要在 ABP 模块中配置 GraphQL 服务,并启用 Federation 特性。以下是如何在 Startup.cs 中配置 GraphQL:

public class MyModule : AbpModule{ public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddGraphQLServer() .AddQueryType<Query>() .AddMutationType<Mutation>() .AddFederation(); // 使能 Federation 特性 }}

4.2 定义 @key@external

在微服务的 GraphQL Schema 中,我们使用 @key@external 来定义跨服务的数据联合。

4.2.1 定义 @key(联合查询主字段)
[Key(\"id\")]public class Customer{ public int Id { get; set; } public string Name { get; set; }}
4.2.2 定义 @external(跨服务引用)
public class Product{ public int Id { get; set; } [External] public int CustomerId { get; set; } // 来自于 Customer 服务}

4.3 微服务的 Query 类型定义

对于每个微服务,我们都需要定义相应的 Query 类型。以下是 订单服务Query 类型定义:

4.3.1 Order Service Schema
public class Query{ private readonly IOrderRepository _orderRepository; public Query(IOrderRepository orderRepository) { _orderRepository = orderRepository; } public IQueryable<Order> GetOrders() => _orderRepository.AsQueryable();}

5. GraphQL 联合查询与服务解耦 🔄

5.1 跨微服务查询

通过 GraphQL Federation,我们可以在多个微服务之间进行联合查询。以下是一个联合查询的例子,查询来自 客户服务产品服务 的数据:

query { customer(id: 1) { id name } product(id: 2) { id name customerId }}

5.2 实现分布式查询与联接

我们可以在 GraphQL 层将来自不同服务的数据进行联合查询。例如,将 订单信息产品信息 联接,跨多个服务聚合数据。

query { order(id: 1) { id customerId productId } product(id: 1) { name price }}

5.3 示例查询

以下是查询多个微服务数据的完整示例:

query { customer(id: \"1\") { name email } product(id: \"1001\") { name description }}

6. 微服务间数据扩展与版本控制 🔧

6.1 扩展类型

为了实现跨服务的数据扩展,我们可以通过 @extend 装饰器在不同服务间进行数据扩展。例如,扩展 产品服务 以获取 客户信息

[ExtendObjectType(\"Product\")]public class ProductCustomerExtension{ private readonly ICustomerRepository _customerRepository; public ProductCustomerExtension(ICustomerRepository customerRepository) { _customerRepository = customerRepository; } public Customer Customer([Parent] Product product) => _customerRepository.GetById(product.CustomerId);}

6.2 版本管理

随着服务的发展,我们可能需要扩展和版本化 GraphQL Schema。每个微服务都可以独立演进其 Schema,保持与其他服务的兼容性。

Schema 合并与版本控制

每个微服务独立演进 GraphQL Schema,保持与其他服务的兼容性。服务的版本可以通过 @key@external 标记的字段实现向后兼容。对于新版本服务,前后端可以通过合并新 Schema 来扩展功能。

extend type Query { newCustomer(id: Int!): Customer}

7. 安全性与权限管理 🔐

7.1 服务级授权

通过 GraphQL 中的 @auth 装饰器管理每个字段或查询的权限控制。结合 ABP 的多租户授权管理,使用 ABP 的权限和角色系统控制跨服务查询权限。

type Query { @auth(roles: [\"admin\"]) getUser(id: ID!): User}

7.2 API 网关与流量控制

使用 OcelotYARP 配合 ABP 实现微服务层的统一授权、认证和流量控制。

{ \"ReRoutes\": [ { \"UpstreamPathTemplate\": \"/api/order/**\", \"DownstreamPathTemplate\": \"/order/**\", \"UpstreamHttpMethod\": [ \"GET\", \"POST\" ] } ]}

8. Kibana 监控与性能优化 📊

8.1 结合 Elastic APM

我们可以通过集成 Elastic APM 监控跨服务的 GraphQL 查询,采集服务性能数据,监控每个 GraphQL 查询的响应时间、吞吐量和错误率。

详细可参见我的另一篇技术博客:ABP VNext + Elastic APM:微服务性能监控

8.2 性能优化

通过分析服务的性能数据,优化查询响应时间和吞吐量,确保系统的高性能和高可用。

{ \"metrics\": { \"responseTime\": 100, \"throughput\": 5000, \"errorRate\": 0.02 }}

9. 实践演示 🎯

9.1 准备项目

用 ABP CLI(或 dotnet CLI + ABP 模板)创建 4 个项目:

# 安装 ABP CLIdotnet tool install Volo.Abp.Cli -g# 创建微服务模板abp new CustomerService -t app -u none --tieredabp new ProductService -t app -u none --tieredabp new OrderService -t app -u none --tiered# 创建 Gateway 项目,用作 Federation 聚合层abp new ApiGateway -t app -u none --tiered

目录结构示例:

/solutions /CustomerService /ProductService /OrderService /ApiGateway

9.2 启动微服务

在各服务的 appsettings.json 中,按需开启 Elastic APM:

// CustomerService/appsettings.json{ \"ElasticApm\": { \"ServerUrls\": \"http://localhost:8200\", \"ServiceName\": \"CustomerService\", \"Environment\": \"dev\" }}

然后分别在 5001、5002、5003 端口启动:

cd CustomerService && dotnet run --urls \"http://localhost:5001\"cd ProductService && dotnet run --urls \"http://localhost:5002\"cd OrderService && dotnet run --urls \"http://localhost:5003\"

9.3 在 Gateway 中配置 Federation

ApiGatewayStartup.cs 中,像这样注册子图:

public class ApiGatewayModule : AbpModule{ public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddGraphQLServer() .AddRemoteSchema(\"customer\", c => c.Http(\"http://localhost:5001/graphql\")) .AddRemoteSchema(\"product\", c => c.Http(\"http://localhost:5002/graphql\")) .AddRemoteSchema(\"order\", c => c.Http(\"http://localhost:5003/graphql\")) .AddTypeExtensionsFromFile(\"./SchemaExtensions.graphql\") .AddApolloFederation(); }}

SchemaExtensions.graphql 可以包含跨图的扩展定义:

extend type Query { customer(id: Int!): Customer @delegate(schema: \"customer\", path: \"customerById(id: $id)\") product(id: Int!): Product @delegate(schema: \"product\", path: \"productById(id: $id)\")}

9.4 执行联合查询

启动 Gateway(默认 http://localhost:5000/graphql),打开 GraphQL Playground,运行:

query { customer(id: 1) { id name orders { # 这里 orders 来自 OrderService 的扩展 id total } } product(id: 2) { id name customer { # 来自 ProductService -> CustomerService 的扩展 name } }}

#mermaid-svg-FMRtSTusyAk7FezC {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-FMRtSTusyAk7FezC .error-icon{fill:#552222;}#mermaid-svg-FMRtSTusyAk7FezC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FMRtSTusyAk7FezC .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-FMRtSTusyAk7FezC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FMRtSTusyAk7FezC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FMRtSTusyAk7FezC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FMRtSTusyAk7FezC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FMRtSTusyAk7FezC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FMRtSTusyAk7FezC .marker.cross{stroke:#333333;}#mermaid-svg-FMRtSTusyAk7FezC svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FMRtSTusyAk7FezC .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-FMRtSTusyAk7FezC text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-FMRtSTusyAk7FezC .actor-line{stroke:grey;}#mermaid-svg-FMRtSTusyAk7FezC .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-FMRtSTusyAk7FezC .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-FMRtSTusyAk7FezC #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-FMRtSTusyAk7FezC .sequenceNumber{fill:white;}#mermaid-svg-FMRtSTusyAk7FezC #sequencenumber{fill:#333;}#mermaid-svg-FMRtSTusyAk7FezC #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-FMRtSTusyAk7FezC .messageText{fill:#333;stroke:#333;}#mermaid-svg-FMRtSTusyAk7FezC .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-FMRtSTusyAk7FezC .labelText,#mermaid-svg-FMRtSTusyAk7FezC .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-FMRtSTusyAk7FezC .loopText,#mermaid-svg-FMRtSTusyAk7FezC .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-FMRtSTusyAk7FezC .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-FMRtSTusyAk7FezC .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-FMRtSTusyAk7FezC .noteText,#mermaid-svg-FMRtSTusyAk7FezC .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-FMRtSTusyAk7FezC .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-FMRtSTusyAk7FezC .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-FMRtSTusyAk7FezC .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-FMRtSTusyAk7FezC .actorPopupMenu{position:absolute;}#mermaid-svg-FMRtSTusyAk7FezC .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-FMRtSTusyAk7FezC .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-FMRtSTusyAk7FezC .actor-man circle,#mermaid-svg-FMRtSTusyAk7FezC line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-FMRtSTusyAk7FezC :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 前端 ApiGateway CustomerService ProductService OrderService 统一 GraphQL 查询 customerById(id:1) Customer 数据 ordersByCustomer(customerId:1) 订单列表 productById(id:2) Product 数据 + CustomerId customerById(id:PS.CustomerId) Customer.name 聚合后的响应 前端 ApiGateway CustomerService ProductService OrderService

9.5 Kibana & Elastic APM 监控

  1. 在 Elasticsearch/Kibana 中创建 APM 应用,监听 CustomerServiceProductServiceOrderServiceApiGateway 服务。
  2. 在 Kibana APM 界面查看分布式 Trace,过滤 URI 包含 /graphql 的请求。
  3. 分析每次联合查询中各服务的响应时间和错误率,并根据查询热度添加 Redis DataLoader 或缓存。

9.6 性能优化建议

  • 分页/过滤:对 .GetOrders() 添加分页参数,避免一次性拉取全部数据。
  • DataLoader:在 GraphQL Resolver 中使用 DataLoader 批量加载跨服务数据,减少子请求数量。
  • 缓存:对高频查询结果在 Redis 中缓存,并结合 APM 监控命中率。
  • 熔断/重试:使用 Polly 实现服务间 HTTP 调用的熔断和重试,提升可用性。