人人都会AI编程

23.4 服务注册与发现、网关层设计

更新时间:2026-07-11

当我们将一个单体应用拆分为多个独立的微服务后,紧接着会遇到两个棘手的问题:服务实例的网络地址如何被调用方知晓? 以及 外部客户端如何统一、安全地访问这些分散的服务? 这就引出了服务注册与发现,以及 API 网关层的设计。这两个组件是微服务架构从“服务拆分”走向“高可用治理”的关键基础设施。

23.4.1 服务注册与发现

为什么需要注册与发现

微服务运行在容器化环境中,服务实例经常会因为自动扩缩容、故障恢复或滚动更新而发生 IP 地址和端口号的变更。如果使用硬编码的 IP 列表或者静态负载均衡策略,一旦实例发生变动,就需要手动更新配置并重启服务,这在动态环境下是不可接受的。服务注册与发现的核心目标就是 让服务实例的位置信息能够动态维护,调用方始终能获取到当前可用的实例列表

一个典型的注册与发现流程包含三个角色:

  • 服务提供者 在启动时将自身信息(服务名、IP、端口、健康检查地址等)注册到注册中心。
  • 注册中心 存储所有可用服务实例的列表,并通过心跳或主动探测检测实例健康状态,自动剔除异常实例。
  • 服务消费者 从注册中心获取指定服务的可用实例列表,通常配合负载均衡策略选出其中一个实例发起请求。

Node.js 生态中的注册中心选型

市面上有很多成熟的注册中心,Node.js 服务可以通过对应的客户端库进行集成:

  • Consul:Hashicorp 出品,功能完善,提供 HTTP/DNS 接口、健康检查、KV 配置存储。Node.js 客户端可使用 consul npm 包。
  • etcd:CoreOS 开发的分布式 KV 存储,强一致性,适合作为配置中心和注册中心。常用客户端为 etcd3
  • ZooKeeper:Apache 顶级项目,历史悠久,但较重。Node.js 可以使用 node-zookeeper-client
  • Kubernetes Service:如果应用已经容器化并运行在 K8s 中,可以直接使用 Kubernetes 的内置 Service 作为服务发现机制,无需额外部署注册中心。Service 提供的固定 ClusterIP 和 DNS 名称(如 my-service.namespace.svc.cluster.local)天然解决了发现需求。

对于中小规模项目,Consul 功能全面、生态良好,是 Node.js 微服务中很常见的选择。如果团队已经深度使用 Kubernetes,则优先利用 K8s Service,减少维护成本。

Node.js 服务注册示例

假设我们使用 Consul 作为注册中心,一个 Node.js 服务的注册过程大致如下:

const express = require('express');
const consul = require('consul')();

const app = express();
const PORT = process.env.PORT || 3000;
const SERVICE_NAME = 'user-service';
const SERVICE_ID = `${SERVICE_NAME}-${PORT}`;

app.get('/health', (req, res) => res.send('OK'));

app.listen(PORT, () => {
  console.log(`服务启动在端口 ${PORT}`);

  // 注册到 Consul
  consul.agent.service.register({
    name: SERVICE_NAME,
    id: SERVICE_ID,
    address: 'localhost', // 实际应为容器内可访问的地址
    port: PORT,
    check: {
      http: `http://localhost:${PORT}/health`,
      interval: '10s',
      timeout: '5s',
      deregistercriticalserviceafter: '30s', // 取消注册非健康实例
    },
  }, (err) => {
    if (err) throw err;
    console.log('成功注册到 Consul');
  });

  // 优雅退出时注销服务
  process.on('SIGINT', () => {
    consul.agent.service.deregister(SERVICE_ID, () => {
      process.exit();
    });
  });
});

关键点:

  • 注册时指定健康检查的 HTTP 端点,Consul 会定期请求该端点来确认实例存活。
  • 设置 deregistercriticalserviceafter,当健康检查连续失败超过一段时间后,自动从注册中心移除实例。
  • 进程退出时主动取消注册,避免短暂不可用的实例残留。

服务发现的两种模式

服务消费者获取实例列表的方式主要有两种:

  1. 客户端发现:消费者直接查询注册中心,获取实例列表,然后在本地执行负载均衡(例如使用 weightedround-robin)。Node.js 中可以这么做:
   const consul = require('consul')();

   async function getServiceInstances(serviceName) {
     const result = await consul.catalog.service.nodes(serviceName);
     return result.map(node => `http://${node.ServiceAddress}:${node.ServicePort}`);
   }
   // 配合简单的随机负载均衡
   async function callUserService() {
     const instances = await getServiceInstances('user-service');
     const url = instances[Math.floor(Math.random() * instances.length)];
     // 使用 axios 或 fetch 请求 url
   }
   

这种方式简单直观,但要求消费者必须知道注册中心的位置,且带有一层客户端依赖。

  1. 服务端发现:消费者通过一个中间层(通常是负载均衡器)调用服务,由该中间层查询注册中心并转发。例如部署一个 Nginx 或专用的 API 网关,消费者只面向网关请求,无需关心后端实例变化。在 Kubernetes 中,这由 kube-proxy + Service 天然实现。

在实际 Node.js 微服务项目中,如果不希望每个服务都直接访问 Consul,可以采用 服务端发现,将发现逻辑集中在网关层。

23.4.2 网关层设计

网关的定位与核心功能

API 网关是外部客户端访问微服务系统的统一入口。它像是一个“看门人”,负责将请求路由到正确的后端服务,同时在此处实现所有横向关注的公共逻辑,避免每个微服务重复造轮子。

网关应该承担的主要职责包括:

  • 路由转发:根据请求路径、域名、Header 等规则分发到对应的微服务。
  • 认证与鉴权:统一校验 JWT、Session 或 API Key,减轻微服务的鉴权压力。
  • 限流与熔断:使用令牌桶或滑动窗口算法保护后端,防止流量尖峰。
  • 请求聚合:将多个微服务的数据聚合为一个响应,减少客户端请求次数(BFF 模式)。
  • 日志与监控:记录所有请求的访问日志、耗时、状态码,推送至集中监控系统。
  • 跨域处理:统一处理 CORS 头,而非在每个微服务中单独配置。
  • 协议转换:对外提供 RESTful API,对内可能转发 gRPC 或消息队列。

网关技术选型

非 Node.js 方案(稳定、性能极高):

  • Nginx + OpenResty:利用 Lua 脚本扩展,可实现动态路由、限流、认证。配合 lua-resty-consul 等库可直接对接注册中心。
  • Kong:基于 Nginx 的成熟 API 网关,插件市场丰富,支持 Rate Limiting、OAuth2、Prometheus 监控。
  • APISIX:Apache 顶级项目,云原生架构,生态扩展性好,支持多种协议。

Node.js 方案(灵活性高,定制成本低):

  • Express Gateway:以 Express 为核心的 API 网关,基于插件系统,可以快速配置路由、限流、认证。
  • 自建网关:使用 Express 或 Fastify 搭配 http-proxy-middleware 实现简单的反向代理,再逐步添加中间件扩展功能。

在团队以 Node.js 为主的全栈架构中,选择自建网关可以充分利用现有技术栈,方便定制个性化需求。但需要注意,网关作为所有流量的必经之路,性能必须足够高,且不能引入过多同步阻塞逻辑。

自建 Node.js 网关示例

下面是一个基于 Express 和 http-proxy-middleware 的动态路由网关,与 Consul 集成实现服务发现:

const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const consul = require('consul')();
const app = express();

// 简单的内存缓存:服务名 -> 实例列表
const serviceCache = new Map();

async function getServiceUrl(serviceName) {
  // 如果缓存有效,直接返回随机实例
  const cached = serviceCache.get(serviceName);
  if (cached && cached.expireAt > Date.now()) {
    const instances = cached.instances;
    return instances[Math.floor(Math.random() * instances.length)];
  }

  // 从 Consul 获取健康实例
  const result = await consul.health.service({ service: serviceName, passing: true });
  const instances = result.map(entry => `http://${entry.Service.Address}:${entry.Service.Port}`);
  serviceCache.set(serviceName, { instances, expireAt: Date.now() + 10 * 1000 }); // 10s 缓存
  if (instances.length === 0) throw new Error('无可用服务实例');
  return instances[Math.floor(Math.random() * instances.length)];
}

// 动态路由中间件
app.use('/api/:serviceName', async (req, res, next) => {
  try {
    const targetUrl = await getServiceUrl(req.params.serviceName);
    // 创建代理中间件并执行
    const proxy = createProxyMiddleware({
      target: targetUrl,
      changeOrigin: true,
      pathRewrite: { [`^/api/${req.params.serviceName}`]: '' },
    });
    proxy(req, res, next);
  } catch (err) {
    res.status(502).json({ error: '服务不可用' });
  }
});

// 通用中间件:认证(示例简单地检查 Header)
app.use((req, res, next) => {
  const token = req.headers.authorization;
  if (!token || !token.startsWith('Bearer ')) {
    return res.status(401).json({ error: '未授权' });
  }
  // 此处可验证 JWT 并解析用户信息
  next();
});

// 请求日志
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    console.log(`${req.method} ${req.path} ${res.statusCode} ${Date.now() - start}ms`);
  });
  next();
});

app.listen(8080, () => console.log('API 网关启动在 8080'));

这个简易网关实现了:

  • 路径约定:前端通过 /api/user-service/users 的方式访问用户服务。
  • 动态服务发现:从 Consul 获取健康实例,并对转发目标进行简单随机负载均衡。
  • 轻量缓存:避免每次请求都查询 Consul,缓存 10 秒。
  • 集成认证和日志:所有通过网关的流量统一进行安全校验和访问记录。

网关层的高级考量

高可用与性能

  • 网关本身需要多实例部署,前面再挂上负载均衡器(如 Nginx 或云平台的 SLB)。
  • Node.js 网关应该避免在主线程执行 CPU 密集计算或同步阻塞操作,认证可异步调用内部服务验证 Token。
  • 对于超高并发场景,可以考虑在网关前引入 OpenResty 或 Envoy 做前置转发,Node.js 仅做轻量的路由与聚合。

与注册中心的深度结合

网关是连接外部世界和内部服务群的枢纽,必须实时感知后端实例的变化。除了上述基于轮询缓存的简单实现,更优雅的做法是 订阅注册中心的服务变更事件,避免额外的轮询延迟。Consul 提供了 Watch 端点,可以将变更推送至网关:

const watcher = consul.watch({ method: consul.health.service, options: { service: 'user-service' } });
watcher.on('change', (nodes) => {
  // 更新本地路由表
});
watcher.on('error', (err) => console.error('Watch 错误', err));

这样一来,后端实例上下线后几乎可以实时反映到网关路由表中,大幅缩短故障转移时间。

配置中心与动态路由

网关的路由规则(路径映射、限流参数等)不应该硬编码在代码里,而应该通过配置中心动态下发。例如,将路由规则存储在 Consul KV 或 etcd 中,网关定期拉取或监听变更,实现零停机更新路由策略。这种方式让网关变得更加灵活,可以随时上线新的微服务或更改限流策略,无需重启网关进程。

安全与限流

网关是所有流量的第一道防线,必须内置足够的安全策略:

  • 利用 express-rate-limitrate-limiter-flexible(支持 Redis 存储)实现全局限流和针对特定用户的限流。
  • 对传入的 Token 或 Cookie 进行 JWT 验证,并把解析出的用户信息通过 Header 传递给下游微服务,如 req.headers['x-user-id']
  • 防止请求头注入,过滤敏感路径,配置 CORS 白名单。

23.4.3 注册、发现及网关的实战组合

在真实的 Node.js 微服务项目中,一个典型的组合是:

  • 服务注册:每个 Node.js 服务在启动时向 Consul 注册,并暴露 /health 端点。
  • 服务发现:API 网关通过 Consul Watch 机制或定期拉取维护最新的服务路由表。
  • 网关层:承担统一入口,进行 JWT 认证、限流、日志、跨域处理,并根据路由表将请求代理到对应的后端微服务。
  • 负载均衡:由网关在多个实例间做客户端负载均衡(基于随机或轮询),或由 Kubernetes Service 自动完成。

这样的架构使得后端的每个微服务只需关注自身业务逻辑,而公共关注点全部沉淀在网关和注册中心中,极大简化了微服务治理的复杂度。

需要注意的是,随着微服务数量的增长,网关可能逐渐成为单点瓶颈。因此,生产环境中的网关必须支持水平扩展,并做好监控、降级和熔断预案,保障整体系统的稳定性。在 Node.js 技术栈中,我们完全可以用轻量的 Express 或 Fastify 构建出这样的网关,而不必依赖重型中间件,印证了 Node.js 在微服务中间层灵活、高效的优势。