微服务入门
本教程共 76 篇 · 第 72 篇 · 更新于 2026-07-25 · 约 9 分钟阅读
72. 微服务入门
本节目标:- 单体到微服务的取舍,别为了拆而拆 - 拆分的几个原则(单一职责、领域边界、数据自治) - 服务间通信的两条路:同步(REST/gRPC)和异步(消息队列/事件) - 几个保命的容错模式(熔断、重试、超时) 先泼盆冷水:微服务不是银弹,它用「运维复杂度」换「开发部署灵活度」。很多团队微服务没玩明白,反而被分布式的一堆问题搞崩。所以第一原则——能单体就先单体,等到真痛了再拆。
你一开始写的多半是「单体应用(Monolith)」——所有功能塞在一个项目里,一个进程跑全部。这没毛病,小团队小项目这么干最快。但当代码涨到几万行、十几个同事改同一个仓库、每次发布都要全量回归,单体就开始反噬了。微服务(Microservices)是把系统按业务能力拆成一堆小服务,各自独立开发、部署、扩容。
这一章不堆概念,讲两件事:什么时候该拆、拆完之后服务怎么互相说话。
什么时候该拆
这几个信号出现,说明单体开始撑不住了:
- 一个改动要改十几个模块,测试和发布越来越慢。
- 不同功能对资源需求差异大(比如图片处理吃 CPU、接口服务吃 IO),却只能一起扩容。
- 团队大了,几十个人改一个仓库,天天冲突、互相阻塞。
- 某个模块总崩,一崩把整个站点带走。
Note「先单体,后微服务」是很多成熟团队的路径。 even Amazon、Netflix 也不是一开始就微服务。先把边界在代码里划清楚(模块化),等真的需要物理隔离时再切出去,风险小得多。
拆分原则
单一职责
每个服务只干一件事,对应一个明确的业务能力。比如电商拆成:用户服务、商品服务、订单服务、支付服务。别把一个服务既管用户又管订单——那是又造了个迷你单体。
按领域边界(Bounded Context)拆
这是领域驱动设计(DDD)里的词,意思是「同一个词在不同业务语境里含义不同」。比如「订单」在订单服务里是交易凭证,在物流服务里是待配送货物。拆的时候按这种业务边界切,比按技术层(controller 层、service 层)切合理得多。
数据自治
每个服务管自己的数据库,别让多个服务直接共享同一张表。谁的数据谁负责,别的服务要数据就走接口或事件。这是微服务最难但也最关键的一点——共享数据库是「分布式单体」的典型症状。
Warning最常见的错误:服务是拆开了,数据库还是共用一张大表。结果改 A 服务的表结构,B 服务立刻炸。这不叫微服务,叫「换了包装的单体」,复杂度白涨。
独立部署
拆出来的服务必须能单独上线,不影响其他服务。如果你的「订单服务 v2」上线,必须等「用户服务」一起发版,那拆分就是失败的。
服务间怎么通信
拆开之后,服务 A 要调服务 B,消息怎么传?两条路。
同步通信:直接调用
服务 A 发请求、等服务 B 立刻回结果,像函数调用一样。
REST:最简单,HTTP + JSON。Node 里用 fetch(v24 内置)或 axios 调:
// order-service 调用 user-service
const res = await fetch(`http://user-service:3001/users/${userId}`);
if (!res.ok) throw new Error(`user-service ${res.status}`);
const user = await res.json();
gRPC:高性能,用 Protocol Buffers 序列化,适合内部服务高频调用。比 JSON 快、类型强,但接入成本也高。
同步的好处是「写完就能拿到结果」,逻辑直观。坏处是直接耦合:user-service 挂了或慢了,order-service 也跟着卡,容易「雪崩」。
异步通信:通过消息
服务 A 发一条消息到消息队列/事件总线,不等结果,继续干自己的活;服务 B 有空了再来消费。两者不直接认识对方。
用 RabbitMQ(消息队列)发事件:
import amqplib from 'amqplib';
const conn = await amqplib.connect(process.env.AMQP_URL);
const channel = await conn.createChannel();
await channel.assertExchange('order_events', 'topic', { durable: true });
// 订单创建后,发个事件出去,通知、库存等各自消费
channel.publish(
'order_events',
'order.created',
Buffer.from(JSON.stringify({ orderId, userId }))
);
下游的「通知服务」消费这个事件,发邮件、发短信:
const queue = 'notification_orders';
await channel.assertQueue(queue, { durable: true });
await channel.bindQueue(queue, 'order_events', 'order.created');
channel.consume(queue, (msg) => {
const order = JSON.parse(msg.content.toString());
console.log(`发确认邮件:订单 ${order.orderId}`);
channel.ack(msg);
});
异步的好处是彻底解耦:下单服务根本不知道有谁在监听。通知挂了不影响下单,恢复后还能接着处理积压的消息。代价是业务流程变「最终一致」——数据不会瞬间全部对上,得接受短暂不一致。
Tip经验法则:能异步就异步。只有「我必须现在拿到结果才能继续」的请求(比如校验库存够不够才能下单)才用同步。其余的(发通知、记日志、算积分)都适合丢到消息队列里。
容错:别让一个服务拖垮全家
分布式系统里,「依赖的服务会挂」是常态而非意外。几个必备模式:
超时(Timeout):调用别无限等。设个上限,到点就放弃,别让连接池被慢服务占满。
重试 + 退避(Retry with Backoff):网络抖一下导致的失败可以重试,但别马上猛重试——用「指数退避」(1 秒、2 秒、4 秒)错开,给对端喘息时间。
熔断(Circuit Breaker):像电路保险丝。某服务连续失败到阈值,就「跳闸」,一段时间内直接拒绝请求、走降级逻辑,不再傻乎乎地狂打已经挂掉的服务,等它恢复再慢慢放量。Node 里用 opossum 库:
import CircuitBreaker from 'opossum';
const breaker = new CircuitBreaker(
(userId) => fetch(`http://user-service:3001/users/${userId}`).then(r => r.json()),
{ timeout: 3000, errorThresholdPercentage: 50, resetTimeout: 10000 }
);
breaker.on('open', () => console.warn('熔断开启:user-service 疑似不可用'));
const user = await breaker
.fire(userId)
.catch(() => ({ id: userId, name: '用户信息服务暂不可用' }));
breaker.fire() 失败时走 .catch 降级,返回兜底数据,至少接口不崩。
Warning熔断、重试、超时必须配合使用。只重试不超时,连接会堆积;只超时不熔断,下游一挂上游照样被打满。这三个是「分布式系统保命三件套」。
API 网关:统一的大门
服务一多,客户端不可能记住十几个地址去分别调用。通常会放一个 API 网关(API Gateway)在前面:客户端只认网关一个入口,网关负责把请求路由到对应服务、做认证、限流、协议转换。
生产环境可以用成熟的 Kong、APISIX,或者云平台托管的网关。自己用 Node 也能快速搭一个(基于 http-proxy-middleware 做反向代理转发),但真上规模还是交给专业组件更稳。
Note网关不是必须的起点。你在单体阶段用 Nginx 反代(第 69 章)就够了。等服务数量涨上来、客户端开始抱怨「接口太碎」,再引入网关不迟。
什么时候不该上微服务
讲了这么多好处,也得泼清楚冷水。下面这些情况,我建议先忍住别拆:
- 团队就两三个人:微服务带来的运维、监控、部署成本,三个人根本摊不起。单体跑得挺好,硬拆反而没人维护。
- 业务还在快速试错:方向天天变,今天拆的边界明天就作废。等模式稳定了再动刀。
- 还没搞定可观测性和自动化部署:第 71 章的监控、第 69 章的部署都没建好,就别急着拆。微服务把「一个问题变十个问题」,没有可观测性你会瞎。
- 数据一致性要求极高:跨服务的分布式事务天生比单体里的本地事务难。强一致场景(比如账务核心)要特别谨慎。
一句话:微服务是「组织规模和技术复杂度逼出来的」,不是「看起来高级所以要用」。
同步通信再细说
前面提到 REST 和 gRPC。补充一点选型直觉:
- REST/JSON:人类可读、调试方便、生态成熟。缺点是把对象序列化成 JSON 再解析有开销,且接口契约靠文档约束,容易漂移。
- gRPC:基于 HTTP/2 + Protocol Buffers,二进制序列化,速度快、类型强(
.proto文件就是契约)。适合内部高频、对性能敏感的服务间调用。缺点是要学一套工具链,浏览器不能直接调。
Node 里用 gRPC 大致长这样(定义好 .proto 后):
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
const packageDef = protoLoader.loadSync('user.proto');
const { UserService } = grpc.loadPackageDefinition(packageDef);
const client = new UserService(
'user-service:50051',
grpc.credentials.createInsecure()
);
// 调用远程方法,和调本地异步函数很像
const { user } = await new Promise((resolve, reject) => {
client.GetUser({ id: userId }, (err, res) => err ? reject(err) : resolve(res));
});
选 REST 还是 gRPC,看你是更在意「开发顺手」还是「运行性能」。多数中小团队用 REST 就够了,别过早优化。
服务发现
服务实例的地址会变(特别是容器环境下 IP 不固定)。「服务发现」就是让服务能动态找到彼此,而不是把地址硬编码。Kubernetes 自带基于 DNS 的服务发现(服务名即地址),所以如果你跑在 K8s 上,直接用服务名通信就行,不用额外引入 Consul、etcd 那一套。