接入层与 BFF:网关职责与多端适配
接入层的边界可以用一句话划定:与业务无关的横切关注点归它,涉及业务语义的判断一律不归它。这条线一旦模糊,网关就会变成变更最频繁、测试最难、单点风险最高的服务。BFF(Backend for Frontend,服务于前端的后端)是接入层的一种形态,不是第二个网关。
一、网关该做什么
| 职责 | 具体内容 | 为什么放在这一层 |
|---|---|---|
| 路由与发现 | 按路径、域名、Header 路由到上游服务,感知实例上下线 | 全网唯一入口,改路由不必动业务 |
| 鉴权与身份传递 | 校验令牌与签名、解析身份,向下游传递受信的用户/租户标识 | 认证集中一处,业务服务只信任内部标识 |
| 限流与配额 | 按 IP、用户、租户、接口的粗粒度配额 | 入口拦截成本最低(算法细节见 system-design 的限流章节) |
| 协议与格式转换 | HTTP 与 gRPC 互转、WebSocket 升级、协议版本适配 | 前端与内部协议解耦,内部可自由演进 |
| 可观测性注入 | 生成并透传 traceId、记录访问日志、打点入口指标 | 链路起点唯一,注入成本最低(体系设计见「可观测性」一章) |
| 安全与流量治理 | TLS 终止、WAF、防重放、灰度染色与标签路由 | 全局策略集中配置,口径统一 |
这几项的共同点:策略是全局的、判断不依赖业务数据、出问题时影响面大但修复方式统一。
二、网关不该做什么
| 反模式 | 典型表现 | 后果 |
|---|---|---|
| 业务校验 | 「满 199 减 30」写在网关里 | 每次营销变更都要发网关,回归范围扩大到全网 |
| 多服务聚合 | 在网关里同时调订单、库存、用户再拼响应 | 网关没有领域知识,聚合很快演变成业务逻辑 |
| 领域规则 | 状态机、库存判定、细粒度权限 | 规则分散两处,长期必然不一致 |
| 业务缓存 | 缓存订单、用户资料等业务数据 | 缓存策略与业务耦合,脏数据难定位 |
| 直连数据库 | 网关连业务库做校验 | 数据所有权被打穿(见「数据所有权」一章) |
判断标准很简单:这段逻辑变更时是否需要产品经理参与、是否会因业务规则调整而变化? 会,它就不属于接入层。
三、分层:网关、BFF、服务网格各管一段
客户端 ──► 网关 ──────────────► BFF ─────────► 领域服务
鉴权 / 限流 / 路由 端侧聚合 / 裁剪 业务规则 / 数据所有权
(全局策略,无业务) (端侧语义,无规则)
▲
服务网格:服务间 mTLS、重试、熔断、流量镜像(进程外,业务无感)
- 网关管南北向入口策略,对客户端负责。
- BFF 管端侧适配,对一个端的体验负责,可以有多个。
- 服务网格管东西向通信策略,对服务间调用的可靠性负责。
三者职责不重叠,接入层才是清晰的;两处都在鉴权、都在限流,出故障时无法判断谁生效。
四、BFF 的适用与反模式
适用信号:
- 各端需要的字段、聚合次数、分页方式差异大,一个通用接口要同时满足三端,会退化成「返回所有字段再由端上丢弃」。
- 端侧团队有独立迭代节奏,需要在不动后端服务的前提下调整接口形状。
- 移动端弱网、流量敏感,需要减少请求数与响应体大小。
反模式:
- BFF 变业务黑洞:聚合里出现库存判断、优惠计算,等于把领域逻辑搬进了一个没有领域模型的地方。
- 多个 BFF 共享库表:BFF 之间通过数据库耦合,等于绕过服务边界;BFF 之间也不能互调。
- 无契约测试:BFF 的接口只服务一个客户端,容易被「悄悄改」,所以 BFF 与服务之间、BFF 与客户端之间各要一层契约校验。
- 与网关职责重叠:鉴权、限流两边都做,问题排查时无法定位生效点。
- 一服务一 BFF:BFF 按端划分而不是按服务划分,按服务拆出来的只是多一层转发。
五、多端适配的三种做法
| 做法 | 说明 | 端侧成本 | 服务端成本 | 发布独立性 | 主要风险 |
|---|---|---|---|---|---|
| 服务端按需字段 | 接口支持字段过滤或 GraphQL,端上声明需要什么 | 低 | 中:查询层要支持投影与权限裁剪 | 中:契约在服务端 | 复杂查询引发 N+1;裁剪漏项导致越权 |
| 每端一个 BFF | 每端一个适配服务,各自聚合与裁剪 | 最低 | 高:适配代码与人员成倍 | 高:端侧可独立发布 | 逻辑重复、版本碎片化 |
| 通用网关 + 客户端适配 | 服务端只提供领域接口,端上多次调用并组合 | 高:请求数与弱网体验差 | 最低 | 高 | 端侧逻辑重、流量与能耗差,端团队维护成本高 |
选择依据不是「哪个先进」,而是端差异是否长期存在与端侧、服务端团队的规模比:端差异大且长期存在、端团队独立,选 BFF;端差异小,选按需字段;端数量少而服务端人力紧张,客户端适配也能活,但要接受端侧复杂度上升。
六、落地要点
BFF 的聚合编排要守住三条:
- 超时预算自上而下分配,每层只花自己那一份,不允许下游把上游拖死。
| 层 | 预算 | 说明 |
|---|---|---|
| 客户端 | 1500ms | 用户可感知的上限 |
| 网关 | 1200ms | 留 300ms 给网络与自身开销 |
| BFF 聚合 | 800ms | 并行扇出,取最慢一路 |
| 单个下游服务 | 300ms(含 1 次重试) | 单跳预算,重试不得突破本级预算 |
- 聚合只做编排,不做决策:缺数据就给缺省展示,而不是替业务判断「该不该允许」。
- 降级路径在 BFF 定义:某一路聚合失败时返回部分数据加标记,好过整个页面 500。
// 聚合示意:并行取数、各自超时、整体预算封顶,失败只降级不阻断
CompletableFuture<UserBrief> user = async(() -> userClient.brief(uid), 200);
CompletableFuture<List<OrderBrief>> orders = async(() -> orderClient.recent(uid, 10), 300);
// 任一失败按缺省值填补:用户信息缺失不阻塞订单列表展示
return assemble(awaitAll(user, orders, 800));
小结:接入层的划分依据是「逻辑是否含业务语义」。网关承担路由、鉴权、限流、协议转换、可观测性注入这些全局横切职责,不做业务判断;BFF 按端划分解决多端适配,按服务划分就只是转发层。多端适配的三种做法各有代价,选哪种取决于端差异的长期性与团队结构;而无论选哪种,超时预算、降级路径与契约测试都必须在上线前落到配置与用例里。