# Shoprocket 的微服务之旅，第一部分：我对拆分单体应用的误判

> 来自 Shoprocket 早期工程团队的一篇文章，讲述首次尝试从 PHP 单体应用中拆出一个服务。我为什么选错了第一个服务，紧耦合教会了我什么，以及我本该早点采纳的 Newman 的建议。

Winston Berty  - 2016年3月17日 - 阅读约 5 分钟

加入 Shoprocket 几个月之后，我开始感到越来越沮丧。我们当时有一个 PHP 应用，由一支分散在世界各地、个个都很精干的开发者小团队共同维护，大家分处不同的时区，沟通本身就要花不少力气。在我看来，修复一个 bug 往往会带出更多的 bug，集成新功能远没有它应有的那么简单，而部署既不频繁，每一次还都让人神经紧绷。归根结底的原因只有一个：代码高度耦合。

我们是一个单体应用，几乎全部代码都放在同一个 GitHub 仓库里。更新或修复其中一个功能，经常需要同时在好几个地方做改动。至于测试，我们靠的是同行评审流程：一个分支要合并进 master 并上线部署之前，必须至少有另外两位开发者签字通过。这一切加在一起意味着，一旦某个 bug 溜了进来，我们往往很难定位到究竟是哪一处改动引入了问题。好不容易找到之后，修复又要走一遍同样漫长的流程，而且整个应用都得重新部署一次。

当时的我是个总想尝鲜、热情十足的年轻开发者，所以当我接触到微服务这个概念时，我满心以为，这下我们所有的问题都能一口气迎刃而解了，再也不用为部署提心吊胆。

## 我以为的微服务

微服务并没有一个正式的定义，但根据我当时的调研，我最终得出的理解是：微服务架构是一种设计软件应用的方式，它把应用设计成由相互独立的服务组成的分布式系统，每个服务都高度专精，并且拥有自己的生命周期。如果想要一份详细的入门介绍，Martin Fowler 的[文章](https://martinfowler.com/articles/microservices.html)写得非常出色。

有两点让我彻底着了迷。第一，把应用拆分成边界严格的各个服务这一想法。这样的设计降低了某个业务上下文所特有的逻辑在不同组件之间泄漏的可能性，因为各个边界之间的接口必须经过仔细的规划。这可不像在本地调用一个函数那么简单，每个接口都要认真设计、认真约定。第二，每个组件都可以拥有自己独立的生命周期。团队可以真正拥有自己的代码，并采用最适合该服务的技术栈；变更可以一次只部署一个服务，出了问题还可以回滚，而不会影响到其余部分。

我完全被说服了，几乎立刻就开始盘算该从哪里下手。Shoprocket 就要拆分单体应用了。这篇文章是记录这段旅程的短系列的第 1 部分。我希望正在规划或正在经历类似转型的读者，能够从我们的成功与失败中学到一些东西，少走一些我已经走过的弯路。

## 第一个挑战是人，而不是技术

我首先得把这个想法推销给团队里的其他每一个人，而在任何一个团队里，说服同事去做一件短期内看不到回报的事，本身就是一项不小的工作。他们对此很好奇，却并不像我那样热情，反而显得有些谨慎，而且在产品生命的早期阶段，他们也不愿意拿出宝贵的开发工时。幸运的是，当时 CTO 想让我研究一下我们的会话管理，于是我抓住了这个难得的时机：我说服了他，让他相信我可以把会话功能拆成一个独立的会话服务。

事后回想，这是一个糟糕的首选服务，原因我稍后会详细说明。我们的产品包含一个 JavaScript 组件，它可以让任何网站都[实现电商功能](https://v3.shoprocket.io/zh/features/sell-on-any-website.md)。我们把会话当作一种资源来对待，会把会话信息持久化到数据库中，用来分析购物车弃购率和其他关键指标。但这种资源不像订单或订单行项目那样具体、清晰，而且应用状态几乎每发生一次变化，我们都会用到会话。这意味着，这个服务与应用其余部分之间的通信必然会非常频繁，每一次状态变化都要来回跑上好几趟网络请求。这一点我当时完全没有预见到，却让我的任务变得难上加难。

## 真正动手构建时我学到的

我意识到自己的功课还做得不够，于是加大了调研的力度，几乎把业余时间都搭了进去。除了 Fowler 之外，Sam Newman 的 **Building Microservices** 被证明是一份不可或缺的资料。

现在回头来看，Newman 的建议恰恰就是我本该遵循的建议：先把现有应用重构成若干模块，让这些模块反映领域中的限界上下文。等到开发者们对这些边界都感到满意之后，*再*把模块拆分成各自独立的服务。这个道理其实很朴素。原因在于，拆解并重写边界划分不佳的模块，要比拆解并重写整个服务容易得多，因为服务还带着额外的粘合代码和持久化的资源，每一样都要跟着一起改动、一起迁移。

可惜，我没有听从这个建议，当时的我觉得这样做太慢了，也太不过瘾。我急着想要构建一个属于自己的组件，于是径直动手做起了会话服务。

我选了一套与单体应用只有细微差别的技术栈，没有借机尝试什么新奇的东西，然后就开始构建了。服务本身很快就成型了，一切看起来都很顺利，只花了几周时间，就做出了一个带有 REST API 的独立应用。难点并不在这里，这部分工作甚至可以说相当愉快。真正的难点在于，让这个服务能够和单体应用顺畅地通信。

事实证明，会话管理与代码库的其余部分紧密耦合在一起，几乎每个角落都能看到它的影子。把函数调用替换成 API 调用，是一项缓慢而繁琐的工作。由于会话服务和单体应用使用的是不同的数据库模式，我不得不重写单体应用里所有使用旧数据库中会话资源的逻辑，并且在应用的边界处重新组织数据结构，好让我们的 JavaScript 组件这样已经在外面运行的客户端依然保持兼容，不至于一夜之间全部失效。

集成所花的时间，比构建本身还要长，这是我事先完全没有料到的。而且，这还没有算上微服务通常会带来的大部分额外开销：服务发现、日志、监控、分布式追踪。更糟糕的是，紧耦合意味着改动会扩散到代码的许多个区域。由于团队在这段时间里一直在并行地开展工作，要把会话服务合并进来并上线生产环境，所需要的协调工作比我原先估计的要多得多。

## 坦率的评估，以及我从中得到的教训

这个服务也许很快就会上线生产，从这个意义上说，这次尝试并不算彻底失败。但我现在觉得，我本该听从 Newman 的建议，先把精力投入到对现有应用做模块化上。让单独一个服务独立存在，带来的好处微乎其微。真正的好处来自于把整个应用都拆开，至少拆成粗粒度的若干服务，并且让它们彼此之间的通信尽可能少，各自安静地做好自己的事。这需要经过深思熟虑的组件边界，而我们当时还远没有走到那一步。

尽管如此，我依然认同微服务这个理念，它的初衷没有错，错的是我实施它的顺序。我把自己的这次尝试看作一次代价不大、却很有价值的实验。我学到了大量关于应用架构、领域建模和 REST 设计原则的知识，而这些经验对我们代码里的其他部分也产生了连带的积极影响，这是这次实验最实在的收获。

对于任何正在考虑类似转型的人，我坦率的建议是：

- 在做出投入之前，尽可能多地研读这方面的书籍和文章，并且多听听已经走过这条路的人怎么说
- 推迟这项工作，直到你对现有的代码库和领域模型真正有把握为止
- 记住，还有其他的办法可以为单体应用带来稳定性和韧性。自动化的单元测试和功能测试能带来极大的帮助，它们让你有底气去改动代码。测试驱动开发可以明显减少回归问题。水平扩展则能让每一次部署都不那么痛苦、不那么提心吊胆
- 先把你的单体应用模块化，等边界稳定、团队也认可之后，再去拆分服务

Shoprocket 的旅程仍将继续，我们也会继续诚实地记录下去。第 2 部分才是故事真正变得更有意思的地方，敬请期待，我们很快再见。

---

*本文最初发表时，是记录 Shoprocket 早期从 PHP 单体应用迁移出来的这个短系列的第一篇。这段完整的、并不总是一帆风顺的旅程，包括在后续迭代中被一一纠正的那些错误，在此后平台的每一次重大重写中都在延续。*

---

- HTML version: https://v3.shoprocket.io/zh/blog/shoprockets-journey-to-microservices-part-1-what-i-got-wrong-about-breaking-up-the-monolith
- Language: 中文 (zh)
- Also in: [العربية](https://v3.shoprocket.io/ar/blog/shoprockets-journey-to-microservices-part-1-what-i-got-wrong-about-breaking-up-the-monolith.md), [Deutsch](https://v3.shoprocket.io/de/blog/shoprocket-microservices-teil-1-monolith-zerlegen-fehler.md), [Ελληνικά](https://v3.shoprocket.io/el/blog/shoprockets-journey-to-microservices-part-1-what-i-got-wrong-about-breaking-up-the-monolith.md), [English](https://v3.shoprocket.io/blog/shoprockets-journey-to-microservices-part-1-what-i-got-wrong-about-breaking-up-the-monolith.md), [Español](https://v3.shoprocket.io/es/blog/shoprocket-microservicios-parte-1-lo-que-hice-mal-monolito.md), [Français](https://v3.shoprocket.io/fr/blog/shoprocket-vers-les-microservices-partie-1-decouper-le-monolithe.md), [עברית](https://v3.shoprocket.io/he/blog/shoprockets-journey-to-microservices-part-1-what-i-got-wrong-about-breaking-up-the-monolith.md), [हिन्दी](https://v3.shoprocket.io/hi/blog/shoprockets-journey-to-microservices-part-1-what-i-got-wrong-about-breaking-up-the-monolith.md), [Bahasa Indonesia](https://v3.shoprocket.io/id/blog/perjalanan-shoprocket-menuju-microservices-bagian-1-salah-memecah-monolit.md), [Italiano](https://v3.shoprocket.io/it/blog/viaggio-di-shoprocket-verso-i-microservizi-parte-1-cosa-ho-sbagliato-sul-monolite.md), [日本語](https://v3.shoprocket.io/ja/blog/shoprockets-journey-to-microservices-part-1-what-i-got-wrong-about-breaking-up-the-monolith.md), [Lietuvių](https://v3.shoprocket.io/lt/tinklarastis/shoprocket-kelione-i-mikropaslaugas-1-dalis.md), [Norsk](https://v3.shoprocket.io/nb/blogg/shoprockets-vei-mot-mikrotjenester-del-1-hva-jeg-tok-feil-av.md), [Nederlands](https://v3.shoprocket.io/nl/blog/shoprocket-microservices-deel-1-monoliet-opknippen.md), [Polski](https://v3.shoprocket.io/pl/blog/droga-shoprocket-do-mikroserwisow-czesc-1-rozbijanie-monolitu.md), [Português](https://v3.shoprocket.io/pt/blog/jornada-do-shoprocket-para-microsservicos-parte-1-o-que-errei-ao-quebrar-o-monolito.md), [Română](https://v3.shoprocket.io/ro/blog/drumul-shoprocket-spre-microservicii-partea-1-ce-am-gresit-cu-monolitul.md), [Русский](https://v3.shoprocket.io/ru/blog/shoprockets-journey-to-microservices-part-1-what-i-got-wrong-about-breaking-up-the-monolith.md), [ไทย](https://v3.shoprocket.io/th/blog/shoprockets-journey-to-microservices-part-1-what-i-got-wrong-about-breaking-up-the-monolith.md), [Türkçe](https://v3.shoprocket.io/tr/blog/shoprocket-mikroservis-yolculugu-1-monoliti-parcalarken-yanlis-yaptiklarim.md)
