site logo

Marico's space

扩展API:GraphQL Federation架构

算法解析 2026-08-27 17:34:23 7

最近在给公司重构GraphQL架构,踩了几个坑,这篇把问题说清楚。

团队刚上GraphQL的时候,那感觉是真的爽。前端想要什么字段自己拼,一次网络请求搞定,类型还自动补全。但业务一扩张,问题就来了——GraphQL服务器变成了一个巨无霸单体。

假设你维护着一个服务几千万用户的国内电商平台,单用一个Node.js或PHP服务处理整个数据图,那这个代码库得同时理解用户、商品、评价、库存、支付……所有业务域全搅在一起。50个后端工程师往同一个仓库push代码,部署队列开始排队,Schema冲突天天吵,"评价"模块的bug能直接让"支付"接口挂掉。

这时候有人说了:拆微服务呗。行,拆成多个REST API,但那不等于自己打脸吗?你当初选GraphQL就是图它数据查询方便,拆了REST,前端又得自己拼数据,GraphQL的优势全没了。

我们的解法是GraphQL Federation。这玩意儿最早是Apollo搞出来的,核心理念是:把你的GraphQL单体Schema拆成多个独立的微服务(叫Subgraph),对外还是给前端提供一个统一入口(叫Supergraph)。

Supergraph架构长什么样

Federation架构分两层:

  • Subgraph(子图):独立的微服务。比如一个PHP服务负责用户,一个Node服务负责商品。每个Subgraph有自己的数据库和Schema,但关键是可以扩展其他Subgraph定义的类型。
  • Gateway(网关):一个高性能代理(Apollo Router之类的),摆在所有Subgraph前面。客户端的查询进来后,网关分析、拆分、并行分发到各个Subgraph执行,最后把结果拼成一个JSON返回给前端。

Phase 1:用PHP写用户子图(Laravel)

先从用户服务开始,用Laravel配合nuwave/lighthouse包,它原生支持Federation。

定义User的Schema。注意@key指令,这是告诉网关:一个User可以用id唯一标识。这是跨服务实体解析的基础。


# Users Subgraph (Laravel) - graphql/schema.graphql type User @key(fields: "id") { id: ID! name: String! email: String! createdAt: String!
} type Query { me: User @auth user(id: ID! @eq): User @find
}

Phase 2:在评价子图里扩展User实体(Node.js)

这里才是Federation真正秀的地方。评价服务是另一个独立微服务,用Node写的。一个评价属于一个用户,但评价数据库不存用户的姓名和邮箱,只存author_id

Federation允许你在评价子图里扩展User类型。前端不用发两次请求,网关会自动处理:


# Reviews Subgraph (Node.js/Apollo) - schema.graphql # 评价类型归这个服务自己管
type Review { id: ID! body: String! rating: Int! author: User!
} # 扩展Laravel服务定义的User类型!
extend type User @key(fields: "id") { id: ID! @external reviews: [Review!]!
} type Query { latestReviews: [Review!]!
}

对应的实体解析器要写一下:


// Reviews Subgraph (Node.js) - resolvers.js
const resolvers = { User: { // 当网关传进来一个User对象时,这个方法解析reviews字段 reviews(user) { // 从本地Reviews数据库里查author_id === user.id的记录 return fetchReviewsByAuthorId(user.id); } }
};

Phase 3:Apollo Gateway(总控台)

前端从来不直接调Laravel或Node,它只跟Gateway聊。Gateway会拉取所有子图的Schema,合并成Supergraph。


// Gateway (Node.js) - index.js
const { ApolloServer } = require('@apollo/server');
const { ApolloGateway, IntrospectAndCompose } = require('@apollo/gateway'); const gateway = new ApolloGateway({ supergraphSdl: new IntrospectAndCompose({ subgraphs: [ { name: 'users', url: 'http://laravel-users-api.internal/graphql' }, { name: 'reviews', url: 'http://node-reviews-api.internal/graphql' }, ], }),
}); const server = new ApolloServer({ gateway });
server.listen({ port: 4000 }).then(({ url }) => { console.log(`🚀 Gateway ready at ${url}`);
});

执行计划(底层怎么跑)

假设React前端发了这个查询:


query { user(id: "1") { name email reviews { rating body } }
}

Gateway会生成一个Query Plan,分两步执行:
1. 先打Laravel子图:"给我查User 1的name和email"
2. 拿到返回的ID后,同时打Node子图:"给我查User 1的所有评价"

最后拼成完整的JSON返回给前端,整个过程毫秒级。

工程收益

Federation彻底解耦了你的工程组织。支付团队用Go写,用户团队用PHP,数据团队用Python,各写各的、各部署各的、各扩各的。但对前端来说,体验还是那个味儿:一个统一入口,类型安全、文档自动生成,查整个公司数据体系跟查本地接口一样。