去年我们把一个运营了五年的单体应用拆成了十二个微服务。拆分前,下单接口平均响应180毫秒;拆分后,同样的接口,P99直接飙到1.2秒。团队一度怀疑是不是拆错了。复盘了两个月,终于把响应时间压回200毫秒以内。这篇文章把踩过的坑和优化过程完整还原,如果你也正被微服务性能问题困扰,应该能少走不少弯路。
第一个坑:把本地方法调用变成了网络调用
单体时代,订单服务调用库存服务,就是一行inventoryService.deduct(),纳秒级。拆成微服务后,这行代码变成了HTTP请求,即使在内网,一次往返也要5-10毫秒。更致命的是,一个下单流程原本有七八个内部调用,全部变成远程调用后,光是网络开销就叠加上百毫秒。
解法:合并粗粒度接口。原来查订单要调用户、商品、库存三个服务,现在订单服务直接聚合,对外只暴露一个接口。跨服务的调用次数从七次降到两次,响应时间立刻砍掉一半。
第二个坑:序列化和反序列化的隐形开销
单体时代,对象在JVM内直接传递,没有序列化。微服务之间用JSON传输,一个订单对象序列化要1毫秒,反序列化再1毫秒,十次调用就是20毫秒。如果对象嵌套深、字段多,开销更大。
解法:换用Protobuf或Avro。我们后来把核心链路改成gRPC+Protobuf,序列化开销降到原来的十分之一。非核心链路继续用JSON,但严格控制字段数量,禁止返回大对象。
第三个坑:数据库拆分后的跨库查询
原来一个库,订单和用户表JOIN一下就行。拆开后,用户数据在用户服务,订单服务只能先查订单,再调用户服务拿用户信息。一次JOIN变成了两次查询加一次网络调用。更糟的是,如果还要查商品信息,又得多调一次。
解法:数据冗余。在订单表里冗余用户名和商品快照,下单时一次性写入。查询时不需要跨服务,直接从订单表读。代价是数据一致性,但订单场景下,快照本身就是业务需求,冗余反而更合理。
第四个坑:服务发现和负载均衡的额外跳转
微服务架构下,每次调用都要经过服务发现(如Nacos、Consul)拿到实例列表,再经过负载均衡选一个节点。如果用了API网关,还多一层转发。这些跳转在单体时代完全不存在。
解法:客户端负载均衡+本地缓存。用Ribbon或Spring Cloud LoadBalancer,实例列表缓存在本地,避免每次调用都查注册中心。网关只做鉴权和路由,不做业务聚合,减少一跳。
第五个坑:分布式事务拖慢响应
拆分后,下单要同时扣库存、创建订单、扣优惠券,三个服务需要保证一致性。我们一开始用了Seata的AT模式,结果每次下单要等全局锁,响应时间直接翻倍。
解法:改最终一致性。下单先写本地订单表,发消息到MQ,库存和优惠券异步消费。前端先返回“下单处理中”,几秒后刷新状态。用户体验没有明显下降,但响应时间从800毫秒降到了120毫秒。
第六个坑:链路追踪和日志拖后腿
拆成微服务后,每个服务都打日志、上报监控。如果日志是同步写磁盘,或者链路追踪采样率过高,都会拖慢响应。我们曾经因为Zipkin全量采样,导致每个请求多出30毫秒。
解法:日志改异步Appender,链路追踪采样率降到5%,错误请求100%采集。生产环境关闭DEBUG级别,只保留WARN和ERROR。
总结
微服务拆分后响应变长,本质是把单体内部的“免费”调用变成了“付费”的网络调用。每一跳都有成本,叠加起来就非常可观。优化的核心思路只有一条:减少远程调用次数,能聚合就聚合,能异步就异步,能冗余就冗余。微服务不是性能银弹,它用响应时间换取了可扩展性和团队自治。如果业务量不大、团队规模小,单体反而是更优解。拆分之前,先算清楚这笔账。