当多个团队同时维护基于 Lit 的独立前端组件,并计划将它们统一部署到 Azure Kubernetes Service (AKS) 集群时,第一个架构岔路口便出现了:如何将这些分散的、独立部署的前端应用,聚合为单一、内聚的用户体验?这个问题的核心,在于网关层的设计。错误的网关选型,会在项目初期带来短暂的便利,但随着团队和业务的扩张,迅速演变为部署耦合、技术栈锁定和维护性的噩梦。
在真实项目中,我们面临的选择通常不是“用不用网关”,而是“用哪种模式的网关”。这里我们深入探讨两种主流的、在 AKS 上可行的方案:路径驱动的 Ingress 路由 与 独立的组合层网关。
graph TD subgraph 用户请求 A[Browser] end subgraph 方案A: 路径驱动 Ingress B(Ingress Controller) B -- /app/profile --> C[Profile MFE Pod] B -- /app/orders --> D[Orders MFE Pod] end subgraph 方案B: 组合层网关 E(Composition Gateway Pod) E -- k8s service call --> F[Profile MFE Service] E -- k8s service call --> G[Orders MFE Service] H(Ingress Controller) -- /app/* --> E end A --> H A --x H A --> B style F fill:#f9f,stroke:#333,stroke-width:2px style G fill:#f9f,stroke:#333,stroke-width:2px style C fill:#ccf,stroke:#333,stroke-width:2px style D fill:#ccf,stroke:#333,stroke-width:2px
上图直观地展示了两种架构在流量处理上的本质区别。方案 A 将路由决策完全暴露给基础设施层(Ingress),而方案 B 则引入了一个专门的应用层网关来处理前端应用的“组合”逻辑,将基础设施与应用逻辑解耦。
方案 A:路径驱动的 Ingress 路由
这是最直观的方案。利用 Kubernetes Ingress 资源,根据 URL 路径将流量分发到不同的微前端(Micro-Frontend, MFE)服务。例如,example.com/profile
路由到 Profile MFE,example.com/orders
路由到 Orders MFE。
架构实现
假设我们有两个独立的 Lit 应用:profile-mfe
和 orders-mfe
。每个应用都有自己的 Dockerfile、Kubernetes Deployment 和 Service。
1. profile-mfe
的 Kubernetes 资源清单 (profile-mfe.yaml
)
# profile-mfe.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: profile-mfe-deployment
labels:
app: profile-mfe
spec:
replicas: 2
selector:
matchLabels:
app: profile-mfe
template:
metadata:
labels:
app: profile-mfe
spec:
containers:
- name: profile-mfe
image: youracr.azurecr.io/profile-mfe:1.0.0
ports:
- containerPort: 8080
# 生产级考量:资源限制与健康检查
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "250m"
memory: "256Mi"
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: profile-mfe-service
spec:
selector:
app: profile-mfe
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: ClusterIP # 仅在集群内部暴露
orders-mfe
的资源清单与此类似,只需替换相应的 name
, labels
和 image
即可。
2. 统一的 Ingress 资源
在 AKS 中,我们可以使用 NGINX Ingress Controller 或者更贴近 Azure 生态的 Application Gateway Ingress Controller (AGIC)。这里以 NGINX Ingress Controller 为例。
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mfe-ingress
annotations:
# 关键配置:重写路径,将 /profile/* 的请求转发到后端的 /*
# 否则,profile-mfe 服务会收到带有 /profile 前缀的请求,可能导致其内部路由失效
nginx.ingress.kubernetes.io/rewrite-target: /$2
# 其他生产级注解
nginx.ingress.kubernetes.io/proxy-body-size: "10m" # 允许更大的请求体
nginx.ingress.kubernetes.io/proxy-connect-timeout: "10s"
nginx.ingress.kubernetes.io/proxy-read-timeout: "30s"
spec:
ingressClassName: nginx
rules:
- host: your-domain.com
http:
paths:
- path: /profile(/|$)(.*)
pathType: Prefix
backend:
service:
name: profile-mfe-service
port:
number: 80
- path: /orders(/|$)(.*)
pathType: Prefix
backend:
service:
name: orders-mfe-service
port:
number: 80
优劣分析
优势:
- 简单直观:配置清晰,完全利用 Kubernetes 原生资源,运维成本较低。
- 技术成熟:Ingress Controller 是云原生生态中非常成熟的组件,稳定性和社区支持都很好。
- 性能优异:流量直接由高性能的反向代理(如 NGINX)处理,没有额外的应用层逻辑,延迟较低。
劣势:
- 路由与实现强耦合:URL 结构直接映射到服务部署结构。如果某个 MFE 需要从
/profile
迁移到/user/profile
,不仅需要修改应用代码,还必须修改 Ingress 资源,这违背了微前端独立部署的初衷。 - 页面组合困难:此模式非常适合“单页面应用”形态的微前端。但如果一个页面需要同时展示来自
profile-mfe
的用户信息卡片和来自orders-mfe
的最新订单列表,实现会非常棘手。客户端需要发起多次请求,或者通过复杂的 iframe/Web Components 动态加载机制,网络开销和实现复杂度都很高。 - 跨应用状态管理和通信复杂:不同源(因为路径不同)的前端应用在浏览器中存在同源策略限制,共享状态(如用户 Token)需要依赖 LocalStorage、Cookie 等方案,并且需要谨慎处理作用域。
- 全局样式与 JS 冲突风险:多个 MFE 如果不严格遵守 CSS-in-JS 或 Shadow DOM 隔离,其全局样式和 JavaScript 变量很容易发生冲突。
一个常见的错误是,团队在项目初期因为简单而选择了方案 A,但随着业务发展,页面上需要聚合的组件越来越多,最终被迫在客户端实现一个复杂的“组合框架”,把复杂性从后端转移到了前端,导致前端性能恶化,代码难以维护。
方案 B:独立的组合层网关
此方案在 Ingress Controller 之后引入了一个专门的、自研的组合层网关(Composition Gateway)。所有面向用户的流量首先由 Ingress 指向这个网关。该网关作为唯一的入口点,负责理解业务路由、从内部的 MFE 服务获取 HTML 片段或数据、组合成最终页面,然后返回给浏览器。
架构实现
1. MFE 服务的变化
profile-mfe
和 orders-mfe
的 Deployment 和 Service 配置基本不变,但它们的定位从“面向最终用户的 Web 服务”转变为“面向组合网关的组件服务”。它们可能不再提供完整的 HTML 页面,而是提供可被组合的 HTML 片段。
2. 组合层网关实现 (Node.js + Express 示例)
这是一个轻量级的 Node.js 服务,部署在 AKS 中,作为所有前端流量的处理器。
Dockerfile
# Dockerfile for Composition Gateway
FROM node:18-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --production
COPY . .
# 暴露的端口
EXPOSE 3000
# 生产环境启动命令
CMD [ "node", "server.js" ]
server.js
核心逻辑
// server.js
const express = require('express');
const fetch = require('node-fetch'); // 用于在服务端发起HTTP请求
const { createLogger, format, transports } = require('winston'); // 生产级日志
// 初始化日志
const logger = createLogger({
level: 'info',
format: format.combine(
format.timestamp(),
format.json()
),
transports: [new transports.Console()],
});
const app = express();
const PORT = process.env.PORT || 3000;
// MFE 服务的内部 Kubernetes Service DNS 名称
// 在真实项目中,这部分配置应该来自环境变量或 ConfigMap
const MFE_SERVICES = {
profile: 'http://profile-mfe-service.default.svc.cluster.local',
orders: 'http://orders-mfe-service.default.svc.cluster.local',
};
// 页面布局模板
const layout = (title, content) => `
<!DOCTYPE html>
<html>
<head>
<title>${title}</title>
<link rel="stylesheet" href="/global.css">
</head>
<body>
<header><h1>My Application Shell</h1></header>
<main>
${content}
</main>
<script src="/main.js" type="module"></script>
</body>
</html>
`;
// 组合路由:聚合来自多个 MFE 的内容
app.get('/dashboard', async (req, res) => {
try {
logger.info('Fetching fragments for dashboard page', { traceId: req.headers['x-request-id'] });
// 并行获取 MFE 片段
const [profileRes, ordersRes] = await Promise.all([
fetch(`${MFE_SERVICES.profile}/fragment`).catch(err => {
logger.error('Failed to fetch profile fragment', { error: err.message });
return { ok: false, status: 500, text: () => Promise.resolve('<div class="error">Profile service unavailable</div>') };
}),
fetch(`${MFE_SERVICES.orders}/fragment`).catch(err => {
logger.error('Failed to fetch orders fragment', { error: err.message });
return { ok: false, status: 500, text: () => Promise.resolve('<div class="error">Orders service unavailable</div>') };
})
]);
const profileFragment = await profileRes.text();
const ordersFragment = await ordersRes.text();
if (!profileRes.ok) {
logger.warn('Profile fragment request failed', { status: profileRes.status });
}
if (!ordersRes.ok) {
logger.warn('Orders fragment request failed', { status: ordersRes.status });
}
// 将片段组合到主布局中
const pageContent = `
<h2>Dashboard</h2>
<section>${profileFragment}</section>
<section>${ordersFragment}</section>
`;
res.send(layout('Dashboard', pageContent));
} catch (error) {
logger.error('Error composing dashboard page', { error: error.message, stack: error.stack });
res.status(500).send(layout('Error', '<h1>Service Temporarily Unavailable</h1>'));
}
});
// 简单的代理路由:将请求直接转发给某个 MFE
app.get('/profile/*', async (req, res) => {
// ... 代理逻辑实现
});
app.listen(PORT, () => {
logger.info(`Composition Gateway listening on port ${PORT}`);
});
这个示例展示了组合层网关的核心职责:
- 请求路由:理解业务 URL(如
/dashboard
),并知道它需要哪些 MFE。 - 内容聚合:通过 Kubernetes 内部 Service DNS 调用下游 MFE 服务,获取 HTML 片段。
- 容错处理:这里的
catch
和对res.ok
的检查是关键。如果某个 MFE 服务失败,网关不会让整个页面崩溃,而是可以返回一个错误占位符,保证了核心应用的可用性。 - 统一布局:提供一个应用外壳(Shell),包含公共的页头、页脚、CSS 和 JS,所有 MFE 片段都被嵌入其中。这从根本上解决了全局样式和库的统一管理问题。
3. 更新后的 Kubernetes 资源
Deployment
和 Service
for composition-gateway
会被创建。而 Ingress 的规则会变得极其简单。
# composition-gateway-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: composition-gateway-ingress
spec:
ingressClassName: nginx
rules:
- host: your-domain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
# 所有流量都指向组合网关
name: composition-gateway-service
port:
number: 80
graph LR subgraph Browser U[User Request to /dashboard] end subgraph AKS Cluster I[Ingress] --> GW[Composition Gateway Service] subgraph GatewayPod GW_Logic[Node.js Logic] end GW --> GW_Logic subgraph MFE Services PS[Profile Service] OS[Orders Service] end GW_Logic -- HTTP GET /fragment --> PS GW_Logic -- HTTP GET /fragment --> OS subgraph MFE Pods PP[Profile Pod] OP[Orders Pod] end PS --> PP OS --> OP PP -- HTML Fragment --> GW_Logic OP -- HTML Fragment --> GW_Logic GW_Logic -- Composed HTML --> I end U --> I I --> BrowserResponse[Full HTML Page]
这个流程图清晰地展示了方案 B 的请求生命周期。复杂性被收敛到了组合网关内部,而 MFE 团队只需关注于提供标准化的“组件片段”,实现了真正的关注点分离。
最终选择与理由
在权衡了长期可维护性、团队自主性和架构扩展性后,我们最终选择了方案 B:独立的组合层网关。
尽管它引入了一个需要额外开发和维护的组件,增加了请求链路上的一跳,但带来的收益是战略性的:
- 团队自治:
profile-mfe
团队和orders-mfe
团队可以独立选择技术栈(只要能输出 HTML 片段)、独立部署、独立发布,而无需协调 Ingress 规则的变更。他们的交付物是“业务组件”,而不是“页面”。 - 架构解耦:路由逻辑从基础设施(YAML)转移到了应用代码(JavaScript/Go)中。这使得实现更复杂的路由规则、A/B 测试、功能开关等变得轻而易举。
- 体验一致性:由组合网关统一提供应用外壳,确保了所有页面拥有一致的页眉、页脚、样式和核心 JS 库,避免了不同 MFE 各自为政带来的体验割裂。
- 韧性增强:如代码所示,网关层是实现优雅降级的天然场所。单个 MFE 的故障不会导致整个应用崩溃。
- 性能优化空间:网关可以实现对下游 MFE 响应的缓存,或者进行一些服务端渲染(SSR)的预处理,以优化首屏加载性能(FCP/LCP)。
在真实项目中,这个组合层网关通常不会像示例中那么简单。它会演化成一个健壮的“前端网关”,集成认证、授权、请求日志、分布式追踪(通过 OpenTelemetry 传递 traceId
)、限流等跨领域关注点。
架构的扩展性与局限性
当前选择的组合层网关方案,其最大的优势在于它的扩展性。未来,这个网关可以演变为支持 Lit 组件服务端渲染(SSR)的核心节点,通过引入 Lit 的 @lit-labs/ssr
包,将组件在服务端直接渲染成静态 HTML,这对 SEO 和首屏性能是巨大的提升。此外,基于请求上下文(如用户角色、地理位置),网关可以动态地组合不同的 MFE,实现高度个性化的页面布局。
然而,该方案并非没有缺点。最显著的局限性是它引入了一个新的单点故障。组合网关自身的稳定性和性能直接决定了整个前端应用的生死。因此,必须对其进行充分的监控、告警,并保证其高可用部署(多副本、水平自动扩缩容 HPA)。
另一个需要关注的是性能开销。相较于方案 A 的直接代理,方案 B 增加了一次网络跳跃和一次应用层处理,这必然会带来毫秒级的延迟增加。在选择此方案时,必须通过全链路压测来评估这点延迟是否在可接受的服务等级目标(SLO)之内。对于延迟极其敏感的应用场景,可能需要探索更底层的技术,如使用 Go/Rust 编写更高性能的组合网关,或者利用 Envoy/Istio 配合 Lua 脚本实现类似的功能,但这又会是另一轮架构上的权衡。