微服务失败的 11 个原因
微服务“很香”,它有许多优势,比如更快的开发、更好的可扩展性、更小的独立团队等等。但是,很多团队却在微服务上举步维艰,没有很好利用其优势。原因到底是什么?这是本文作者试图回答的。
过去几年,我对推进数字化转型的多家产品团队进行了架构审查。我发现:大多数团队都是遵循微服务架构来构建产品。更快的开发、更好的可扩展性、更小的独立团队、独立部署和使用正确的技术来完成工作等等,这些优势让他们有充足的理由采用微服务架构。
但是,我经常发现:很多团队在微服务上举步维艰。他们并未充分利用微服务的优势。为什么许多团队在微服务之路上“举步维艰”?这是我试图回答的。
如果你是微服务新手,我推荐你阅读 Martin Fowler 关于微服务的文章。
https://martinfowler.com/articles/microservices.html
这篇文章阐释了何为微服务架构:
微服务架构的风格是一种将单个应用程序开发成一套小型服务的方法,每个应用程序都在自己的进程中运行,并与轻量级机制(通常是 HTTP 资源 API)进行通信。这些服务是围绕业务功能而构建的,并且可以由完全自动化的部署机制来独立部署。这些服务只有最低限度的集中管理,可以用不同的编程语言编写,并使用不同的数据存储技术。
1. 管理层低估开发微服务的复杂性
我曾与许多非常看好微服务的客户一起合作过。对他们来说,微服务就是解决他们所有问题的“灵丹妙药”。
当讨论逐渐深入,我发现:大多数团队及其管理层都低估了微服务开发的复杂性。
要开发微服务,开发人员需要一个高效的本地环境设置。
随着系统中服务数量开始增加,在一台机器上运行应用程序的子集变得越来越困难。特别是当你使用消耗较多内存的语言(如 Java)构建应用程序时,更是如此。
下面是与本地开发设置相关的要点:
1.本地开发的第一个重要方面是要有一个好的开发机器。
我发现,大多数组织想要使用所有最新的、最先进的技术,但却不想替换掉糟糕的 Windows 开发机器。开发人员受限于他们的开发机器。我曾见过开发人员使用 VDI 映像或配置交叉的机器来构建基于微服务的系统。这不仅降低了他们的工作效率,而且让他们无法完全投入工作。使用糟糕的开发机器带来的副作用就是,开发人员无法得到快速反馈。如果知道了必须等数分钟才能运行集成测试套件,那么你就不会编写更多的集成测试套件,免得让你更痛苦。糟糕的开发机器将会导致糟糕的开发实践。
2.一旦你为开发人员配备了合适的开发机器,那么下一步就是确保所有服务都使用构建工具。
你应该能在一台新机器上构建整个应用程序,而不需要进行太多配置。根据我在微服务方面的经验,使用能构建整个应用程序的根构建脚本也会有所帮助。
3.下一个要点是要让开发人员能轻松地在他们的系统上运行应用程序的各个部分。在配置了所有端口和卷的情况下,你应该使用多个 docker-compose 文件来提供不同服务。
4.接下来,如果你使用的是类似于 Kubernetes 的容器编排工具,那么你应该投资类似于 Telepresence 这样的工具,以便轻松调试 Kubernetes 集群中的应用程序。
如果组织对微服务开发的复杂性缺乏理解,那么团队速度将会随着时间的推移而下降。
2. 没有将库和工具更新到最新版
我发现一个新的平台已经成为一种遗产。团队没有确保依赖项是最新的版本,或者将像数据库这样的工具升级到最新版本。
因此,两年前开始的现代化改造,如今已经背负长达数月的技术债。
几年前,许多团队开始将 Spring Cloud Netflix OSS 项目用于微服务。他们使用像 Kubernetes 这样的容器编排工具,但是因为是从 Netflix OSS 开始的,所以他们没有使用 Kubernetes 提供的所有功能。当 Kubernetes 内置了服务发现时,他们仍然使用 Eureka 作为服务发现。
此外,通过类似 Istio 这样的服务网格,你可以摆脱 Netflix OSS 提供的大部分服务。这有助于降低复杂性,并将许多“横向关注点”(cross cutting concerns)转移到平台上。
需要记住的另一点是,要使所有服务的依赖项版本保持同步。
最近,我在帮助一个客户,它使用 Spring Boot 来构建微服务。在过去两年,他们已经构建了 20 多个 Spring Boot 服务。在其环境中,他们使用的 Spring Boot 版本从 1.5 到 2.1 不等。这意味着一旦有人设置他们的机器时,他们必须下载多个版本的 Spring Boot。
此外,他们还缺乏自 1.5 版本以来在 Spring Boot 中所做的许多改进。
建议是,组织应该在其积压中为这些升级创建技术债务项。这些技术债务项应作为架构委员会会议的一部分加以讨论,并定期予以解决。在我的上一个项目中,我们每三个月设置为期一周的 sprint,将所有依赖项更新到最新版本。
3. 利用共享服务促进本地开发
由于本地开发状况不佳,大多数团队开始依赖于共享环境来获得关键服务。开发人员机器中的第一件事就是数据库。大多数年轻的开发人员并没有意识到基于共享数据库的开发是“邪恶的”。
下面,是我在共享数据库中看到的主要问题:
- 团队成员必须建立一个工作的社会契约,以避免最后写入者胜出(Last write wins,LWW)问题。一个开发人员可以删除其他开发人员为他们工作编写的数据。这种工作方式既痛苦又容易失败,迟早会影响整个团队。
- 开发人员害怕实验,因为他们的工作会影响其他团队成员。我们都知道,更好的学习方法是实验和快速反馈。有了共享数据库,就可以进行实验。我们需要进行实验,以提出数据库模式,并执行任务,如性能调优之类。
- 另一个副作用就是,很难单独测试更改。你的集成测试将变得不可靠,从而进一步降低开发速度。
- 共享数据库必须像宠物一样对待,因为你不希望它出现不一致和不可预测的状态。你可能会遇到这样一种场景,开发人员希望在表是空的时候测试边缘情况,但其他开发人员需要一个表来记录。
- 只有共享数据库拥有系统工作所需的所有数据。随着时间推移,团队成员失去了更改的可追溯性,因此没有人知道,他们该如何在他们的机器上复制相同的设置。唯一的方法是获取完整的数据库转储并使用它。
- 如果未连接到网络,就很难开展工作。这种情况通常发生在你通勤时间过长或乘飞机的时候。
数据库只是共享服务的一个示例,但它也可以是消息队列、集中缓存(如 Redis)或任何其他服务可以发生改变的服务。
解决这一问题的最好方法是,让开发人员可以轻松地在他们的机器上运行数据库(作为 Docker 容器),并投资创建 SQL 脚本来设置模式和初始主数据。这些 SQL 脚本应该保存在版本控制中,并像维护任何其他代码一样进行维护。
4. 版本控制托管平台缺乏可见性
我曾与一个客户进行合作,当时,他们的版本控制系统中有 1000 多个仓库。他们使用的是 Gitlab 版本控制平台。他们有 5 个产品,每个产品都由多个微服务组成。
我问他们的第一个问题是帮助我们了解哪些服务及其各自代码库是产品 A 的一部分。他们的首席架构师不得不花一天时间弄清楚所有产品 A 的仓库。一天过去了,她还是不能确定是否弄清楚了所有服务。
解决这个问题的最好方法是,从一开始就以某种方式对你的微服务进行分组,这样,你就可以随时了解产品的生态系统。Gitlab 提供一种方法来创建一个组,然后在其中创建项目仓库。Github 并没有组功能,因此你可以使用主题或命名约定来实现它。
我个人更喜欢 mono repos,因为我发现他们真的很方便。我遇到的大多数开发人员都认为它是一种反模式。我同意 Dan Lua 的观点,他提到了 mono repo 的以下好处:
- 简化的组织
- 简化的依赖关系
- 工具
- 跨项目变更
5. 服务没有明确定义
大多数团队并不知道什么应该被视为服务。关于究竟是什么构成一个单一的微服务,人们对此存在很多混淆的认识和困惑的概念。
让我们举一个例子,假设你的应用程序具有类似插件的架构,在这个架构中,你集成了多个第三方服务。每个集成应该是一个微服务吗?我看到很多团队,都在为每个集成创建一个微服务。随着集成数量的增加,这种情况很快就会失控,以至于无法管理。这些服务通常太小,以至于将它们作为单独的进程运行,会增加更多的开销。
我认为,哪怕只拥有少量的大型服务,总比提供太多的小型服务要好得多。我将从创建一个服务开始,该服务对业务组织中的整个部门进行建模。这也符合 DDD(领域驱动设计, Domain Driven Design)。我将一个域分为子域和有界上下文。有界上下文表示公司内部的一个部门,如财务部门和营销部门。你可能认为,这会导致大型服务的出现,你是对的。
随着知识经验越来越多,你可以转向代表更小问题的细粒度微服务。你能应用单一责任原则(Single Responsibility Principle)来了解微服务是否变得过大,做的事情是否过多。
然后,你可以将它分解成更小的独立服务。任何服务都不应该直接与其他服务的数据库通信。他们应该只通过已发布的合同进行沟通。在 Microservices.io 网站上,你能了解更多关于按子域模式分解的内容。
https://microservices.io/patterns/decomposition/decompose-by-subdomain.html
我也遵循了 Backendlore 文档中提到的建议。
https://github.com/fpereiro/backendlore
这个建议可以帮助将服务限制在服务通信上,而服务通信是微服务系统性能低下的首要原因。如果两条信息相互依赖,那么它们应该属于同一个服务器。换句话说,服务的自然边界应该是其数据的自然边界。
6. 代码重用策略不明确
我曾经和一个客户合作,该客户在他们所有基于 Java 的微服务复制了四个与特定问题相关的 Java 文件。因此,如果在该代码中发现 bug 的话,就需要将其修复应用到所有地方。我们都知道,在时间紧迫的情况下,我们会错过将更改应用于一个或多个服务。这样会浪费更多时间,增加挫败感。
这并非说开发团队不懂正确的事情。但是,按照组织结构,人们总是默认采用简单且容易出错的做事方式。
正确的方法是,使用像 Bintray 或 Nexus 这样的工件管理器,并在那里发布依赖项。然后,每个微服务都应该依赖于这个库。你需要构建工具,以便在发布新版本的库时,所有的微服务都应该进行更新和重新部署。
使用微服务并不意味着你不应该使用迄今为止对我们有用的最佳实践。你需要对工具进行投资,使微服务的升级变得更容易,这样人们就不必这样做了。
在没有适合的工具和自动化的情况下,使用微服务会导致灾难。
7. 多语言编程设计
我发现团队使用多种编程语言、多种数据库、多种缓存,并以最佳工具的名义进行工作。所有这些都在项目初始阶段起作用,但随着你的产品投入生产,这些选择开始显露“本色”。
原因就像我们在构建 JavaSpringBoot 应用程序,但是我们意识到 Java 占用更多的内存,而且性能也很差,所以我们决定改用 Node.js。在我的上一次任务中,我向团队解释说他们的推理能力很弱。
1.Note.js 比 Java 性能更好
如果你的工作负载是基于 I/O 的,Node.js 通常会表现的更好。但在任何计算密集型的工作负载上,Java 都胜过 Node.js。通过响应式范式(reactive paradigm),你可以使用 Java 为 I/O 工作负载提供更好性能。在 I/O 工作负载方面,Spring Boot Reactor 的性能相当于 Node.js。
https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/javascript.html
https://blogs.sap.com/2018/08/03/spring-boot-reactive-vs.-node.js-in-sap-cloud-platform-reflection-on-throughput-measurement/
2.Node.js 比 Java 消耗更少的内存
在一定程度上,这是正确的说法。Java Spring Boot 应用程序并不像大多数想象的那么糟糕。我在一个 Spring Boot Java 微服务上运行了负载测试,内存消耗仍然没超过 1 GB。你可以通过 OpenJ9 JVN,限制对类路径的依赖,并通过调优默认的 JVM 参数来优化 Java 内存利用率。此外,在 Java 中还有 Spring Boot 的新替代品,如 Micronaut 和 Quarkus,它们消耗的内存相当于 Node.js。
3.Node.js 比 Java 效率更高
这取决于编写代码的开发人员。使用静态类型和静态分析工具的 Java 可以帮助在开发生命周期的早期发现问题。
大多数情况下,这完全取决于上下文。如果你的开发人员还不够成熟的话,那么无论你使用什么编程语言,你开发的都将是糟糕的产品。
我建议一家组织要发布一个团队可以使用的语言列表。我认为 2~3 就是个很不错的数字。另外,要列出一种语言比另一种语言更适合的理由。
在选择一门语言前,你应该考虑以下一些问题:
- 找到成熟的企业软件开发人员有多容易?
- 重新培训开发人员掌握新技术有多容易?我们发现 Java 开发人员可以相对容易地学习 Golang。
- 初始团队之外的开发人员贡献、转移和维护其他人编写的代码有多容易?
- 就工具和库的方面而言,生态系统有多成熟?
这不仅仅局限于编程语言,也适用于数据库领域。如果你的系统中已经有了 MongoDB,那么你为什么要在生态系统中使用 ArangoDB 呢?它们都主要是文档数据库。
要始终考虑使用多种技术的维护和操作方面。
8. 人员的依赖性
这并非微服务特有的现象,但在微服务生态系统中却变得更加普遍。原因是,大多数团队专注于他们的特定服务,因此他们并不了解完整的生态系统。在我与不同客户的工作中,我发现,只有一群架构师了解整体情况。但是,这些架构师的问题在于,他们并不积极参与日常活动,因此他们对开发的影响力是有限的。
我认为最好的办法是,确保所有团队都有一个架构团队的代表,这样他们就可以使他们的团队与整个架构团队的路线图和目标保持一致。要成为一个成熟的组织,你需要投资于建立一个轻量级的治理。
9. 文档的缺乏
在过去几年,我们接触过的大多数组织都在文档方面遇到困难。大多数开发人员和架构师要么不去编写文档,要么编写的文档毫无用处。即使他们想写,他们也不知道应该如何记录他们的架构。
我们至少应该记录以下内容:
- 设计文档
- C4 模型中的上下文和容器图
- 以架构决策记录的形式跟踪关键架构决策
- 开发人员入门指南
我建议在版本控制系统中维护所有的文档。
https://c4model.com/
http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions
10. 功能超过平台成熟度
在其他观点中,我简要地提到了这个原因,但我认为,它值得作为一个顶级原因来提及。微服务要比传统的单体式应用(monolithic application)更为复杂,因为你正在构建一个包含许多活动部件的分布式系统。大多数开发人员还不了解系统的不同故障模式。大多数微服务在构建时都考虑了令人快乐的路径。因此,如果你的管理层只想仅仅关注功能,那么你注定会失败。因为在薄弱平台上构建的功能是无法提供价值的。
组织需要有平台思维。平台思维可不仅仅意味着使用容器和 Kubernetes。它们是解决方案的一部分,但本身并非完整的解决方案。你还需要考虑分布式跟踪、可观察性、混沌测试、函数调用与网络调用、服务间通信的安全服务、可调试性等等。这需要在构建正确的平台和工具团队方面付出认真的努力和投资。
如果你是一家资源有限的初创公司,我的建议是,你要重新考虑微服务战略。了解你所面临的问题是什么。
11. 缺乏自动化测试
大多数团队都知道自动化测试对产品的整体质量有多重要,但是他们仍然没有做到。
微服务架构为测试地点和测试方式提供了更多选择。如果你不进行彻底的自动化测试,那么你将会失败得很惨。关于这一点,我不会再赘述,因为网上很多人都写过这方面的内容了。
下图是我从微服务测试的文章找到的,这篇文章来自 Martin Fowler 的网站,讨论了基于微服务的系统的测试金字塔。