专栏名称: 架构文摘
每天一篇架构领域重磅好文,涉及一线互联网公司的互联网应用架构、大数据、机器学习等各个热门领域。
目录
相关文章推荐
架构师之路  ·  别TM浪费算力了,这样才能最大限度发挥dee ... ·  3 天前  
高可用架构  ·  漫谈DeepSeek及其背后的核心技术 ·  2 天前  
美团技术团队  ·  CVPR 2025 NTIRE赛事 | ... ·  2 天前  
51好读  ›  专栏  ›  架构文摘

Medium的微服务架构最佳实践

架构文摘  · 公众号  · 架构  · 2019-07-29 09:00

正文

译者: 蓝梦,十余年研发经验,现就职于某上市互联网公司

作者: 小马,Medium 首席架构师

来源: 技术琐话


译者:如果你的项目正在从单体升级为微服务而忧心; 或者你在实践微服务过程中手忙脚乱,本文都是你不容错过的好文。



微服务架构的目标是帮助工程团队更快、更安全、更高质量地交付产品。 拆分服务允许团队快速迭代的同时,保证了对系统剩余部分的最小影响。

在Medium,我们的技术堆栈始于2012年的单体Node.js应用程序。 我们已经构建了几个卫星服务,但我们还没有制定一个系统地采用微服务架构的策略。 随着系统变得越来越复杂并且团队不断发展,我们在2018年初转向了微服务架构。 在这篇文章中,我们希望分享我们有效地做到这一点并避免微服务综合症的经验。


什么是微服务架构?

首先,让我们花一点时间来思考微服务架构应该是怎么样到,不应该是怎么到。 “微服务”是解决那些过载和混乱的软件工程的手段之一。 我们在Medium至少这样认为:

在微服务架构中,多个松散耦合的服务协同工作。 每项服务都专注于一个目的,并具有相关行为和数据的高度凝聚力。

该定义包括三个微服务设计原则:

  • 单一职责 - 每项服务应专注于一个目的并做得好。

  • 松耦合 - 服务对彼此知之甚少。 对一项服务的更改不应要求更改其他服务。 服务之间的通信应仅通过公共服务接口进行。

  • 高内聚性 - 每项服务将所有相关行为和数据封装在一起。 如果我们需要构建新功能,则所有更改应仅本地化为一个服务。




当我们对微服务进行构建模型时,我们应该遵守所有三个设计原则。 这是实现微服务架构全部潜力的唯一途径。 错过任何一个都会成为反模式。

  1. 如果缺少单一职责,每个微服务最终会做太多事情,成长为多个“单体”服务。 我们不会从微服务架构中获得全部好处,我们也会支付运营成本。

  2. 如果缺少松散耦合,对一个服务的更改会影响其他服务,因此我们无法快速安全地发布更改,这本应该是微服务架构的核心优势。 更重要的是,紧密耦合引起的问题可能是灾难性的,例如数据不一致甚至数据丢失。

  3. 如果缺少高凝聚力,我们将最终得到一个分布式单体系统 - 一组混乱的服务,必须同时进行更改和部署才能构建单一功能。 由于多个服务协调的复杂性和成本(有时跨多个团队),分布式单体系统通常比集中式单体系统差得多。

于此同时,认识到微服务不应该长成那样,也同样重要:

微服务不是具有少量代码行或“微”任务的服务。 这种误解来自“微服务”这个名字。 微服务架构的目标不是拥有尽可能多的小型服务。 只有符合上述三项原则,服务的提炼就是恰当的。

微服务不是一直使用新技术构建的服务。 尽管微服务架构允许团队更轻松地测试新技术,但它并不是微服务架构的主要目标。 只有从接偶的服务中受益,团队才能使用完全相同的技术堆栈去构建新到服务。

微服务不是必须从头开始构建的服务。 如果您已经拥有一个架构良好的单一应用程序,请避免养成从头开始构建每个新服务的习惯。 可能有机会直接从单体服务中提取逻辑。 同样,上述三个原则应该仍然有效。


为什么是现在?

在Medium,我们总是在做出重大产品或工程决策时会问“为什么是现在? ”这个问题。 “为什么? ”是一个显而易见的问题,但它假设我们拥有无限的人,时间和资源,这是一个危险的假设。 当你想到“为什么是现在? ”时,你突然有了更多的限制 - 对当前工作的影响,机会成本,分心的开销等等。 这个问题有助于我们更好地优先考虑。

我们现在需要采用微服务的原因是我们的Node.js单体应用程序已经成为瓶颈。

首先,最紧迫和最重要的瓶颈是其性能。 某些计算量很大且I / O很重的任务不适合Node.js. 我们一直在逐步改进整体应用程序,但事实证明它是无效的。 它的低劣性能使我们无法提供更好的产品而不会使已经非常慢的应用程序变慢。

其次,重要且有点紧迫的瓶颈是单体应用程序会拖慢产品开发速度。 由于所有工程师都在单个应用程序中构建功能,因此它们通常紧密耦合。 我们无法灵活地改变系统的一部分,因为它也可能影响其他部分。 我们也害怕做出重大改变,因为影响太大,有时甚至难以预测。 整个应用程序作为一个整体进行部署,因此如果由于一次错误提交导致部署停滞,那么所有其他更改(即使它们完全正常工作)也无法顺利完成发布。 相比之下,微服务架构允许团队更快地开发和迭代。 因为这些功能与复杂系统是解耦的,所以工程师可以专注于他们正在构建的功能。 自然的,他们就可以轻易安全地做到重大变更。

在我们新的微服务架构中,功能修改后可以在一个小时内发布上线。 同样的,工程师不必担心它会如何影响系统的其他部分。 项目组的团队还探索了在开发中安全使用生产数据的方法,这在多年前是无法实现的。 随着我们的工程团队的发展,我们有机会去突破现状。

第三,单块应用程序使系统很难针对特定任务进行扩展,也很难针对不同类型的任务分离资源问题。 使用单一的单块应用程序,我们必须在整个系统上下扩展,以完成更需要资源的任务,尽管这意味着系统对其他更简单的任务的准备过度了。 为了缓解这些问题,我们分解了不同类型的请求来分离Node.js进程。 它们在一定程度上起作用,但不会扩展,因为,同样,这些微版本的单片服务是紧密耦合的。

最后一点也同样重要的是,单体架构阻碍了团队尝试新技术。 微服务架构的一个主要优点是每个服务都可以使用不同的技术堆栈,并与不同的技术集成。 这使我们能够选择最适合工作的工具,于此同时,我们还可以快速安全地完成工作。


微服务策略

采用微服务架构并非易事。 它可能会出错,实际上会损害工程生产力。 在本节中,我们将分享七个在采用早期阶段帮助我们的策略:

  • 建立具有明确价值的新服务

  • 单体数据存储被认为是有害的

  • 解耦“服务构建”和“服务治理”

  • 彻底和一致的可观察性

  • 并非每项新服务都需要从头开始构建

  • 尊重失败因为它们会发生

  • 从第一天开始就避免使用“微服务综合症”

建立具有明确价值的新服务

有人可能会认为采用新的服务器架构意味着产品开发的长时间停顿以及对所有内容的大量重写。 这是错误的做法。 我们永远不应该为了建立新的服务而建立新的服务。 每次我们建立新服务或采用新技术时,都必须具有明确的产品价值和/或工程价值。

产品价值应以我们可以为用户提供的利益为代表。 与在单体Node.js应用程序中构建值相比,需要一项新服务来提供值或使其更快地交付值。 工程价值应该使工程团队更好,更快。

如果构建新服务没有产品价值或工程价值,我们将其留在单一的应用程序中。 如果十年内Medium仍然有一个支持某些表面的单体Node.js应用程序,那就完全没了问题。 从单一应用程序开始实际上有助于我们战略性地对微服务进行建模。

单体数据存储被认为是有害的

构建微服务的很大一部分工作是对其持久数据存储(例如,数据库)进行建模。 跨服务共享持久数据存储通常似乎是将微服务集成在一起的最简单方法,然而,它实际上是有害的,我们应该不惜一切代价避免它。 这就是原因。

首先,持久数据存储是关于实现细节的。 跨服务共享数据存储会将一个服务的实现细节暴露给整个系统。 如果该服务更改了数据的格式,或者添加了缓存层,或者切换到不同类型的数据库,则还必须相应地更改许多其他服务。 这违反了松散耦合的原则。

其次,持久数据存储不是服务行为,即如何修改,解释和使用数据。 如果我们跨服务共享数据存储,则意味着其他服务也必须复制服务行为。 这违反了高内聚的原则 - 给定域中的行为泄露给多个服务。 如果我们修改一个行为,我们将不得不一起修改所有这些服务。

在微服务架构中,只有一个服务应该负责特定类型的数据。 所有其他服务应该通过负责服务的API请求数据,或者保留数据的只读非规范(可能具体化)副本。

这可能听起来很抽象,所以这是一个具体的例子。 假设我们正在构建一个新的推荐服务,它需要来自规范帖子表的一些数据,目前在AWS DynamoDB中。 我们可以通过两种方式之一为新推荐服务提供发布数据。

在单体存储模型中,推荐服务可以直接访问单体应用程序所执行的相同持久存储。 这是一个坏主意,因为:

  1. 缓存可能很棘手。 如果推荐服务与单体应用程序共享相同的缓存,我们也必须在推荐服务中复制缓存实现细节;如果推荐服务使用自己的缓存,当单体应用更新帖子数据时,我们将不知道何时使其缓存无效。

  2. 如果单体应用程序决定更改为使用RDS而不是DynamoDB来存储帖子数据,我们将不得不重新实现推荐服务中的逻辑以及访问帖子数据的所有其他服务。

  3. 单体应用程序具有解释帖子数据的复杂逻辑,例如,如何确定帖子是否应该对给定用户不可见。 我们必须在推荐服务中重新实现这些逻辑。 一旦整体应用程序更改或添加新逻辑,我们也需要在任何地方进行相同的更改。

  4. 即使推荐服务是自己的数据访问模式的错误选项,推荐服务仍然停留在DynamoDB上。

在解耦存储模型中,推荐服务不能直接访问发布数据,也不能直接访问任何其他新服务。 发布数据的实现细节仅保留在一个服务中。 有不同的方法来实现这一目标。

理想情况下,应该有一个拥有帖子数据的邮政服务,其他服务只能通过邮政服务的API访问邮政数据。 但是,为所有核心数据模型构建新服务可能是一项昂贵的前期投资。

当人员配置有限时,还有一些更实用的方法。 根据数据访问模式,它们实际上可能是更好的方式。 在选项B中,单一应用程序可让推荐服务知道何时更新相关的帖子数据。 通常,这不必立即发生,因此我们可以将其卸载到排队系统。 在选项C中,ETL管道生成推荐服务的发布数据的只读副本,以及可能对推荐有用的其他数据。 在这两个选项中,推荐服务完全拥有其数据,因此它可以灵活地缓存数据或使用最适合的数据库技术。

解耦“服务构建”和“服务治理”

如果构建微服务很难,那么治理服务往往更难。 当治理服务与构建每个服务相结合时,它会减慢工程团队的速度,团队必须不断重新发明这样做。 我们希望让每项服务都专注于自己的工作而不用担心如何运行服务的复杂问题,包括网络,通信协议,部署,可观察性等。 服务治理应该与每个服务的实现完全分离。

将“服务构建”和“服务治理”分离的策略是使运行服务任务与服务技术无关,并且使自己的意见,以便应用工程师可以完全专注于每个服务自己的业务逻辑。

由于最近在容器化,容器编排,服务网格,应用程序性能监控等方面的技术进步,“服务治理”的解耦变得比以往更容易实现。

网格化,网格(例如,服务发现,路由,负载平衡,流量路由等)是服务治理的关键部分。 传统方法是为每种平台/语言提供库。 它工作但不理想,因为应用程序仍然需要非常繁琐的工作来集成和维护库。 通常,应用程序仍然需要单独实现某些逻辑。 现代解决方案是在Service Mesh中运行服务。 在Medium,我们使用Istio和Envoy作为sidecar 代理。 构建服务的应用工程师根本不需要担心网络问题。

通信协议。 无论您选择哪种技术堆栈或语言来构建微服务,从一个高效,类型化(typed),跨平台且需要最少开发开销的成熟RPC解决方案开始是非常重要的。 支持向后兼容性的RPC解决方案也使部署服务更加安全,即使它们之间存在依赖关系。 在Medium,我们选择了gRPC。

一个常见的替代方案是RESTJSON over HTTP,长期以来,它一直是服务器通信的良好解决方案。 但是,尽管该堆栈非常适合浏览器与服务器通信,但它对于服务器到服务器的通信效率很低,尤其是当我们需要发送大量请求时。 如果没有自动生成的存根和样板代码,我们将不得不手动实现服务器/客户端代码。 可靠的RPC实现不仅仅包装网络客户端。 另外,REST是“固执己见的”,认知存在门槛,但总是让每个人都对每个细节都达成一致很困难,例如,这个调用真的是REST,还是只是一个RPC? 这是一种资源还是一种操作,诸如此类!

部署。 拥有一致的方法来构建,测试,打包,部署和管理服务非常重要。 所有Medium的微服务都在容器中运行。 目前,我们的编排系统是AWS ECS和Kubernetes的混合体,但朝着Kubernetes的方向在走。

我们构建了自己的系统来构建,测试,打包和部署服务,称为BBFD。 它在“跨服务工作一致性”和“为个人服务提供采用不同技术堆栈的灵活性”之间取得平衡。 它的工作方式是让每个服务提供基本信息,例如,要监听的端口,构建/测试/启动服务的命令等,BBFD将负责其余的工作。

良好且一致的可观察性

可观察性包括允许我们了解系统如何工作的过程,约定和工具,以及在不工作时对问题进行分类。 可观察性包括日志记录,性能跟踪,指标,仪表板,警报,并且对于微服务架构的成功至关重要。

当我们从单个服务迁移到具有许多服务的分布式系统时,可能会发生两件事:

我们失去了可观察性,因为它变得更难或更容易被忽视。

不同的团队重新发明了轮子,我们最终得到了零碎的可观察性,这实际上是低可观察性,因为很难使用碎片数据连接点或分类任何问题。

从一开始就具有良好且一致的可观察性非常重要,因此我们的DevOps团队提出了一致的可观察性策略,并构建了支持实现这一目标的工具。 每项服务都会自动获取详细的DataDog仪表板,警报和日志搜索,这些服务在所有服务中也是一致的。 我们还大量使用LightStep来了解系统的性能。

并非每项新服务都需要从头开始构建

在微服务架构中,每个服务都做一件事并且做得非常好。 请注意,它与如何构建服务无关。 如果您从单一服务迁移,请记住,如果您可以从单体应用程序中剥离微服务并不总是必须从头开始构建。

在这里,我们采取务实的态度。 我们是否应该从头开始构建服务取决于两个因素: (1)Node.js适合该任务的程度如何;(2)在不同的技术堆栈中重新实现的成本是多少。

如果Node.js是一个很好的技术选项并且现有的实现很好,我们将代码从单体应用程序中删除,并用它创建一个微服务。 即使采用相同的实现,我们仍将获得微服务架构的所有好处。

我们的Node.js单体应用程序的架构使我们可以相对轻松地使用现有实现构建单独的服务。 我们将在本文稍后讨论如何正确构建单体应用。

尊重失败,因为他们会发生

在分布式环境中,更多的东西可能会失败,而且它们会失败。 如果处理不当,任务关键型服务的失败可能是灾难性的。 我们应该始终考虑如何测试故障并优雅地处理故障。

  • 首先,我们应该期待一切都会在某些时候失败。

  • 对于RPC调用,需要付出额外的努力来处理故障情况。







请到「今天看啥」查看全文