写点什么

微服务沉思录 - 伸缩性

发布于: 2021 年 08 月 15 日
微服务沉思录-伸缩性

1.伸缩性

相信读者朋友们在日常工作中都听说过这样的笑话:当页面访问量、订单数量、数据存储量上来后,系统扛不住压力了,工程师往往会故作轻松的调侃道“不行就加机器”。


这里虽有戏谑的成分,但也反应了一种质朴的系统伸缩性思想。而现实中,能通过增加机器来解决系统的容量问题,从某种意义上来说,反而体现了一种优良的架构伸缩特性。


所谓伸缩性(Scalability),是指当工作负载发生变化时,为了继续保持良好的性能和可靠性,系统随之高效的增加或减少容量的技术能力。


这里的工作负载是一个广义的概念,通常是指流量、数据量和资源的使用率、饱和度等。工作负载的变化通常是由外界输入条件变化引起的,如微博热门事件、电商网站秒杀活动、DDoS 攻击等。

1.1.伸缩的必要性

在讨论分布式系统伸缩性之前,我们有必要先了解下系统工作负载的变化趋势。


以互联网业务的流量来举例说明,其变化往往具备一定的规律。如常见的打车业务,一般会存在早、晚两个高峰,用户访问量和订单量也会比其他时间要高,而在中午或凌晨,流量会随之回落。这种有规律的流量,我们姑且称之为“潮汐流量”。


如图 1‑1 所示,这是一个视频播放服务的典型流量模型。可以看出,每天有两个高峰期:中午 12 点左右和晚上 9 点左右,而在凌晨 4 点则是流量的低谷,这种流量的曲线呈规律的周期性变化。

图 1‑1 流量的潮汐效应


还有一种常见的流量变化方式,在短暂的瞬间,流量突发增长,达到一个很高的峰值 QPS 后又迅速回落。我们称之为“脉冲流量”,如图 1‑2 所示。脉冲流量通常是一些突发事件或运营活动导致的瞬间用户请求大量涌入,如热门娱乐事件推送、促销活动上线等。

图 1‑2 流量的脉冲效应


以上是两种常见的流量变化趋势,当然,随着业务的变化还会有不同的流量模型。但不管如何变化,对于一个稳定的业务系统来说,这些变化通常是周期性的、有规律的、可预测的。


当然,除了常见的由业务驱动的正常流量变化以外,我们还会发现在异常情况下,也会触发流量的反复波动,这些变化通常不可预测。比如:


DDoS 攻击:由物理位置分散(通常是分布在全球)的僵尸网络节点,向服务提供方发起持续的请求,占用服务端的网络带宽和 CPU、内存资源。DDoS 攻击最直观的表现就是请求流量大幅提升,可以持续很长时间,系统资源迅速消耗殆尽,无法为正常用户提供服务。


重试风暴:由于微服务调用链中某些局部服务或组件失败,导致请求方大量重试,重试可能来自终端用户,或内部系统自动发起。重试的结果可能会导致流量突增,甚至超出系统的正常承载能力。


故障转移:正常的故障处理流程中,故障从发现到恢复,必然少不了故障转移阶段。所谓故障转移,是指将失效的组件屏蔽,将用户请求转移到冗余的组件中。很多时候,由于缺少系统容量规划,或规划不合理,就会导致承载多余流量的组件发生过载,系统被进一步压垮。


由以上的分析可见,微服务系统的容量规划至关重要,它可以在关键时刻抵御突发的工作负载,让系统可以继续保持高性能和高可用,提供稳定可靠的服务。


同时,也不难看出,传统的容量规划方式并不理想,如图 1‑3 所示。当我们采取“经济”的容量规划方式时,如图中实线所示,资源可以得到充分利用。但是当流量发生突变,或者出现了预期的峰值 QPS 时,容量显然不够使用,系统就会出现性能下降甚至失败率提升,当高峰期过去后,流量又会回落到容量水平线以下。


如果采用“安全”的容量规划方式,如图中虚线所示。显然系统的性能和可靠性能得到保障,用户满意度也比较高,但弊端是会带来不必要的资源浪费。

图 1‑3 传统的容量规划方式


那么,是否存在一种可以随着流量变化而随时调节系统容量的方式?答案是肯定的,这就是系统的伸缩特性,如图 1‑4 中虚线所示。沿着系统的工作负载变化曲线,系统能够通过手动或自动的方式任意调整容量,这种方式兼顾了成本、性能和可靠性,且具备很大的灵活性。

图 1‑4 系统伸缩性

1.2.伸缩模型

微服务系统要想做到伸缩自如,首先需要对系统的伸缩性做一层抽象描述,仔细分析该抽象模型的特征,并尝试得出最优解。本章节我们从伸缩模型的角度,来分析垂直伸缩和水平伸缩两种方式的特点和利弊。

1.2.1.模型描述

如图 1‑5 所示,我们将常见的微服务系统抽象成由三种节点组成的简化模型。这三种节点分别为:代理节点、无状态节点和有状态节点。

图 1‑5 伸缩模型


这种伸缩性模型足够的简单和易于理解,用户请求通过代理节点,进入无状态节点内部进行逻辑单元计算和数据存取。当需要访问数据时,可以通过代理节点将数据读写请求发送到有状态节点上,执行读写操作。


代理节点是一种中间层节点,它一般不会执行大量的计算逻辑,也不会存储太多状态。代理节点的基本作用就是对请求进行转发处理,代理存在的目的通常是解决负载均衡、鉴权、可观测性、流量控制、请求预处理、安全等问题。代理节点可以是服务端代理,也可以是客户端代理。举个例子,常见的服务端代理有 Nginx、HAProxy 等 4 层或 7 层负载均衡代理软件;常见的客户端代理可以是嵌入到应用中的组件,如 Apache Dubbo 框架、各种数据库和中间件的 Driver 等,当然也可以是和应用进行解耦的近距离组件,如服务网格、各种 Agent 软件等等。


无状态节点很好理解,就是指只执行逻辑处理和计算的节点,节点本身不会保存状态或只保留那些可随时擦除又不会对功能、性能造成影响的弱状态。在实际生产环境中,无状态节点通常指服务层,如 WEB Server、API Server 集群的节点。


有状态节点,顾名思义,节点本身会存储数据。数据就意味着状态。这些节点除了要对外提供读写之外,还需要考虑节点之间数据的复制以及一致性处理。有状态节点的伸缩性远比无状态节点要复杂的多,也是伸缩性架构设计的难点之一。


从以上的伸缩性模型分析不难看出,要想将该模型的规模进一步扩大或缩减,就需要对三类节点就行扩缩容。常见的做法有垂直伸缩和水平伸缩,下面来重点介绍这两种伸缩方式的特点和利弊。

1.2.2.垂直伸缩

垂直伸缩也称为 Scale-up,是最容易想到的扩缩容技术方案,如图 1‑6 所示,每个节点由当前的实线扩展到虚线位置、或由虚线缩减到实线位置,即可视为垂直伸缩。通过对节点的硬件、软件配置进行升级换代,使得节点具备更大的容量、更高的处理吞吐量和更低的处理延迟。

图 1‑6 垂直伸缩示意图


举例说明,某电商业务系统,服务节点当前的硬件配置为 8 核 CPU、32GB 内存和 100GB 磁盘空间大小,而数据节点配置为 16 核 CPU、32GB 内存和 300GB 磁盘大小。最近该业务迅猛发展,用户流量增加较快,峰值 QPS 以每日新增 5%的速度持续增长,服务节点的性能瓶颈已经很明显,很快就要发生过载;同时,用户量的增加也带来了订单、客服、互动等数据量的持续积累,原有的数据节点无论是处理速度还是磁盘空间都已经捉襟见肘,300GB 的空间占用率已达到 90%以上,亟待扩容。


垂直扩容的方式很简单,只需要对这两个节点的流量、数据量等工作负载的当前值和未来一段时间做充分的容量评估,升级配置即可。在本案例中,我们将服务节点升级到 32 核 CPU、64GB 内存和 300GB 磁盘,同时将数据节点升级到 48 核 CPU、128GB 内存和 1TB 磁盘。


垂直伸缩的优势很明显,方案简单、易于理解且操作简便,早期的传统行业基本都是采用垂直伸缩的方式来应对工作负载的变化,这些企业一般都会采购如 IBM 大型机、EMC 存储、Oracle RDBMS 等专业的、昂贵的硬件和软件来搭建系统。


这种伸缩模式的劣势也非常突出,首先就是垂直扩容很容易达到系统的物理上限,随着工作负载的增加,不可能一直无限制的升级配置。


其次,垂直伸缩不具备高可用能力。任何时候系统只能保持一个或有限的几个节点,单点故障风险很明显。即使节点配置再高,当遇到故障因素发生时,这些节点可能就会失效,整个系统就会面临宕机的风险。


再次,垂直伸缩的操作周期可能较长,不具备良好的敏捷性。这些节点要升级优化配置,可能要做在线系统、数据的迁移,动辄几个小时,极端情况下甚至需要通过业务停服的方式来升级到新的系统。

1.2.3.水平伸缩

和垂直伸缩恰恰相反,水平伸缩是通过在水平方向上对节点进行复制和部署,达到系统容量扩张和收缩的目的,如图 1‑7 所示。水平伸缩也称为 Scale-out,它和分布式系统的设计理念密切相关。

图 1‑7 水平伸缩示意图


和垂直伸缩不同,水平伸缩往往不需要节点具备多高的配置,服务器一般采用普通的、廉价的 PC 服务器即可。这种方式可以通俗的理解成是一种“小鱼吃大鱼”的效应,如图 1‑8 所示。

图 1‑8 多实例带来的小鱼吃大鱼效应


水平伸缩的优势是显而易见的,它的重点是采用多实例来提升系统容量和承载能力,并提供优良的性能,同时多实例的冗余度也带来了足够高的可用性。


另外,随着现代化的架构和容器化、云原生等技术的普及,水平伸缩的速度更快、效率更高,也更加自动化,笔者在实际生产环境中所运行的基于 Kubernetes 的云原生环境,通常只需要不到一分钟就可以将某服务的容量提升 2 倍。


还是以上面描述的电商业务系统为例,当我们通过指标监控发现系统流量、数据量增加时,并不需要去准备硬件升级改造,而是直接增加新的实例节点,加到业务集群即可。这个扩容过程相对容易,将应用发布到新增的节点或增加更多的数据节点即可,甚至可以让系统自动的、弹性伸缩,无需人工值守,关于弹性伸缩后续会有章节重点说明。


在现代化的微服务架构中,笔者高度鼓励采用水平伸缩的模式来改变系统的规模。除非特殊说明,本章接下来的内容都是围绕水平伸缩来展开讨论。

1.3.AKF 伸缩立方体

水平伸缩模式是打造微服务系统可伸缩性的一剂良药,但水平伸缩必须按一定的章法来实施,方可做到事半功倍。本章节重点介绍 AKF 伸缩立方体的理论体系。


AKF 伸缩立方体(也译作 AKF 扩展立方体)是《架构即未来》一书中提出的一种伸缩模型,从广义上来说,也是一种抽象的扩展思维的提炼。AKF 伸缩立方体如图 1‑9 所示。

图 1‑9 AKF 伸缩立方体


AKF 伸缩立方体分为三个轴。其中,X 轴伸缩代表实例的复制,这是对无状态节点或有状态节点完整的克隆和部署;Y 轴伸缩代表按功能、模块或服务进行垂直拆分;而 Z 轴伸缩则代表根据数据的特征进行水平拆分。


读者务必要注意的一点是,AKF 伸缩立方体所描述的 X、Y、Z 轴扩展,是针对水平伸缩概念的,和上文提到的垂直伸缩(增加单节点的配置)无关,请不要将两个相似的概念弄混淆了。


为了更好的理解 AKF 伸缩立方体,我们举一个偏实际业务的例子来说明问题,用该例子来贯穿于整个 AKF 伸缩理论体系。


假设我们现在需要从头开始创建一个视频网站,该网站提供 7*24 小时的视频播放服务,服务的对象主要为大陆、港澳台和海外用户。该网站包括的功能点有:

  • 视频的分类筛选功能,如电视剧、电影、综艺、短视频、小视频等;

  • 视频的搜索功能,如根据关键字搜索电视剧、电影、小视频、作者等;

  • 视频的推荐功能,根据用户的画像信息,推荐优质的内容;

  • 视频的播放功能,提供播放、快进、续播、下载、弹幕等功能;

  • 广告的分发功能,将广告插入到视频的播放间隙或页面的固定位置;

  • 会员购买与管理功能,提供会员售卖、会员积分等功能;

  • 用户个人信息管理功能,如注册、登录、昵称设置等;


从领域建模的角度,不难看出,视频的分类、搜索、推荐解决的是用户获取内容的问题;视频的播放解决的是用户最核心的观看诉求;视频的广告、会员解决的是该业务的变现问题;最后一部分是解决用户的个人信息管理诉求。


好了,这些功能已经能满足用户的最基本需求,可以支撑起一个视频网站正式投入运营了。经过一个月的紧张开发,该网站上线了,我们来看下网站后端的物理部署架构,如图 1‑10 所示,姑且称之为视频网站架构 V1.0。

图 1‑10 视频网站架构 V1.0


迫于时间和进度的压力,这个系统匆匆上线了,架构也很简单:单一的服务实例和单一的数据实例,服务实例内部按分类、搜索、推荐、播放、广告、会员、用户等 7 个模块进行开发,数据层面也是单一数据库按 7 个模块分成了 7 张表。之所以使用负载均衡是考虑了请求的流控、鉴权、预处理等能力,这些用 SLB 来实现更加成熟可靠。

1.3.1.实例复制

网站上线的第一个月,用户寥寥无几,每天只有有限的请求量和累计数据量,使得架构 V1.0 可以充分发挥作用且能游刃有余。


然而,从第二个月开始情况就大不相同了。随着网站的大面积推广,各种活动及丰厚的奖励,越来越多的用户开始了解并访问该网站,从监控数据看,QPS 峰值增长速度很快,另外 V1.0 架构的单实例应用和数据节点资源消耗越来越大,扩容势在必行。我们顺势推出了架构 V2.0,如图 1‑11 所示。

图 1‑11 视频网站架构 V2.0


视频网站架构 V2.0 是一个典型的 AKF  X 轴伸缩模型的实现,即对 V1.0 的单实例服务节点和单实例数据节点进行克隆、复制和部署。


对于服务层面来说,多个应用实例通过负载均衡 SLB 来分摊流量,负载均衡这时候开始真正发挥作用了。通过这种方式,单实例的性能得到了保障,并且由于多实例的存在,系统的可靠性也得到进一步加强,当一个或部分实例出现故障时,可将其进行屏蔽和故障转移,同时追加相同数量级的实例进行补偿即可。


对于数据层面来说,需要注意的是,这里特指的是依赖 Master-Slave 读写分离机制而制定的伸缩计划。Master-Slave 架构一般是读流量分摊到各 Slave 实例,而写流量还是继续写入 Master 实例。当数据层面需要扩容时,通常直接复制 Slave 实例加入到数据集群即可。关于一些分片数据库,我们在水平拆分的章节会详细说明。


X 轴实例复制方式,是一种理论上可任意扩展的机制,当工作负载出现预期、异常的增长时,我们完全可以通过增加更多的实例来扩大系统规模。

1.3.2.垂直拆分

多实例复制的架构 V2.0 在生产环境中运行了半年左右,基本符合业务需求,运行还算稳定。但有些问题开始陆陆续续暴露出来了。


首先,在开发态层面,这是一个典型的“单体应用”。这 7 个功能区域在一个代码库中进行开发和维护,耦合度极高。经常出现“牵一发而动全身”的效应,由于大量的人员参与该项目,每天都有大量的代码冲突产生,光解决这些代码冲突就要耗费大量的人力和时间。CI/CD 时经常发生由于底层的模块被改动而无法持续集成的问题。当出现功能性 BUG 时,有时候定位半天后才发现是队友引入的问题,BUG 需要再次流转。


随时时间的推移,这个应用变成了庞大的巨无霸系统,编译、打包和部署动辄需要几十分钟甚至一个小时。


其次,在运行态层面,每个服务实例中,7 个功能在一个进程中运行,缺乏有效的隔离机制。当一个功能出现功能或性能缺陷时,往往会导致其他功能一起受到影响,极端情况下甚至导致系统直接宕机,所有功能均无法正常对外提供服务。比如:某个底层的数据访问模块出现故障、Java 应用出现频繁的 Full GC、服务耐以生存的运行时基础环境被破坏,等等。


每个数据实例中(如 MySQL 主库或从库),由于 7 个功能所产生的数据都存储在该实例的磁盘中,导致存储空间的占用率急剧上升,很快就会达到磁盘的物理上限,这时候靠垂直伸缩已经无能为力了。另外,服务层实例的大量复制,尽管应用层面使用了连接池技术,但数据库的连接数还是出现了大幅提升,这些连接消耗了数据库的大量资源(CPU、内存等),使得数据的读写操作延迟进一步增大。


单体应用的弊端显而易见,在架构 V2.0 运行半年左右,我们实施了 AKF Y 轴伸缩方案,即对该业务按功能、模块和领域进行了领域建模和微服务划分,并推出了架构 V3.0,如图 1‑12 所示。

图 1‑12 视频网站架构 V3.0


视频网站架构 V3.0 是一种典型的垂直拆分思路,战略层面利用 DDD 的思想对系统进行了领域建模,识别出具体的领域(本案例中还是 7 个领域)、聚合根、实体、限界上下文,等等。战术层面采用了微服务来进行系统改造和落地。


架构 V3.0 上线后,原来的一个系统演变成了 7 个系统,并且可以独立变化和部署。可控制粒度进一步变细,开发、运行可以更加灵活,开发冲突变少,运行环境互相隔离。数据存储层面,经过垂直拆分后,单数据库实例可以承载更多的数据量,而且读写性能表现更佳。

1.3.3.水平拆分

架构 V3.0 在线上运行了 1 年左右,具备较强的扩展性、伸缩性和性能,系统的 SLA 也提升明显。然而,一年以后,随着用户量和数据量的持续积累,新的瓶颈出现了。


一方面,服务层面需要多机房(多 AZ)和多 Region 部署,尤其是在不同地理位置的数中心部署,为了避免跨区域数据中心带来不必要的延迟,需要将同一个用户的请求路由到固定的服务集群中。当前的架构不能满足按用户或其他维度分流的诉求。


另一方面,数据层面,垂直拆分后的数据库很快就暴露出短板。对大部分 Master-Slave 的数据库集群而言,写入依旧在单一的主库中进行,这就意味着在大量的写请求下,写库的性能严重不足。另外,由于单实例承担了该业务(如搜索)的所有数据量,当业务发展壮大后,单实例的存储空间靠垂直伸缩已无法满足海量数据的存储需求。

图 1‑13 视频网站架构 V4.0


鉴于此,我们设计了架构 V4.0,如图 1‑13 所示,核心思路是践行了 AKF Z 轴伸缩模型。我们将流量划分为服务请求和数据读写请求两大类,每一类请求都可以根据一些固定的维度来划分 sharding_key,根据一定的哈希算法(如一致性 Hash),将请求路由到服务或数据的不同分片中。


如图 1‑13 中服务层所示,当用户请求进入时,根据以下计算公式对请求进行哈希分流:

service_sharding = func(service_request, sharding_key)


哈希算法是稳定的,因此该用户请求会被固定路由到应用服务的某个实例中,如图中的 a、c 请求落到服务实例 1 中,而 b、d 请求则落到实例 N 中。


如图 1‑13 中数据层所示,当系统发起数据读写请求时,根据以下计算公式对请求进行哈希分流:

data_sharding = func(data_read_write_request, sharding_key)


我们可以将传统的 Master-Slave 数据库集群,进一步分库分表,根据 sharding_key 将该数据路由到对应的分片中(如 db0-tbl10,或 db1-tbl15),如图中的 1、3 请求落到数据库实例 1 中,而 2、4 请求则落到实例 N 中。


需要说明的是,之前的架构并没有提及 NoSQL 存储,原因是为了在这里统一描述,用于理解数据的水平拆分思路。很多 NoSQL 存储甚至是一些中间件,原生提供了数据的分片功能,如 MongoDB 分片集、HBase、Couchbase、Cassandra 和 Elasticsearch,等等。架构 V4.0 也使用了这一类存储体系。


到目前为止,这个视频网站架构的案例就结束了。简单总结下,这个案例体现了 AKF X、Y 和 Z 轴伸缩的设计原则,系统可以通过复制实例、垂直拆分和水平拆分,打破伸缩性的各种瓶颈,理论上可以实现无止境的伸缩能力。

1.4.伸缩设计模式

1.4.1.无状态

状态意味着数据的滞留,状态是系统可伸缩性的头号难题。要想系统规模能够伸缩自如、能够无穷的扩展,首先需要考虑的就是无状态设计模式。


应用层面的状态通常比较简单,比如用户会话的保持,如购物车、个人信息的维持等等。在早些年的系统设计中,会话(Session)保存在应用层面本身,要想用户能稳定的访问到这些会话,可以采取常见的两种措施:

  • 通过负载均衡的粘滞会话(Sticky Session)机制,将相同的用户请求引流到同一个应用实例中,保证每次访问的都是同一个会话对象。如 NGINX 反向代理的 ip_hash 功能。

  • 应用之间进行 Session 共享复制,如 Tomcat 提供的 Session 同步功能,可以让实例之间互相感知彼此的 Session 信息。


以上两种传统方式,劣势很突出。采用粘滞会话可能导致严重的数据倾斜,比如同一个 IP 地址段可能会发起大量的请求,负载均衡所路由的目标应用实例可能会面临过载的风险,其次要对负载均衡水平伸缩或迁移,也会有不小的成本。


应用之间的 Session 复制,可能会产生同步延迟,导致数据更新不及时。并且,在高并发和高访问量的情况下,Session 复制会频繁发生,占用大量的网络带宽,也会给应用实例带来大量的性能开销。


因此,理想情况下应该消除应用层面的状态,如将用户的 Session 信息存储到分布式缓存(如 Memcached、Redis 等)中,请求可以打到任意应用实例,需要存取 Session 时,直接从中心的缓存中读写即可。


需要再次强调的是,我们在讨论应用层无状态时,并不是说应用节点完全无状态。事实上,可以保留一些弱状态,通常指一些缓存在本地的可擦除(Eviction)数据,譬如使用 EhCache、Guava Cache 或本地磁盘临时文件等方式暂存的一些用户数据,这些数据在失效或剔除后,对业务没有影响,因为应用完全可以从存储中再次回源并重新加载。


可以看出,所谓的无状态设计模式,大部分时间并不是指状态真的消失了,而是转移到其他层面,如缓存、存储层。


数据层面的状态是天然就存在的,不可避免,也是伸缩性架构中需要重点解决的问题。

1.4.2.模块化

模块化严格上来说是一种开发态的概念,当然也可以引申为一种广义上的扩展性必要因素。关于扩展性,会有单独的章节来说明,本章节只讨论模块化设计模式在系统可伸缩性方面的影响,如图 1‑14 所示。

图 1‑14 泥球式设计和模块化设计


当系统在开发和部署时,如果处处遵循模块化的设计原则,则可以细粒度的控制开发和部署单元,伸缩性自然也可以从最小的颗粒度来进行资源的调配和扩展。


如图 1‑14 的 1)所示,如果把整个立方体看成是一个完整的业务系统(如视频网站),或者业务系统的一部分(如搜索业务),当这个立方体的内部模块出现大量的耦合,同时模块的内聚性几乎丧失时,可想而知,这样的系统要想合理的划分边界,独立开发和部署,几乎不可能实现。我们称这种不良的设计为“泥球式设计”。


相反,如图 1‑14 的 2)所示,如果立方体的内部依旧是由一个个小的立方体组成,这些小的立方体就是一个个模块。模块具有高度的内聚性,同时模块之间尽可能松散耦合,互相之间不用了解彼此内部的构造原理,只需要通过标准的协议进行交互即可。这种设计看上去就像“搭积木”一样,令人赏心悦目,很明显,这种优良的设计为系统伸缩性奠定了良好的基础。系统只需要按这些立方体进行切割,划分边界上下文,即可实现微服务的拆分和独立部署、伸缩。

1.4.3.面向服务

模块化与服务化是拆分的基本实现方式,模块化是服务化的理论基础,服务化是一种落地的表现形式。常见的概念如模块即服务(Module as a Service,MaaS)即来自如此。

图 1‑15 微服务伸缩示例


从系统伸缩性的角度来考虑,微服务将业务按领域模型进行切割、划分和独立部署,粒度更细。我们完全可以根据工作负载的需要,只对部分需要的微服务系统进行伸缩。


如图 1‑15 所示,这是一个典型的打车业务对应的司乘后台系统,经过一段时间的线上运营后,我们观察到乘客服务流量出现了大幅提升,同时支付系统的资源消耗比较大,而其他服务如司机服务、行程服务、建筑服务和通知服务工作负载正常,符合容量预期。在这种情况下,我们只需要对乘客服务和支付服务进行容量预估,并执行水平扩容即可。

1.4.4.分片与复制

在讨论 AKF 伸缩立方体的时候,已经仔细分析过 X 轴实例复制和 Z 轴水平拆分。这两种伸缩方式从设计模式的角度上说,体现了分片和复制的原则。


分片(Sharding,也称为 Partition),是将服务或数据按一定的维度进行切分的过程。很多情况下特指数据的分区,如 Kafka 的 Producer 根据数据的 Key 进行 Hash 计算,并将该数据投递到对应 Partition 的 Leader 节点上。分片的本质是拆分,有了拆分就可以消除单一存储空间的局限性,达到无限伸缩的目的。


复制(Replication),是将服务或数据的实例进行克隆,将新的节点加入到集群的过程。当然也指数据分片的复制和冗余。还是以 Kafka 为例,当 Partition 的 Leader 节点妥善保存好当前数据后,会将该分片以同步或异步的方式在其他 Broker 节点上创建副本。复制可以解决由于性能瓶颈而导致的伸缩性问题,同时也可以消除数据丢失的风险。


分片和复制也是分布式架构中的重要组成部分,会在分布式章节中重点说明。

1.4.5.尽可能异步化

异步化是解耦的一种常见实现方式,可以使得消息生产者或消费者独立伸缩和变化,无需考虑彼此。


还记得在伸缩模型中提到的代理节点吧?常见的异步化实现方式就是采用消息代理中间件,如 Kafka。数据的发送方只需要负责生产数据,并将消息投递到目标消息中间件上。而数据的消费方无需关心数据的源头,只需要根据预先配置的策略,从消息中间件消费数据即可。


通常,消息中间件自身具备优秀的性能、可靠性和伸缩特性,如由 5 个物理机节点组成的 Kafka 集群,在最弱级别的 ACK 模式下,吞吐量可以达到每秒百万级别消息量,另外 Kafka 在线扩容也是非常易于操作的。


在这种情况下,我们无需考虑生产者和消费者之间的匹配,只需要对有容量瓶颈的一侧进行扩缩容即可。

1.4.6.指标监视与健康检查

指标监控和闭环反馈是实现自动伸缩的必备因素。


当我们讨论系统指标时,通常是指峰值 QPS、响应时间、错误率、CPU 使用率、内存使用率、网络带宽使用率、系统平均负载情况等一系列基础设施指标和业务指标。这些指标代表了系统的运行健康度,指标的变化预示了系统可能要进行伸缩。


采集和监视这些指标并不困难,在可观测性一章中我们已经讨论过这一系列的数据流 Pipeline 工作方式,通常我们会使用时序数据库来存储这些数据。这里不再赘述。


从系统可靠性的角度来看,健康检查是实现故障发现和转移的重要途径。从系统伸缩性的层面看,健康检查也是执行系统伸缩的一个重要因素,因为对不健康节点进行摘除和补偿也是伸缩性的一个重要体现。这两者是密切相关的,这里讨论下健康检查的机制。

  • 健康检查是指周期性的检测系统指标数据,和预期的阈值进行对比。当出现指标异常时,可以发出告警,或通过闭环反馈的方式,让系统自动执行故障转移和伸缩。一个合理的健康检查通常包括以下部分:

  • 指标检测的周期。如每隔 1 分钟执行一次检查;

  • 检测的指标对象。如 HTTP 请求路径,QPS、延迟、错误率等指标;

  • 指标异常或正常的判断标准。如 QPS 超过多少视为过载、延迟及错误率超过多少视为服务节点不健康,或 HTTP 响应码为 4XX 或 5XX 视为节点故障,等等。

  • 判断健康、不健康状态的累计次数。之所以需要设计该门槛值,是防止由于瞬间抖动造成健康检查出现误判。一般来说,2~5 次的计数就可以判定状态发生变化。


健康检查按实现方式通常可分为 TCP、HTTP 和 Command。TCP 方式指通过指定的目标主机和端口,进行探活测试,达到健康监测的目的,如目标端口关闭,则可以视为节点宕机;HTTP 是指健康检查器根据目标 URL 进行周期性访问,并收集响应状态码和延迟时间,来监测节点健康度;Command 方式相对灵活,可以在健康检测器中自定义监测逻辑,对目标对象进行测试。


健康检查一般由负载均衡、集群的管理节点发起。用户也可以自研健康检查组件,但是需要考虑组件本身的高可用。

1.4.7.闭环反馈

指标监控和闭环反馈是实现自动伸缩的必备因素。闭环反馈控制的概念来自控制工程,通过将输出行为反馈到输入端,并和预期行为进行比较,从而辅助动态调整决策,实现一个闭合回路。如图 1‑16 所示,是一个微服务系统的闭环反馈控制系统。

图 1‑16 闭环反馈


在该系统中,控制器为负载均衡、服务集群控制中心和数据集群控制中心;受控对象为服务实例和数据实例;信号反馈通路是由负载均衡、健康检查器出发形成的反馈通道。


反馈信号可以是正反馈或负反馈,一般来说以负反馈居多。当系统指标好转时,可以视为正反馈信号,这会触发集群规模缩容;反之,当系统指标恶化时,可以根据具体的策略来决定是杀掉不健康实例,还是采取集群扩容。


对负载均衡来说,当检测到目标服务节点状态变为不健康时,则可以选择将该节点从负载均衡策略中摘除,同时发送负反馈信号到服务集群控制中心,由控制中心负载对节点进行补偿或扩容处理。


对健康检查器来说,同样的道理,当检测到目标服务节点或数据节点变为不健康时,则将负反馈信号输送到负载均衡用于决定是否摘除节点,同时输送到集群控制中心用于调整集群规模,比如补偿故障节点或进行扩缩容。

1.4.8.虚拟化与容器化

如图 1‑17 最左边的部分所示,传统的部署模式是采用物理机来充当服务器运行时环境,应用和物理机强绑定,这种方式简单直接,但非常不利于系统伸缩。当需要做系统迁移或扩容时(不论是垂直扩容还是水平扩容)都需要重新升级配置、装机、部署,耗时耗力,无法响应快速变化的业务需求。


随着云计算的兴起及 Iaas(Infrastructure as a Service)概念的推广,越来越多的企业基础架构已经实施了虚拟化。虚拟化也是云计算的一项重要使能技术,如图 1‑17 中间部分所示,虚拟化是通过一层 Hypervisor 中间件来对硬件进行虚拟化,可以在单一物理机上创建多个虚拟操作系统,并且可以提供多租户的能力,使得用户之间资源互相隔离。


虚拟化技术分为两种:Type 1 和 Type 2。Type 1 是指直接在物理机设备上安装 Hypervisor 操作系统,这种方式性能高,常见的 Type 1 型软件有 VMWare ESXi,这种方式有时候也称为裸金属(Bare Metal)。Type 2 是指安装在操作系统上的一层 Hypervisor 中间件,常见的有 Linux KVM、VMWare Workstation 等。

图 1‑17 运行时环境的变化


此外,延伸开来说,虚拟化不仅仅指的 VM,也可以是广义的虚拟化,如负载均衡的 VIP、各种 Proxy、Elasticsearch 的 Alias 等等。这样做的好处是上层不用关心底层的实现,底层也可以灵活安排迁移和伸缩。


虚拟机将应用和运行时解耦,部署简单,提供了迁移、监控、管理等能力。但是,在日常运维中,我们也发现虚拟机提升资源使用率困难,几乎不具备弹性伸缩能力。在流量突增时,通过人工扩容工序复杂、响应速度慢。而在流量低峰期,资源浪费严重。除此之外,研发人员 24 小时疲于应付服务器宕机、指标异常、流量过载等运维工作,严重影响了开发和交付效率。


容器化的目标之一就是为了解决虚拟机的伸缩性问题,除此之外,容器还可以实现各种环境的标准化与统一,不用再担心测试环境、预发布环境和生产环境不一致。容器化如图 1‑17 右边部分所示,以常见的 Docker 容器引擎来说明,容器实例本质上只是宿主机上的一个进程,它通过 Linux Cgroups、Namespace 和 Change Root 等技术实现了实例之间的资源共享和隔离。


容器的创建和部署速度非常快速,可以秒级别实现创建、启动和销毁。今天,在 Kubernetes 等容器管理系统的加持下,使得大规模的容器云已经成为了可能。容器及 Kubernetes 编排是云原生的重要组成部分。

1.4.9.弹性伸缩

弹性伸缩是一种自动化的、无人值守的伸缩方式,从某种意义上来说,可以认为是系统伸缩的终极形态。


要想实现系统的弹性伸缩,需要指标监视、健康检查和闭环控制,这些在之前的章节已具体描述,这里不再赘述。另外,我们还需要指定弹性伸缩的策略,比如伸缩应该基于哪些技术指标?基于哪些考量因素(性能、成本、可靠性)?伸缩应该以什么方式进行?


弹性伸缩一般会结合容器化部署来进行,因为这是一种高效的部署方式。以一种常见的伸缩策略来举例说明,通常我们可以在控制台指定某个服务的最小和最大实例数,当 QPS、响应时间、请求失败率、CPU 使用率、内存或磁盘使用率达到预设的阈值时,系统会启动扩容,自动拉起新的实例,渐进式填充集群规模,直到达到实例数目的最大值。


更高级一点,我们还可以尝试周期性伸缩和预测性伸缩。所谓周期性伸缩,指的是利用流量的周期性特点(读者应该还记得潮汐流量和脉冲流量的规律性变化吧?),提前设置定时任务,在流量洪峰到来之前,提前扩大容量,避免在高峰期瞬间扩容带来一些稳定性的毛刺。预测性伸缩偏人工智能方向,是 AIOps 的重要组成部分,通过分析流量的变化,结合大数据和机器学习,预测未来可能会出现的流量高峰期和低谷期,并提前对容量进行调整。


弹性伸缩是云计算的重要特性,也是各大云计算产商争相宣传、引以为傲的卖点。需要警惕的是,弹性伸缩可能会带来不要的成本问题,譬如系统发生了 DDoS 攻击,或测试团队对线上服务进行了压测,如果开启了弹性伸缩,将会产生一笔价格不菲的账单。

1.5.服务伸缩

从本章节开始,我们来讲解下伸缩性在服务层面和数据层面的具体实现方案。

1.5.1.负载均衡

负载均衡在系统伸缩中起到了重要的作用。一方面,负载均衡通过健康检查,实时调整策略,下掉不健康节点或对其进行恢复。另外,负载均衡在服务实例发生变化后,还可以对工作负载进行 Rebalance,使得流量可以平均分配到各实例上。


在生产环境,我们大多使用 Nginx 或 Nginx 的衍生品来进行 7 层代理。Nginx 可以执行被动健康检查和主动健康检查。以下是一个例子:

upstream {   server 192.168.10.1:8080 weight=1 max_fails=3 fail_timeout=20s;   server 192.168.10.2:8080 weight=1 max_fails=3 fail_timeout=20s;
#主动健康检查 check interval=10000 rise=2 fall=3 timeout=1000 type=http default_down=false; check_http_send "GET /status HTTP/1.1\r\nConnection: keep-alive\r\nHost: $host\r\n\r\n"; check_http_expect_alive http_2xx http_3xx;}
location /api/v1/ { #超时设置 proxy_connect_timeout 5s; //跟后端服务器连接超时时间(代理连接超时),默认60秒 proxy_send_timeout 5s; //后端服务器数据回传时间(代理发送超时),默认60秒 proxy_read_timeout 5s; //连接成功后,后端服务器响应时间(代理接收超时),默认60秒
#重试设置 proxy_next_upstream error|timeout|invalid_header|http_500|http_502|http_504|off proxy_next_upstream_timeout 10s; proxy_next_upstream_tries 2;
#支持Keep-Alive proxy_http_version 1.1; proxy_set_header Connection “";
#传递Header信息给后端服务器 proxy_set_header xxx xxx; proxy_pass http://upstream;}
复制代码

Nginx 通过 proxy_next_upstream 系列参数定义,来决定什么情况下会判断当前请求失败,从而触发失败重试机制。请求失败可以分成两类:

  • 默认错误,包括 error、timeout 等;

  • 选择定义错误,包含 invalid_header 以及各种异常 HTTP 状态码错误等;


其中 timeout 的参数定义,是指代理请求过程中达到对应的超时配置(即通过 proxy_xxx_timeout 系列参数配置)。当失败次数达到 max_fails 次,Nginx 就会自动将该服务器屏蔽 fail_timeout 时间段,这种机制被称为 Nginx 的被动健康检查。这种检查方式是借助正常请求处理的契机而执行的,不会带来额外资源消耗。


Nginx 也可以执行主动健康检查,如本例所示,Nginx 每隔 10 秒钟执行一次健康检查,判断不健康的标准是超时(1 秒钟)或不符合预期的 HTTP 响应状态码(2XX、3XX 以外的),当累计出现 3 次不健康状态时,Nginx 即将目标节点屏蔽。相反,当累计出现 2 次健康状态时,Nginx 再次将该目标节点加入负载均衡策略。主动健康检查会发起间歇性查询请求,会带来一定的性能开销,尤其是 Nginx 集群的实例较多的情况下,对服务层形成的冲击比较可观。有条件的读者可以自行实现二次开发,比如考虑只在部分 Nginx 节点上开启健康检查,其他节点共享信息即可。

1.5.2.手动伸缩

手动添加服务节点相对简单,通过修改负载均衡配置或在控制中心添加、删除节点即可完成手动伸缩。


还是以上面的 Nginx 来举例说明,我们可以在对应的 upstream 中添加更多的服务器实例来扩容,如:

server 192.168.10.3:8080 weight=1 max_fails=3 fail_timeout=20s;


通过 nginx -s reload 命令即可实现平滑重启,Nginx 会重新平衡负载均衡策略,请求立刻可以路由到新的实例上。

1.5.3.自动伸缩

手动添加、删除节点的方式比较原始,且容易出错。现在有很多自动的方式可以支持动态调整集群规模。


一种方式是利用服务的自动注册和发现,如 Apache Dubbo,当新的服务器实例部署后,会自动将服务的自描述信息注册到服务注册中心(如 Zookeeper),服务的调用方可以通过订阅注册中心的事件通知,自动发现新的节点,再根据客户端的负载均衡策略,重新调整请求的路由方向。类似的做法还有利用 Consul 和负载均衡联动机制实现的自动伸缩。


上面这种方式,本质上只能算是半自动,因为还需要人工去部署和删减应用节点。真正的自动伸缩还是上个章节提到的弹性伸缩,利用指标监控、闭环控制,实现系统根据工作负载的变化而自动调整容量。

1.6.数据伸缩

数据层天然携带状态,这个层面的伸缩是最困难的。当我们对常见的存储、中间件系统的伸缩性能力了如指掌时,这些问题也可以迎刃而解。

1.6.1.数据层代理

伸缩模型章节我们讲解了代理节点的角色和功能,应用要想访问数据层,通常是需要通过一层数据层代理来实现。这种数据层代理可以来自客户端,如 MySQL Driver、MongoDB Driver、Jedis 等等;也可以来自服务端,如利用 HAProxy 或 DNS 方式组建的读负载均衡等。


对于客户端数据层代理,由于这种是和应用捆绑在一起的,随着应用单元内的伸缩而同步变化。因此,我们认为这类代理不存在伸缩性瓶颈。


对于服务端数据层代理,通常由性能强劲的负载均衡来充当,这些组件在迁移和扩缩容方面都具备成熟的经验,此外也并不需要频繁的变化。

1.6.2.MySQL 主从与分库分表

MySQL 的主从集群模式是使用面最为广泛的数据存储系统,以其性能稳定、社区成熟的特点,备受中小型互联网企业青睐。


很明显,MySQL 原生的主从集群只能遵循 AKF 的 X 和 Y 轴伸缩设计模式。在业务系统简单、数据量级少的情况下,通常只需要一个 Master 执行写操作,多个 Slave 执行负载均衡的读操作,即可满足大部分业务场景的需要。业务规模再大一点,再垂直拆分出来几个主从集群即可。实际场景中,很多技术团队也是这么设计和运行的。


当写请求量和数据存储量增加到一定的量级时,比如单表已经达到几千万、亿级别记录,或单库快达到磁盘空间上限时,我们就需要考虑 Z 轴伸缩了,即设计分库分表方案。


MySQL 的分库分表方案比较成熟,首先需要选取切分的 sharding_key,如用户 ID,产品 SKU 等等。可以采用两种常见的方式来对数据进行分片:

  • 按区间分片。比如 ID 为 1~1000000 的用户数据路由到 db0-tbl0,而 1000001~2000000 的用户数据路由到 db0-tbl1,以此类推;

  • 按哈希分片。比如通过用户 ID 和分库分表数目进行取模操作,将数据落到对应编号的数据库和表中;


按区间分片操作简单,扩容方便,但有数据倾斜的风险。按哈希分片,数据更容易均衡的写入数据库,但扩容尤其是在线平滑扩容的成本较高。这里介绍一种“指数裂变”的方式来平滑扩容:


假设某业务系统第一期只有一个 MySQL 主从集群,采用单库多表的形式,拥有 512 张表。因此,数据的哈希分片规则为:

db = db0, tbl = user_id % 512


现在需要对数据库扩容,增加一个新的数据库主从集群。首先,需要准备一套新的集群 db1,并且和原集群之间开启 master-master 主主复制,与此同时,我们开始着手准备对程序进行改造,新的哈希分片规则调整为:

db = user_id % 2, tbl = user_id % 512


和之前不一样的地方就是会根据用户 ID 对 db 进行路由选择,偶数 ID 继续落到 db0,奇数 ID 落到 db1。上线后,一部分数据就会进入 db1 进行读写。


为了让新的规则切换尽可能做到原子,避免出现大的数据不一致,我们在新的代码上增加开关。当代码改造上线后,我们开始观察主主复制进度,一旦完成,则开启开关,让新的路由规则生效,再断开复制链路。


现在偶数 ID 继续在原有的 db0 上进行读写,而奇数 ID 则在新的 db1 上读写(存量数据已经通过主主复制同步过来了)。最后,我们写一个清理脚本,将两个 db 上不属于该侧的数据删除即可。

1.6.3.MongoDB 复制集与分片集

MongoDB 的复制集和 MySQL 的主从集群模式很相似,也是遵循 AKF 的 X 和 Y 轴伸缩设计模式,这里不打算赘述。MongoDB 单实例遇到瓶颈后,通常我们不会采用自研的分库分表的方式,而是采用分片集的部署模式。

图 1‑18 MongoDB 分片集架构


MongoDB 分片集集群提供了原生的数据分片和管理能力,如图 1‑18 所示。其中,mongos 是和客户端打交道的路由组件,负责处理读写请求和高可用;Config Servers 存储分片等元数据信息;而 Shard 是数据的存储单元。


Shard 内部被进一步拆分成 chunk,当某个 chunk 数据量超过预设的阈值(如 64MB)时,MongoDB 会对该 chunk 进行裂变。MongoDB 有一种自动 Rebalance 的机制,可以检查所有 Shard,当发现有过多 chunk 的 Shard 存在时,会择机将过多的 chunk 迁移到其他负载较轻的 Shard 上。


MongoDB 分片集的使用需要指定 Collection 的 sharding_key,和 MySQL 分库分表类似,我们也可以指定按分区分片,还是按哈希分片。选择 sharding_key 需要非常谨慎,因为后续不可再更改,一旦选取不合理,会造成数据的倾斜问题。


可以看出,和 MySQL 分库分表方式比,MongoDB 分片集更加自动,扩容、数据迁移对应用来说完全透明,使用方只需要提供合理的分片键即可。

1.6.4.Redis 主从与集群

和 MySQL 主从集群及 MongoDB 复制集类似,Redis 也提供了主从部署模式,他们的伸缩方式类似,不再过多阐述。在实际生产环境中,考虑 Redis 单实例从奔溃到恢复的时间成本,运维通常不会提供很大容量的单实例,一般不超过 30GB。这种情况下,对 Redis 进行 Z 轴伸缩改造就显得格外重要了。


Redis 的数据分片是通过客户端来实现的,我们使用 ShardedJedisPool 来协调数据的分片和读写负载均衡,下面是一个例子:

<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig" primary="true">    <property name="maxTotal" value="${redis.maxTotal}”/>    <property name="maxIdle" value="${redis.maxIdle}"/>    <property name="minIdle" value="${redis.maxIdle}"/>    <property name="blockWhenExhausted" value="false”/>    <property name="maxWaitMillis" value="${redis.maxWait}"/>     <property name="testOnCreate" value="false”/>     <property name="testOnBorrow" value="false”/>    <property name="testOnReturn" value=“false”/>     <property name="testWhileIdle" value=“false”/>    <property name="timeBetweenEvictionRunsMillis" value=“30000”/>    <property name="numTestsPerEvictionRun" value=“50”/>    <property name=“minEvictableIdleTimeMillis" value=“50”/></bean><bean id="shardedJedisPool" class="redis.clients.jedis.ShardedJedisPool">    <constructor-arg index="0" ref="poolConfig"/>    <constructor-arg index="1">        <list>            <bean class="redis.clients.jedis.JedisShardInfo">                <constructor-arg name="host" value="${redis.host}"/>                <constructor-arg name="port" value="${redis.port}"/>                <constructor-arg name="timeout" value="150"/>                <constructor-arg name="name" value="shard_0"/>                <property name="password" value="${redis.password}"/>            </bean>            <bean class="redis.clients.jedis.JedisShardInfo">                <constructor-arg name="host" value="${redis.host1}"/>                <constructor-arg name="port" value="${redis.port}"/>                <constructor-arg name="timeout" value="150"/>                <constructor-arg name="name" value="shard_1"/>                <property name="password" value="${redis.password}"/>            </bean>        </list>    </constructor-arg></bean>
复制代码

配置方面,继续沿用 redis.clients.jedis.JedisPoolConfig 提供的参数来创建带有分片功能的 Jedis 连接池(redis.clients.jedis.ShardedJedisPool),这些参数的具体用法读者可以自行搜索文档了解。

@Autowired@Qualifier("shardedJedisPool ")private ShardedJedisPool shardedJedisPool;
public String get(String key) { String value = null; try (ShardedJedis jedis = shardedJedisPool.getResource()) { value = jedis.get(key); } catch (Exception e) { LOG.error("failed to get data, key={}", key, e); } return value;}
public void set(String key, String value) { try (ShardedJedis jedis = shardedJedisPool.getResource()) { jedis.set(key, value); } catch (Exception e) { LOG.error("failed to set data, key={}", key, e); }}
复制代码

代码层面,采用 Spring 的方式将上面配置的 id 为 shardedJedisPool 的连接池注入 IoC 容器,并自动完成初始化。可以看出,基本的操作(本例只列出了 get 和 set)很简单,只需要从连接池中获取 ShardedJedis 实例并执行读写操作即可。ShardedJedisPool 内部采用了一致性 Hash 算法,并且增加了虚拟节点使得数据分布更均匀,以寻求更好的负载均衡效果。


Redis 官方从 3.0 版本开始提供了 Redis Cluster,这是一种去中心化的集群,从实际生产环境来看,使用的场景不算多,感兴趣的读者可自行搜索资料学习。

1.6.5.Cassandra 集群

Cassandra 是一种无中心化的存储集群,内部所有节点对等,采用 Gossip 协议来感知彼此节点的状态,因此没有单点故障风险。客户端可以连接任意节点进行数据读写,集群内部采用读修复(Read Repair)和最后写入胜出(Last-Write-Wins,LWW)方式对数据进行合并处理。Cassandra 的设计理念来自 Amazon DynamoDB,代表了一类典型的分布式系统的设计方式,采用可调一致性(Tunable Consistency)和 NWR 法则来实现数据的强、弱一致语义。


由于是无中心化集群架构,Cassandra 的扩容和缩容非常简单直观。Cassandra 采用一致性 Hash 和 Vnode 方式来实现数据的分片和负载均衡。当我们需要增加或删除节点时,只需要准备好新的节点,安装 Cassandra 软件,并运行一些 nodetools 之类的运维脚本,结合对 token 进行调节,即可实现集群规模的动态调整。

1.7.伸缩的瓶颈

到此为止,我们从伸缩的可行性、理论基础和实现方式等各个层面剖析了微服务系统的可伸缩性。然而,伸缩性并没有银弹,我们在实际实施的过程中,还是可能会遇到一些瓶颈,这些问题需要我们仔细权衡利弊并有所取舍。

1.7.1.资源与成本约束

在实际执行系统扩容的时候,我们可能会遇到资源达到物理上限的情况。比如:物理机部署受机架、机柜或机房的容量大小约束。这种情况我们就需要考虑增加更多的机房或可用区,甚至是 Region 数据中心来容纳更多的资源。


另一方面,扩容往往也会受到成本的限制,遇到这种瓶颈我们需要仔细分析,是否可以从其他方面来“变相”扩容。比如对服务进行性能调优,减少每个实例的资源消耗,这样就可以对单个实例垂直缩容,将多出来的资源归还到资源池,再利用这些资源进行水平扩容。数据层面,减少不必要的存储,如冗余的日志,把单实例的存储能力提升上来。

1.7.2.Rebalance 问题

服务层的 Rebalance 通常很高效,服务器实例调整后,负载均衡可以马上识别并重新分配流量权重。


数据层的 Rebalance 可能是影响系统伸缩高效与否的重要因素,前面的章节也分析了常见的存储中间件伸缩方式。可以看出,有些存储方案在执行伸缩后,Rebalance 需要花费较高的人力和时间成本。这就要求我们尽可能将数据的伸缩自动化和流程化,另外需要考虑带有原生伸缩能力的中间件,这种软件一般都经过了充分的功能和性能测试,也有成熟的社区支持,比自研的方案往往要更稳定可靠。

1.7.3.伸缩的联动问题

伸缩的联动问题是一种客观存在的困扰,当我们对微服务 A 进行扩容时,服务 B 可能会面临流量增加(如定时任务的执行导致 A 调用 B 频次升高),数据库 C 可能出现连接数突增导致 CPU 使用率上升。


伸缩的联动需要对微服务的完整链路有个全局的画像,当局部扩容或缩容时,可以采用自动化的方式对链路上其他资源做相应的调整。

发布于: 2021 年 08 月 15 日阅读数: 33
用户头像

还未添加个人签名 2018.08.18 加入

资深软件工程师,技术Leader。10多年IT从业经验,长期专注于软件开发和技术管理,喜欢思考总结与分享传播。

评论

发布
暂无评论
微服务沉思录-伸缩性