微服务拆分后接口从 800ms 降到 120ms:我劝你先别上全套微服务

🔑 关键词:微服务拆分,Nacos,Sentinel,分布式事务,SLO

📖 摘要:从一次订单接口 P99 2.3s 的事故讲起,给出微服务拆分判断表、Outbox 最终一致、Nacos/Sentinel/Feign/Hikari/K8s 参数,以及我为什么把用户服务合回单体。

微服务拆分后接口从 800ms 降到 120ms:我劝你先别上全套微服务

图片

我,老周,写 Java 第 11 年。2023 年双 11 前,我们订单接口 P99 干到 2.3s,老板在群里 @ 我三次。当时单体是 Spring Boot 2.7 + MySQL 8.0.32,订单表 2.3 亿行,日订单 47 万,团队 9 个人,CI 一次 18 分钟。我们拍脑袋拆了订单、库存、支付、用户四个服务,结果第一周故障 5 次,两次是 Nacos 心跳超时,一次是 Feign 默认 readTimeout 60s 把 Tomcat 200 个线程打满。后来我把参数一个个拧回来,P99 从 2.3s 到 120ms,但结论很反直觉:不是所有服务都值得拆。

先算账,再看别人拆不拆

图片

很多文章一上来讲 CAP、DDD、六边形,我建议你先打开 MySQL 和 APM 看四张表:

  1. 表大小。SELECT table_name, table_rows, data_length/1024/1024 AS mb FROM information_schema.tables WHERE table_schema='order_db' ORDER BY data_length DESC LIMIT 20;。订单表 2.3 亿行、87GB,这种表不管在单体还是微服务里都是麻烦,拆分不会自动解决。
  2. 慢查询。pt-query-digest slow.log --limit 20,我们 TOP1 是 select * from order where user_id=? order by created_at desc limit 20,缺 (user_id, created_at) 联合索引,扫描 120 万行。
  3. 写频率。用 SHOW BINLOG EVENTS 或 canal 统计,订单 1.8k QPS 峰值,库存 700 QPS,用户 120 QPS。用户服务写频率低、事务边界短,拆出去收益很小。
  4. 发布冲突。过去 90 天,订单模块和营销模块同时改 order 包 37 次,CI 排队 18 分钟。这个才是拆分的硬理由,不是简历理由。

图片

判断标准我自己用 6 个指标:团队超过 30 人、每周发布超过 2 次、两个模块独立扩缩容、数据一致性可接受最终一致、故障隔离收益大于分布式成本、领域边界能画清楚。6 个里少于 3 个,先做模块化单体。我们后来把用户服务合回单体,只留订单、库存、支付三个,CI 从 18 分钟降到 9 分钟,故障从每月 5 次到 1 次。

拆分落地 7 步,别先建注册中心

第 1 步,先按 schema 隔离,不急着拆进程。订单库、库存库、支付库分三个 schema,禁止跨库 join。 第 2 步,把跨模块调用改成模块内接口,比如 OrderFacade,只在 order 模块暴露。 第 3 步,建领域事件表 domain_eventid, biz_id, event_type, payload, status, retry_count, next_retry_at, created_at,唯一键 uk_biz_event(biz_id,event_type)。 第 4 步,把同步调用改成 Outbox + Kafka。订单创建先写本地订单和 outbox,定时任务每 500ms 扫 status=0 limit 200,发 Kafka,成功后 status=1;失败 retry_count+1,退避 1s、2s、4s、8s、16s,5 次进死信。 第 5 步,消费端幂等表:event_id, consumer_group, created_at,唯一键 uk_event_consumer(event_id,consumer_group)。没有这个,Kafka 至少一次投递会把你打哭。 第 6 步,加 Nacos 2.2.3 和 Sentinel 1.8.6。Nacos 临时实例 ephemeral=true,心跳 5s,健康检查 15s,命名空间按 dev/test/prod 隔离。Sentinel 流控 QPS 500,熔断慢调用 RT>500ms,比例阈值 0.5,时间窗口 10s,最小请求数 10。 第 7 步,Feign 调参:connectTimeout=1000readTimeout=3000maxConnections=500maxConnectionsPerHost=200。HikariCP maximumPoolSize=20connectionTimeout=3000validationTimeout=1000idleTimeout=600000maxLifetime=1800000。Tomcat maxThreads=200acceptCount=100maxConnections=8192

图片

分布式事务:Seata AT 不是默认答案

我们试过 Seata 1.7.1 AT 模式,订单扣库存,全局锁冲突一上来,P99 从 180ms 拉到 900ms。AT 适合短事务、强一致、并发不高,比如后台调账。资金类可以用 TCC,但开发成本大约是普通接口 3 倍,还要处理空回滚、悬挂、幂等。 更稳的是本地消息表 + 最终一致。订单状态机:INIT -> PAID -> DELIVERED -> FINISHED,库存服务收到 ORDER_PAID 后扣减,失败就重试,不阻塞主链路。对账任务每小时跑一次,比对订单支付金额和库存扣减流水,差异写 reconcile_diff。 如果非要用 Seata,至少把 undo_log 表放独立库,全局事务超时别用默认 60s,改成 10s;client.rm.report.success.enable=false 可以少一次上报,但排查会难。别问我怎么知道的。

图片

可观测和 K8s,不配就是盲拆

OpenTelemetry Java agent 1.30.0 挂上:OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317OTEL_TRACES_SAMPLER=parentbased_traceidratioOTEL_TRACES_SAMPLER_ARG=0.1。W3C traceparent 要透传,Kafka 消息头里带 trace_id,不然异步链路断成两截。 Prometheus 抓 /actuator/prometheus,Grafana 看 P99、错误率、饱和度。SLO 定 99.9%,月度错误预算 43m12s。告警只留三条:P99>500ms 持续 5m、错误率>1% 持续 2m、Kafka lag>10000 持续 5m。其他先关掉,不然告警群没人看。 K8s 资源:replicas=3requests.cpu=500mrequests.memory=512Milimits.cpu=1000mlimits.memory=768Mi,JVM -Xms512m -Xmx512m -XX:MaxMetaspaceSize=192m。liveness initialDelaySeconds=40 periodSeconds=10 failureThreshold=3,readiness initialDelaySeconds=20 periodSeconds=5 failureThreshold=2。Kafka acks=allretries=5enable.idempotence=truemin.insync.replicas=2replication.factor=3max.in.flight.requests.per.connection=1 保序。Redis 7.0.12 缓存空值 60s,TTL 加 300-600s 随机,防雪崩。

图片

我的观点:微服务是组织手术,不是技术升级

如果你 9 个人、日订单 50 万、CI 18 分钟,先把单体拆成模块,别急着拆进程。模块化单体 + schema 隔离 + 事件表,能拿到 70% 的好处,成本只有 20%。我们合回用户服务后,故障少了,发布没变慢,因为真正冲突的是订单和营销,不是用户。 拆之前算三笔账:一次跨服务调用多 2-5ms,一次分布式事务多 20-80ms,一次故障排查多 2 小时。如果省下的编译时间、发布风险、扩缩容成本覆盖不了,就先别拆。微服务不是目标,它只是团队变大后不得不做的妥协。你先把边界画清楚,再谈 Nacos、Sentinel、Seata,顺序反了,参数调得再漂亮也白搭。