微服务架构设计中的核心挑战与实战解决方案

公孙光浩 框架 阅读 2,254
赞 20 收藏
二维码
手机扫码查看
反馈

先看效果,再看代码

我最近在搞一个项目,前端是 Vue3 + Vite,后端拆成了七八个微服务,全部跑在本地 Docker 里。一开始想着用 Nginx 做反向代理完事,结果接口一多,跨域、路径冲突、调试困难的问题全来了。折腾了半天,最后还是上了 BFF(Backend For Frontend)这一套——说白了就是写个 Node.js 中间层,统一转发请求。

微服务架构设计中的核心挑战与实战解决方案

亲测有效的方式:直接上 Express 写个轻量级网关,所有前端请求先打到这个中间层,它再根据路径转发给对应的服务。简单粗暴,但特别适合中小型项目。

// bff-server.js
import express from 'express'
import { createProxyMiddleware } from 'http-proxy-middleware'

const app = express()
const PORT = 3000

// 微服务映射表(开发环境)
const services = {
  '/api/user': 'http://localhost:4001',
  '/api/order': 'http://localhost:4002',
  '/api/product': 'http://localhost:4003',
  '/api/payment': 'http://localhost:4004',
}

// 动态注册代理
Object.keys(services).forEach((path) => {
  const target = services[path]
  app.use(
    path,
    createProxyMiddleware({
      target,
      changeOrigin: true,
      pathRewrite: {
        [^${path}]: '',
      },
      onProxyReq: (proxyReq, req) => {
        console.log([PROXY] ${req.method} ${path} -> ${target}${req.url})
      },
    })
  )
})

// 静态资源服务(Vite 构建后的)
app.use(express.static('dist'))

// SPA fallback
app.get('*', (req, res) => {
  res.sendFile('dist/index.html', { root: '.' })
})

app.listen(PORT, () => {
  console.log(BFF Server running on http://localhost:${PORT})
})

就这么一段代码,把所有微服务的入口统一了。前端配置很简单:

// vite.config.js
export default defineConfig({
  server: {
    proxy: {
      // 所有请求都走本地 BFF
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
      },
    },
  },
})

这样前端发 fetch('/api/user/profile'),实际是先到 BFF 的 3000 端口,再被转发到用户服务的 4001 端口。完美避开浏览器跨域限制,而且开发时完全无感。

这个场景最好用

你可能会问:为啥不直接让每个服务都配 CORS?或者用 Nginx 统一代理?

我的答案是:可以,但不好维护。

  • Nginx 配置改一次就得 reload,开发时频繁重启太烦人
  • CORS 每个服务都要单独处理,header、credentials 容易出错
  • 某些服务可能只允许内网调用,不能直接暴露给前端

而 BFF 模式下,这些都不是问题。你可以在这个中间层加日志、鉴权、缓存,甚至聚合接口。举个例子:

// 聚合接口:获取用户+订单+商品信息
app.get('/api/dashboard', async (req, res) => {
  try {
    const [userRes, orderRes, productRes] = await Promise.all([
      fetch('http://localhost:4001/user/profile'),
      fetch('http://localhost:4002/order/latest'),
      fetch('http://localhost:4003/product/recommended'),
    ])

    const userData = await userRes.json()
    const orderData = await orderRes.json()
    const productData = await productRes.json()

    res.json({
      user: userData,
      latestOrder: orderData,
      recommendedProducts: productData,
    })
  } catch (err) {
    res.status(500).json({ error: 'Failed to fetch dashboard data' })
  }
})

前端一个请求搞定三份数据,减少请求数,提升首屏速度。这种聚合逻辑放在 BFF 层比在前端拼三个异步请求清爽多了。

踩坑提醒:这三点一定注意

这里注意,我踩过好几次坑:

  1. Cookie 和认证头丢失:默认情况下,http-proxy-middleware 不会自动转发 Cookie。如果你用的是 Session 认证,必须手动设置:
app.use(
  '/api/user',
  createProxyMiddleware({
    target: 'http://localhost:4001',
    changeOrigin: true,
    secure: false,
    headers: {
      // 确保原始 host 不被覆盖
      Host: 'localhost:4001',
    },
    // 关键!保持 cookie 透传
    onProxyReq: (proxyReq, req) => {
      if (req.headers.cookie) {
        proxyReq.setHeader('Cookie', req.headers.cookie)
      }
    },
    onProxyRes: (proxyRes, req, res) => {
      // 如果后端设置了 Set-Cookie,允许前端接收
      proxyRes.headers['access-control-allow-credentials'] = 'true'
      proxyRes.headers['access-control-allow-origin'] = 'http://localhost:5173'
    },
  })
)
  1. WebSocket 代理容易断:比如你有个服务提供 SSE 或 WebSocket 接口,普通 HTTP 代理不行。得单独处理:
import { WebSocketServer } from 'ws'
import { createProxyServer } from 'http-proxy'

const wsProxy = createProxyServer({
  target: 'http://localhost:4002',
  ws: true,
})

app.get('/api/ws/info', (req, res, next) => {
  req.url = '/info' // 重写路径
  wsProxy.web(req, res, next)
})

// 升级处理
app.server.on('upgrade', (req, socket, head) => {
  if (req.url.startsWith('/api/ws')) {
    req.url = req.url.replace('/api/ws', '')
    wsProxy.ws(req, socket, head)
  }
})
  1. 超时和重试机制缺失:微服务之间网络不稳定,建议加一层熔断和超时控制。虽然完整实现要用 Sentinel 或 Resilience4j,但在 BFF 层可以简单做一下:
const TIMEOUT = 5000

function withTimeout(promise, ms) {
  return Promise.race([
    promise,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('Request timeout')), ms)
    ),
  ])
}

// 使用示例
app.get('/api/slow-service', async (req, res) => {
  try {
    const result = await withTimeout(
      fetch('http://localhost:4005/data').then(r => r.json()),
      TIMEOUT
    )
    res.json(result)
  } catch (err) {
    console.error('[Timeout]', err.message)
    res.status(504).json({ error: 'Service timeout, please retry' })
  }
})

高级技巧:动态路由 + 环境隔离

开发、测试、预发环境地址都不一样,别硬编码。我现在的做法是通过环境变量动态加载配置:

// config.js
const ENV = process.env.NODE_ENV || 'development'

const serviceConfig = {
  development: {
    user: 'http://localhost:4001',
    order: 'http://localhost:4002',
  },
  staging: {
    user: 'https://staging.jztheme.com/api-user',
    order: 'https://staging.jztheme.com/api-order',
  },
  production: {
    user: 'https://api.jztheme.com/user',
    order: 'https://api.jztheme.com/order',
  },
}

export const getServices = () => {
  const config = serviceConfig[ENV]
  return {
    '/api/user': config.user,
    '/api/order': config.order,
  }
}

然后在启动时注入:

const services = getServices()

Object.keys(services).forEach((path) => {
  app.use(
    path,
    createProxyMiddleware({
      target: services[path],
      changeOrigin: true,
      pathRewrite: { [^${path}]: '' },
    })
  )
})

这样一来,不同环境下自动切换目标地址,配合 CI/CD 非常顺滑。

部署建议:别忘了压缩和日志

上线前记得加 gzip 压缩和访问日志:

import compression from 'compression'
import morgan from 'morgan'

app.use(compression()) // 启用 gzip
app.use(morgan('dev')) // 日志输出

还有,一定要加健康检查接口:

app.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() })
})

K8s 或 PM2 都靠这个判断服务是否存活。

结语:这不是银弹,但够用

微服务架构下,BFF 层确实解决了很多实际问题。虽然它引入了一个额外节点,增加了延迟(一般也就几毫秒),但从开发效率、调试便利性和安全性来看,这笔账算得过来。

这个方案不是最优的,但最简单。我们团队现在所有新项目都用这套模式,连移动端 H5 也走同一个 BFF。

以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,比如接入 JWT 鉴权、限流、AB 测试路由分流,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论