背景:一个”改不动”的单体系统

我们的客户是一家区域零售企业,有一套用了8年的ERP系统,Java写的,单体架构。刚上线的时候还挺好的,代码量不大,团队也小,改起来挺快。

但8年下来,系统越来越大:

  • 代码量从最初的几万行涨到了几十万行;
  • 开发团队从3个人涨到了20多个人;
  • 功能模块加了一个又一个,什么都往里面塞。

到了最后,系统变成了一个”巨无霸”:

  • 改不动:改一个小功能,要摸清楚一大堆代码,生怕改了A影响了B;
  • 上线慢:每次上线都要全量部署,一次上线要几个小时,而且风险大,出了问题回滚也慢;
  • 扩展难:某个模块压力大,只能整个系统一起扩容,浪费资源;
  • 协作乱:20多个人在同一个代码库里改代码,冲突不断,merge比写代码还费劲。

团队早就想拆微服务了,但一直不敢动——系统正在跑,业务不能停,万一拆出问题来,谁负责?

这是很多企业的共同困境。单体系统用久了,就像一座老房子,住着不舒服,但又不敢拆了重建——因为你还住在里面。

迁移策略:绞杀者模式,而不是推倒重来

面对单体迁移,有两种思路:

思路一:推倒重来

停掉老系统的开发,集中全部力量用新技术重新写一套,写完了一次性切换。

这种方式听起来很美好,但风险极大:

  • 周期长,往往要半年到一年以上;
  • 中间业务需求不能停,老系统还要继续改,新系统也要跟着改,两边同步很痛苦;
  • 一次性切换风险高,万一新系统有问题,整个业务就停了;
  • 最可怕的是,等新系统写出来了,业务需求又变了,新系统又过时了。

我们见过太多”推倒重来”的项目,最后都失败了。

思路二:绞杀者模式(Strangler Pattern)

不推倒老系统,而是在老系统外面,逐步用新的微服务来”包裹”老系统。新功能用微服务做,老功能逐步从老系统里”剥”出来,迁到微服务里。

就像绞杀榕一样——一棵小树苗长在大树旁边,慢慢长大,把大树包裹起来,最后大树死掉了,小树苗长成了新的大树。

这种方式的好处是:

  • 风险可控:可以一个模块一个模块地迁,出了问题影响范围小;
  • 业务不中断:老系统一直在跑,新服务逐步上线,业务不受影响;
  • 可以随时停:迁到一半,发现不值得迁了,可以停下来,损失不大;
  • 边迁边用:迁完一个模块就能用一个模块,不用等全部迁完。

我们给客户选的就是绞杀者模式。 事实证明,这是最稳妥的迁移方式。

整体架构:中间加一层API网关

绞杀者模式的核心,是在客户端和老系统之间,加一层API网关:

┌──────────┐    ┌──────────┐    ┌──────────┐
│  前端/   │    │ API 网关  │    │  老单体   │
│  客户端  │───►│          │───►│  系统     │
└──────────┘    └────┬─────┘    └──────────┘

                     │    ┌──────────┐
                     └───►│  新微服务 │
                          └──────────┘

所有请求都先经过API网关。网关根据URL路径,把请求路由到对应的地方:

  • 已经迁完的模块 → 路由到新的微服务;
  • 还没迁的模块 → 路由到老单体系统。

对前端来说,完全感知不到后端是单体还是微服务——反正都是调用同一个网关的接口。

这样做的好处是:

  • 迁移对前端透明,前端不用改;
  • 可以灵活控制哪些接口走新服务、哪些走老系统;
  • 出了问题,可以一键切回老系统,秒级回滚。

迁移路径:分四个阶段

我们把整个迁移过程分成了四个阶段:

第一阶段:搭架子(1个月)

先不着急迁业务,先把基础设施搭好:

  • 搭建微服务基础设施:注册中心、配置中心、API网关、链路追踪、监控告警;
  • 搭建CI/CD流水线:自动化构建、自动化测试、自动化部署;
  • 制定微服务规范:服务怎么拆、接口怎么设计、代码怎么写、日志怎么打……

这一步很重要。很多团队一上来就拆服务,结果基础设施没搭好,服务拆完了,运维更痛苦了,还不如单体。

先把路修好,再跑车。

第二阶段:外围功能先迁(2个月)

从最外围、最独立的功能开始迁,比如:

  • 消息通知服务(短信、邮件、站内信);
  • 文件上传服务;
  • 数据报表服务;
  • 系统管理(用户、角色、权限)。

为什么先迁这些?

  • 这些功能相对独立,和核心业务耦合少,迁起来风险小;
  • 这些功能出问题,不会影响核心交易,即使出问题也能快速回滚;
  • 可以让团队先练手,熟悉微服务的开发和运维。

先迁简单的,积累经验,建立信心,然后再迁复杂的。

第三阶段:核心业务逐步迁(6个月)

外围迁完了,团队也有经验了,就开始迁核心业务模块。

核心业务不能一下子全迁,要一个模块一个模块地来:

  1. 先迁商品中心;
  2. 再迁会员中心;
  3. 然后迁订单中心;
  4. 最后迁库存和支付。

每个模块的迁移,都遵循同样的步骤:

  1. 写新服务:用微服务重新实现这个模块的功能;
  2. 数据双写:新老系统都写数据,保证两边一致;
  3. 流量切分:先切1%的流量到新服务,观察没问题,再逐步增加到10%、50%、100%;
  4. 下线老代码:新服务稳定运行一段时间后,把老系统里对应的代码删掉。

这个阶段是最漫长的,也是最考验耐心的。不能急,稳扎稳打,一个模块一个模块地来。

第四阶段:老系统下线(1个月)

所有功能都迁完了,老系统就可以下线了。

但下线之前,一定要做几件事:

  • 确认所有流量都已经切到新服务了,老系统已经没有请求了;
  • 确认所有数据都已经迁到新数据库了,老数据库可以停了;
  • 留一台老系统的服务器做备份,万一出问题还能切回去;
  • 观察一段时间,确认没问题,再正式下线。

服务拆分:怎么拆才合理?

微服务最关键的问题,不是技术,而是怎么拆服务。拆得太细,服务太多,运维成本高;拆得太粗,跟单体没区别。

我们的拆分原则:

1. 按业务领域拆,不是按技术层拆

不要把一个业务功能拆成”数据访问层服务""业务逻辑层服务""接口层服务”——那是把单体的分层搬到了微服务里,完全是错的。

正确的拆法是按业务领域拆:商品服务、会员服务、订单服务、库存服务……每个服务负责一个完整的业务领域,从数据到接口都自己管。

这就是领域驱动设计(DDD)里的限界上下文的思路。

2. 服务粒度:一个服务2-5个人能维护

服务多大合适?没有标准答案,但有一个经验法则:一个服务,2-5个人的团队能完全维护得过来,就是合适的粒度。

太小了,一个人维护好几个服务,上下文切换太频繁,效率低;太大了,一个团队维护不过来,又变回单体了。

3. 先粗后细,逐步拆分

不要一开始就拆得很细。先拆成几个大的服务,跑起来看看。如果某个服务确实太大了、团队太大了、迭代冲突太多了,再往下拆。

先粗后细,而不是一步到位。

服务治理:微服务的”操作系统”

微服务不是拆完就完事了。服务多了,治理是个大问题。

我们的服务治理体系,包括这几个部分:

1. 注册中心 + 配置中心

服务之间怎么发现对方?用注册中心(我们用的是Nacos)。服务启动的时候注册自己,调用的时候从注册中心找地址。

配置怎么统一管理?用配置中心。所有服务的配置都存在配置中心,改配置不用重新部署。

2. API网关

所有请求统一入口,做路由、鉴权、限流、熔断、日志、监控……

网关是微服务的”大门”,很多横切关注点都放在网关层做,不用每个服务都做一遍。

3. 链路追踪

服务多了,一个请求要经过好几个服务,出了问题怎么排查?用链路追踪(我们用的是SkyWalking)。

一个请求从进来到出去,经过了哪些服务、每个服务花了多长时间、有没有报错,都能看到。排查问题效率大大提高。

4. 监控告警

服务多了,不能靠人盯着。必须有完善的监控和告警:

  • 基础设施监控:CPU、内存、磁盘、网络;
  • 应用监控:QPS、响应时间、错误率;
  • 业务监控:订单量、支付成功率、库存预警;
  • 告警:出问题了自动推送到群里、打电话、发短信。

5. 熔断降级

微服务架构下,一个服务挂了,可能会导致调用它的所有服务都挂了,最后整个系统雪崩。

所以必须有熔断降级机制(我们用的是Sentinel):

  • 某个服务出错率太高了,自动熔断,直接返回降级结果,不让请求再打过去;
  • 流量太大了,自动限流,保护系统不被冲垮。

踩过的几个坑

迁移过程中踩了不少坑,说几个印象深的:

坑1:分布式事务

单体的时候,一个事务搞定。拆成微服务了,订单、库存、支付在不同的服务里,怎么保证事务一致性?

我们的方案:尽量避免分布式事务,能用最终一致性就用最终一致性。

真的需要强一致的场景,用Seata做分布式事务。但不要什么场景都用分布式事务——性能差,复杂度高。大部分业务场景,最终一致性就够了。

坑2:数据迁移的一致性

把老系统的数据迁到新服务,怎么保证数据一致?

我们的做法:

  1. 先全量迁移一次;
  2. 然后增量同步,老系统有新数据,实时同步到新服务;
  3. 切流量之前,做一次数据对账,两边对一遍,有不一致的修一下;
  4. 切流量之后,双写一段时间,再慢慢停掉老系统的写。

坑3:服务间依赖混乱

服务拆多了,你调用我、我调用他、他又调用你,最后依赖关系一团乱麻,谁也不敢改。

我们的做法:

  • 制定依赖规范:核心服务不能依赖外围服务;
  • 定期梳理依赖关系,发现不合理的依赖及时调整;
  • 用领域事件解耦:能异步的就异步,不要同步调用。

坑4:运维复杂度飙升

从一个单体变成几十个服务,运维复杂度不是线性增加,而是指数级增加。

我们的做法:

  • 大力投入自动化:CI/CD、自动化测试、自动化部署、自动化运维;
  • 标准化:所有服务用同样的框架、同样的规范、同样的部署方式;
  • 平台化:把通用的运维能力做成平台,不用每个服务自己搞。

效果:迁完之后怎么样了?

整个迁移过程花了大约10个月,全部迁完之后,效果很明显:

  • 迭代速度:从原来的每月一次上线,变成了每周都能上线,小功能随时可以发;
  • 系统稳定性:出问题的影响范围小了,某个服务挂了,不影响其他服务,整体可用性从99.5%提升到了99.9%;
  • 团队协作:每个团队负责自己的服务,代码冲突少了,协作效率高了;
  • 扩展性:哪个服务压力大,就单独扩哪个服务,资源利用率提高了。

当然,也不是全是好处。微服务也带来了一些新的问题:运维更复杂了、分布式问题更多了、对团队的要求更高了。

但总体来说,对于这个规模的系统,微服务的收益大于成本。

写在最后

从单体到微服务,不是一个”技术升级”的问题,而是一个系统工程。它涉及到技术、架构、团队、流程、运维的方方面面。

最重要的一点:不要为了微服务而微服务。

如果你的系统很小、团队很小、业务很简单,单体就挺好的,不要硬拆微服务。微服务不是银弹,它有它的适用场景,也有它的成本。

什么时候该拆?当你的单体系统已经”改不动”了、团队协作已经很痛苦了、系统扩展已经遇到瓶颈了——这时候,再考虑微服务。

而且,拆的时候一定要稳,用绞杀者模式,逐步迁移,不要推倒重来。稳扎稳打,比什么都强。

Background: A Monolith That “Can’t Be Changed”

Our client is a regional retail company with an 8-year-old ERP system, written in Java, monolithic architecture. When it first launched, it was fine — small codebase, small team, changes were fast.

But after 8 years, the system kept growing:

  • Codebase went from tens of thousands of lines to hundreds of thousands.
  • Dev team grew from 3 people to 20+.
  • Feature modules kept getting added — everything got stuffed in there.

Eventually, the system became a “giant”:

  • Hard to change: Modifying one small feature requires understanding tons of code, always afraid that changing A breaks B.
  • Slow releases: Every release requires full deployment, takes hours, high risk, rollback is also slow.
  • Hard to scale: When one module is under pressure, you have to scale the whole system — wasteful.
  • Chaotic collaboration: 20+ people modifying code in the same repository, constant conflicts, merging is harder than writing code.

The team has wanted to split into microservices for a long time, but never dared — the system is running, business can’t stop, and if something goes wrong during the split, who takes responsibility?

This is a common dilemma for many companies. After using a monolith for years, it’s like an old house — uncomfortable to live in, but you dare not tear it down and rebuild — because you’re still living in it.

Migration Strategy: Strangler Pattern, Not Tear-Down-and-Rebuild

When it comes to monolith migration, there are two approaches:

Approach 1: Tear Down and Rebuild

Stop development on the old system, concentrate all efforts on rewriting everything with new technology, then switch all at once.

This approach sounds great, but the risk is enormous:

  • Long cycle, often 6 months to a year or more.
  • Business requirements don’t stop in the meantime — old system keeps getting changes, new system has to keep up — syncing both sides is painful.
  • One-time switch is high risk — if the new system has problems, the whole business stops.
  • Scariest of all: by the time the new system is done, business requirements have changed again, and the new system is already outdated.

We’ve seen too many “tear down and rebuild” projects fail in the end.

Approach 2: Strangler Pattern

Don’t tear down the old system. Instead, gradually “wrap” the old system with new microservices from the outside. New features are built as microservices, and old features are gradually “peeled” out of the old system and migrated to microservices.

It’s like a strangler fig — a small sapling grows next to a big tree, slowly grows, wraps around the tree, and eventually the tree dies and the sapling becomes the new big tree.

The benefits of this approach:

  • Controllable risk: Migrate module by module, if something goes wrong, the impact is small.
  • No business interruption: Old system keeps running, new services go live gradually — business isn’t affected.
  • Can stop anytime: If halfway through you decide it’s not worth migrating, you can stop with minimal loss.
  • Use as you migrate: Each module is usable as soon as it’s migrated — no need to wait for everything to finish.

We chose the strangler pattern for our client. And it proved to be the safest migration approach.

Overall Architecture: Add an API Gateway in the Middle

The core of the strangler pattern is adding an API gateway layer between clients and the old system:

┌──────────┐    ┌──────────┐    ┌──────────┐
│ Frontend/│    │ API      │    │  Old     │
│  Client  │───►│ Gateway  │───►│ Monolith │
└──────────┘    └────┬─────┘    └──────────┘

                     │    ┌──────────┐
                     └───►│  New     │
                          │Microservices│
                          └──────────┘

All requests go through the API gateway first. The gateway routes requests based on URL path:

  • Already migrated modules → route to new microservices.
  • Not yet migrated modules → route to the old monolith.

To the frontend, it’s completely transparent whether the backend is monolithic or microservices — they just call APIs from the same gateway.

Benefits of this approach:

  • Migration is transparent to frontend — frontend doesn’t need to change.
  • Flexible control over which interfaces go to new services, which go to old system.
  • If problems arise, one-click switch back to old system — second-level rollback.

Migration Path: Four Phases

We divided the entire migration process into four phases:

Phase 1: Build the Foundation (1 Month)

Don’t rush to migrate business logic first — build the infrastructure first:

  • Set up microservices infrastructure: service registry, config center, API gateway, distributed tracing, monitoring & alerting.
  • Set up CI/CD pipeline: automated builds, automated testing, automated deployment.
  • Establish microservices standards: how to split services, how to design APIs, how to write code, how to log…

This step is crucial. Many teams start splitting services right away, but without proper infrastructure, after splitting services, operations are even more painful — worse than the monolith.

Build the road first, then drive on it.

Phase 2: Migrate Peripheral Functions First (2 Months)

Start with the most peripheral, most independent functions, like:

  • Message notification service (SMS, email, in-app messages).
  • File upload service.
  • Data reporting service.
  • System management (users, roles, permissions).

Why migrate these first?

  • These functions are relatively independent, less coupled to core business — lower risk to migrate.
  • If these functions have issues, they don’t affect core transactions — even if something goes wrong, you can roll back quickly.
  • They let the team practice, get familiar with microservices development and operations.

Migrate simple things first, accumulate experience, build confidence — then migrate the complex stuff.

Phase 3: Gradually Migrate Core Business (6 Months)

After peripherals are done and the team has experience, start migrating core business modules.

Core business can’t be migrated all at once — do it module by module:

  1. First migrate product center.
  2. Then member center.
  3. Then order center.
  4. Finally inventory and payment.

Each module’s migration follows the same steps:

  1. Write new service: Reimplement the module’s functionality as a microservice.
  2. Dual-write data: Both old and new systems write data, ensuring consistency.
  3. Traffic splitting: First route 1% of traffic to the new service, observe no issues, then gradually increase to 10%, 50%, 100%.
  4. Retire old code: After the new service runs stably for a while, delete the corresponding code from the old system.

This phase is the longest and most tests patience. Don’t rush — steady and methodical, one module at a time.

Phase 4: Retire the Old System (1 Month)

Once all features are migrated, the old system can be retired.

But before retiring, make sure to do several things:

  • Confirm all traffic has been switched to new services, old system has no more requests.
  • Confirm all data has been migrated to new databases, old databases can be stopped.
  • Keep one old system server as backup — if something goes wrong, you can still switch back.
  • Observe for a while, confirm no issues, then officially retire.

Service Splitting: What’s the Right Way to Split?

The most critical question in microservices isn’t technology — it’s how to split services. Split too fine, too many services, high operations cost. Split too coarse, no different from a monolith.

Our splitting principles:

1. Split by Business Domain, Not by Technical Layer

Don’t split a business function into “data access layer service,” “business logic layer service,” “interface layer service” — that’s just moving monolith layers into microservices, completely wrong.

The right way is split by business domain: product service, member service, order service, inventory service… each service is responsible for a complete business domain, from data to APIs, all self-managed.

This is the bounded context idea from Domain-Driven Design (DDD).

2. Service Granularity: One Service Maintainable by 2-5 People

How big should a service be? No standard answer, but a rule of thumb: a service that a team of 2-5 people can fully maintain is the right granularity.

Too small, one person maintains several services — too much context switching, low efficiency. Too big, one team can’t maintain it well — becomes a monolith again.

3. Coarse First, Then Fine — Split Gradually

Don’t split too fine at the beginning. First split into a few large services, get them running, see how it goes. If a service really is too big, team is too large, too many iteration conflicts — then split further.

Coarse first, then fine — not all at once.

Service Governance: The “Operating System” of Microservices

Microservices aren’t done after splitting. With more services, governance becomes a big problem.

Our service governance system includes these parts:

1. Service Registry + Config Center

How do services discover each other? Use a service registry (we use Nacos). Services register themselves on startup, and look up addresses from the registry when calling.

How to manage configurations centrally? Use a config center. All service configurations live in the config center — change config without redeploying.

2. API Gateway

Unified entry point for all requests — handles routing, authentication, rate limiting, circuit breaking, logging, monitoring…

The gateway is the “front door” of microservices. Many cross-cutting concerns are handled at the gateway layer, so each service doesn’t have to do them all over again.

3. Distributed Tracing

With many services, one request goes through several services — how do you troubleshoot when something goes wrong? Use distributed tracing (we use SkyWalking).

From request entry to exit, you can see which services it passed through, how long each took, whether there were errors. Troubleshooting efficiency improves dramatically.

4. Monitoring & Alerting

With many services, you can’t rely on people watching. You must have comprehensive monitoring and alerting:

  • Infrastructure monitoring: CPU, memory, disk, network.
  • Application monitoring: QPS, response time, error rate.
  • Business monitoring: order volume, payment success rate, inventory alerts.
  • Alerting: when problems occur, automatically push to group chats, call phones, send SMS.

5. Circuit Breaking & Degradation

In a microservices architecture, if one service goes down, it can cause all services that call it to go down too — eventually the whole system雪崩 (cascades).

So you must have circuit breaking and degradation mechanisms (we use Sentinel):

  • If a service’s error rate gets too high, automatically circuit break — return degraded results directly, don’t let requests hit it anymore.
  • If traffic is too high, automatically rate-limit — protect the system from being overwhelmed.

Pitfalls We Stepped In

We stepped in many pitfalls during migration. Here are a few memorable ones:

Pitfall 1: Distributed Transactions

In a monolith, one transaction handles everything. After splitting into microservices, orders, inventory, payments are in different services — how do you ensure transaction consistency?

Our approach: Avoid distributed transactions whenever possible. Use eventual consistency if you can.

For scenarios that really need strong consistency, use Seata for distributed transactions. But don’t use distributed transactions for everything — poor performance, high complexity. For most business scenarios, eventual consistency is enough.

Pitfall 2: Data Migration Consistency

When migrating data from the old system to new services, how do you ensure data consistency?

Our approach:

  1. First, do a full migration.
  2. Then incremental sync — new data in the old system is synced to the new service in real-time.
  3. Before switching traffic, do a data reconciliation — compare both sides, fix any inconsistencies.
  4. After switching traffic, dual-write for a while, then gradually stop writing to the old system.

Pitfall 3: Chaotic Service Dependencies

With more services, you call me, I call him, he calls you back — eventually dependencies become a tangled mess, nobody dares change anything.

Our approach:

  • Establish dependency rules: core services must not depend on peripheral services.
  • Regularly review dependency relationships, adjust unreasonable dependencies promptly.
  • Decouple with domain events: use async where possible, don’t use synchronous calls.

Pitfall 4: Operations Complexity Skyrockets

Going from one monolith to dozens of services, operations complexity doesn’t increase linearly — it increases exponentially.

Our approach:

  • Heavily invest in automation: CI/CD, automated testing, automated deployment, automated operations.
  • Standardization: all services use the same framework, same standards, same deployment method.
  • Platformization: build common operations capabilities into a platform — each service doesn’t have to build its own.

Results: How’s It After Migration?

The whole migration took about 10 months. After everything was migrated, the results were clear:

  • Iteration speed: From monthly releases to weekly releases — small features can ship anytime.
  • System stability: Problem impact is smaller — if one service goes down, it doesn’t affect others. Overall availability improved from 99.5% to 99.9%.
  • Team collaboration: Each team owns its own services, fewer code conflicts, more efficient collaboration.
  • Scalability: When a service is under pressure, scale that service alone — better resource utilization.

Of course, it’s not all benefits. Microservices also bring new problems: more complex operations, more distributed issues, higher team requirements.

But overall, for a system of this scale, the benefits of microservices outweigh the costs.

Closing Thoughts

Going from monolith to microservices isn’t a “technology upgrade” issue — it’s a systems engineering problem. It involves technology, architecture, team, process, operations — all aspects.

The most important point: don’t do microservices for the sake of microservices.

If your system is small, your team is small, your business is simple — a monolith is just fine. Don’t force microservices. Microservices isn’t a silver bullet. It has its适用场景 (applicable scenarios) and its costs.

When should you split? When your monolith “can’t be changed” anymore, team collaboration is painful, system scaling has hit bottlenecks — then consider microservices.

And when you do split, do it steadily — use the strangler pattern, migrate gradually, don’t tear down and rebuild. Steady and methodical beats everything.