作者 | Holly Cummins
译者 | 王强
策划 | 辛晓亮
本文要点:
- 微服务是一种手段而非目标
- 分布式并不能确保解耦性
- 契约测试在所有微服务架构中都是重要组成部分
- 前端、后端和集成层,以及业务逻辑中都需要做分解
- 如果企业没有能力快速、独立地发布微服务,就会丧失微服务的许多收益
在去年 11 月的 QCon Plus 上,我介绍了微服务可能走入歧途的一些路径。我是 IBM 的一名顾问,我的一部分工作是帮助业务迈向云原生。本文提到的这些问题都是从我的经验总结出来的 -- 不幸的是,它们在实践中是非常常见的。
我看到的第一个问题是,有时我们甚至不知道问题出在哪里。我们认为自己应该去做微服务,但却并没有真正花足够的时间去搞清楚为什么我们要这样做。
我们要解决的是什么问题?现在我们遇到了哪些痛点?我们做了微服务之后,哪些事情会迎来改善?人们总是倾向于尝试新事物,尤其是对我们这些技术人员来说更是如此。我们想一步跳到解决方案上,想要把玩闪亮的新玩意儿。可悲的是,虽说弄清楚我们要解决的问题是非常重要的,但这远没有尝试新的解决方案来得有趣。
容器加速了我们渴望尝试新方案的倾向,因为它是一种魔法般的技术,堪称伟大的解决方案。它们是如此轻巧、如此便携,让许多事情都能变得更好。于是到最后我们打定主意:“因为我已经有了这么多容器,如果只在一个容器中运行我的应用程序,那将是对容器能力的严重浪费。我应该在尽可能多的容器中运行它!”不幸的是,“没有足够的容器”并不是一个有意义的问题表述。
简历驱动的开发
我看到的另一个问题是简历驱动的开发。我们瞧了瞧自己的简历,里面应该写着“微服务”的地方空白一片。这看起来可不够水平,所以我们会说,“我可以通过重构公司的技术栈来解决这个问题”。你可能在想,“不,这也太夸张了,Holly。肯定没有人真的会为了他们的简历来做架构决策吧?”事实证明...... 他们会的。
红帽公司最近做了一项调查,研究了基于容器的开发工作的主要驱动因素,结果发现职业发展是头号驱动力。所谓职业发展,就是简历驱动开发的高情商说法而已。
在简历上缺少微服务的开发经验可是了不得的大事,因为目前微服务简直就是新的标准规范。就算我们没有加入疫情以来的离职大潮,此刻也没有在寻找新的工作,我们也不希望自己成为异类。当我们环顾四周,发现似乎其他人都在做微服务,那么我们当然会想:如果他们都在做微服务,我却不做会不会有什么问题?我把这种心理称为“微服务羡慕”。
微服务不是目标
“微服务羡慕”是一个问题,因为微服务并不是我们应该羡慕的那种东西。我们的一个顾问有一个诀窍,如果一个客户一直在谈论 Netflix 并想要转向微服务,他就知道这桩合作有问题了。几乎可以肯定的是,他们转向微服务并没有正确的理由。如果对话更深入一些,涵盖了耦合和内聚等内容,那么他就知道合作的方向没什么问题。
微服务转型的出发点不应该是微服务本身。微服务是实现业务敏捷性 / 弹性或同等事物等更高层次目标的手段。实际上,微服务甚至不是唯一的手段,它们只是一种选择。
分布式单体
有一个问题是一定要问的:“你做的是微服务,还是分布在数百个 Git 仓库上的一个单体?“不幸的是,这就是我们经常看到的情况。这是一个分布式的单体,是一种可怕的存在。它很难推理。它比正常的单体更容易出错。在传统的单体中,所有的东西都包含在一个单一的开发环境中,你可以从中得到一些好处,如编译时检查和 IDE 重构支持。因为你总是在一个进程中执行操作,所以函数执行起来会有保障。你不必费心记住那些分布式计算的错误,用不着管服务发现,也不需要处理你试图调用的东西已经停止存在的那种情况。正常单体里各种事物都是比较安全的。相比之下,如果我们去掉了单体的安全性,但保留了它的耦合性,我们最终得到的会是云原生意大利面条。
分布不等同于解耦
几年前,我被叫到一个陷入困境的项目中执行救援任务。我到岗以后,团队对我说的第一件事就是“每当我们改变一个微服务时,另一个就会坏掉“。如果你一直在关注微服务的各种承诺,你就会知道这种情况与应该发生的事情完全相反。微服务应该是相互独立、解耦的。然而,如果你只是让你的系统分布化,解耦并不会是免费附赠。“分布式“和“解耦“的共同点不过是这俩单词都以 D 开头,但它们就不是一回事。
我们很容易陷入这样的困境:拥有一个高度分布式的系统,它有着分布式所带来的所有痛苦,同时仍然是完全纠缠和耦合在一起的。在上述案例中发生的事情就是这样。当我开始探索他们的代码库时,我不断在每个仓库中都看到相同的代码。这个应用程序的对象模型是相当复杂的,有大约 20 个类,其中一些类有 70 个字段。这是一个复杂的模式。
微服务开发的原则之一是放手 DRY,避免使用公共库,因为它们是耦合的源头。在这个案例中,为了避免中央对象库的耦合,每个微服务在其代码中都有一个剪切粘贴的对象模型副本。但如果领域模式仍然是共享的,耦合依旧无法避免。复制对象代码并不能消除耦合,它只是消除了编译时检查的可能性。一个字段名改变了仍然会破坏所有事物,但这种破坏直到运行时才会发生。
这个悲伤的故事表明了领域驱动的设计原则在微服务中的重要性。我们要实现的理想是,每个微服务都能整齐地映射到一个领域。这样做的一个副作用,也是你做得对的一个标志,就是你的微服务会有很小的接口。如果我们沿着技术边界而非领域边界来做分解,最终就会出现像我看到的那种情况;每个微服务都有一个巨大的、脆弱的接口。其结果会是一团乱糟糟的意大利面条。
火星气候轨道航天器
虽然从技术上讲它是一个航天器,而不是一个微服务平台,但火星气候轨道航天器很好地展示了分布和解耦之间的区别。NASA 在 1998 年发射了火星气候轨道航天器,其任务是研究火星气候。遗憾的是,航天器没有成功绕过火星在轨道上运行;相反,探测器坠入了火星大气层。NASA 的事后调查发现,问题源于两个不同的控制系统之间的关系,这两个系统是由不同的团队建造的。大多数时候,转向操作是由探测器本身的一个系统完成的。每隔几天,当轨道飞行器进入地球的视野时,佛罗里达州的监督控制系统就会发出航线修正指令。这可能是一个系统可以做到的最分布化的设计了;它的一部分是在太空中飞行。但这两个系统之间的领域实际上是相似的:两者都在处理发动机推力计算问题。这两个团队在沟通中并没有搞清楚接口应该是什么样子,所以他们最终使用了不同的度量单位——太空部分使用公制单位,地球部分使用英制单位,所以灾难发生了。我们可以有把握地说,在这个案例中系统是非常分散的,但这种分布并没有什么收益。
消费者驱动的契约测试
上面提到的这种微妙的沟通问题在有多个团队参与开发的场景中经常出现。还好有一种很好用的缓解措施:消费者驱动的契约测试。在 IDE 不会帮助我们检查类型的系统中,我们需要测试各种集成,但又希望尽量减少全面的集成测试。集成测试很重,运行成本很高,很脆弱,而且本身就是耦合的。如果我们已经投资开发了微服务,那么肯定不想在测试时倒退到一个巨大的集成单体上。那么,我们怎样才能确信我们所构建的东西是真正有效的呢?
Mock 是一种常见的解决方案,但它本身就有一个问题。为了设置 mock,生产团队和消费团队在开发之初就会讨论好接口应该是什么样子的。他们达成了一个协议,然后消费者去尝试写一个 mock,这个 mock 就是他们印象中生产团队所说的代码的样子。理想情况下他们会做得很好。问题是他们把自己的假设也写进了 mock 中,因为 mock 是他们写的,而他们也许并不知道其他代码应该是什么样子或者应该有怎样的行为,因为那不是他们写的代码。
运气好的话,他们会得到正确的结果。单元测试全部通过,而且在集成阶段也继续绿灯,一切都很好。不幸的是,这种运气并不是每次都有的。有时,实际的实现与消费团队所理解的并不一样,要么是因为生产团队改变了他们的想法,要么是因为某个位置上有人做了一个不正确的假设。在这种情况下,测试仍然会通过。然而,当我们真正开始整合真实服务时,它就会失败。问题是,mock 的行为没有经过真实服务的验证。生产团队甚至很可能从未见过创建出来的 mock。
一个更好的选择是做一个消费者驱动的契约测试。契约测试的好处,也是它与 mock 的不同之处在于,双方都会与契约测试互动。对于消费者来说,契约测试就像一个方便的 mock。
在另一边,契约测试对提供方来说是一个方便的功能测试。它是一种更深入的验证措施,而不仅仅是像 OpenAPI 那样的语法检查。契约测试实际上也是对语义和行为的检查,节省了提供方团队编写功能测试的时间。
如果所有东西都能兼容、运作正常,那么所有的契约测试都会通过。它可以快速提升团队信心,因为它们运行起来很便宜、轻便。如果提供方团队破坏了什么东西,他们的测试将失败,并在破坏性变更逃逸到集成环境之前发出早期警报。如果 API 发生变更,就会在双方(或连接双方的 broker)推出新版契约。
现在市面上有几种契约测试系统可选。如果你在 Spring 的生态系统中,Spring Contract 就非常好用。如果你涉猎广泛,那么我推荐 Pact,它有着几乎所有你可能使用的语言的绑定。
企业毛球
当然,即使我们解决了所有的测试问题,即使我们在业务逻辑层有一套漂亮的解耦微服务,也不能保证我们一定能成功。在系统中还会有其他许多元素,这些元素可能是我们在设计真正干净的微服务架构时没有考虑到的。我们对业务逻辑感到非常兴奋,而忘记了前端和后端的事物,以及所有的胶水。在企业架构中胶水尤其常见,而且非常粘手。我们的一位架构师把这种情况称为企业毛球。
如果我们把所有的功能分解工作都集中在业务层,最后往往会得到一堆整齐的解耦微服务,夹在一个单体的前端和一个单体的数据库层之间。在这些类型的系统中,变更会很难做。然而,从行业视角来看,我们正在进一步分解数据库以将其映射到各个微服务上,并且我们正在开发各种微前端。
但我们还没有完成分解工作。如果系统不是很复杂,我们就会有一个集成层。这个层可能负责消息传递,也可能是其他一些集成解决方案,负责将复杂的系统拉到一起。即使在架构的其他部分实现现代化之后,集成层往往还会是单体的,缺乏灵活性。团队本身可能会承受巨大的压力——我同事说这叫“恐慌三明治“。因为集成层是单体的,他们必须小心翼翼地安排所有的变更,这就阻碍了其他所有人的工作。
这种现象可能会带来很多挫折,特别是对集成团队来说更是如此。在外界看来,他们似乎反应迟钝、速度缓慢,尽管他们正在努力工作。为了解决耦合问题,我们需要采用模块化的集成模式。
如果我们不把集成层、数据库和前端层分割开来,会发生什么事情呢?几乎可以肯定的是,我们的微服务不会实现想要的效果。毛球各部分之间的依赖关系将让任何部分都无法快速移动。业务层的微服务将不能独立部署,部署的步伐也会明显断断续续。
阻碍发布的累赘
你们有多少人见过这样的情况?你非常努力地工作,创造了一些惊人的东西。你知道用户会喜欢它,但它还没有送到他们手中。价值就摆在架子上,你那惊艳四座的成果没法发布出去。即使你有一个微服务架构和一个发布板,你也无能为力。其他所有微服务都需要同时发布,因为它们需要一起测试;而除了大批量的测试,这样做的成本太高。只是填写发布清单的操作也很昂贵。企业害怕发布,因为它在过去吃过质量低劣的版本的教训。发布检查表、发布委员会、单线程测试和其他发布咒语都是为了减少可感知的风险。因为整个组织的发布期限是统一的,所以我们最后只能争分夺秒地在最后期限前把各种功能都塞进去。当然,这也让发布的风险变得更大了。某人正在跟踪一个电子表格,上面有所有微服务之间的依赖关系,这些微服务的耦合度超出了应有的水平。当然,发布的日期也必须是黄道吉日才行。当我们选择微服务架构时,可并没有想过会陷入这样的困境!所有这些用心良苦的过程都是累赘,拖累了价值到达用户手中的过程,而且往往增加了实际风险。
测试自动化
为什么会发生这样的情况?通常情况下,我们如此害怕发布的原因在于,在发布过程中需要涉及大量人工工作。尤其重要的是,真正能给我们带来信心的测试并不是自动化的,所以我们需要做大量的工作来弄清楚应用程序是否能正常工作。当我访问一个客户,听到他抱怨“我们的测试没有自动化“时,我听到的是“我们不知道我们的代码目前是否能正常工作。它可能是正常的。上次我们做人工 QA 的时候它是有效的;我们希望它还能正常跑起来“。这是一种可悲的情况。
如果你关心它,就把它自动化——你应该关心的是质量。特别是当架构已经快乱成意大利面条,并且耦合性已经悄然出现的时候,就很可能出现断点。去意大利面条化是很难的,所以我们要在一个能够快速反馈的地方尽早发现断点。如果你要做意大利面条,至少要做经过测试的意大利面条。
发布周期
人工测试只是发布过程中的一部分。在受监管的行业或以合规性为重点的行业,几乎总是会有一堆人工合规工作。合规是我们非常关心的事情——所以我们应该把它自动化。
有了所有这些人工流程和所有这些累赘后,这意味着就算我们正在部署到云端,也没有得到云原生承诺的那些好处。我们在使用的云看起来好像并不是美丽的云彩。讽刺的是,在云中,我们曾做过的那些好事情、那些好主意、那些让我们更安全的事情,实际上正在伤害我们。旧式的治理机制在云中是行不通的。它不能实现我们所期望的业务成果,而且让我们失去了很多云端应有的业务优势。
观察企业发布周期就很容易发现企业是否实现了云计算承诺的收益。几年前,我的一位同事与一家大型传统银行开了一次销售会议。他们的市场份额正被金融科技公司和新兴的挑战者银行对手吃掉。这家企业也知道他们为什么会输——因为他们无法快速跟上市场步伐。他们来找我们,解释说他们有大量 COBOL 资产,而这正是拖累他们的原因所在。(这很可能是事实。)然后他们补充说,他们显然需要摆脱所有的 COBOL 并转向微服务,因为其他人都在做微服务。然后他们又说,他们的发布委员会一年只开两次会。到这时候我的同事心就凉了半截。如果你的发布委员会每六个月才开一次会,你就会知道你们的发布节奏将是每六个月一次。你有多少个可独立部署的微服务并不重要。这么慢的节奏意味着你们不可能获得敏捷性。
这家银行需要的帮助其实并不是技术层面的;他们需要改变他们对风险的思考方式,以及他们的运营方式。他们的发布计划需要彻底的改革,他们需要一大堆的自动化。缺乏持续交付的纪律才是阻碍他们的原因,COBOL 只是背锅侠。
“我想解耦“是一个常见的客户要求,但解耦的意思不止一种。当我们希望有一个解耦的应用程序时,这并不能保证模块化。有时它只是意味着乱七八糟的东西被分散得更广而已。如果存在一些外部约束,比如发布板和陈旧的流程这样的累赘让我们止步不前,那么在我们解决这些问题之前,是不是解耦都不重要。
作者介绍:
Holly Cummins 是 IBM 的高级技术人员和创新领导者。Holly 利用技术实现创新,为各行各业的客户(从银行、餐饮、零售到非政府组织)提供服务。在她担任 IBM Garage 的首席开发人员期间,她领导了一些项目,如计算鱼的数量、帮助盲人运动员在沙漠中独自跑超级马拉松、改善老年人的医疗保健服务,以及改变城市停车的方式等。Holly 也是 Oracle Java 冠军、IBM Q 大使和 JavaOne Rock Star。在加入 IBM Garage 之前,她是 WebSphere Liberty Profile(现在的 Open Liberty)的交付负责人。Holly 与人合著了《Manning's Enterprise OSGi in Action》,并且是定期主题演讲者。她是活跃的演讲者,曾在 KubeCon(主题演讲)、GOTO、JavaOne、Devoxx、Sonar D、JavaZone、JFokus、The ServerSide Java Symposium、GOTO、JAX London、QCon、GeeCon 和 Great Indian Developer Summit 以及一些用户小组发表演讲。在加入 IBM 之前,Holly 完成了量子计算的博士课程。
原文链接:
https://www.infoq.com/articles/microservices-seven-fail/