跳到主要内容

设计 BFF 网关层

一、需求分析

1.1 什么是 BFF

BFF(Backend For Frontend,服务于前端的后端)是一种架构模式,它在前端应用和后端微服务之间引入一个专属的中间层,负责请求聚合、数据裁剪、协议转换、认证鉴权等职责。BFF 的核心理念是:不同的前端消费者(Web、App、小程序)拥有各自专属的后端服务层

面试要点

面试中回答「BFF 是什么」时,需要强调三个关键词:专属(每种客户端有自己的 BFF)、聚合(组合多个微服务的数据)、适配(根据端的特性裁剪数据格式)。

1.2 为什么需要 BFF

在微服务架构下,前端直接调用后端微服务面临以下问题:

问题描述BFF 解决方式
接口冗余一个页面需要调用 5-10 个微服务接口BFF 聚合为 1 个接口
数据过载微服务返回的字段远多于前端所需BFF 裁剪数据,只返回需要的字段
协议不一致不同服务使用 REST / gRPC / GraphQLBFF 统一对外暴露 REST 或 GraphQL
耦合严重前端直接依赖微服务地址和接口格式BFF 作为中间层解耦
多端差异Web 和 App 需要不同的数据格式和字段每端有专属 BFF,独立适配
安全风险微服务内部接口暴露给前端BFF 统一鉴权,屏蔽内部服务
性能瓶颈前端串行请求多个接口,链路长BFF 在服务端并行调用,减少 RTT

1.3 功能需求

功能模块核心能力说明
请求聚合组合多个微服务数据支持并行、串行、依赖编排
数据裁剪按端返回最小数据集减少传输体积,提升性能
认证鉴权统一身份验证和权限校验JWT / OAuth / Session
缓存管理多级缓存策略Redis / 内存缓存 / CDN
限流熔断保护下游服务令牌桶、滑动窗口、熔断降级
日志追踪全链路可观测requestId、OpenTelemetry
错误处理统一错误格式错误码规范、降级策略
协议转换对外 REST/GraphQL,对内 gRPCProtocol Buffers 编解码

1.4 非功能需求

需求目标实现方式
延迟P99 < 200ms(不含下游)连接池、缓存、并行调用
吞吐量单实例 > 5000 QPS异步非阻塞、事件驱动
可用性99.99%(四个九)熔断降级、多副本、健康检查
可扩展性水平扩展,无状态K8s HPA、Redis 外置状态
安全性防攻击、防数据泄露Rate Limit、WAF、数据脱敏

二、整体架构

2.1 系统架构全景

2.2 请求生命周期


三、核心模块设计

3.1 请求聚合与编排

请求聚合是 BFF 最核心的能力。根据数据依赖关系,分为三种模式:

并行聚合

多个微服务之间无数据依赖,可同时发起请求:

bff/aggregator/parallel.ts
interface ServiceResponse<T> {
code: number;
data: T;
message: string;
}

interface AggregatedPageData {
user: UserProfile;
orders: Order[];
recommendations: Product[];
}

async function aggregateParallel(userId: string): Promise<AggregatedPageData> {
const [userRes, ordersRes, recsRes] = await Promise.allSettled([
userService.getProfile(userId),
orderService.getRecentOrders(userId),
productService.getRecommendations(userId),
]);

return {
user: userRes.status === 'fulfilled' ? userRes.value : getDefaultUser(),
orders: ordersRes.status === 'fulfilled' ? ordersRes.value : [],
recommendations: recsRes.status === 'fulfilled' ? recsRes.value : [],
};
}
注意

使用 Promise.allSettled 而非 Promise.all,确保某个服务超时或失败不影响其他数据的返回。对于非核心数据(如推荐列表),返回默认值即可。

串行编排(有依赖关系)

当后续请求依赖前一个请求的返回值时,需要串行执行:

bff/aggregator/serial.ts
interface OrderDetailPage {
order: OrderDetail;
seller: SellerInfo;
logistics: LogisticsInfo;
}

async function aggregateSerial(orderId: string): Promise<OrderDetailPage> {
// Step 1: 获取订单详情
const order = await orderService.getDetail(orderId);

// Step 2: 基于订单数据并行获取卖家信息和物流信息
const [seller, logistics] = await Promise.all([
userService.getSellerInfo(order.sellerId),
logisticsService.getTracking(order.trackingNumber),
]);

return { order, seller, logistics };
}

DAG 依赖编排引擎

对于复杂场景,可以实现一个基于有向无环图(DAG)的编排引擎:

bff/aggregator/dag-orchestrator.ts
interface TaskNode<T = unknown> {
name: string;
dependencies: string[];
execute: (context: Map<string, unknown>) => Promise<T>;
}

class DAGOrchestrator {
private tasks = new Map<string, TaskNode>();

register<T>(task: TaskNode<T>): this {
this.tasks.set(task.name, task);
return this;
}

async execute(): Promise<Map<string, unknown>> {
const results = new Map<string, unknown>();
const inDegree = new Map<string, number>();
const dependents = new Map<string, string[]>();

// 构建入度表和依赖关系
for (const [name, task] of this.tasks) {
inDegree.set(name, task.dependencies.length);
for (const dep of task.dependencies) {
const list = dependents.get(dep) ?? [];
list.push(name);
dependents.set(dep, list);
}
}

// 找出入度为 0 的任务(可立即执行)
const ready: string[] = [];
for (const [name, degree] of inDegree) {
if (degree === 0) ready.push(name);
}

// 逐层并行执行
while (ready.length > 0) {
const batch = ready.splice(0);
const batchResults = await Promise.allSettled(
batch.map(async (name) => {
const task = this.tasks.get(name)!;
const result = await task.execute(results);
results.set(name, result);
return name;
})
);

// 更新入度,释放下游任务
for (const res of batchResults) {
if (res.status === 'fulfilled') {
const completedName = res.value;
for (const dep of dependents.get(completedName) ?? []) {
const newDegree = (inDegree.get(dep) ?? 1) - 1;
inDegree.set(dep, newDegree);
if (newDegree === 0) ready.push(dep);
}
}
}
}

return results;
}
}

使用示例:

bff/routes/product-detail.ts
const orchestrator = new DAGOrchestrator();

orchestrator
.register({
name: 'product',
dependencies: [],
execute: async () => productService.getDetail(productId),
})
.register({
name: 'seller',
dependencies: ['product'], // 依赖 product 任务的结果
execute: async (ctx) => {
const product = ctx.get('product') as Product;
return userService.getSellerInfo(product.sellerId);
},
})
.register({
name: 'reviews',
dependencies: [],
execute: async () => reviewService.getTopReviews(productId),
})
.register({
name: 'similar',
dependencies: ['product'],
execute: async (ctx) => {
const product = ctx.get('product') as Product;
return productService.getSimilar(product.categoryId);
},
});

const results = await orchestrator.execute();

3.2 GraphQL BFF

GraphQL 是实现 BFF 的天然选择。它允许客户端按需查询数据,避免 Over-fetching 和 Under-fetching 问题。

bff/graphql/schema.ts
import { makeExecutableSchema } from '@graphql-tools/schema';

const typeDefs = `
type User {
id: ID!
name: String!
avatar: String
orders(limit: Int = 10): [Order!]!
}

type Order {
id: ID!
status: OrderStatus!
totalAmount: Float!
items: [OrderItem!]!
createdAt: String!
}

enum OrderStatus {
PENDING
PAID
SHIPPED
DELIVERED
CANCELLED
}

type OrderItem {
product: Product!
quantity: Int!
price: Float!
}

type Product {
id: ID!
name: String!
price: Float!
images: [String!]!
}

type Query {
user(id: ID!): User
product(id: ID!): Product
searchProducts(keyword: String!, page: Int): [Product!]!
}
`;

DataLoader 解决 N+1 问题

警告

GraphQL 中嵌套查询最容易引发 N+1 问题。例如查询 10 个订单的商品信息,会导致 10 次 productService 调用。必须使用 DataLoader 批量合并请求。

bff/graphql/dataloaders.ts
import DataLoader from 'dataloader';

// DataLoader 会自动将同一事件循环内的多次 load 合并为一次批量请求
function createProductLoader(): DataLoader<string, Product> {
return new DataLoader(async (productIds: readonly string[]) => {
// 一次性批量查询所有商品
const products = await productService.batchGetByIds([...productIds]);

// 保持返回顺序与输入 ID 顺序一致
const productMap = new Map(products.map((p) => [p.id, p]));
return productIds.map((id) => productMap.get(id) ?? new Error(`Product ${id} not found`));
});
}

function createUserLoader(): DataLoader<string, User> {
return new DataLoader(async (userIds: readonly string[]) => {
const users = await userService.batchGetByIds([...userIds]);
const userMap = new Map(users.map((u) => [u.id, u]));
return userIds.map((id) => userMap.get(id) ?? new Error(`User ${id} not found`));
});
}

// 每个请求创建独立的 DataLoader 实例(避免跨请求缓存泄露)
interface GraphQLContext {
loaders: {
product: DataLoader<string, Product>;
user: DataLoader<string, User>;
};
}

function createContext(): GraphQLContext {
return {
loaders: {
product: createProductLoader(),
user: createUserLoader(),
},
};
}

Resolver 使用 DataLoader:

bff/graphql/resolvers.ts
const resolvers = {
Query: {
user: async (_: unknown, args: { id: string }, ctx: GraphQLContext) => {
return ctx.loaders.user.load(args.id);
},
},
OrderItem: {
// 即使有 100 个 OrderItem,也只会发起 1 次批量 product 查询
product: async (parent: { productId: string }, _: unknown, ctx: GraphQLContext) => {
return ctx.loaders.product.load(parent.productId);
},
},
User: {
orders: async (parent: User, args: { limit: number }) => {
return orderService.getByUserId(parent.id, args.limit);
},
},
};

3.3 认证鉴权

BFF 层统一处理认证鉴权,下游微服务只需信任 BFF 传递的用户上下文。

bff/middleware/auth.ts
import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import * as jwt from 'jsonwebtoken';

interface JWTPayload {
userId: string;
roles: string[];
exp: number;
iat: number;
}

@Injectable()
export class AuthMiddleware implements NestMiddleware {
constructor(
private readonly redisService: RedisService,
private readonly configService: ConfigService,
) {}

async use(req: Request, _res: Response, next: NextFunction): Promise<void> {
const token = this.extractToken(req);

if (!token) {
throw new UnauthorizedException('Missing authorization token');
}

// 1. 检查 Token 是否在黑名单中(用户主动登出)
const isBlacklisted = await this.redisService.get(`token:blacklist:${token}`);
if (isBlacklisted) {
throw new UnauthorizedException('Token has been revoked');
}

try {
// 2. 验证并解码 JWT
const secret = this.configService.get<string>('JWT_SECRET');
const payload = jwt.verify(token, secret) as JWTPayload;

// 3. 将用户信息注入请求上下文
req['user'] = {
userId: payload.userId,
roles: payload.roles,
};

// 4. 向下游服务传递用户身份(内部通信使用 header)
req.headers['x-user-id'] = payload.userId;
req.headers['x-user-roles'] = payload.roles.join(',');

next();
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new UnauthorizedException('Token expired');
}
throw new UnauthorizedException('Invalid token');
}
}

private extractToken(req: Request): string | null {
const authHeader = req.headers.authorization;
if (authHeader?.startsWith('Bearer ')) {
return authHeader.slice(7);
}
return null;
}
}

权限装饰器

bff/decorators/roles.ts
import { SetMetadata, UseGuards, applyDecorators } from '@nestjs/common';

// 角色装饰器
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

// 组合使用
export function Auth(...roles: string[]) {
return applyDecorators(
Roles(...roles),
UseGuards(AuthGuard, RolesGuard),
);
}

// 在 Controller 中使用
@Controller('admin')
export class AdminController {
@Get('dashboard')
@Auth('admin', 'super_admin')
getDashboard() {
return this.adminService.getDashboard();
}
}

3.4 缓存策略

BFF 层实现多级缓存,从近到远依次:内存缓存 -> Redis 缓存 -> 下游服务

bff/cache/multi-level-cache.ts
import { Injectable } from '@nestjs/common';
import { Redis } from 'ioredis';
import { LRUCache } from 'lru-cache';

interface CacheOptions {
/** 内存缓存 TTL,单位毫秒 */
memoryTTL?: number;
/** Redis 缓存 TTL,单位秒 */
redisTTL?: number;
/** 缓存 key 前缀 */
prefix?: string;
}

@Injectable()
export class MultiLevelCache {
// L1: 内存 LRU 缓存(进程级别,重启丢失)
private memoryCache = new LRUCache<string, string>({
max: 1000, // 最多缓存 1000 个 key
ttl: 1000 * 60, // 默认 1 分钟过期
maxSize: 50 * 1024 * 1024, // 最大 50MB
sizeCalculation: (value) => Buffer.byteLength(value, 'utf8'),
});

constructor(private readonly redis: Redis) {}

async get<T>(key: string, options: CacheOptions = {}): Promise<T | null> {
const fullKey = options.prefix ? `${options.prefix}:${key}` : key;

// L1: 检查内存缓存
const memoryResult = this.memoryCache.get(fullKey);
if (memoryResult) {
return JSON.parse(memoryResult) as T;
}

// L2: 检查 Redis 缓存
const redisResult = await this.redis.get(fullKey);
if (redisResult) {
// 回写 L1
this.memoryCache.set(fullKey, redisResult, {
ttl: options.memoryTTL ?? 60_000,
});
return JSON.parse(redisResult) as T;
}

return null;
}

async set<T>(key: string, value: T, options: CacheOptions = {}): Promise<void> {
const fullKey = options.prefix ? `${options.prefix}:${key}` : key;
const serialized = JSON.stringify(value);

// 同时写入 L1 和 L2
this.memoryCache.set(fullKey, serialized, {
ttl: options.memoryTTL ?? 60_000,
});
await this.redis.set(fullKey, serialized, 'EX', options.redisTTL ?? 300);
}

/**
* Cache-Aside 模式:先查缓存,未命中时执行 fetcher 并回写缓存
*/
async getOrSet<T>(
key: string,
fetcher: () => Promise<T>,
options: CacheOptions = {},
): Promise<T> {
const cached = await this.get<T>(key, options);
if (cached !== null) return cached;

const data = await fetcher();
await this.set(key, data, options);
return data;
}

async invalidate(key: string, prefix?: string): Promise<void> {
const fullKey = prefix ? `${prefix}:${key}` : key;
this.memoryCache.delete(fullKey);
await this.redis.del(fullKey);
}

/** 批量失效:按前缀清除 */
async invalidateByPrefix(prefix: string): Promise<void> {
// Redis 使用 SCAN 避免阻塞
let cursor = '0';
do {
const [nextCursor, keys] = await this.redis.scan(cursor, 'MATCH', `${prefix}:*`, 'COUNT', 100);
cursor = nextCursor;
if (keys.length > 0) {
await this.redis.del(...keys);
}
} while (cursor !== '0');

// 内存缓存清除所有带此前缀的 key
for (const key of this.memoryCache.keys()) {
if (key.startsWith(`${prefix}:`)) {
this.memoryCache.delete(key);
}
}
}
}

请求级缓存(Request-Scoped Cache)

同一个请求内多次查询相同数据时,避免重复调用:

bff/cache/request-cache.ts
import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.REQUEST }) // 每个请求创建新实例
export class RequestCache {
private cache = new Map<string, Promise<unknown>>();

/**
* 请求级去重:同一请求内相同 key 只执行一次 fetcher
*/
async dedupe<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
if (this.cache.has(key)) {
return this.cache.get(key) as Promise<T>;
}

const promise = fetcher();
this.cache.set(key, promise);
return promise;
}
}

3.5 限流与熔断

限流器

bff/ratelimit/token-bucket.ts
class TokenBucket {
private tokens: number;
private lastRefillTime: number;

constructor(
private readonly capacity: number, // 桶容量
private readonly refillRate: number, // 每秒补充令牌数
) {
this.tokens = capacity;
this.lastRefillTime = Date.now();
}

tryConsume(count: number = 1): boolean {
this.refill();

if (this.tokens >= count) {
this.tokens -= count;
return true; // 允许通过
}
return false; // 限流拒绝
}

private refill(): void {
const now = Date.now();
const elapsed = (now - this.lastRefillTime) / 1000;
const tokensToAdd = elapsed * this.refillRate;
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
this.lastRefillTime = now;
}
}

熔断器

bff/circuit-breaker/circuit-breaker.ts
enum CircuitState {
CLOSED = 'CLOSED', // 正常通行
OPEN = 'OPEN', // 熔断,拒绝请求
HALF_OPEN = 'HALF_OPEN', // 半开,试探性放行部分请求
}

interface CircuitBreakerOptions {
failureThreshold: number; // 失败次数阈值
successThreshold: number; // 半开状态下成功次数阈值
timeout: number; // 熔断持续时间(毫秒)
fallback?: () => unknown; // 降级函数
}

class CircuitBreaker {
private state: CircuitState = CircuitState.CLOSED;
private failureCount = 0;
private successCount = 0;
private lastFailureTime = 0;

constructor(private readonly options: CircuitBreakerOptions) {}

async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === CircuitState.OPEN) {
// 检查是否超过熔断超时时间
if (Date.now() - this.lastFailureTime >= this.options.timeout) {
this.state = CircuitState.HALF_OPEN;
this.successCount = 0;
} else {
// 执行降级逻辑
if (this.options.fallback) {
return this.options.fallback() as T;
}
throw new Error('Circuit breaker is OPEN');
}
}

try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}

private onSuccess(): void {
this.failureCount = 0;
if (this.state === CircuitState.HALF_OPEN) {
this.successCount++;
if (this.successCount >= this.options.successThreshold) {
this.state = CircuitState.CLOSED;
}
}
}

private onFailure(): void {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.options.failureThreshold) {
this.state = CircuitState.OPEN;
}
}

getState(): CircuitState {
return this.state;
}
}

熔断器状态流转:

3.6 日志与链路追踪

requestId 中间件

bff/middleware/request-id.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { randomUUID } from 'crypto';

@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction): void {
// 优先使用上游传递的 requestId(如 Nginx / 网关),否则生成新的
const requestId = (req.headers['x-request-id'] as string) ?? randomUUID();
req.headers['x-request-id'] = requestId;
res.setHeader('x-request-id', requestId);

// 注入到 AsyncLocalStorage,确保全链路可追踪
requestContext.run({ requestId, startTime: Date.now() }, () => {
next();
});
}
}

OpenTelemetry 集成

bff/tracing/otel-setup.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import { Resource } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';

const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: 'web-bff',
}),
traceExporter: new OTLPTraceExporter({
url: 'http://jaeger:4318/v1/traces',
}),
instrumentations: [
new HttpInstrumentation(), // 自动追踪 HTTP 出入请求
new ExpressInstrumentation(), // 追踪 Express 路由
],
});

sdk.start();

结构化日志

bff/logger/structured-logger.ts
import { Injectable, LoggerService } from '@nestjs/common';

interface LogEntry {
timestamp: string;
level: 'info' | 'warn' | 'error' | 'debug';
requestId: string;
message: string;
duration?: number;
service?: string;
statusCode?: number;
error?: {
name: string;
message: string;
stack?: string;
};
[key: string]: unknown;
}

@Injectable()
export class StructuredLogger implements LoggerService {
log(message: string, context?: Record<string, unknown>): void {
this.write('info', message, context);
}

error(message: string, trace?: string, context?: Record<string, unknown>): void {
this.write('error', message, { ...context, stack: trace });
}

warn(message: string, context?: Record<string, unknown>): void {
this.write('warn', message, context);
}

private write(level: LogEntry['level'], message: string, context?: Record<string, unknown>): void {
const { requestId, startTime } = requestContext.getStore() ?? {};

const entry: LogEntry = {
timestamp: new Date().toISOString(),
level,
requestId: requestId ?? 'system',
message,
duration: startTime ? Date.now() - startTime : undefined,
...context,
};

// 输出 JSON 格式,方便 ELK / Loki 采集
process.stdout.write(JSON.stringify(entry) + '\n');
}
}

3.7 统一错误处理

bff/filters/global-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';

interface ErrorResponse {
code: number;
message: string;
requestId: string;
timestamp: string;
path: string;
details?: unknown;
}

// 下游服务的错误不应该直接暴露给客户端
const SERVICE_ERROR_MAP: Record<string, { code: number; message: string }> = {
USER_NOT_FOUND: { code: 404001, message: '用户不存在' },
ORDER_NOT_FOUND: { code: 404002, message: '订单不存在' },
INSUFFICIENT_STOCK: { code: 400001, message: '库存不足' },
PAYMENT_FAILED: { code: 500001, message: '支付处理中,请稍后查询' },
};

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
constructor(private readonly logger: StructuredLogger) {}

catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const req = ctx.getRequest<Request>();
const res = ctx.getResponse<Response>();

let status: number;
let message: string;
let code: number;

if (exception instanceof HttpException) {
status = exception.getStatus();
message = exception.message;
code = status;
} else if (exception instanceof ServiceError) {
// 将下游服务错误映射为对客户端友好的错误
const mapped = SERVICE_ERROR_MAP[exception.code];
status = Math.floor((mapped?.code ?? 500000) / 1000);
message = mapped?.message ?? '服务暂时不可用';
code = mapped?.code ?? 500000;
} else {
status = HttpStatus.INTERNAL_SERVER_ERROR;
message = '服务内部错误';
code = 500000;
}

const errorResponse: ErrorResponse = {
code,
message,
requestId: req.headers['x-request-id'] as string,
timestamp: new Date().toISOString(),
path: req.url,
};

// 记录详细错误日志(包含堆栈),但不返回给客户端
this.logger.error('Request failed', (exception as Error)?.stack, {
path: req.url,
method: req.method,
statusCode: status,
errorCode: code,
});

res.status(status).json(errorResponse);
}
}

超时控制与降级

bff/utils/timeout.ts
/**
* 为 Promise 添加超时控制
*/
function withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
fallback?: T,
): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => {
if (fallback !== undefined) {
resolve(fallback);
} else {
reject(new Error(`Request timeout after ${timeoutMs}ms`));
}
}, timeoutMs);

promise
.then((result) => {
clearTimeout(timer);
resolve(result);
})
.catch((error) => {
clearTimeout(timer);
reject(error);
});
});
}

// 使用示例
const userData = await withTimeout(
userService.getProfile(userId),
3000, // 3 秒超时
{ name: '加载中...', avatar: '' }, // 降级数据
);

四、关键技术实现

4.1 技术选型对比

框架语言优势适用场景
NestJSTypeScript模块化、DI、装饰器、生态丰富中大型 BFF,团队协作
FastifyTypeScript高性能(2x Express)、Schema 校验高吞吐量 BFF
ExpressJavaScript/TS简单灵活、中间件丰富轻量 BFF、快速原型
MidwayTypeScript面向对象、函数式双范式、阿里生态全栈 Node.js 应用
tRPCTypeScript端到端类型安全、零代码生成全 TypeScript 栈
HonoTypeScript轻量、Edge Runtime 支持、Web Standard APIEdge BFF、Cloudflare Workers
推荐

中大型项目推荐 NestJS,它提供了完善的模块化架构、依赖注入、中间件/拦截器/管道/守卫等分层能力,天然适合 BFF 网关的需求。高性能场景可以将 NestJS 的底层 HTTP 适配器切换为 Fastify。

4.2 NestJS BFF 项目结构

bff-gateway/
├── src/
│ ├── main.ts # 启动入口
│ ├── app.module.ts # 根模块
│ ├── common/
│ │ ├── middleware/ # 中间件
│ │ │ ├── auth.middleware.ts
│ │ │ ├── request-id.middleware.ts
│ │ │ └── logger.middleware.ts
│ │ ├── filters/ # 异常过滤器
│ │ │ └── global-exception.filter.ts
│ │ ├── interceptors/ # 拦截器
│ │ │ ├── cache.interceptor.ts
│ │ │ ├── timeout.interceptor.ts
│ │ │ └── transform.interceptor.ts
│ │ ├── guards/ # 守卫
│ │ │ ├── auth.guard.ts
│ │ │ └── roles.guard.ts
│ │ └── decorators/ # 自定义装饰器
│ ├── modules/
│ │ ├── user/ # 用户聚合模块
│ │ │ ├── user.controller.ts
│ │ │ ├── user.service.ts # 聚合逻辑
│ │ │ └── user.module.ts
│ │ ├── order/ # 订单聚合模块
│ │ └── product/ # 商品聚合模块
│ ├── services/ # 下游服务代理
│ │ ├── user.service.proxy.ts
│ │ ├── order.service.proxy.ts
│ │ └── product.service.proxy.ts
│ ├── graphql/ # GraphQL 相关
│ │ ├── schema.ts
│ │ ├── resolvers/
│ │ └── dataloaders/
│ └── config/ # 配置
│ ├── redis.config.ts
│ └── service-registry.ts
├── Dockerfile
├── docker-compose.yml
└── package.json

4.3 服务代理层

BFF 通过 HTTP Client 调用下游微服务,需要统一封装 HTTP 客户端:

bff/services/base-service-proxy.ts
import { Injectable, Logger } from '@nestjs/common';
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';

interface ServiceConfig {
baseURL: string;
timeout: number;
serviceName: string;
}

@Injectable()
export class BaseServiceProxy {
protected readonly client: AxiosInstance;
protected readonly logger = new Logger(this.constructor.name);

constructor(config: ServiceConfig) {
this.client = axios.create({
baseURL: config.baseURL,
timeout: config.timeout,
});

// 请求拦截器:注入 requestId 和用户信息
this.client.interceptors.request.use((reqConfig) => {
const store = requestContext.getStore();
if (store?.requestId) {
reqConfig.headers['x-request-id'] = store.requestId;
}
return reqConfig;
});

// 响应拦截器:统一日志和错误处理
this.client.interceptors.response.use(
(response: AxiosResponse) => {
this.logger.log(
`${config.serviceName} ${response.config.method?.toUpperCase()} ${response.config.url} ${response.status}`,
);
return response;
},
(error) => {
this.logger.error(
`${config.serviceName} request failed: ${error.message}`,
error.stack,
);
throw new ServiceError(config.serviceName, error);
},
);
}

protected async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.get<ServiceResponse<T>>(url, config);
return response.data.data;
}

protected async post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.post<ServiceResponse<T>>(url, data, config);
return response.data.data;
}
}

五、性能优化

5.1 优化策略总览

策略效果实现复杂度
并行请求聚合减少总耗时 50%-70%
多级缓存减少下游调用 60%-80%
连接池复用减少 TCP 握手开销
请求级去重消除同请求内重复调用
gRPC 替代 HTTP序列化速度提升 5-10 倍
响应压缩传输体积减少 60%-80%
DataLoader 批量解决 N+1,减少请求数
预加载 & 预计算首次请求无延迟

5.2 连接池配置

bff/config/http-agent.ts
import { Agent as HttpAgent } from 'http';
import { Agent as HttpsAgent } from 'https';

// 复用 TCP 连接,避免频繁三次握手
const httpAgent = new HttpAgent({
keepAlive: true,
keepAliveMsecs: 30_000, // Keep-Alive 持续 30 秒
maxSockets: 100, // 每个 host 最大连接数
maxFreeSockets: 20, // 最大空闲连接数
});

const httpsAgent = new HttpsAgent({
keepAlive: true,
keepAliveMsecs: 30_000,
maxSockets: 100,
maxFreeSockets: 20,
});

5.3 响应压缩与数据裁剪

bff/interceptors/transform.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, map } from 'rxjs';

// 按客户端类型裁剪字段
type ClientType = 'web' | 'mobile' | 'mini';

const FIELD_CONFIG: Record<ClientType, Record<string, string[]>> = {
web: {
user: ['id', 'name', 'avatar', 'email', 'bio', 'followers', 'following'],
product: ['id', 'name', 'price', 'images', 'description', 'specs', 'reviews'],
},
mobile: {
user: ['id', 'name', 'avatar'], // 移动端只需基础信息
product: ['id', 'name', 'price', 'images'],
},
mini: {
user: ['id', 'name', 'avatar'],
product: ['id', 'name', 'price', 'images'],
},
};

@Injectable()
export class DataTrimInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const req = context.switchToHttp().getRequest();
const clientType = (req.headers['x-client-type'] as ClientType) ?? 'web';

return next.handle().pipe(
map((data) => this.trimData(data, clientType)),
);
}

private trimData(data: Record<string, unknown>, clientType: ClientType): Record<string, unknown> {
// 根据配置裁剪字段...
return data;
}
}

六、扩展设计

6.1 灰度发布

BFF 层是实现灰度发布的理想位置,可以根据用户特征将请求路由到不同版本的下游服务。

bff/middleware/canary.middleware.ts
interface CanaryRule {
name: string;
percentage: number; // 灰度流量百分比
targetVersion: string; // 目标服务版本
userCondition?: {
userIds?: string[]; // 白名单用户
regions?: string[]; // 地区
tags?: string[]; // 用户标签
};
}

function resolveServiceVersion(
userId: string,
rules: CanaryRule[],
): string {
for (const rule of rules) {
// 白名单用户直接命中
if (rule.userCondition?.userIds?.includes(userId)) {
return rule.targetVersion;
}

// 基于用户 ID 的一致性哈希,确保同一用户始终命中同一版本
const hash = consistentHash(userId, 100);
if (hash < rule.percentage) {
return rule.targetVersion;
}
}
return 'stable'; // 默认稳定版本
}

function consistentHash(key: string, buckets: number): number {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash = ((hash << 5) - hash + key.charCodeAt(i)) | 0;
}
return Math.abs(hash) % buckets;
}

6.2 多端 BFF 与共享层

架构建议

多端 BFF 之间应共享通用能力(认证、缓存、日志、熔断),通过 npm 包或 Monorepo 的 shared 包复用代码。每端的 BFF 只关注数据编排和端特性适配

6.3 BFF 与 API Gateway 的配合

在实际生产中,BFF 通常和 API Gateway 配合使用,各司其职:

6.4 部署方案

docker-compose.yml
version: '3.8'
services:
web-bff:
build:
context: .
dockerfile: Dockerfile
ports:
- '3000:3000'
environment:
- NODE_ENV=production
- REDIS_URL=redis://redis:6379
- USER_SERVICE_URL=http://user-service:8001
- ORDER_SERVICE_URL=http://order-service:8002
depends_on:
- redis
deploy:
replicas: 3
resources:
limits:
cpus: '1.0'
memory: 512M
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3000/health']
interval: 10s
timeout: 3s
retries: 3

redis:
image: redis:7-alpine
ports:
- '6379:6379'
volumes:
- redis-data:/data

volumes:
redis-data:

七、常见面试问题

Q1: BFF 和 API Gateway 有什么区别?

答案

两者在微服务架构中扮演不同角色,经常被混淆但职责不同:

维度API GatewayBFF
定位基础设施层,通用网关应用层,服务于特定前端
职责SSL 终止、全局限流、路由分发、IP 黑白名单请求聚合、数据裁剪、业务鉴权、协议适配
实例数通常 1 个,面向所有客户端每种客户端可以有各自的 BFF
技术栈Nginx、Kong、APISIX、EnvoyNestJS、Express、Fastify
业务逻辑不包含业务逻辑包含数据编排和轻业务逻辑
部署位置集群入口API Gateway 之后、微服务之前
关注点安全、流量治理前端体验、数据适配

典型的部署链路是:客户端 -> API Gateway -> BFF -> 微服务

在小型项目中,BFF 可以兼任 Gateway 的部分职责(如认证鉴权)。在大型项目中,两者分离更合理——Gateway 负责基础设施层面的流量治理,BFF 专注于业务层面的数据编排。

Q2: 微服务超时了怎么处理?

答案

BFF 层处理下游超时需要分级应对,核心策略如下:

1. 设置合理的超时时间

timeout-config.ts
// 根据服务重要性设置不同超时时间
const TIMEOUT_CONFIG = {
userService: 3000, // 核心服务,3 秒
orderService: 5000, // 可能涉及 DB 查询,5 秒
recommendService: 1000, // 非核心服务,1 秒快速失败
};

2. 区分核心与非核心服务

  • 核心服务超时(如用户信息):重试 1-2 次,仍失败则返回错误
  • 非核心服务超时(如推荐列表):直接返回默认值 / 空数据,不阻塞主流程

3. 熔断降级

当某服务错误率超过阈值时,自动熔断,快速返回降级数据,避免雪崩:

timeout-handling.ts
const result = await circuitBreaker.execute(
() => withTimeout(recommendService.getList(userId), 1000),
// 降级:返回热门推荐的缓存
() => cacheService.get('hot-recommendations'),
);

4. 超时 budget 分配

整个 BFF 请求有一个总超时 budget(如 10 秒),串行编排中需要把 budget 合理分配给每一步:

timeout-budget.ts
const TOTAL_BUDGET = 10_000; // 10 秒总预算
const step1Timeout = 3000;
const step2Timeout = Math.min(5000, TOTAL_BUDGET - elapsed); // 动态计算剩余 budget

Q3: BFF 层的缓存一致性如何保证?

答案

BFF 缓存一致性是一个经典难题,需要在性能数据新鲜度之间权衡。常用策略:

1. TTL 过期策略(最常用)

为不同数据设置合理的过期时间:

数据类型建议 TTL说明
用户基本信息5-10 分钟变更频率低
商品详情1-5 分钟价格可能变化
库存数量不缓存或 10 秒实时性要求高
推荐列表30 分钟-1 小时允许一定延迟
系统配置10-30 分钟变更后主动失效

2. 主动失效(Write-Through)

数据变更时,由下游服务通过消息队列通知 BFF 清除缓存:

cache-invalidation.ts
// BFF 监听消息队列,接收缓存失效通知
messageQueue.subscribe('cache.invalidate', async (event: CacheInvalidateEvent) => {
await multiLevelCache.invalidate(event.key, event.prefix);
logger.info('Cache invalidated', { key: event.key });
});

3. 版本号/ETag 机制

在缓存 key 中加入数据版本号,数据更新时版本号递增,旧缓存自动失效。

4. 实践建议

  • 非核心数据允许最终一致性(TTL 过期自动刷新)
  • 金额、库存等敏感数据不走 BFF 缓存,直接穿透到下游
  • 内存缓存的 TTL 要比 Redis 缓存短(如内存 30 秒,Redis 5 分钟),避免多实例间不一致

Q4: BFF 灰度发布怎么做?

答案

BFF 层的灰度发布分为两个层面:

1. BFF 自身的灰度

使用 Kubernetes 的金丝雀发布或蓝绿部署:

通过 Ingress 配置流量比例:

k8s/canary-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: bff-canary
annotations:
nginx.ingress.kubernetes.io/canary: 'true'
nginx.ingress.kubernetes.io/canary-weight: '10'
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-bff-canary
port:
number: 3000

2. BFF 作为灰度路由层

BFF 根据用户特征将请求路由到不同版本的下游服务:

  • 白名单灰度:指定用户 ID 命中新版本
  • 百分比灰度:基于用户 ID 哈希分流,确保同一用户始终命中同一版本
  • 特征灰度:按地区、设备类型、用户标签等维度分流

关键原则:

  • 灰度规则存储在配置中心(如 Apollo / Nacos),支持动态调整
  • 使用一致性哈希确保同一用户体验一致
  • 灰度期间加强监控,对比新旧版本的错误率、延迟、成功率
  • 发现问题时能快速回滚(将灰度比例设为 0)

相关链接