微服务架构的目标是帮助工程师团队更快、更安全、更高质量地交付产品。解耦服务使团队能够快速迭代,并尽可能降低对系统其余部分的影响。
在 Medium,我们的技术栈始于 2012 年的单体 Node.js 应用程序。我们已经构建了几个卫星通讯服务,但还没有系统地制定一个采用微服务架构的策略。随着系统变得越来越复杂并且团队不断发展,我们在 2018 年初转向了微服务架构。在这篇文章中,我们希望分享关于如何有效实现微服务架构并避免微服务综合症的经验。
首先,让我们花点时间思考微服务架构是什么,不是什么。“微服务”是那些超负荷和混乱的软件工程的趋势之一。在 Medium 我们认为:
在微服务架构中,多个松耦合的服务协同工作。每项服务都专注于一个目的,并对相关行为和数据高内聚。
该定义包括三个微服务设计原则:
单一目的 - 每项服务应专注于一个目的并做好。
松耦合 - 服务彼此间没有太多联系。更改一项服务时不应要求更改其他服务。应仅通过公共服务接口进行服务之间的通信。
高内聚 - 每个服务将所有相关行为和数据封装在一起。如果我们需要构建新功能,所有更改仅限于一个服务。
当我们对微服务建模时,我们应该遵守这三个设计原则。这是实现微服务架构全部潜力的唯一途径。缺少任何一个都会成为反模式。
缺少单一目的,每个微服务最终会做太多事情,成长为多个“单体”服务。我们不会从微服务架构中获得全部好处,还要支付运营成本。
缺少松耦合,对一项服务的更改会影响其他服务,因此我们无法快速安全地发布更改,这是微服务架构的核心优势。更重要的是,紧耦合引起的问题可能是灾难性的,例如数据不一致甚至数据丢失。
缺少高内聚,我们将最终得到一个分布式的单体系统,一组混乱的服务,必须同时进行更改和部署才能构建单一功能。由于多个服务协调的复杂性和成本(有时跨多个团队),分布式单体系统通常比集中式单体系统差得多。
与此同时,了解微服务不是什么也很重要:
微服务不是代码行数少或处理“微”任务的服务。这种误解来自“ 微服务” 这个名称。微服务架构的目标不是拥有尽可能多的小型服务。只要符合上述三项原则,服务也能够是复杂而重要的。
微服务并不总是由新技术构建。尽管微服务架构能够让团队更轻松地验证新技术,但它并不是微服务架构的主要目标。只要团队从分离的服务中受益,就可以使用完全相同的技术栈构建新服务。
微服务不是必须从头开始构建的服务。如果你已经拥有一个架构良好的单体应用程序,请避免养成从头开始构建每个新服务的习惯。也可以直接从单体服务中直接提取逻辑。同样,应该仍然遵循上述三个原则。
在 Medium,在做出重大产品或工程决策时,我们总是会问“为什么是现在?”这个问题。“为什么?”是一个显而易见的问题,但它假设我们拥有无限的人、时间和资源,这是一个危险的假设。当你想到“为什么是现在?”时,就突然有了更多的限制,对当前工作的影响、机会成本、分心的开销等等。这个问题有助于我们更好地考虑优先级。
为什么我们需要现在采用微服务,是因为我们的 Node.js 单体应用在多个方面成为了瓶颈。
首先,最紧迫和最重要的瓶颈是性能。某些计算繁重且 I/O 繁重的任务不适合 Node.js。我们一直在逐步改进这个单体应用程序,但事实证明收效甚微。它糟糕的性能使我们无法提供更好的产品,虽然应用程序不会变得更慢。
其次,另一个重要且有点紧迫的瓶颈是单体应用程序会降低产品开发速度。由于所有工程师都在单个应用程序中构建功能,因此它们通常是紧密耦合的。我们无法对系统某部分进行灵活地更改,因为它也可能影响其他部分。我们也害怕做出重大改变,因为影响太大,有时难以预测。整个应用程序作为一个整体进行部署,如果由于一次错误提交导致部署停滞,所有其他更改(即使它们完全正常工作)也无法完成。相比之下,微服务架构可以使团队更快地交付、学习和迭代。他们可以专注于自己正在构建的功能,这些功能与复杂系统的其余部分是分离的。变更可以更快地进入生产。他们可以灵活安全地尝试重大变革。
在我们新的微服务架构中,变更会在一小时内发布到生产环境中,工程师不必担心它会影响系统的其他部分。团队还探索了在开发中安全使用生产数据的方法,在这么多年以来这件事就像是白日梦。随着我们的工程团队的发展,所有这些都非常重要。
第三,单体应用程序很难为特定任务扩展系统,也不能为不同类型的任务进行资源隔离。对于消耗大量资源的任务,单体应用程序只能对整个系统进行伸缩,这意味着对于其他的简单任务存在超配。为了缓解这些问题,我们对不同类型的请求进行分片,以分离 Node.js 进程。在一定程度上这起到了些作用,但伸缩依然受限,因为这些单体服务的微型版本是紧耦合的。
最后,一个重要且即将成为紧迫的瓶颈是它阻碍我们尝试新技术。微服务架构的一个主要优点是每个服务都可以使用不同的技术栈构建,并与不同的技术集成。这使我们能够选择最适合工作的工具,更重要的是,这样可以快速安全地完成任务。
采用微服务架构并非易事。它可能会出错,使得实际上损害工程生产力。在本节中,我们将分享早期阶段对我们有益的七个策略:
构建具有明确价值的新服务
单一的持久存储是有害的
解耦“构建服务”和“运行服务”
详尽和一致的可观察性
无需从头构建新服务
重视发生的故障
从一开始就避免“微服务综合症”
有人可能会认为,采用新的服务端架构意味着长时间暂停产品开发和对所有内容的大量重写。这其实是错误的做法,我们绝不应该为了构建新的服务而构建。我们每次构建新服务或采用新技术时,都必须具有明确的产品价值或工程价值。
产品价值就是我们可以带给用户的好处。相对于单体的 Node.js 应用而言,新服务需要提供更多的价值,或者是更快的提供价值。工程价值意味着让工程团队更快、更好的工作。
如果构建新服务既没有产品价值,也没有工程价值,我们将仍然停留在单体应用中。如果十年后 Medium 仍然有单体 Node.js 应用在提供服务,那也没什么问题。实际上单体应用能够帮助我们更有策略地对微服务进行建模。
建模微服务的很大一部分工作是对其持久化存储的数据(例如,数据库)进行建模。跨服务共享持久数据存储似乎是将微服务集成在一起的最简单方法,然而,它实际上是有害的,我们应该不惜一切代价避免它。下面解释一下为什么。
首先,持久数据存储事关实现细节。跨服务共享数据存储会将服务的实现细节暴露给整个系统。如果服务更改了数据的格式,或者添加了缓存层,或者切换到不同类型的数据库,则还必须相应地更改许多其他服务。这违反了松耦合的原则。
其次,持久数据存储,即如何修改、描述和使用数据,不是服务行为。如果我们跨服务共享数据存储,则意味着其他服务也必须复制这些服务行为。这违反了高内聚的原则,给定域中的行为泄露给了多个服务。如果我们修改一个行为,我们将不得不一起修改所有这些服务。
在微服务架构中,特定类型的数据只应该有一个服务负责。所有其他服务应该通过该服务的 API 请求数据,或者保留数据的只读非规范(可能具体化)副本。
这听起来可能有点抽象,下面是一个具体的例子。假设我们正在构建一个新的推荐服务,它需要来自发帖数据表中的一些数据,目前存储在 AWS DynamoDB 中。我们可以通过下面两种方式为新推荐服务提供发布数据。
在单一存储模型中,推荐服务可以直接访问单体应用的持久存储。这是一个坏主意,因为:
缓存可能很棘手。如果推荐服务与单体应用共享相同的缓存,我们将不得不在推荐服务中复制缓存实现细节; 如果推荐服务使用自己的缓存,当单体应用更新帖子数据时,我们不知道什么时候使其缓存无效。
如果单片应用程序决定改为使用 RDS 而不是 DynamoDB 来存储帖子数据,我们将不得不重新实现推荐服务中的逻辑以及访问帖子数据的所有其他服务。
单体应用具有描述帖子数据的复杂逻辑,例如,如何确定帖子是否对给定用户可见。我们必须在推荐服务中重新实现这些逻辑。一旦单体应用更改或添加新逻辑,我们也需要在所有地方进行相同的更改。
尽管推荐服务的数据访问模式是错的,它仍然和在 DynamoDB 耦合在一起。
在解耦存储模型中,推荐服务不能直接访问发贴数据,也不能访问任何其他新服务。发贴数据的实现细节仅保留在一个服务中。有不同的方法来实现这一目标。
理想情况下,发帖数据应该有一个发帖服务,其他服务只能通过发帖服务的 API 访问发帖数据。但是,为所有核心数据模型构建新服务可能是一项昂贵的前期投资。
当人员配备有限时,还有一些更实用的方法。根据数据访问模式,它们实际上可能是更好的方式。在选项 B 中,单体应用可以让推荐服务知道相关的发帖数据何时更新。通常,这种时效性不高,因此我们可以将其发送到消息队列系统。在选项 C 中,ETL 管道为推荐服务生成一份发帖数据的只读副本,以及其他可能对推荐服务有用的数据。在这两个选项中,推荐服务完全拥有数据,因此它可以灵活地缓存数据或使用最合适的数据库技术。
如果构建微服务很难,那么运行服务往往更难。当运行服务需要构建每个服务,并不断的重复这件事时,会拖慢工程团队的速度。我们希望让每项服务都专注于自己的工作而不用担心如何运行服务这一复杂问题,包括网络、通信协议、部署、可观察性等。服务管理应该与每个服务的实现完全分离。
解耦“构建服务”和“运行服务”的策略是使运行服务任务与服务技术无关,并且独立,以便应用工程师可以完全专注于每个服务自己的业务逻辑。
感谢近年来容器化、容器编排、服务网格、应用性能监控等方面的技术进步,“运行服务”的解耦变得比以往更容易实现。
网络。网络(例如,服务发现,路由,负载均衡,流量路由等)是运行服务的关键部分。传统方法是为每种平台 / 语言提供库。它可以正常工作但不够理想,因为应用仍然需要非常繁琐的工作来集成和维护库。不仅如此,应用还需要单独实现某些逻辑。现代解决方案是在服务网格中运行服务。在 Medium,我们使用 Istio 和 Envoy 作为边车代理。构建服务的应用工程师根本不需要担心网络问题。
通信协议。无论选择哪种技术栈或语言来构建微服务,选择一个高效、典型、跨平台且需要最少开发工作量的成熟 RPC 解决方案都是非常重要的。即使它们之间存在依赖关系,支持向后兼容性的 RPC 解决方案也使部署服务更安全。在 Medium,我们选择了 gRPC。
一种常见的替代方案是基于 HTTP 的 REST + JSON,这种解决方案长期以来一直是服务器通信的好手段。但是,尽管该技术栈非常适合浏览器与服务器通信,但它对于服务器之间的通信效率很低,尤其是当我们需要发送大量请求时。如果没有自动生成的客户端和模板代码,我们将不得不手动实现服务器 / 客户端代码。可靠的 RPC 实现不仅仅包装网络客户端。另外,REST 是“自描述的”,但它很难让人对每个细节都形成共识,例如,这个调用真的是 REST,还是只是一个 RPC?这是一种资源还是一种操作?等等。
部署。拥有一致的方法来构建、测试、打包、部署和管理服务非常重要。Medium 所有的微服务都在容器中运行。目前,我们的编排系统是 AWS ECS 和 Kubernetes 的混合体,但正在向全 Kubernetes 迁移。
我们构建了自己的系统来构建、测试、打包和部署服务,称为 BBFD。它在跨服务的一致性和为特定服务提供采用不同技术栈的灵活性之间取得平衡。它的工作方式是让每个服务提供基本信息,例如,要监听的端口,构建 / 测试 / 启动服务的命令等,BBFD 将负责其余的工作。
可观察性包括允许我们了解系统如何工作的过程,约定和工具,以及在故障时对问题进行鉴定。可观察性包括日志记录、性能跟踪、指标、仪表盘、警报,它对微服务架构成功与否至关重要。
当我们从单个服务迁移到具有许多服务的分布式系统时,可能会发生两件事:
我们失去了可观察性,因为它实现起来更难,更容易被忽视。
不同的团队重复造轮子,最终得到了碎片化的可观察性,这种可观察性实际上很低,因为很难使用碎片数据对问题进行关联和鉴定。
从一开始就具有良好且一致的可观察性非常重要,因此我们的 DevOps 团队提出了一致的可观察性策略,并构建了支持实现这一目标的工具。每个服务都会自动获取详细的 DataDog 仪表盘、警报和日志搜索,这些服务在所有服务中是一致的。我们还大量使用 LightStep 来了解系统的性能。
在微服务架构中,每个服务都专注于做好一件事。请注意,它与如何构建服务无关。从单体服务迁移时,如果可以从单体应用中剥离,就无需从头开始构建微服务。
务实的说,是否应该从头开始构建服务取决于两个因素:
Node.js 是否适合新任务;
在不同的技术栈中重新实现的成本是多少。
如果 Node.js 是一个不错的技术选择,并且现有的实现方式没有问题,我们就将代码从单体应用中删除,并用它创建一个微服务。即使采用相同的实现,我们仍将获得微服务架构的所有好处。
我们的 Node.js 单体应用的架构使我们可以相对轻松地使用现有实现构建单独的服务。我们将在本文稍后讨论如何正确构建单体。
在分布式环境中,故障的可能性会更高。如果处理不当,关键服务的故障可能是灾难性的。我们应该始终考虑如何测试故障并优雅地处理故障。
首先,要明白故障无处不在。
对于 RPC 调用,需要对故障进行额外处理。
确保发生故障时具有良好的可观察性(如上所述)。
新服务上线前必须进行故障测试。它应该是新服务检查列表的一部分。
尽可能自动恢复。
微服务不是灵丹妙药,它解决了一些问题,但也创造了一些其他问题,我们将其称为“ 微服务综合症 ”。如果我们一开始就不去考虑它们,那么事情很快会变得很麻烦,处理起来也要付出更多代价。以下是一些常见症状。
建模不良的微服务弊大于利,特别是在数量庞大时。
允许选择太多不同的语言 / 技术,这会增加运维成本并使工程团队碎片化。
将运行服务与构建服务相结合,这极大地增加了服务的复杂性并拖慢了团队的速度。
忽略数据建模,最终得到具有单体数据存储的微服务。
缺乏可观察性,难以对性能问题和故障进行定位。
遇到问题时,团队倾向于创建新服务而不是修复现有服务,即使后者可能是更好的选择。
即使服务松耦合,缺乏系统的整体视角也可能存在问题。
随着最近的技术创新,采用微服务架构变得更加容易。这是否意味着我们都应该停止构建单体服务?
答案是否定的。虽然新技术可以更好的支持微服务架构,但仍然存在较高的复杂度和难度。对于小型团队来说,单体应用仍然是更好的选择。但是,可以花时间对单体应用进行更好的架构设计,以便于在系统和团队成长后,可以更方便的迁移到微服务架构中。
从单体结构开始没什么问题,但确保模块化并使用上述三种微服务原则(单一目的、松耦合和高内聚)来构建它,除了“服务”在同一技术栈中实现,部署在一起并在同一个进程中运行。
在 Medium,我们在早期的单体应用中做出了一些很好的架构决策。
我们的单体应用的组件是高度模块化的,尽管它已经发展成为一个非常复杂的应用,包括 Web 服务器、后端服务和离线事件处理器。离线事件处理器单独运行,但使用完全相同的代码。这使得将一大块业务逻辑剥离到单独的服务相对容易,只要新服务提供与原始实现相同(上层)的接口即可。
我们的单体应用程序在较低级别封装了数据存储的细节。每种数据类型(例如,数据库表)具有两层实现:数据层和服务层。
数据层处理一个特定类型数据的 CRUD 操作。
服务层处理一个特定类型数据的上层逻辑和为系统的其余部分提供公共 API。服务之间不共享数据存储。
这有助于我们采用微服务架构,因为数据类型的实现细节对其他部分代码完全隐藏。创建新服务来处理某一类型的数据相对容易且安全。
单体应用还可以帮助我们对微服务进行建模,让我们能够灵活地专注于系统中最重要的部分,而不是从头开始为所有微服务建模。
单体 Node.js 应用已经为我们服务了好几年,但它开始拖慢我们交付大项目和快速迭代的速度。我们开始系统性和战略性地采用微服务架构。我们仍然处于这一旅程的早期阶段,但我们已经看到了它的优势和潜力,它极大的提高了开发效率,使我们能够大胆思考并大幅提升产品,同时解放工程团队,能够安全地测试新技术。
https://medium.engineering/microservice-architecture-at-medium-9c33805eb74f
点个好看少个 bug
1、头条易读遵循行业规范,任何转载的稿件都会明确标注作者和来源;
2、本文内容来自“InfoQ”微信公众号,文章版权归InfoQ公众号所有。