近年来, 我们发布了很多文章介绍企业向微服务迁移的成败经验. 最近, Segment 的 Alexandra Noonan https://segment.com/blog/goodbye-microservices/ 写了一篇文章, 讲述了他们从单体架构迁移到微服务, 之后又退回单体应用的经历. 文中 Alexandra 具体介绍了他们从原来的简单架构迁移到微服务的过程:
我们原来有个 API 负责拦截事件并将它们转发到一个分布式的消息队列中. 这里的一个事件指的是由网页或移动应用生成的, 包含用户与用户动作信息的 JSON 对象. 当队列中的事件被消费后, 系统会检查用户设置来决定接收事件的目标.(......) 之后事件被逐个发送到每个目标的 API. 这样的流程很合理, 因为开发者只需要将事件发送到 Segment 的 API 这一个目标即可, 无需创建几十个集成.
如果事件交付失败, 就会被系统重新加入队列, 也就是说有时工作者进程要一边发送新的事件, 一边尝试重新发送之前失败的事件. 这样会导致所有目标都出现延迟, Alexandra 解释说:
为解决最紧迫的阻塞问题, 团队为每个目标各创建了一个独立的服务和队列. 这一新架构包括一个新的路由进程, 它会接收入站事件并向每个选定的目标分发一份该事件的拷贝. 现在如果某个目标出现问题, 只有它自己的队列会回溯, 不会影响其它目标. 这一微服务化的架构将各个目标独立开来, 这样当某个目标又出现常遇到的问题时就会非常有用.
文章之后写到, Segment 的开发团队一开始将所有代码存放在一起, 但这引发了许多问题:
最大的麻烦在于, 只要一个测试崩溃, 那么所有目标的测试都会失败. 当我们试图部署一项改动时, 我们必须先费劲修复崩溃的测试, 就算改动与一开始的变动毫无关联也得这样做. 为了解决这个问题, 我们决定将代码拆分到每个目标各自的存储库里.
这样一来开发团队的灵活性的确改善了许多. 然而随着目标数量的增加, 存储库的数量也在同步增长. 为了让开发者免受维护这么多代码库的麻烦, Segment 团队创建了许多共享库, 存放所有目标通用的变换和功能. 这组共享库大大减轻了他们维护工作的压力. 但这个措施也有不太容易发现的负面影响: 向共享库更新并测试改动会花费大量时间, 还会增加破坏无关目标的风险. 最后这些库开始分裂为不同的版本, 各自不统一, 引发了之前没有想到的一个问题: 每个目标的代码库都会依赖不同版本的共享库. Alexandra 承认, 他们当时可以开发工具来自动将改动更新到这些库中, 但那时他们又遇到了这个微服务架构产生的一些新问题.
新出现的问题是, 每个服务都有自己的载入模式. 有些服务一天处理几个事件, 有的服务每秒就能处理几千个. 如果目标只处理很少的事件, 工作者线程就得在出现载入问题时手动扩展服务以满足需求.
他们的系统集成了自动扩展能力, 但因为每个服务都需要指定 CPU 和内存资源分配, 调整自动扩展的设置 "更像玄学而非科学". 如前所述, 每次存储库的数量增长时他们都要增加目标, 最后团队平均每月要增加三个目标, 当然还要加上更多的队列和服务.
2017 年初, Segment 的一个产品 https://segment.com/product 的核心部分使我们达到了峰值负载. 当时的情况下我们好像在从微服务的大树上摔下来, 一路撞上了所有的树枝. 我们这个小团队非但没有提升效率, 反而陷入了愈加复杂的泥潭. 这个架构的核心优势都成了负担. 我们的速度暴降, 故障率却在暴增.(......) 于是, 我们决定回退一步, 重新考虑整个流程.
文章最后 Noonan 回顾了他们如何摆脱这个微服务架构, 其中他们还开发了 Centrifuge https://segment.com/blog/introducing-centrifuge/ 来替换所有独立的队列, 将所有事件都发送到一个单体服务上. 他们还将所有目标的代码都迁移到一个存储库里, 不过这一次新增了一些代码管理的规则: 所有目标都要使用同一个版本, 每次更新时同步更替到新版本. 他们再也不用操心各个独立版本之间的差异了, 因为所有的目标都在使用一个版本, 以后也是如此. 对于开发者来说, 管理越来越多的目标所花费的时间减少了, 风险也降低了.
Noonan 的文章还写了很多内容, 都是关于他们退回单体服务的经历. 感兴趣的读者应该去仔细读一下, 文章里面有很多架构细节, 关于存储库架构的思考和建立弹性测试集的方法. 最后, 团队将回退的好处总结如下:
2016 年时我们还在使用微服务架构, 我们为共享库带来了 32 项改进. 而仅仅今年到现在我们就做出了 46 项改进. 过去半年来我们为库带来的改进比 2016 年全年都多. 因为所有的目标都处于同一服务内, 我们可以很好地搭配 CPU 密集型服务和内存密集型服务, 所以扩展服务以满足性能需求变得非常容易. 更大的工作者池能载入更多内容, 所以我们不再需要将处理少量载入的目标挂起到页面了.
不过这个架构回退过程也有一些负面影响, 包括: 隔离错误变得更困难 (一个目标的错误导致目标崩溃, 结果会传染到所有目标); 升级一个目标的版本可能会破坏其它一些目标, 于是后者也需要升级. Noonan 在文章最后写下了诚恳的总结:
在微服务和单体架构之间做选择时, 要注意它们各自都有自己需要考虑的因素. 我们的架构中有些部分是微服务表现更出色, 但服务端的目标迁移到微服务后的一系列麻烦是一个很好的教训, 证明这一流行趋势在某些情况下能对生产力和性能有多大负面影响. 结果对于我们来说, 单体架构才是最终解决方案.
其实他们关于微服务的某些看法是很眼熟的. 今年早些时候我们报道说, ThoughtWorks 根据观察认为微服务尚未进入普及周期. 当时的报道写到:"主要原因之一是很多组织并没有为微服务做好准备, 他们缺少一些关于运营和自动化的基础实践". 此外, Jan 在另一篇文章中总结了多年来微服务迁移的失败案例. Berico 科技的首席软件工程师 Richard Clayton https://rclayton.silvrback.com/ 提到了他们当时遇到的一个问题:
在不同服务之间共享通用功能代码, 以消灭各个服务中的重复功能的努力却带来了巨大的负面影响, 最终导致了大规模回退.
回到原文, 有很多关于这个话题的讨论, 比如 Hacker News https://news.ycombinator.com/item?id=17499137 和 Reddit https://www.reddit.com/r/programming/comments/8xrek7/goodbye_microservices_from_100s_of_problem/ 上的这些; 有些讨论者认为与微服务无关的一些因素可能导致了这些问题. 比如, 有些评论指出 Noonan 的文章并没有引用 CI, 只有 CD, 起码这是一个奇怪的组合. 还有 评论 https://www.reddit.com/r/programming/comments/8xrek7/goodbye_microservices_from_100s_of_problem/ 认为不止微服务会引发这些问题, 所有的分布式系统都是一个样. 关于这一点我们之前也提到过, 有人使用 SOA 时有过类似的经验:
我曾在一个类似的代码库中工作过, 那时他们管它叫 SOA, 云还没开始流行. 对服务的每次调用都会启动一个完整的服务实例. 我想我们应该强制将网络延迟规定为架构设计的要素之一.
有趣的是很多讨论串谈到了微服务中数据上下文的问题. 这个话题我们探讨过很多次, 这也是微服务反对者的主要论据之一. HackerNews 的一条评论举例说:
比这还糟呢. 据我观察多数微服务架构根本就没考虑一致性 ("我们才不要乱七八糟的事务!"), 盲目地随大流还乐在其中. 我搞不懂为啥子人们会觉得, 把软件模块拆分开来然后用缓慢不可靠的网络和弱爆的手动连接 REST 处理串起来, 就能神奇地让架构面目一新哩? 我觉得人们产生这种生产力幻觉的原因是:"我把这些都搞定啦, 现在我也有一套'管它是什么即服务'的先进玩意儿喽! 看看那酷毙的数据面板上闪烁的小绿灯吧, 我们可是为了它干了好几个月呢!"
另外, 为微服务定义域是多年来我们一直强调的微服务部署关键环节. 有一篇 PPT 介绍了如何使用 DDD 解构单体应用, Reddit 的一个讨论串也谈到了这一点:
建立一个出色的微服务架构是很难的. 我现在觉得关键在于恰当地分隔你的域, 当系统进化时持续关注这一层面. 微服务并不像它的名字那样, 它不必非得那么小, 但是要搭配适合这个架构的元素. 很多人的失败正是因为忽视了这一点.
其他人怎么看? 比如说, Segment 的微服务架构出现的问题能否用其它方式解决, 无需退回单体应用? 或者一开始的单体架构是否有办法进化得更好, 解决原来的问题, 而无需切换到微服务?
来源: http://www.tuicool.com/articles/meeQF3N