「微服务架构」Medium的微服务架构实践

「微服务架构」Medium的微服务架构实践,第1张

微服务¹架构的目标是帮助工程团队更快,更安全,更高质量地交付产品。解耦服务允许团队快速迭代,对系统的其余部分影响最小。

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

首先,让我们花一点时间来思考微服务架构是什么,不是什么。 “微服务”是那些过载和混乱的软件工程趋势之一。这就是我们在Medium认为它是什么:

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

Three Principles of Modeling Microservices

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

没有一个目的,每个微服务最终会做太多事情,成长为多个“单片”服务。我们不会从微服务架构中获得全部好处,我们也会支付运营成本。

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

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

与此同时,了解 微服务不是什么 很重要:

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

我们现在需要采用微服务的原因是我们的Nodejs单片应用程序已经成为多个方面的瓶颈。

首先,最紧迫和最重要的瓶颈是其性能。

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

其次,整体应用程序的一个重要且有点紧迫的瓶颈是它会减慢产品开发速度。

由于所有工程师都在单个应用程序中构建功能,因此它们通常紧密耦合。我们无法灵活地改变系统的一部分,因为它也可能影响其他部分。我们也害怕做出重大改变,因为影响太大,有时难以预测。整个应用程序作为一个整体进行部署,因此如果由于一次错误提交导致部署停滞,那么所有其他更改(即使它们完全正常工作)也无法完成。相比之下,微服务架构允许团队更快地发货,学习和迭代。他们可以专注于他们正在构建的功能,这些功能与复杂系统的其余部分分离。更改可以更快地进入生产。他们可以灵活地安全地尝试重大变革。

在我们新的微服务架构中,更改会在一小时内完成生产,工程师不必担心它会如何影响系统的其他部分。该团队还 探索 了在开发中安全使用生产数据的方法²多年来一直是白日梦。随着我们的工程团队的发展,所有这些都非常重要。

第三,单一应用程序使得难以为特定任务扩展系统或隔离不同类型任务的资源问题。

使用单一的单一应用程序,我们必须扩展和缩小整个系统,以满足更多资源需求的任务,即使这意味着系统过度配置用于其他更简单的任务。为了缓解这些问题,我们对不同类型的请求进行分片,以分离Nodejs进程。它们在一定程度上起作用,但不会扩展,因为这些微单一版本的单片服务是紧密耦合的。

最后但同样重要的是,一个重要且即将成为紧迫的瓶颈是它阻止我们尝试新技术。微服务架构的一个主要优点是每个服务都可以使用不同的技术堆栈构建,并与不同的技术集成。这使我们能够选择最适合工作的工具,更重要的是,我们可以快速安全地完成工作。

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

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

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

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

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

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

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

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

单片持久存储被认为是有害的

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

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

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

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

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

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

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

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

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

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

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

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

当人员配置有限时,还有一些更实用的方法。根据数据访问模式,它们实际上可能是更好的方式。

选项B 中,单一应用程序可让推荐服务知道何时更新相关的帖子数据。通常,这不必立即发生,因此我们可以将其卸载到排队系统。

选项C 中,ETL管道生成推荐服务的发布数据的只读副本,以及可能对推荐有用的其他数据。在这两个选项中,推荐服务完全拥有其数据,因此它可以灵活地缓存数据或使用最适合的数据库技术。

解耦“建立服务”和“运行服务”

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

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

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

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

一种常见的替代方案是基于>

微服务在最近几年大行其道,很多系统的研发都在考虑采用微服务架构,同时,随着 Docker 容器技术和DevOps开发运维一体化等相关技术发展,微服务变得更容易管理,这为微服务架构快速发展创造了有利条件。
在落地微服务的路上,拆分服务是个很热的话题。我们应该按照什么原则将现有的业务进行拆分?是否拆分得越细就越好?接下来一起聊聊服务拆分的策略和原则。

不忘初心方得始终。
在介绍如何拆分之前,我们需要了解下拆分的目的是什么,这样才不会在后续的拆分过程中忘了最初的目的。拆分的本质是 为了将复杂的问题简单化 ,那么我们在单体架构阶段遇到了哪些复杂性问题呢?

首先来回想下当初为什么选用了单体架构,在很多项目刚启动的时候,我们只希望能尽快地将项目搭建起来,方便将产品更早的投放市场进行快速验证。在开发初期,这种架构确实给开发和运维带来了很大的便捷,主要体现在:

但是随着功能越来越多,开发团队的规模越来越大,单体架构的缺陷慢慢体现出来,主要有以下几个方面:

产品初期,应该以单体架构优先。因为面对一个新的领域,对业务的理解很难在开始阶段就比较清晰,往往是经过一段时间之后,才能逐步稳定,如果拆分过早,导致边界拆分不合理或者拆的过细,反而会影响生产力。很多时候,从一个已有的单体架构中逐步划分服务,要比一开始就构建微服务简单得多。同时公司的产品并没有被市场验证过,有可能会失败,所以这个投入的风险也会比较高。另外,在资源受限的情况下,采用微服务架构很多优势无法体现,性能上的劣势反而会比较明显。如下图所示。当业务复杂度达到一定程度后,微服务架构消耗的成本才会体现优势,并不是所有的场景都适合采用微服务架构,服务的划分应逐步进行,持续演进。产品初期,业务复杂度不高的时候,应该尽量采用单体架构。

随着公司的商业模式逐渐得到验证,且产品获得了市场的认可,为了能加快产品的迭代效率快速占领市场,公司开始引进更多的开发同学,这时系统的复杂度会变得越来越高,就出现单体应用和团队规模之间出现矛盾,研发效率不升反降。上图中的交叉点表明,业务已经达到了一定的复杂度,单体应用已经无法满足业务增长的需求,研发效率开始下降,而这时就是需要考虑进行服务拆分的时机点。这个点需要架构师去权衡。当我们清楚了什么时候进行拆分,就可以直接落地了吗?不是的,微服务拆分的落地还要提前准备好配套的基础设施,如:服务接口设计(描述)、注册中心、微服务框架选型、服务监控、服务追踪、服务治理等几大基本组件,以上每个组件缺一不可,每个组件展开又包括很多技术,比如:持续集成、容器技术、持续部署、DevOps 等相关概念,以及人才的储备和观念的变化。 微服务不仅仅是技术的升级,更是开发方式、组织架构、开发观念的转变。

何时进行微服务的拆分,整体总结如下:

1 单一服务内部功能高内聚低耦合
也就是说每个服务只完成自己职责内的任务,对于不是自己职责的功能交给其它服务来完成。
2 闭包原则(CCP)
微服务的闭包原则就是当我们需要改变一个微服务的时候,所有依赖都在这个微服务的组件内,不需要修改其他微服务。
3 服务自治、接口隔离原则
尽量消除对其他服务的强依赖,这样可以降低沟通成本,提升服务稳定性。服务通过标准的接口隔离,隐藏内部实现细节。这使得服务可以独立开发、测试、部署、运行,以服务为单位持续交付。
4 持续演进原则
在服务拆分的初期,你其实很难确定服务究竟要拆成什么样。从微服务这几个字来看,服务的粒度貌似应该足够小,但是服务多了也会带来问题,服务数量快速增长会带来架构复杂度急剧升高,开发、测试、运维等环节很难快速适应,会导致故障率大幅增加,可用性降低,非必要情况,应逐步划分,持续演进,避免服务数量的爆炸性增长,这等同于灰度发布的效果,先拿出几个不太重要的功能拆分出一个服务做试验,如果出现故障,则可以减少故障的影响范围。
5 拆分的过程尽量避免影响产品的日常功能迭代
也就是说要一边做产品功能迭代,一边完成服务化拆分。比如优先剥离比较独立的边界服务(如短信服务等),从非核心的服务出发减少拆分对现有业务的影响,也给团队一个练习、试错的机会。同时当两个服务存在依赖关系时优先拆分被依赖的服务。
6 服务接口的定义要具备可扩展性
服务拆分之后,由于服务是以独立进程的方式部署,所以服务之间通信就不再是进程内部的方法调用而是跨进程的网络通信了。在这种通信模型下服务接口的定义要具备可扩展性,否则在服务变更时会造成意想不到的错误。比如微服务的接口因为升级把之前的三个参数改成了四个,上线后导致调用方大量报错,推荐做法服务接口的参数类型最好是封装类,这样如果增加参数就不必变更接口的签名,而只需要在类中添加字段就可以了。
7 避免环形依赖与双向依赖
尽量不要有服务之间的环形依赖或双向依赖,原因是存在这种情况说明我们的功能边界没有化分清楚或者有通用的功能没有下沉下来。

8 阶段性合并
随着你对业务领域理解的逐渐深入或者业务本身逻辑发生了比较大的变化,亦或者之前的拆分没有考虑的很清楚,导致拆分后的服务边界变得越来越混乱,这时就要重新梳理领域边界,不断纠正拆分的合理性。

目前很多传统的单体应用再向微服务架构进行升级改造,如果拆分粒度太细会增加运维复杂度,粒度过大又起不到效果,那么改造过程中如何平衡拆分粒度呢?
弓箭原理

平衡拆分粒度可以从两方面进行权衡,一是业务发展的复杂度,二是团队规模的人数。如上图,它就像弓箭一样,只有当业务复杂度和团队人数足够大的时候,射出的服务拆分粒度这把剑才会飞的更远,发挥出最大的威力。比如说电商的商品服务,当我们把商品从大的单体里拆分出来的时候,就商品服务本身来讲,逻辑并没有足够复杂到 2~3 个人没法维护的地步,这时我们没有必要继续将商品服务拆的更细,但是随着业务的发展,商品的业务逻辑变的越来越复杂,可能同时服务公司的多个平台,此时你会发现商品服务本身面临的问题跟单体架构阶段面临的问题基本一样,这个阶段就需要我们将商品拆成更细粒度的服务,比如:库存服务、价格服务、类目服务、商品基础信息服务等等。虽然业务复杂度已经满足了,如果公司此时没有足够的人力(招聘不及时或员工异动比较多),服务最好也不要拆分,拆分会因为人力的不足导致更多的问题,如研发效率大幅下降(一个开发负责与其不匹配数量的服务)。这里引申另外一个问题,一个微服务究竟需要几个开发维护是比较理性的?

三个火q手原则
为什么说是三个人分配一个服务是比较理性的?而不是 4 个,也不是 2 个呢?首先,从系统规模来讲,3 个人负责开发一个系统,系统的复杂度刚好达到每个人都能全面理解整个系统,又能够进行分工的粒度;如果是 2 个人开发一个系统,系统的复杂度不够,开发人员可能觉得无法体现自己的技术实力;如果是 4 个甚至更多人开发一个系统,系统复杂度又会无法让开发人员对系统的细节都了解很深。其次,从团队管理来说,3 个人可以形成一个稳定的备份,即使 1 个人休假或者调配到其他系统,剩余 2 个人还可以支撑;如果是 2 个人,抽调 1 个后剩余的 1 个人压力很大;如果是 1 个人,这就是单点了,团队没有备份,某些情况下是很危险的,假如这个人休假了,系统出问题了怎么办?最后,从技术提升的角度来讲,3 个人的技术小组既能够形成有效的讨论,又能够快速达成一致意见;如果是 2 个人,可能会出现互相坚持自己的意见,或者 2 个人经验都不足导致设计缺陷;如果是 1 个人,由于没有人跟他进行技术讨论,很可能陷入思维盲区导致重大问题;如果是 4 个人或者更多,可能有的参与的人员并没有认真参与,只是完成任务而已。“三个火q手”的原则主要应用于微服务设计和开发阶段,如果微服务经过一段时间发展后已经比较稳定,处于维护期了,无须太多的开发,那么平均 1 个人维护 1 个微服务甚至几个微服务都可以。当然考虑到人员备份问题,每个微服务最好都安排 2 个人维护,每个人都可以维护多个微服务。
综上所诉,拆分粒度不是越细越好,粒度需要符合弓箭原理及三个火q手原则。

拆分策略可以按功能和非功能维度进行考虑,功能维度主要是:划分清楚业务的边界,非功能维度主要考虑六点包括:扩展性、复用性、高性能、高可用、安全性、异构性。接下来详细介绍下。
1 功能维度
功能维度主要是划分清楚业务边界,采用的主要设计方法可以利用 DDD(即领域驱动设计,关于 DDD 的理论知识可以参考网上其它资料),DDD 的战略设计会建立领域模型,可以通过领域模型指导微服务的拆分,主要分四步进行:

以电商的场景为例,交易链路划分的限界上下文如下图左半部分,根据一个限界上下文可以设计一个微服务,拆解出来的微服务如下图右侧部分。

2 非功能维度
当我们按照功能维度进行拆分后,并不是就万事大吉了,大部分场景下,我们还需要加入其它维度进一步拆分,才能最终解决单体架构带来的问题。

以上几种拆分方式不是多选一,而是可以根据实际情况自由排列组合。 同时拆分不仅仅是架构上的调整,也意味着要在组织结构上做出相应的适应性优化,以确保拆分后的服务由相对独立的团队负责维护。

古希腊哲学家赫拉克利特曾经说过:“人不能两次踏进同一条河流。”随着时间的流逝,任何事物的状态都会发生变化。线上系统同样如此,即使一个系统在不同时刻的状况也绝不会一模一样。现在拆分出来的服务粒度也许合适,但谁能保证这个粒度能够一直正确呢。

1 不打无准备之仗
开发团队是否具备足够的经验,能否驾驭微服务的技术栈,可能是第一个需要考虑的点。这里并不是要求团队必须具备完善的经验才能启动服务拆分,如果团队中有这方面的专家固然是最好的。如果没有,那可能就需要事先进行充分的技术论证和预演,至少不打无准备之仗。避免哪个简单就先拆哪个,哪个新业务要上了,先起一个服务再说。否则可能在一些分布式常见的问题上会踩坑,比如服务器资源不够、运维困难、服务之间调用混乱、调用重试、超时机制、分布式事务等等。
2 不断纠正
我们需要承认我们的认知是有限的,只能基于目前的业务状态和有限的对未来的预测来制定出一个 相对合适的拆分方案 ,而不是所谓的最优方案,任何方案都只能保证在当下提供了相对合适的粒度和划分原则,要时刻做好在未来的末一个时刻会变得不和时宜、需要再次调整的准备。因此随着业务的演进,需要我们重新审视服务的划分是否合理,如服务拆的太细,导致人员效率反而下降,故障的概率也大大增加,则需要重新划分好领域边界。
3 要做行动派,而不是理论派
在具体怎么拆分上,也不要太纠结于是否合适,不动手怎么知道合不合适呢?如果拆了之后发现真的不合适,在重新调整就好了。你可能会说,重新调整成本比较高。但实际上这个问题的本质是有没有针对服务化架构搭建起一套完成的能力体系,比如服务治理平台、数据迁移工具、数据双写等等,如果有的话,重新调整的成本是不会太高的。

2021年6月11日整理于去往大连的动车上,前方到站:盘锦。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zz/10916024.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-12
下一篇 2023-05-12

发表评论

登录后才能评论

评论列表(0条)

保存