在文章《微服务架构概览》中,我详细讨论了微服务架构以及在现代软件开发中使用它的优缺点。那么,什么是微服务架构呢?我给出的定义是:

微服务架构是将软件系统分解成可独立部署的自治模块,这些模块通过轻量级的、语言无关的方式进行通信,共同实现业务目标。

软件系统是复杂的。由于人脑只能处理一定程度内的复杂性,大型软件系统的高复杂性导致了许多问题。大型复杂的软件系统难于开发、增强、维护、现代化和规模化。多年来,为解决软件系统的复杂性做过许多尝试。在上世纪 70 年代, David Parnas Edsger W. Dijkstra 引入了模块化软件开发,以解决软件系统的复杂性。在上世纪 90 年代,引入了分层软件架构来处理业务应用程序的复杂性。自本世纪初以来,面向服务的架构(Service Oriented Architecture, SOA)成为开发复杂业务应用程序的主流。微服务架构是处理现代软件应用复杂性的最新方法。此时,大家可能会提出一个问题:为什么我们突然需要一个新的软件开发方法?简而言之,与软件开发相关的整个生态系统在过去十年中发生了巨大的变化。如今,软件使用敏捷方法开发,使用 CI/CD 在容器 + 云上部署,在 NoSQL 数据库上持久化,在现代浏览器或智能手机上展现,机器通过高速网络连接。由于这些因素的出现,在 2012 年诞生了微服务架构。

微服务还是单体架构

主要有两类人对微服务和单体架构持相反的观点。对于一群人来说,微服务架构完全是关于货物崇拜或炒作驱动的开发,这只是痴迷于技术的开发人员的游乐场。对于另一群人来说,微服务架构是“一个管控所有的架构”,它可以消除软件系统的任何复杂性。在我看来,微服务和单体架构是互补的。如果从长远来看,这个应用程序仍然会较小,则单体架构是适合的方法。另一方面,对于大型而复杂的应用程序或有潜力变得大型而复杂的应用程序,微服务架构是正确的解决方案。现代软件开发是如此庞大,以至于微服务架构和单体架构将像 SQL 和 NoSQL 一样的方式共存。

微服务的最佳实践

正确设计微服务架构非常具有挑战性和困难。与单体架构为所有问题提供一个解决方案相反,微服务架构为不同的问题提供不同的解决方案。如果选择了错误的解决方案,那么微服务架构就是一个注定要爆炸的定时炸弹。一个设计糟糕的微服务架构比一个单体架构还要糟糕。为微服务架构定义一组最佳实践也很有挑战性。我曾在一些会议上看到一些著名的、受人尊敬的软件工程师提出了微服务架构的最佳实践,但这些实践却适得其反。

在这里,我提出了一些最佳实践,这些实践将有助于开发有效的微服务应用程序,在这些应用程序中,目标项目应该存在超过 6 个月的时间,并且团队规模从中等到大型(6+ 开发人员)。另外,还有一些关于微服务架构最佳实践的文章,例如 Martin Fowler()的《微服务架构的特征(  Characteristics of a Microservice Architecture )》、Chris Richardson 的《微服务模式(  Microservices Patterns  )》、以及 Tony Mauro 的《在 Netflix 采用微服务:架构设计的教训( Microservices at Netflix: Lessons for Architectural Design )》。也有一些很棒的演讲,例如 Stefan Tilkov 的《微服务模式和反模式(  Microservices Patterns and Antipatterns  )》,David Schmitz 的《微服务严重失败的 10 个技巧( 10 Tips for failing badly at Microservices )》,Sam Newman 的《微服务原理(  Principles of Microservices  )》。

1. 领域驱动设计:

开发微服务的首要挑战是将大型、复杂的应用程序分割成小型、自主、独立的可部署模块。如果微服务没有以正确的方式进行分割,将会出现紧耦合的微服务,这些微服务将具有单体架构的所有缺点,并具有分布式单体架构的所有复杂性。幸运的是,已经有一个解决方案可以在这方面提供很大的帮助。Eric Evans 当时是一名软件工程顾问,他在不同公司的业务应用程序中遇到了关于软件复杂性的反复出现的问题,于是在 2004 年出版的《领域驱动设计:处理软件核心的复杂性》一书中总结了他的宝贵见解。该书概述了三个核心概念:

  • 软件开发团队应该与业务部门或领域专家密切合作。

  • 架构师 / 开发人员和领域专家应该首先进行战略设计:找到有界的上下文和相关的核心域以及普遍存在的语言、子域、上下文映射。

  • 然后,架构师 / 开发人员应该进行战术设计,将核心域分解为细粒度的构建块:实体、值对象、聚合、聚合根。

领域驱动设计的详细讨论超出了这篇文章的讨论范围,但是你应该读一下起初的 DDD 书籍 Eric Evans 的《领域驱动设计:处理复杂的软件(蓝皮书)( Domain Driven Design: Tackling Complexity in the Heart of Software  (Blue Book))》,或者更现代一点儿的 DDD 书籍  Vaughn Vernon 的《实现领域驱动设计(红皮书)( Implementing Domain Driven Design (Red Book))》。如果将一个大型系统划分为核心域和子域,然后将核心域和子域映射到一个或多个微服务,那么我们将得到理想的松耦合微服务。

2. 每个微服务一个数据库

在将复杂的应用程序拆分为多个微服务模块之后,下一个挑战出现了,如何处理数据库?我们是否应该在微服务之间共享数据库?这个问题的答案是一把双刃剑。一方面,在微服务之间共享数据库将导致微服务之间的强耦合,这与微服务架构的目标正好相反。即使是数据库中的一个小更改也需要团队之间的协调同步。此外,在一个服务中管理数据库的事务和锁就已经足够具有挑战性了。而在多个分布式微服务之间管理事务和锁更是一项艰巨的任务。另一方面,如果每个微服务都有自己的数据库或私有表,那么在微服务之间交换数据就打开了挑战的潘多拉盒子。因此,许多著名的软件工程师都提倡在微服务之间共享数据库,将其作为一种实用的解决方案。然而,在我看来,微服务是关于可持续和长期的软件开发的。因此,每个微服务都应该有自己的数据库(或者私有表)。

3. 微前端

不幸的是,大多数的后端开发人员对前端开发有一种过时的看法,认为前端开发很简单。由于大多数软件架构师都是后端开发人员,他们很少关注前端,而前端在架构设计中往往被忽视。通常在微服务项目中,后端与它们的数据库被很好地模块化,但只有一个整体前端。在最好的情况下,他们考虑用最热门的单页面应用(React、 Angular、Vue)其中之一来开发独体前端。这种方法的主要问题是,前端的单体架构和后端单体架构一样糟糕,正如我前一篇文章所述。另外,当前端因为浏览器的变化而需要更新时,它就需要一个大的更新(这就是为什么那么多公司仍然使用过时的 Angular 1 框架的原因)。网络是简单的,但非常强大,并天生提供了穿透力。开发基于单页面应用的微前端有很多方法:使用 iFrame、Web 组件或借助于(Angular/React)元素。

4. 持续交付

微服务架构的关键卖点之一是每个微服务都可以独立部署。如果你有一个系统,例如 100 个微服务,并且只需要更改一个微服务,那么你可以只更新一个微服务,而不需要修改其他 99 个微服务。但是,在没有自动化的情况下独立部署 100 个微服务(DevOps、CI/CD)是一项艰巨的任务。要充分利用微服务的这一特性,需要 CI/CD 和 DevOps。使用没有 CI/CD、DevOps 的微服务架构,自动化就像购买最新的保时捷,然后用手刹去驾驶它。难怪微服务专家 Martin Fowler 将 CI/CD 列为使用微服务架构的三个先决条件之一。

5. 可观察性

微服务架构的主要缺点之一是,软件开发变得简单,而牺牲了运维。使用单体架构,监视应用程序要简单得多。但是许多微服务在容器上运行,整个系统的可观察性变得非常重要和复杂。甚至日志记录也变得很复杂,要将来自许多容器 / 机器的日志聚集到一个中心位置。幸运的是,市场上已经有很多企业级的解决方案了。例如,ELK/Splunk 提供微服务的日志记录。Prometheus/App Dynamics 提供工业级监控。微服务世界中另一个非常重要的可观察性工具是 Tracing。通常,对一个微服务的一个 API 请求会导致对其他微服务的几个级联调用。要分析微服务系统的延迟,需要测量每个微服务的延迟。Zipkin/Jaeger 为微服务提供了出色的跟踪支持。

6. 统一技术栈

微服务架构告诉我们,对于一个微服务,采用最适合该微服务的编程语言和框架。这种说法不能照字面理解。有时,一个微服务可能需要一个新的技术栈,例如 CPU 密集 / 高性能任务,可能会选择如 c++ /Rust 之类的编程语言。如果微服务与机器学习一起工作,那么 Python 可能是更好的选择。但是在没有任何理由的情况下使用不同的编程语言 / 框架会导致过多的编程语言和框架而没有任何实际的好处。思考这么一个应用场景,一个微服务是使用 Spring Boot + Kotlin+ React + MySQL 开发的,另一个用的是 JakartaEE + Java + Angular + PostgreSQL,下一个是 Scala + Play Framework + VueJS + Oracle,则需要大量的工作去维持这些不同的编程语言、数据库、框架,而没有太多的收益。

7. 异步通信

微服务架构中最具挑战性的设计决策之一是服务之间如何通信和共享数据。当每个微服务都有自己的数据存储时,这一点就更为重要了。通常,一个微服务可以单独存在,但是它不能单独实现所有的业务目标。所有微服务为了实现业务目标而在一起工作,为了在一起工作,它们需要交换数据或触发其他微服务来执行任务。在微服务之间进行通信的最简单和最常见的方式是通过同步的 REST API,这是一种实用但临时的解决方案。如果服务 A 同步调用服务 B,服务 B 同步调用服务 C,服务 C 同步调用服务 D,那么延迟就会增加。此外,由于微服务主要是分布式系统,它们有可能会失败。通常,同步微服务会导致级联失败,即一个服务的失败会导致其他服务的失败。微服务之间的同步通信也导致了微服务之间的紧密耦合。对于长期解决方案,微服务应该异步通信。微服务之间的异步通信有很多方式:通过消息队列,例如 Kafka,通过异步的 REST (ATOM)或 CQRS。

8. 微服务优先

许多专家认为,对于新项目,最好从松耦合的单体架构开始,因为微服务架构需要大量的初始工作来设置运维。在他们看来,一旦项目变得足够成熟,“漂亮的”设计就可以很容易地转化为微服务。然而,在我看来,这种方法在大多数情况下都会失败。实际上,实体内部的模块将是紧耦合的,这将使其很难转换成微服务。而且,一旦应用程序投入生产,在不破坏应用程序的情况下将其转换为微服务将变得更加困难。因此,我的建议是,如果最终有使用微服务架构的计划,那么就从微服务开始。

9. 基础设施优于类库

在微服务软件开发的早期,Netflix 主要使用 Java 编程来开发微服务。他们还开发了许多类库(Netflix 的 OSS 栈,包括 Hystrix、Zuul)。许多公司通过 Netflix 跟进并开始使用 Netflix OSS。后来,许多公司(包括 Netflix)发现,Java 实际上并不是开发微服务的语言,因为它体积庞大,而且冷启动问题严重。Netflix 后来转向 Polyglot 微服务范式,并决定不再进一步开发 Netflix OSS,这导致了追随者公司陷入困境。因此,与其大量投资于特定语言的类库(如基于 Java 的 Netflix OSS),不如使用框架(如服务网格、API 网关)。

10. 组织考虑

大约 50 年前(1967 年),Melvin Conway 观察到公司的软件架构受到组织结构的限制(Conway 定律)。尽管这一观察发现已有 50 年历史,但麻省理工学院(MIT)和哈佛商学院(Harvard Business School)最近发现,这一法则在当今仍然有效。如果一个组织计划开发微服务架构,那么它应该使团队规模更为恰当(两个“美国”比萨团队:7±2 人)。此外,团队应该是跨职能的,最好有前端 / 后端开发人员、运维工程师和测试人员。微服务架构只有在高层管理者也相应地改变他们的观点和愿景的情况下才能发挥作用。

作者介绍:

Md Kamaruzzaman,热情的软件架构师,终身学习者,热心的读者,偶尔也写写文章。

原文链接:

Effective Microservices: 10 Best Practices