那一年大促,仓库系统到晚上八点突然“卡死”。不是慢,是彻底不响应。WCS的呼叫队列堵了三千多条,AGV不派活,拣货台全部亮红灯。我盯着监控面板,数据库连接池被打满,batch任务全卡在同一个表锁上,而运营群里已经在喊“客服电话被打爆”。
那一晚之后,我们把“WMS系统演进”从PPT里搬到了代码里。后来花了将近一年时间,把一套单体WMS拆成了十几个微服务。整个过程踩坑无数,也走了一些弯路。这篇文章就当是复盘笔记,把拆分的边界、演进路线、技术选型和那些最容易翻车的点都摊开讲清楚。
1. 单体WMS的黄金年代与崩溃临界点
1.1 一套典型单体WMS的模块构成
绝大多数仓储管理系统,不管用什么语言写的,从业务模块上看都逃不出这几块:
- 入库管理:ASN预约、收货、质检、上架指令
- 出库管理:订单接收、波次生成、拣货任务分配、复核、打包、称重、运单对接
- 库存管理:可用库存、冻结库存、在途库存、锁定库存、库存流水
- 库内作业:盘点、移库、补货、容器管理
- 基础资料:物料档案、库区库位、容器类型、计量单位、客户信息
- 报表统计:库存报表、作业效率报表、KPI看板
单体架构下,这些模块基本都在同一个应用里,模块之间直接方法调用,共享一个数据库,或者最多加上几个Redis缓存。初期开发确实快,两三周就能出一版能用的系统,业务逻辑都在一个事务里,改起来也方便。
我见过很多WMS厂商,一套Java单体或PHP单体跑了五六年,客户最多二三十个仓库,单仓单量一天几千单,真的绰绰有余。这个阶段去谈微服务,纯属给自己找事。
1.2 压垮单体的三根稻草
单体WMS什么时候开始撑不住?我复盘下来,基本是三个信号同时出现:
第一,数据库连接池和锁竞争成为瓶颈。WMS和普通电商后台不一样,它有大量高频写操作,尤其是库存扣减、库存流水插入。大促时,几千个拣货任务同时完成,几十个线程同时去更新同一批SKU的库存记录,行锁竞争直接把数据库拖死。更头疼的是batch任务,比如波次自动释放、库存周转分析,这些定时任务一旦某个SQL写得不好,全表扫描加上长时间锁表,整套系统的接口全部连带变慢。
第二,发布和回归成本失控。单体应用改一行代码,就得整包重新发布。WMS又是个强流程系统,改出货逻辑很有可能影响收货模块,因为共用了一套库存表。每次发版,测试团队要在十几个仓库的账目场景里做全量回归,发版窗口越拉越长,出了线上问题回滚更是两头难。
第三,无法按资源维度独立扩容。大促期间,入库预约和出库波次的负载模型完全不一样,入库是早晨集中到货,出库是下午到晚上持续高位。单体架构下,不管哪块压力大,都只能把整个应用水平扩容,扛不住的那一个模块会拖累所有模块。数据库连接池配置是写死的,扩再多实例,连接池总量还是那些,反而因为多个实例争抢连接,问题更严重。
这三个信号凑齐了,基本就说明单体到了崩溃临界点。我们当时最典型的场景就是:大促前一周,所有仓库的数据要提前做库存快照,报表查询和实时作业抢同一个库,连正常的PDA扫描枪响应都变得一顿一顿的。
1.3 别急着拆,先想清楚WMS到底要不要微服务化
说句泼冷水的话,不是所有WMS都需要微服务。如果你的系统是给单仓小体量业务用的,一天几千单,三五年内看不到大规模增长,那单体不仅够用,而且是最优解。微服务的前提,是痛点已经具体到单体解决不了,而不是“因为大家都在用微服务”。
我们决定拆,不是为了技术时髦,而是因为当时已经接了三个大客户,每个客户都是多仓多区域的复杂网络,单仓峰值单量到了五万单/天。而且我们有明确的多仓独立部署、多客户隔离、波次算法按客户定制这些需求。这些在单体里不是不能做,但改起来极其痛苦,每次都是给A客户改了算法,B客户跟着出bug。
如果你判断不清,我建议你先回答三个问题:
- 是否有某个模块需要独立扩缩容?
- 是否有某些业务逻辑需要按客户/仓库做隔离定制?
- 是否有多团队并行开发、各自交付的协作需求?
三个里面至少占两个,才有值得拆的基础。否则先别动,把单体的慢SQL、大事务、索引优化做好,性价比高得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 服务拆分的边界:哪些能拆、哪些打死不能拆
2.1 先按业务域拆,别按技术层次拆
很多团队做微服务拆分,第一反应是按Controller、Service、DAO这种技术层次拆,或者按“基础服务”“业务服务”这种模糊维度拆,最后拆出来的服务之间还是一坨纠缠的关系,互相调来调去,比单体还难维护。
正确姿势是围绕业务能力拆,也就是DDD里说的限界上下文。WMS这种系统,我最终拆出来的服务边界是这样的:
| 服务名 | 核心业务能力 | 独立数据表范围 |
|---|---|---|
| 主数据服务 | 物料、库区库位、容器、客户、地址簿 | 基础资料表 |
| 入库服务 | ASN预约、收货、质检、上架指令 | 收货单、质检单、上架任务 |
| 出库服务 | 订单接收、波次生成、拣货分配、复核打包 | 订单、波次、拣货任务 |
| 库存服务 | 库存账实、冻结/锁定/在途、库存流水 | 库存表、库存流水表 |
| 调度服务 | WCS对接、设备任务下发、AGV/输送线交互 | 任务队列、设备状态 |
| 报表统计服务 | 经营报表、作业效率看板 | 汇总宽表、报表快照 |
这套边界最重要的原则是:库存服务单独拆出来,并且是全系统的核心。因为所有其他模块都要读写库存,收货要增加可用库存,出库要扣减库存,盘点要冻结库存再调整,这个模块的稳定性和一致性直接决定整个WMS能不能用。
2.2 库存这类的“强一致地带”,必须集中管控
拆成微服务后,最大的麻烦就是跨服务的数据一致性问题。所以拆分的时候就要想清楚:哪些业务必须放在同一个服务、同一个数据库事务里,绝对不能因为追求服务化就强行拆开。
以库存为例。库存在账扣减这件事,涉及三个核心动作:
- 校验可用库存是否充足
- 扣减库存并写入库存流水
- 记录预占/锁定的明细
这三个动作如果拆到三个服务里,任何一步失败了,都可能导致超卖、库存错账。这种场景必须留在库存服务内部,用本地数据库事务保证强一致。
还有一个容易踩坑的边界:波次分配和库存扣减。波次生成时要从库存池里锁定库存,而实际出库时还要再扣减一次。如果把波次放到出库服务、库存锁定放到库存服务,就会变成跨服务的两阶段操作。我们当时的解法是,波次的库存预占通过库存服务提供的预占接口完成,并且用“预占单”把状态持久化,后续出库扣减直接关联预占单。这样即使流程中断,也有一张预占单可以查账、释放。
2.3 数据归属和“数据所有权”的实操原则
微服务拆分有一句烂大街的话叫“每个服务只拥有自己的数据”,但落地上最难的就是数据归属怎么划分。
我总结的三条实操原则:
- 谁产生这份数据,谁拥有它。 库存流水是由库存变动产生的,归库存服务;波次记录是出库流程产生的,归出库服务。
- 别的服务需要这份数据,只能通过接口或消息获取,不能直连数据库。 一开始我们为了省事,报表服务直连了订单表的数据库,后来订单表结构一改,报表直接崩,被迫重新接接口。
- 对于需要在多个服务间复用的基础数据(物料档案、库位信息),用“本地快照+变更通知”的方式做副本,不要每次远程调用实时拉取。 比如拣货任务里需要带物料名称和规格,这一份数据在出库服务本地存一份物料的精简副本,主数据服务变更时发一个MQ消息,出库服务更新本地副本。这样既解耦了实时依赖,又不会出现显示延迟半天的情况。
数据归属的理解决定了后续所有改造能不能顺利。拆服务说白了是拆数据权限,数据边界没想清楚就动手,后面一定会反反复复“拆了又合”。
3. 演进路线:从第一行代码到平稳切换
3.1 绞杀者模式:不要搞“推倒重来”
微服务改造最大的工程风险是重写。单体WMS里的业务逻辑,很多都是几年迭代积累下来的,各种边界条件、历史兼容、客户定制,代码写得很丑,但它是对的。直接推倒重来,基本等于自杀。
我采用的是绞杀者模式(Strangler Pattern),一句话讲:让新服务在老系统旁边慢慢长出来,通过网关逐步切换流量,直到老系统被“绞杀”替换干净。
落地顺序我们是这样排的:
- 先搭好Nacos注册中心、SpringCloud Gateway网关、统一监控这些基础设施。
- 把最独立、依赖最少的模块先拆出去:基础资料服务、报表统计服务。
- 把库存服务拆出来,这是最难也最核心的,要花大力气做数据迁移和双写校验。
- 最后拆出库、入库、调度这些强流程模块。
每一步的发布时间点,都要求老功能继续可用,新的客户端逐步接入网关,老的接口保留。
3.2 双写与数据迁移:别让库存账对不上
拆分服务,最难的不是代码,是数据。尤其是库存、订单这种核心表,从老数据库迁到新服务独立的库,必须做到对账无误。
我采用的做法是“双写+对账”两步走:
- 代码层面,改造老模块的写操作,让它在写老库的同时,通过MQ发出一条数据变更消息,新服务消费消息后写入自己的库。
- 启动一个定时对账任务,每小时同步一次老库和新库的数据差异,发现不一致就告警并自动修正。
双写阶段大概持续了两周。以库存表为例,每天几百万条流水同步,对账出现差异的次数其实很少,大部分是因为老代码里有些直接UPDATE库存的SQL没有走我们改造过的Service层。所以双写的前提是:把所有的库存写操作都收敛到同一个出口,如果老系统里到处是直接写库的野SQL,你根本没法定向同步。
这里要特别强调一点:双写期间,老系统的代码冻结,只做bug修复,不允许新增正常功能。如果一边迁移一边加需求,双写逻辑会越补越破,最后变成没完没了的历史包袱。
3.3 网关层的兼容路由策略
流量切换这块,我建议用“URL前缀+灰度标识”双维度路由,而不是一把梭地把所有流量切到新服务。
具体思路是这样:
- 老客户端调用的接口路径不变,比如
/api/order/create,网关先接住。 - 网关根据规则,判断这个请求走老单体还是新出库服务。最初阶段所有请求走老单体,新服务只接收MQ消息和内部调用。
- 当新服务功能稳定后,按仓库维度灰度,比如华东仓的订单请求切到新服务,华南仓继续走老单体。灰度仓库的选择标准是业务量小的、愿意配合测试的。
- 等所有仓库灰度完毕,再把老单体的接口下线。
网关路由的匹配规则不能写死在代码里,要放到Nacos配置中心动态下发。因为灰度比例和仓库名单随时会变,每次改都要重新发版的话,运维会疯的。
另外,不管网关怎么路由,都要做好请求日志的全量记录。没有日志,后面一旦出现两边数据不一致,排查起来会非常痛苦。我们当时在网关层统一打印了请求参数、响应体、路由目标、耗时四个字段,为排查立了大功。
4. SpringCloud+Nacos落地实战:基础设施选型和代码要点
4.1 注册中心与配置中心:为什么选Nacos
微服务基础设施里,最基础的就是注册中心和配置中心。当时对比了Eureka、Consul、Nacos,最后选了Nacos。
原因很实际:
- Nacos同时提供注册中心和配置中心,部署一套就够,Eureka还得多搭Config Server。
- Nacos的控制台对运维友好,服务列表、上下线、配置灰度、命名空间管理一目了然。
- 支持namespace隔离环境(dev/test/prod),也支持group隔离业务域,这对多客户部署很关键。
- 社区活跃,遇到问题好搜解决方案。
如果你用的不是Java技术栈,可以选Consul或者etcd,但SpringCloud全家桶场景下,Nacos基本是最顺手的选择。
注册中心配置很简单,核心就一个服务注册依赖:
xml复制<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
应用配置文件里,关键配置项是这三个:
yaml复制spring:
cloud:
nacos:
discovery:
server-addr: nacos-prod:8848
namespace: prod_wms
group: WMS_GROUP
register-enabled: true
config:
server-addr: nacos-prod:8848
namespace: prod_wms
file-extension: yaml
这里要提醒一下namespace和group的使用。我们按环境建了namespace,按服务域建了group。环境隔离用namespace,业务域分组用group,别混用,否则配置权限会乱。
4.2 服务间调用:OpenFeign的正确用法和超时陷阱
服务之间同步调用,Java技术栈基本就是OpenFeign。但我见过太多团队,Feign配置乱写,最终把自己坑了。
OpenFeign最关键的配置不是“能不能调到”,而是“超时和重试怎么设置”。默认情况下,Feign的超时时间是60秒,这意味着,如果下游服务卡死,上游线程会在阻塞等响应,大量请求一进来,线程池立刻被打满。
我最终采用的配置是:
yaml复制feign:
client:
config:
default:
connectTimeout: 2000
readTimeout: 5000
inventory-client:
connectTimeout: 1000
readTimeout: 3000
针对核心的库存服务,超时时间给得更短,因为库存接口必须响应快,超过3秒基本就是出问题了,与其让调用方一直等,不如快速失败走降级。
还有两个铁律:
- 读接口可以重试,写接口绝对不能重试。 如果下游写接口超时了,但实际交易已成功,重试就会造成重复扣减。我们当时就吃过这个亏,所以后来写接口全部关闭Feign重试。
- Feign的最佳实践是配合Sentinel做熔断降级,而不是只靠超时。 超时只是兜底,熔断是主动保护。当库存服务错误率超过阈值时,Sentinel会快速失败或者走降级返回,避免雪崩。
4.3 异步解耦的典型场景:波次释放和WCS任务下发
WMS里很多操作是天然异步的,不该用同步Feign调用强行串起来。我们做了大量异步化改造,最有代表性的两个场景:
一个是波次释放。出库服务生成波次后,不需要同步等待拣货任务分配完毕,而是发一个MQ消息出去,由调度服务异步拉取波次,按照库区路径算法生成拣货任务。这样即使调度服务暂时压力大,消息会在MQ里积压,不会影响上游订单接收。
另一个是WCS任务下发。拣货完成、复核通过后,需要通知输送线或者AGV执行搬运。这个动作如果做同步,一旦WCS卡顿,出库流程全部堵住。所以我们把任务下发改成了MQ消息,WCS服务消费后调用设备接口,然后通过状态回执异步更新任务状态。
MQ选型我们用的是RocketMQ。原因不复杂:支持事务消息,最终一致性方案好落地;延迟消息、重试机制、死信队列都齐全,适合WMS这种强对账场景。如果你的团队规模小,用RabbitMQ也能做,但事务消息这块RabbitMQ要自己落地,复杂度会高一些。
4.4 链路追踪:没有它,微服务出了问题根本没法查
单体时代查问题,翻日志、看线程栈就行。拆了微服务之后,一个请求可能跨三四个服务,没有链路追踪,排查一次线上问题要多花十倍的精力。
我们接入的是SkyWalking。接入方式很粗暴,服务加一个agent参数,不用改代码:
bash复制java -javaagent:/opt/skywalking-agent/skywalking-agent.jar \
-Dskywalking.agent.service_name=wms-outbound-service \
-Dskywalking.collector.backend_service=oap-server:11800 \
-jar wms-outbound-service.jar
有了SkyWalking,你可以在拓扑图上看到每个请求在哪个服务停留了多少时间、有没有报错、SQL耗时多少。特别是我们曾经遇到过的一个性能问题,最后定位出来是出库服务调库存服务的Feign超时时间设置过长,导致大量线程堆积,SkyWalking的拓扑和慢调用分析帮了大忙。
如果你不想额外部署SkyWalking,用SpringCloud Sleuth+Zipkin也能做,功能上弱一些,但胜在轻量。监控告警这块,建议Prometheus+Grafana,JVM指标、接口QPS、响应时间、线程池使用率都做成大盘,大促期间盯这几张图就够了。
5. 微服务化之后,WMS最难的三个技术问题
5.1 分布式事务:Seata还是本地消息表
跨服务的数据一致性,是WMS微服务化绕不开的坎。以“订单取消释放库存”为例:出库服务要把订单状态改成已取消,库存服务要把预占库存释放。这两个操作不在同一个数据库里,怎么保证要么都成功要么都失败?
业界方案无非两种主流,我分别说下适用场景。
第一种:Seata AT模式。它对代码侵入小,框架自动生成回滚SQL,基本是拿到Service方法上加个@GlobalTransactional注解就行。适合的是一致性要求极高、链路短、并发不高的场景。
java复制@GlobalTransactional(rollbackFor = Exception.class)
public void cancelOrderAndReleaseStock(OrderCancelCmd cmd) {
orderService.updateStatus(cmd.getOrderId(), CANCELED);
inventoryService.releaseStock(cmd.getSkuId(), cmd.getQty());
}
第二种:本地消息表+MQ最终一致性。事务范围限制在单个服务内,先把自己库里的事务操作和一条消息记录放在同一个本地事务里,然后通过定时任务扫描消息表,把消息发给MQ下游。下游消费成功后回执更新消息状态。
我个人的倾向是:WMS里尽量用本地消息表方案,不要滥用Seata全局事务。
原因很简单,WMS的核心是库存,库存这个动作一旦包进分布式事务里,意味着整个事务的锁周期会被拉长。拿Seata AT模式来说,全局事务提交前,资源锁是一直持有的。如果订单取消和库存释放之间隔了好几个服务调用,库存行可能被锁几十秒,对高并发的出库扣减来说是无法忍受的。
所以我的分配原则是这样的:
- 同步强一致要求极高、且并发量低的场景(比如盘点差异调整、批次属性变更),用Seata AT。
- 异步链路、高并发、可对账兜底的场景(比如订单取消、波次释放、WCS任务回执),用本地消息表+MQ。
5.2 分布式锁在库存扣减中怎么用才不踩坑
微服务之后,原来的synchronized锁、数据库行锁,在多实例场景下都不够用了。我们需要的是分布式锁。但分布式锁不是银弹,用不好反而会造成新问题。
我们初期直接用Redis的SETNX锁,后来发现几个坑:
- 锁没有设置过期时间,服务宕机锁永远不释放。
- 锁的value不是唯一值,A服务拿到锁后超时释放,B服务又拿到锁,A服务业务执行完,把B的锁释放了。
- 锁粒度过大,整个库存服务一个锁,所有SKU的扣减互相等待。
后来换成了Redisson,上面的坑基本都避开了。Redisson的分布式锁默认有watch dog自动续期,而且锁的值是UUID+线程ID,只能自己释放自己。
真正的关键是锁粒度。以库存扣减为例,最细粒度是“SKU+批次+仓库(或库位)”维度锁。比如SKU001、批次20250601、华东仓01库区的库存记录,这是一个锁维度;同SKU别的批次、别的库区,不应该互相阻塞。
java复制RLock lock = redissonClient.getLock("stock:lock:" + warehouseId + ":" + skuId + ":" + batchNo);
boolean locked = lock.tryLock(2, 30, TimeUnit.SECONDS);
if (!locked) {
throw new BizException("库存服务繁忙,请稍后重试");
}
try {
stockService.deductStock(warehouseId, skuId, batchNo, qty);
} finally {
lock.unlock();
}
tryLock的等待时间给2秒就够了,拿不到就快速返回失败,让上游重试,而不是无限阻塞。锁的持有时间30秒,正常情况下一个扣减操作几十毫秒就结束了,给足余量。
另外,即使有分布式锁,数据库层的乐观锁也可以做兜底。扣减库存的SQL里带上版本号或者库存充足条件:
sql复制UPDATE stock SET available_qty = available_qty - #{qty}, version = version + 1
WHERE sku_id = #{skuId} AND batch_no = #{batchNo}
AND available_qty >= #{qty}
如果影响行数为0,说明库存不足或者版本冲突,由应用层抛异常。这是最后一道防线,防的就是分布式锁万一出现极端异常,账也不会错。
5.3 跨服务分页和聚合查询:把读请求从业务链路里摘出来
库存流水、订单列表、拣货任务列表,这些查询在单体里就是简单的JOIN,拆服务后瞬间变得很难受。因为数据分散在不同的服务各自的库里,不可能跨服务JOIN。
我的解法是“写读分离+CQRS”,虽然名字听着高端,落地起来不玄乎:
- 业务写入走正常的服务接口,操作各自的数据表。
- 新增一个“查询模型”服务,专门负责拼装查询用的宽表。
- 各业务服务的数据库变更,通过Canal监听binlog或者通过MQ发消息,把数据同步到查询模型服务的宽表。
比如出库订单列表界面上要展示订单号、客户名、物料名、仓库名、拣货状态、波次号、承运商。这些字段分布在订单表、基础资料表、波次表里,拆服务后,查询模型服务监听出库服务的订单变更消息,把客户名、物料名等冗余字段直接写入宽表,查询时一条SQL搞定。
宽表和业务表的数据一致性,用最终一致性就够了。界面上多几秒延迟完全感知不到,但查询性能提升了十倍不止。这个方案落地后,原来几千行的大报表SQL,从几秒降到了几十毫秒。
如果你不想额外维护一套Canal,也可以用“业务服务发消息+查询服务订正”的方式,简单说就是:每个业务服务在写完数据后,主动发一条MQ消息,查询服务消费后更新宽表。没有引入额外中间件,但需要业务侧有意识地发消息,容易漏,必须配定时对账任务兜底。
6. 演进过程中的“棺材坑”清单:故障实录与复盘
6.1 故障一:Feign超时配太长,数据库连接池被“吃”光
上线第三周,华东仓出库服务突然大面积超时。看监控,出库服务的Tomcat线程池被打满,数据库连接池也耗尽,线上基本瘫痪。
排查过程是这样的:首先看SkyWalking拓扑,发现出库服务调用库存服务的接口,平均耗时从30ms飙升到3秒,大量请求挂在库存服务的数据库查询上。然后看库存服务的数据库慢SQL,发现一条查询库存流水表的分页SQL,因为数据量过大、索引失效,全表扫描耗时5秒以上。再看Feign配置,默认超时是60秒,导致出库服务线程一直等下游,大量线程阻塞,数据库连接池的链接也被持有不放。
复盘结论:
- Feign超时绝不能依赖默认值,必须根据接口性能压测结果设置合理的超时时间。
- 数据库分页查询必须做索引优化,分页深翻页的问题要提前改掉。
- 关键服务之间要配置线程池隔离,某一条链路堵住,不要影响其他链路。
后来我们把库存流水分页查询改成了游标分页,Feign超时全部收紧,给库存服务调用增加了Sentinel线程隔离,故障没有再出现过。
6.2 故障二:MQ消息重复消费,库存错账
我们异步化之后,库存扣减消息是一个高频消息。结果某一个晚上,对账程序报警:华东仓的库存账实不符,差了273件。查下来发现是MQ消费者在更新库存时,因为网络抖动造成消费超时,RocketMQ触发了重试,同一个扣减消息被消费了两次,库存多扣了一倍。
这是个典型的重复消费问题。RocketMQ提供的At Least Once语义,它在网络异常时会重试投递,业务方必须自己保证幂等。我们当时漏了这一步。
解决方式很常用:消费幂等表。消费者在执行业务之前,先往幂等表插入一条唯一的消息ID记录,如果插入失败说明这条消息已经处理过了,直接返回成功跳过。
sql复制INSERT INTO msg_idempotent (msg_id, service_name, create_time) VALUES (#{msgId}, 'wms-inventory', now());
利用消息ID的唯一索引做防重。只有插入成功才继续执行业务逻辑。这样无论消息被投递多少次,实际库存影响只有一次。
这里要提醒:像库存扣减这种操作,不能只靠数据库更新语句本身的一次性判断来保证幂等,因为扣减的上下文不一样,同样的消息重复到达后,数据库里的“当前库存”可能已经被别的操作改动过,再扣一次就是错账。必须用独立的幂等记录。
6.3 故障三:网关超时引发的连锁雪崩
还有一次,数据库主库因为一台机器磁盘故障,整体性能下降。库存服务接口响应变慢,网关的默认超时是10秒,所有请求都在网关层堆积等待,最终网关线程池也满了,连基础数据接口都进不来。
这个故障的教训是:网关层必须配置合理的全局超时,并且针对不同服务设置不同的路由超时。
网关是流量的入口,也是保护屏障。它不能只是转发请求,还要承担限流、超时、熔断的第一道防线。我们最终的配置思路是:
- 全局超时3秒,超过直接返回504,不让请求无限堆积在下游。
- 对核心的库存服务路由,超时2秒,并在网关层配了每分钟的QPS限流,超过直接丢弃并返回“系统繁忙”。
- 各服务内部再配置Sentinel熔断规则,错误率超过50%就快速失败。
三层防护叠加之后,后面再遇到下游抖动,最多是少量请求快速失败,业务方重试即可,不会再出现网关线程池被拖垮的连带故障。
6.4 编排优先,别一口吃成胖子
最后再聊聊演进节奏。我见过一个团队,规划了十二个微服务,然后让五个开发组同时开工,结果三个月后一个服务都没上生产。原因是对老系统的业务逻辑没吃透,硬拆导致接口设计反复改。
我们的节奏是:每个月最多上两个服务。第一个月先做主数据服务和报表服务,跑通注册中心、网关、MQ、监控全套流程。第二个月攻坚库存服务,这是硬骨头,数据迁移和双写校验用了整整两周。第三个月出库服务、入库服务同时拆。到第四个月才做调度服务和WCS对接。
每拆一个服务,都要定一个“稳定期”,稳定期内在生产环境观察一两周,确认没有账实差异、没有性能回退,再动下一个。微服务改造不是竞速赛,宁可慢,不能烂尾。
7. 拆分跑稳之后的收益,和一句大实话
拆完跑半年之后,变化还是很明显的。
大促扩容从原来的“整个应用全部扩容”变成了“只扩出库服务和库存服务”,资源成本大概省了一半。华东仓爆单时,其他区域的仓库业务不受影响,因为各区域的数据和服务可以做隔离。给A客户定制波次算法,改的是出库服务里的策略模块,B客户的流程完全不受影响。代码发布也轻松了很多,小步快跑,每两天发一次版,再也不用拉着全团队做一小时的发版评审了。
但我也想说句大实话:如果时间倒流,让我重新选一次,我不会一上来就把所有模块全拆了。更稳妥的做法是,先拆出库服务和库存服务这两个最痛的,其他模块继续留在单体里通过接口对外暴露,等真的需要了再拆。微服务是有管理成本的,运维复杂度不会骗人,你得有人盯着Nacos、MQ、SkyWalking、监控告警,这些活都是实打实的工作量。
所以,如果你现在正面临WMS单体架构的性能瓶颈,先按我开头说的三个问题自检:是不是真的需要拆、哪些模块最痛、团队有没有足够的人力和运维能力。想清楚了再动手,比一腔热血直接开干要重要得多。
