Azure AKS 环境下 Lit 微前端的网关模式选型与架构实现


当多个团队同时维护基于 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-mfeorders-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, labelsimage 即可。

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-mfeorders-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 资源

DeploymentService 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:独立的组合层网关

尽管它引入了一个需要额外开发和维护的组件,增加了请求链路上的一跳,但带来的收益是战略性的:

  1. 团队自治profile-mfe 团队和 orders-mfe 团队可以独立选择技术栈(只要能输出 HTML 片段)、独立部署、独立发布,而无需协调 Ingress 规则的变更。他们的交付物是“业务组件”,而不是“页面”。
  2. 架构解耦:路由逻辑从基础设施(YAML)转移到了应用代码(JavaScript/Go)中。这使得实现更复杂的路由规则、A/B 测试、功能开关等变得轻而易举。
  3. 体验一致性:由组合网关统一提供应用外壳,确保了所有页面拥有一致的页眉、页脚、样式和核心 JS 库,避免了不同 MFE 各自为政带来的体验割裂。
  4. 韧性增强:如代码所示,网关层是实现优雅降级的天然场所。单个 MFE 的故障不会导致整个应用崩溃。
  5. 性能优化空间:网关可以实现对下游 MFE 响应的缓存,或者进行一些服务端渲染(SSR)的预处理,以优化首屏加载性能(FCP/LCP)。

在真实项目中,这个组合层网关通常不会像示例中那么简单。它会演化成一个健壮的“前端网关”,集成认证、授权、请求日志、分布式追踪(通过 OpenTelemetry 传递 traceId)、限流等跨领域关注点。

架构的扩展性与局限性

当前选择的组合层网关方案,其最大的优势在于它的扩展性。未来,这个网关可以演变为支持 Lit 组件服务端渲染(SSR)的核心节点,通过引入 Lit 的 @lit-labs/ssr 包,将组件在服务端直接渲染成静态 HTML,这对 SEO 和首屏性能是巨大的提升。此外,基于请求上下文(如用户角色、地理位置),网关可以动态地组合不同的 MFE,实现高度个性化的页面布局。

然而,该方案并非没有缺点。最显著的局限性是它引入了一个新的单点故障。组合网关自身的稳定性和性能直接决定了整个前端应用的生死。因此,必须对其进行充分的监控、告警,并保证其高可用部署(多副本、水平自动扩缩容 HPA)。

另一个需要关注的是性能开销。相较于方案 A 的直接代理,方案 B 增加了一次网络跳跃和一次应用层处理,这必然会带来毫秒级的延迟增加。在选择此方案时,必须通过全链路压测来评估这点延迟是否在可接受的服务等级目标(SLO)之内。对于延迟极其敏感的应用场景,可能需要探索更底层的技术,如使用 Go/Rust 编写更高性能的组合网关,或者利用 Envoy/Istio 配合 Lua 脚本实现类似的功能,但这又会是另一轮架构上的权衡。


  目录