架构有时很难——人们不断提出一些新想法,这些想法很快成为主流的“做事方式”,微服务是最新的趋势,现在是我们剖析这个想法并找到正在发生的事情的真正根源的时候了。
在微服务 的核心,我们被告知我们会发现…… ...
很多好东西 (TM)!
- “可扩展性”:“代码可以分解成更小的部分,可以独立开发、测试、部署和更新。”
- “Focus”:“……开发人员专注于解决业务问题和业务逻辑。”
- “可用性”:“后端数据必须始终可用于各种设备……”
- “简单性”:“提供了大型企业级应用程序的简化开发。”
- “响应能力”:“......使分布式应用程序能够扩展是对不断变化的事务负载的响应......”
- “可靠性”:“通过提供可在发生故障时继续运行的复制服务器组来确保没有单点故障。在发生故障后将正在运行的应用程序恢复到良好状态。”
这些听起来都比较熟悉,我想,但是关于这六个引用的有趣部分是两个来自微服务文献(博客文章、论文等),两个来自 20 年前的 EJB 文献,两个来自 Oracle,这是四十多年前的技术。在这个行业中,我们倾向于一遍又一遍地重复使用我们的炒作点。
关于微服务炒作,一家公司的博客文章[1]提供了10 个采用微服务的理由:
- 他们推广大数据最佳实践。微服务自然适合面向数据管道的架构,该架构符合大数据的收集、摄取、处理和交付方式。数据管道中的每个步骤都以微服务的形式处理一个小任务。
- 它们相对容易构建和维护。它们的单一用途设计意味着它们可以由较小的团队构建和维护。每个团队都可以是跨职能的,同时也可以专注于解决方案中的微服务子集。
- 它们支持更高质量的代码。将整体解决方案模块化为离散组件有助于应用程序开发团队一次专注于一小部分。这简化了整个编码和测试过程。
- 它们简化了跨团队的协调。与通常涉及重量级进程间通信协议的传统面向服务架构 (SOA) 不同,微服务使用事件流技术来实现更轻松的集成。
- 它们支持实时处理。微服务架构的核心是发布-订阅框架,支持实时数据处理以提供即时输出和洞察。
- 它们促进快速增长。微服务使代码和数据重用模块化架构,更容易部署更多数据驱动的用例和解决方案以增加业务价值。
- 它们可以实现更多输出。数据集通常以不同的方式呈现给不同的受众;微服务简化了为各种最终用户提取数据的方式。
- 更容易评估应用程序生命周期中的更新。高级分析环境,包括那些用于机器学习的分析环境,需要一些方法来根据新创建的模型评估现有的计算模型。微服务架构中的 AB 和多变量测试使用户能够验证他们更新的模型。
- 它们使规模成为可能。可伸缩性不仅仅是处理更多容量的能力。这也与所涉及的努力有关。微服务可以更轻松地识别扩展瓶颈,然后在每个微服务级别解决这些瓶颈。
- 许多流行的工具都可用。大数据世界中的各种技术(包括开源社区)在微服务架构中运行良好。Apache Hadoop、Apache Spark、NoSQL 数据库和许多流分析工具可用于微服务。
让我们花点时间检查一下其中的每一个,但这次是根据现有技术:
- 他们推广大数据最佳实践。自 70 年代以来,管道和过滤器架构[2]一直是软件领域的一部分,当时Unixes 提出了几个想法[3]:
- 让每个程序做好一件事。要完成一项新工作,请重新构建而不是通过添加新“功能”使旧程序复杂化。
- 期望每个程序的输出成为另一个未知程序的输入。不要用无关信息混淆输出。严格避免列式或二进制输入格式。不要坚持交互式输入。
- 它们相对容易构建和维护。请参阅上面的 Unix 哲学。
- 它们支持更高质量的代码。如果一次专注于一小部分有助于提高质量,那么请参阅上面的 Unix 哲学。
- 它们简化了跨团队的协调。这个很有趣;它表明“面向服务的体系结构(SOA)……通常涉及重量级进程间通信协议”——比如 JSON over HTTP?或者这是否意味着所有 SOA 都需要 SOAP、WSDL、XML Schema 和 WS-* 规范的完整集合?具有讽刺意味的是,微服务并没有以任何方式阻止它使用任何这些“重量级”协议,并且一些微服务甚至建议使用 gRPC,这是一种与 IIOP 更相似的二进制协议,来自 CORBA,它是“重量级”协议”的前身...... SOAP、WSDL、XML Schema 和 WS-* 规范的完整集合。
- 它们支持实时处理。实时处理实际上已经存在了很长一段时间,虽然许多此类系统使用发布-订阅或“事件总线”模型来完成它,但它几乎不需要微服务来完成。
- 它们促进快速增长。“复用模块化架构”——我们算过有多少不同的东西都以“复用”为卖点?语言当然已经做到了(OOP、功能语言、过程语言)、库、框架……有一天我想看到一些大肆宣传的东西,明确地说“能否重用?我们不关心。”
- 它们可以实现更多输出。“数据集通常以不同的方式呈现给不同的受众”——这听起来很像Crystal Reports[4]的主页。
- 更容易评估应用程序生命周期中的更新。机器学习和高级分析环境需要“根据新创建的模型评估现有的计算模型”……听起来像是一大堆动作词,但背后几乎没有实质内容。
- 它们使规模成为可能。多么有趣——EJB、事务性中间件处理(类似 Tuxedo)和大型机也是如此。
- 许多流行的工具都可用。我不认为我必须非常努力地指出工具总是可以用于我们行业的每一次重大炒作——尤其是在炒作扎根一段时间之后。大多数读者的年龄甚至不足以记住CASE 工具[5],但也许他们会记住UML。
但眼光敏锐的读者会注意到,上面大约一半的观点有一个非常共同的主题——创建和维护小的、独立的代码和数据“块”的想法,彼此版本化,使用共同的输入和输出以实现更大的系统集成。就好像……
在微服务的核心,我们发现…… ...
模块!
是的,低级的“模块”,这个核心概念自 1970 年代以来一直是大多数编程语言的核心。(甚至更早,尽管使用未将模块作为一流核心概念的旧语言更难处理。)在 CLR(C#、F#、Visual Basic 等)中称它们为“程序集”, JVM(Java、Kotlin、Clojure、Scala、Groovy 等)上的“JAR”或“包”,或来自您最喜欢的操作系统的动态链接库(Windows 上的 DLL, *nixes 上so的 s 或as,以及当然 macOS 将“框架”隐藏在 /Library 目录中),但在概念层面上,它们都是模块。每个都有不同的内部格式,但每个都有相同的基本目的:一个独立构建、管理、版本化、
考虑模块的这个工作定义,引用自计算机科学的一篇基础论文:
“项目工作的明确定义的细分确保了系统模块化。每个任务形成一个单独的、不同的程序模块。在实施时每个模块及其输入和输出都是明确定义的,与其他系统的预期接口没有混淆模块。在结帐时独立测试模块的完整性;在结帐开始之前同步完成多个任务几乎没有调度问题。最后,系统以模块化方式维护;系统错误和缺陷可以追溯到特定的系统模块,从而限制了详细错误搜索的范围。”
这来自 David Parnas 的开创性论文“On the Criteria To Be Used in Decomposing Systems into Modules”[6],该论文写于 1971 年——距撰写本文时已有 50 多年的历史。
定义明确的“独立的、不同的程序模块”涵盖了微服务建议的大约一半好处,我们已经这样做了 50 年。
那么为什么要炒作微服务呢?
因为微服务真的与微服务、服务甚至分布式系统无关。
在微服务的核心,我们应该找到…… ...组织清晰度!
Amazon 是最早公开讨论微服务概念的公司之一,实际上并没有像他们试图推动独立开发团队的想法那样努力推动架构原则,其阻碍者很少。等待 DBA 团队进行架构更改?QA 需要构建来测试以便他们可以发现错误?还是我们在等待基础架构团队采购服务器?还是 UX 团队为演示创建原型?
SCHHHLLLUURRRRRRRPPPPpppp...
你听到的声音是开发团队聚集了所有可能(并且经常会)阻止他们前进的依赖项的所有权。
- 这意味着这些团队是普通 IT 团队各个部分(分析、开发、设计、测试、数据管理、部署、管理等)的一个小缩影。
- 这确实意味着现在团队要么必须由各种不同的技能集组成,要么我们必须要求每个团队成员都具备完整的技能集(所谓的“全栈开发人员”),这意味着雇用这些人们变得更加狡猾。
- 这也意味着现在团队要为自己的生产中断负责,这意味着团队本身现在必须承担随叫随到的责任(以及相应的工资单和随之而来的法律影响)。
但是,当所有这一切都被驾驭后,这意味着每个团队都可以独立地建造他们的艺术品,除了时间和手指在键盘上飞舞的速度的物理学限制外,没有其他限制。
在微服务的核心,我们经常发现......分布式计算的谬误!
对于那些不熟悉这些谬论的人来说,这些谬论是Peter Deutsch在80年代给他在Sun公司的同行们的一次演讲中首次提出的。他们在1994年由Ann Wolrath和Jim Waldo撰写的开创性论文 "A Note on Distributed Computing "中再次出现,它们基本上都说了同样的话。"使分布式系统正确--性能、可靠性、可扩展性,无论 "正确 "意味着什么--都是困难的。"(松散的转述)
当我们把系统分解成在单个操作系统节点上运行的内存模块时,即使在五十年前,跨进程或库边界传递数据的成本也是可忽略的。然而,当跨越网络线路传递数据时--就像大多数微服务所做的那样--会给通信增加五到七个数量级的延迟。这不是我们可以通过在网络上添加更多的节点来 "消除 "的,这实际上使问题变得更糟。
是的,通过将微服务托管在同一台机器上,通常是将它们加载到运行独立微服务的容器化镜像的虚拟机集群中,可以使其中的一些问题变得不那么重要。(如使用Docker Compose或Kubernetes来托管Docker容器的集合)。然而,这样做会增加虚拟机进程边界之间的延迟(因为我们必须按照七层模型的规则,在虚拟网络堆栈中上下移动数据,即使其中一些层被完全模拟),并且仍然会产生在单个节点上运行的可靠性问题。
更糟糕的是,即使我们开始与分布式计算的谬误作斗争,我们也开始遇到了一个相关的、但独立的问题集 :The Fallacies of Enterprise[7]
在微服务的核心,我们需要......
开始重新思考我们真正需要什么!
你是否需要将问题分解成独立的实体?你可以通过接受托管在Docker容器中的独立进程来做到这一点,或者你可以通过接受应用服务器中服从标准化API惯例的独立模块来做到这一点,或者其他各种选择。这不是一个需要放弃任何已经建立的技术问题--它可以使用过去20年中任何地方的技术,包括servlets、ASP.NET、Ruby、Python、C++,甚至可能是颤抖的Perl。关键是要建立一个共同的架构背板,并有公认的集成和通信惯例,无论你想或需要它是什么。
你是否需要减少你的开发团队所面临的依赖性?那就从研究这些依赖性开始,与合作伙伴一起确定哪些依赖性你可以带入团队的轮子里。如果企业不想正式打破组织结构图中 "以技能为中心 "的本体(意味着你有一个 "数据库 "小组、一个 "基础设施 "小组和一个 "QA "小组作为 "开发 "小组的同行),那么与高级管理人员合作,至少允许一个 "点线 "报告结构,这样每个小组都有个人现在被 "矩阵 "在一个团队中了。
但是,最重要的是,确保这个团队对他们要建立的东西有一个清晰的愿景,而且他们可以自信地对街上随便走过的陌生人描述他们的服务/微服务/模块的核心。
关键是要给团队以方向和目标,给他们完成目标的自主权,并发出完成目标的号角。
(banq注:这篇文章忽视了软件工程中最重要原则:组织架构决定了软件架构,当然,最后一句话也是关于组织架构的建设,但是他没有指出,因为有了团队建设这些组织架构,才催生了微服务架构,这是一套人与技术组成的有机生态系统)
黑客新闻网友:
1、微服务,虽然经常被当作解决技术问题的卖点,但实际上解决的是组织扩展中的人性问题。 微服务声称要解决两个技术问题:模块化(关注点的分离、隐藏实现、文件接口和所有这些好东西)和可扩展性(能够增加计算量、内存和IO到需要它的特定模块)。
第一个问题,即模块,可以在语言层面上得到解决。模块可以做这个工作,这就是这篇博文的重点。
第二个问题,可扩展性,在那些被设计成在分布式环境中运行的语言之外的大多数语言中,很难在语言层面上解决。但是大多数人对它的需求比他们想象的要少得多。通常情况下,数据库是你的瓶颈,如果你保持你的应用服务器无状态,你可以只运行大量的数据库;数据库最终会成为一个瓶颈,但你可以大量扩展数据库。
微服务可能有意义的真正原因是:它们使人们在模块边界周围保持诚实。它们使人们:
- 更难保留对持久性内存状态的访问,
- 更难驾驭对象图来依赖他们不应该依赖的东西,
- 更难在没有关于设计变化和未来证明的对话的情况下,在模块边界的两边创建复杂变化的PR。
(banq注:以上三点保证不让人们变成全能上帝,因为如果一个人变成上帝,他的工作产品就很难被替换,不符合模块化思想,人就应该被困在自己的代码上下文界限内,这就是代码所有权,不应该凌驾于多个上下文界限变成上帝)
当组织规模扩大时,团队的代码所有权是你需要的,如果只是为了减少开发人员需要做的上下文切换,如果被视为完全可替换的话;拥有一个服务比拥有一个模块更有说服力,因为团队将拥有发布时间表和质量门槛。
我不太赞成每个微服务都维护自己的状态副本,可能还有自己的独立数据存储。我认为这通常会在同步方面增加更多的持续复杂性,而不是通过隔离模式来节省。一个更好的规则是一个服务拥有一个表的写入,而其他服务只能读取该表,甚至可能不是所有的列或所有的非自有表。状态同步的问题是分布式应用中最常见的故障模式之一:队列需要备份,重试 "坏 "事件导致阻塞等等。
2、我只想指出,对于第二个问题(CPU/内存/io的可扩展性),微服务几乎总是让事情变得更糟。 做一个RPC必然意味着数据的序列化和反序列化,而且几乎总是意味着通过套接字发送数据。再加上大多数服务都有一些运行RPC代码和其他事情(健康检查、统计数据收集等)的持续开销,这些开销通常被捆绑在每个服务中。即使额外的CPU/内存对你来说不是什么大问题,做RPC也会增加延迟,如果你有太多的微服务,延迟的数字会开始真正增加,而且以后很难修复。
而在单个进程中运行代码的开销要低得多,因为你不需要转接网络层,而且你通常只是在传递数据的指针,而不是序列化/反序列化。
在某些情况下,使用微服务确实能使事情的CPU/内存效率更高,但这比人们想象的要少得多。一个真正能提高效率的例子是像地理围栏服务(想象一下Uber、Doordash等),地理围栏的定义可能很大,必须存储在内存中。根据地理围栏查询的频率,让少量的地理围栏服务实例在内存中加载地理围栏定义,而不是让这个逻辑作为一个模块被许多工作者加载,可能更有效率。但同样,像这样的情况比服务导致的大量臃肿要少得多。
我在Uber工作时,他们开始从单体机过渡到微服务,几乎普遍将逻辑分割到微服务中,需要配置更多的服务器,这对端到端的延迟时间是灾难性的。
(banq注:这位Uber工程师没有理解第二个问题其实是代码所有权问题,是和团队组织架构有关,不是关于技术问题,技术是为获得代码所有权好处而付出成本费用)
3、我在亚马逊工作时,他们开始从单体过渡到微服务,其中最大的胜利是数据和缓存的定位。 所有的目录数据都被转移到一个只提供目录数据的服务中,所以它的缓存为目录数据进行了优化,在它前面的负载均衡器可以通过一致的散列来优化这个缓存。
这与前端网络层不同,前者使用一致的散列法将客户定位到各个网络服务器上。
对于像订单历史或客户数据这样的东西,这些服务坐在他们各自的数据库前面,提供一致的散列和可用性(在当时使用的SQL数据库前面提供一致的写通缓存)。
我不会把这些使事情更有效率的领域称为罕见,而是实际上很常见,它来自于让你的数据决定你的微服务,而不是让你的组织决定你的微服务(尽管如果团队拥有数据,那么他们应该排队)。
(banq注:数据架构决定微服务架构,很新颖的观点,其实数据代码业务,类似于DDD界限上下文决定微服务,界限上下文和组织团队划分,这两个标准也是相互借用的,有的团队依据业务领域或上下文划分,但是对于不熟悉的业务,例如亚马逊这种创新的电商新领域,人们很难做到战略上提前计划与预测,大家都是摸着石头过河,依据数据划分可能比较有意义)
4、微服务效率较低,但可扩展性更高。
服务器只能变得这么大。如果您的单体应用需要的资源超过单个服务器所能提供的资源,那么您可以将其拆分为微服务,每个微服务都可以获得自己的强大服务器。然后你可以在微服务前面放一个负载均衡器,并在 N 个强大的服务器上运行它。但这只在 Facebook 规模上很重要。我认为大多数开发人员会对运行高效代码的单个强大服务器能做的事情感到震惊。
我不同意,在我看来,微服务阻碍了部署和开发的可扩展性--至少我看到大多数企业使用它们的方式。一般来说,他们会把代码分解到不同的存储库中,所以现在你必须运行70个不同的CI/CDS管道来部署70个微服务,而Repo A不知道Repo B对他们的API进行了破坏性的修改。或者lib B拉入了lib D,现在污染了lib A的类路径,而lib A对lib B有依赖性。通常你需要大规模部署所有的微服务来解决一个关键漏洞(想想log4shell)。
解决这个问题的办法是使用正确的工具,即像Bazel这样支持单点的构建系统。Bazel很好地解决了这个问题。它只构建/测试/容器化/部署(rules_k8s, rules_docker)需要重建、重新测试、重新容器化和重新部署的东西。构建速度更快,开发人员对一个组织的所有代码有上帝一样的可见性(banq注:这个观点就是把人变成上帝,违背代码所有权,是很危险的,导致不小心的耦合依赖),可以轻松地搜索整个代码库,并保证他们的变化不会破坏其他模块, 所以你可以用任何最适合它的语言来实现你的服务。它允许你更容易地管理横向依赖关系,在整个组织的代码库中管理版本。
当然,Bazel的学习曲线很陡峭,所以它要像Maven、Gradle等那样被广泛采用还需要几年时间。但在我工作过的银行里,它可以为他们节省数千万美元。
另外,git也需要赶上大型代码库的速度。我认为Meta最近发布了一个新的源码控制工具,与git类似,但可以处理大型单体。
5、微服务解决了第三个技术问题,也是我最喜欢的:隔离。
使用单体,您可以向整个单体提供所有秘密,并且一个模块中的漏洞可以访问任何其他模块可用的任何秘密。类似地,导致一个模块失效的错误会导致整个过程失效(考虑到级联故障时,可能还会导致整个应用程序失效)。在大多数主流语言中,每个模块(即使是最不重要的依赖树上的叶节点)都需要进行搜索以寻找潜在的安全性或可靠性问题,因为任何模块都可能导致整个事情崩溃。这在大多数主流语言的语言级别都没有解决。Erlang 语言家族通常会解决可靠性问题,但大多数语言都将其放在了一起。
微服务可能有意义的真正原因是它们让人们在模块边界周围保持诚实。同意。与静态类型系统一样,微服务是“轨道”。大多数组织都有以权宜之计走捷径的人,而带有 轨道 的系统会抑制这些捷径(重要的是,它们并不排除它们)。
”
6、模块化和微服务都解决了扩展问题:
模块化允许开发扩展。微服务允许运维扩展。
7、微服务的优点在于,无论您的技能和资历水平如何,它们都会创建硬边界。 任何无人监督的初级人员都可能会消除模块边界。但他们不能简单地消除在另一个地方提供服务的硬边界。
8、在实践中,与微服务相比,库和模块是服务器端编程较不受欢迎的代码分割解决方案,这是有充分理由的:
- 部署:当所有的东西都以单体形式出现时,就失去了快速和独立部署代码的能力。
- 隔离:一个团队的bug修复会影响很多团队。
- 运维复杂性:在系统上工作的人必须处理许多团队的模块在同一服务中运行这一事实。调试和排除故障变得更加困难。记录和遥测也往往变得更加复杂。
- 依赖性耦合:每个人都必须使用完全相同的版本,每个人都必须同步升级。你可以用允许依赖性隔离的模块系统来解决这个问题,但在我看来,这往往会导致其自身的复杂性问题,使之不值得。
- 模块API的界限:根据我的经验,开发人员处理服务API比处理库API要容易得多。API的表面积比较小,而且你需要处理向后兼容和如何处理的问题也比较明显。与库的边界相比,服务的边界 "作弊 "或破坏封装的机会也更少。
9、我已经做了很多所谓的微服务的服务开发。 在我看来,这篇文章说得很对:
- 微服务与组织上的限制有很大关系。
- 它与服务的边界有很大关系。如果服务是相互调用,它们就会耦合。
- 一个服务所做的事情必须被指定,即它接收哪些数据和输出哪些数据。这些数据可以而且应该是事件。
- 服务应该在队列、主题、流等方面的消息传递的基础上进行依赖和合作。
- 服务通常是数据充实服务,其中一个服务根据一个事件/数据充实一些数据。
- 你永远不会同时测试一个以上的服务。
- 服务不应该共享那些在频繁更新方面具有活力或短命的代码。
- 征服和划分。从开发一个小的单体开始,为你所期望的多个服务开发。然后划分代码。分开后,每个服务都有自己的实现,而不是在它们之间共享代码。
- IaaS是很重要的。你应该能够推送部署,并且服务的设置与所有基础设施的依赖性。
- 领域的界限是很重要的。围绕着它们的架构组织你的团队,以某种能力为基础。例如,客户、预订、开票。每个团队拥有一个能力和其基础服务。
- 让其他团队有可能读取你的所有数据。他们可能需要这些数据来解决他们的问题。
- 不要使用kubernetes,除非你不能用云提供商的paas来实现你的愿望。Kubernetes是一头野兽,会让你经受考验。
- 如果你不了解事情是如何沟通、失败和恢复的,服务将无法解决你的问题。
- 一切最终都是一致性的。直面这个问题的心态需要时间。
10、微服务和模块化是正交的,并不相同:
模块化与业务概念相关,微服务与基础设施概念相关。例如:我可以将模块 A 部署到微服务 A1 和 A2 中。在这种情况下,A 几乎是抽象概念。当然,我可以使用 1 个大型服务(单体)部署所有模块 A、B、C。此外,我可以为所有模块共享一个微服务 X。
微服务的所有混淆,都是由微服务 = 模块的误解造成的。更糟糕的是,我学到的大多数“专家建议”实际上都将领域驱动设计与微服务联系起来,他们确实没有关系。微服务对我来说就是规模化:扩展基础设施。扩大团队规模(管理理念)。
(banq注:微服务是一个不同于模块的角度,有业务角度,参考DDD;有数据角度;也有运维角度;也有技术架构角度)