冀 超 彭 鑫 赵文耘
(复旦大学软件学院 上海 201203) (上海市数据科学重点实验室 上海 201203)
微服务(Microservice)架构是一种目前在企业实践中得到广泛应用的软件架构。微服务架构将一整个软件应用按照细粒度的模块划分规则解耦成一个个功能独立的模块[1]。每个模块独立开发与部署,拥有独立的进程与运行环境。各个模块通过RPC或HTTP等轻量级的通信协议调用其他模块的接口,协同完成系统功能。这样的模块称之为微服务。目前微服务架构已成为云原生技术[2]领域的关键技术[3],并在企业中得到广泛使用[4]。例如,Netflix公司用了7年时间完成了由单体系统向微服务系统的迁移工作[5];腾讯公司的微信系统包含了部署在20 000多台虚拟机上的3 000多个微服务[6]。
微服务细粒度功能划分与独立运行环境的特性使得软件系统的部署更具灵活性与动态性。一个微服务一般部署若干个副本共同分流请求,称为微服务实例。当系统某些部分的负载产生变化时,系统将会调整这些功能涉及微服务实例的数量。例如,在用餐高峰时间,外卖订餐系统增加负责点单的微服务实例以应对增长的请求;而在夜间和凌晨时段,一些微服务部署副本将会被删除以节约运算资源。开发人员希望微服务实例可以自动伸缩,自动地适应系统不断变化的负载。但是,微服务之间存在复杂的网状调用负载关系。如果独立地对各个微服务的伸缩需求进行计量,很容易导致负载瓶颈从一个微服务沿着网状关系传递到另一个微服务上,使得系统在多轮伸缩操作后才达到稳态,导致自动伸缩操作所耗时间较长。
为了解决上述问题,本文提出基于负载关系图的微服务自动伸缩方法。首先,本文对微服务负载关系图进行了定义,并介绍了其构建与更新过程及数据抽取方法。接着,本文介绍了基于负载关系图的自动伸缩方法。该方法在自动伸缩操作触发后,根据微服务负载关系图评估各个微服务的新时段预期负载强度,并计算各个微服务的伸缩需求,最后一并完成所有微服务的伸缩操作。基于前述方法,本文设计并实现一个面向部署在Kubernetes平台上的微服务系统的自动伸缩工具LRGAS。最后,本文使用TrainTicket开源微服务基准系统开展了自动伸缩方法的对比实验,并通过实验验证了本文方法的高效性。
微服务架构功能划分的细粒度性与运行环境的隔离使得各个微服务的部署具有很高的灵活性与动态性。当系统负载增加时,系统可以为特定的微服务分配更多资源以提高其请求处理能力。系统可以为现有的微服务实例分配更多的CPU使用时间、内存空间以提高单个实例的请求处理能力,这种方式称为纵向伸缩;系统也可以保持单个微服务实例的资源限额不变,但增加微服务实例的数量以分流请求,这样的方式称为横向伸缩。横向伸缩相比纵向伸缩具有更高的灵活性与可操作性,因此目前微服务的自动伸缩大多使用横向伸缩的方式。该方法的工作方式为:开发人员在部署微服务时设定某个监控指标的上下门限值;当负载变化使得监控指标超出门限值范围时,微服务系统自动增加或减少实例数量,确保指标在门限值范围内。该方法常被用到作为门限值的监控指标有CPU使用时间、内存占用量或是请求响应时间等。目前使用最为广泛的Kubernetes容器编排平台的横向伸缩方法便是使用该方法。
由于目前的自动伸缩方法在微服务指标超出门限值范围后才触发伸缩操作,导致伸缩操作存在滞后性。一些研究尝试对微服务实例监控指标的未来走势进行预测[7-10],进而在系统流量变化前预先完成伸缩操作。其中,文献[7]提出了一套基于微服务工作负载预测的自动伸缩框架。该框架借助人工神经网络、递归神经网络及资源扩展优化算法构建了一个自动化系统,借助微服务系统的基础架构层数据完成各个服务的自动伸缩操作。此外,由于指标的选取和门限值的设定依赖于开发人员的经验,这导致门限值的设定并不准确。一些研究[11-12]致力于寻找可以更精准反映微服务伸缩需求的指标。文献[11]在自动伸缩所使用的监控指标类型上进行了改进。该方法构建了工具Microscaler,通过对接口的响应时间进行统计和计算定义了微服务的“服务能力”指标。该指标对微服务负载强度的描述比其他监控指标更为准确,计算伸缩需求时也更加精确。目前的工具和研究大多关注微服务监控指标门限值的预测、设定和监控指标的选取和定义。这些研究存在的问题是,微服务之间存在着复杂的调用关系,但目前的伸缩方法独立地考虑各个微服务的伸缩需求,无法避免负载瓶颈转移的问题发生。自动伸缩方法应当统筹规划整个系统中各个微服务伸缩操作,一次性并行对多个相关微服务并容,使得系统可以尽快达到稳态以满足新的负载需求。
微服务架构的引入使得软件应用的部署方式产生了变化。一个微服务系统包含许多独立部署的微服务实例,它们的部署配置环境可能互相产生冲突,这使得运行环境管理变得复杂。容器技术的出现解决了这个问题。容器技术借助操作系统提供的命名空间和资源隔离机制,将一台虚拟机上同时运行的多个微服务实例的运行环境隔离,避免了进程间运行环境冲突的问题。容器平台还提供了统一的部署与运行接口,抹平了不同虚拟机之间的差异性,使得各个微服务可以无差异地在各种机器环境下分发与部署。目前最为广泛使用的容器技术是Docker[13]。
容器技术解决了微服务独立部署和运行的问题,但是没有解决微服务集群的维护与管理的问题。在微服务系统的部署过程中,部署平台需要把各个微服务实例调度到空闲资源充足的虚拟机上去;同时为了避免各个微服务实例网络地址冲突问题,需要从全局对各个微服务实例的地址进行唯一性分配;此外,为了保证系统的正常运行,系统需要确保有预期数量的微服务实例处于正常运行状态。为了解决这样的需求,容器编排平台出现。借助容器编排平台,开发人员可以自定义微服务系统的部署方案,简化微服务系统的部署与维护工作。目前在实际生产实践中广泛使用的平台有Kubernetes[14]、Docker Swarm[15]、Mesos[16]、Spring Cloud[17]等,这些平台都对微服务系统所需要的功能进行了不同程度的实现。目前最为广泛使用的容器编排平台是Kubernetes,已成为容器编排平台的事实标准。目前也有一些关于微服务编排工具的研究[18-19]实现了前述功能。例如,文献[18]提出了一套名为MiCADO的微服务编排工具,提供了云应用的自动伸缩功能。
随着微服务系统规模的扩大,微服务之间的交互关系也变得越来越复杂,这使得系统内部的网络配置与操控变得困难。例如,在新服务上线时开发人员希望流量按照比例逐渐切换到新的服务上;而有些时候,开发人员希望根据请求的元信息分流某些请求。在这种情况下,需要一个全局的控制平面来对系统内部网络流量进行管理,而服务网格技术解决了这个问题。服务网格向每一个运行的微服务实例中添加一个边车组件(sidecar)用于接管微服务实例的所有网络操作,使得微服务系统内部的网络交互实际上变成sidecar之间的网络交互。如此一来,开发人员即可通过配置规则并将其应用于sidecar的方式来对整个应用内部的网络调用进行操控。Istio[20]是服务网格技术的典型代表。
微服务系统内部存在着复杂的调用关系,一个请求的处理过程往往需要一到多个微服务的协同。在系统的运行过程中,为了在故障发生时对请求的调用关系进行追踪以便查明故障,需要对微服务系统的调用链进行追踪,以便获取请求被各个微服务实例处理的顺序和处理时间等数据。
OpenTracing标准[21]是目前被广为接纳的微服务链路追踪标准。根据OpenTracing标准的数据模型定义,一个完整的请求链路与OpenTracing中的一条Trace所对应,而一个完整请求链路中的每次跨服务服务调用(如一次RPC或HTTP调用)则对应OpenTracing的一条Span。也就是说,一条Trace是由若干个Span构成的树状结构或是有向无环图。如图1所示,用户向微服务A发送一条请求,系统生成对应的调用链Trace_x。该请求分别产生了对微服务B、微服务C、微服务D、微服务E的调用,并生成对应的4条Span。OpenTracing还定义了Trace和Span中应该包括的诸多指标数据、标签等信息。通过这些信息,开发人员可以得到请求调用中的细节信息,例如请求的来源、目的地、发生时间、时延等。
图1 Request、Trace与Span关系
OpenTracing标准目前有很多开源项目实现。Spring Cloud Sleuth是一个Java库,将其引入基于Spring框架的Java微服务项目后,即可通过在跨服务调用的请求Header头上插入特定请求ID的方式,将整条调用链串接起来。Zipkin是一个开源调用链收集和展示工具,包含数据存储和UI界面两部分。Spring Cloud Sleuth会将自动生成调用链传输给Zipkin进行存储。开发人员可以借助其UI界面完成调用链的查询和展示。该调用链追踪系统的架构如图2所示。
图2 Zipkin调用链追踪系统架构
与单体应用不同,微服务系统的各个微服务完成独立的功能并使用独立的方式部署,并通过调用其他微服务的方式协同完成一系列功能。这样的情况使得不同微服务之间的依赖关系不同:有些微服务之间联系紧密,而有些微服务之间很少发生调用关系。整个系统的自动伸缩操作必须考虑到微服务间的负载关系的不同,并对各个微服务实行不同程度的伸缩操作。
为了达到前述目的,本文设计并定义微服务负载关系图。该图描述了过去时间内系统中各个微服务之间的负载强度关系,其中:节点表示微服务;两个节点间关系表示两个微服务发生过调用关系;关系上的数字是对一段时间内调用次数的记录,反映了两个微服务之间一段时间内的负载强度。每个微服务的负载强度等于所有指向其关系上的负载总和。如图3是一个包含6个微服务的微服务负载关系图的示例。“微服务系统外部”是一个特殊的节点,代表使用微服务系统的用户方。微服务A、F直接接收外部请求,并调用系统内部的微服务B、C、D等。本文将微服务A、F这样的系统外部请求首先到达的微服务称为系统的入口微服务。入口微服务同时也是请求调用链中第一个出现的微服务。
图3 微服务负载关系图示例
微服务负载关系图的数据来源于调用链监控系统收集的调用链数据。正常运行的微服务系统一段时间会采集到大量调用链日志,而每条调用链日志(即Trace)中包含若干次跨服务调用(即Span)。每条Span都可以提取出一个微服务对另一个微服务的一次调用。图4为一条Span数据的示例。可以看出,这是一次ts-login-service对ts-sso-service的调用。
图4 调用链日志示例
微服务负载关系图随着系统运行持续进行构建与更新。该过程固定间隔时间循环执行,主要包括以下步骤:1) 从调用链平台中抽取最近一个时间段内新产生的Trace数据;2) 对于每条Trace,逐个检查其包含的Span数据,记录调用发起方的微服务名称和调用接收方的微服务名称;3) 对记录的两个微服务间的每次调用,将负载关系图中对应关系上记录的数字加一;4) 将新的负载关系数据存入数据库中,取代旧数据,等待一段时间后返回1)步继续下一轮更新。经过更新后的节点间关系上记录的数字便是两个微服务之间的负载强度。
目前广泛使用的基于指标门限值设定和监控的微服务自动伸缩方法存在一些不足。按照该方法,当仅仅针对某一个繁忙的服务进行伸缩时,系统的负载瓶颈很有可能会很快转移到另一个下游的微服务,进而引发另一次伸缩,如此迭代多次后才能最终达到相对稳定状态。这样的方式将会使得系统较长时间处于正在伸缩状态,不利于系统迅速适应新的负载需求。当某些功能的访问量变化时,需要一并找到所有可能受到较大影响的微服务进行伸缩,而不是孤立地对监控指标超标的微服务逐个进行伸缩。
图3为包含6个微服务的调用负载示意图。可以看出,在一段时间内,微服务A共接收了100次调用,并分别调用了80次微服务B和5次微服务C;微服务F接受了10次调用,并分别调用了10次微服务C和10次微服务D;微服务B和微服务D分别调用了微服务E共计130次和20次。根据数据看出,微服务A和微服务B、微服务E三者之间关系密切,而微服务A和微服务C之间关系并不紧密。这意味着,如果微服务A为了应对剧增的用户请求而进行伸缩,那么微服务B首先会受到大量请求的冲击而引发伸缩操作,进而影响蔓延到微服务E,微服务C由于接收微服务A的负载较少,基本不会受到影响。假设微服务A伸缩所需时间为TA,微服务B的伸缩所需时间为TB,微服务E的伸缩所需时间为TE。如果使用传统的微服务自动伸缩方法,微服务系统最终完成所有伸缩操作所需总时间Told为:
Told=TA+TB+TE
(1)
如果可以在微服务伸缩操作前分析微服务间的调用关系,确定微服务的伸缩需求后再并行进行伸缩,那么所需要的总时间Tnew为:
Tnew=max(TA,TB,TE)
(2)
根据上述分析可以看出,在各个微服务伸缩所需时间相近的情况下,改进后的方法可以大大减少整个系统伸缩操作所需要的时间。其主要原理是,通过分析服务间负载关系的方法,将多个需要伸缩的微服务从串行伸缩变成并行伸缩。
本节提出的微服务自动伸缩方法主要包括两个部分:首先该方法从微服务历史调用链日志中提取整个系统过去一个时段的微服务负载关系图。接着,通过监控系统获取到的入口微服务的请求频率变化幅度,借助微服务负载关系图分析系统中各个微服务的负载水平,最终得出各个微服务的伸缩需求。
基于负载关系图的微服务自动伸缩方法的流程如图5所示,共包含4个步骤:1) 触发自动伸缩。此步使用与传统自动伸缩相同的触发方法,即设置监控指标的门限值并对其进行监控,在指标越过门限值时触发自动伸缩。2) 计算各个微服务的预期负载。自动伸缩触发后,根据入口微服务请求频率的变化,借助微服务间负载关系评估各个微服务在新时段的预期负载。3) 根据各个微服务新时段的预期负载计算其伸缩需求。4) 向容器编排平台发送请求并执行微服务伸缩操作。该流程的核心步骤为微服务预期负载的计算和微服务伸缩需求的计算。
图5 基于负载关系图的微服务自动伸缩方法流程
该过程的目的是借助系统的新旧时段入口微服务请求频率变化幅度和微服务间负载关系来对各个微服务新时段预期负载进行预估。该方法的执行过程如算法1所示。该方法首先实时监控系统入口微服务的请求频率变化情况,计算自动伸缩操作触发前后入口微服务的请求频率变化比值关系。接着,将入口微服务的旧负载乘以该比值作为入口微服务的新负载。由于入口微服务的新负载水平也会对其调用的微服务造成影响,接下来计算因为入口微服务负载变化而受到影响的微服务的预期负载变化,进而逐个计算系统其他微服务的预期负载变化。如此往复,最终得到整个系统中各个微服务在新时段的预期负载水平。
算法1计算微服务在新时段的预期负载强度
输入:微服务负载关系图中的微服务节点集V,微服务间负载关系集E。
输出:每个微服务节点v和其预期新负载水平newLoadOfV。
1.forvinV
2.v.inbound=count(e∈Eande.to==v)
3.end for
4.将集合V中的所有入口微服务节点加入S
5.forsvcinS
6.定义svc与系统外部的负载关系为enterEdge,其伸缩操作触发前后该系统外部请求频率的比值为enterChange
7.enterEdge.newFlow=enterEdge.oldFlow*enterChange
8.end for
9.forsvcinS
10.oldLoad=0,newLoad=0
11.定义集合E中终点为svc的关系集为inboundEdges
12.foredgeininboundEdges
//计算该微服务新旧负载
13.oldLoad=oldLoad+edge.oldFlow
14.newLoad=newLoad+edge.newFlow
15.end for
16.map.put(svc,newLoad)
//该微服务的新时段预期负载
17.change=newLoad/newLoad
//计算该微服务新旧负载比值
18.定义集合E中起点为svc的关系集为outboundEdges
19.foredgeinoutboundEdges
//计算该微服务负载变化后,对
//其调用微服务的负载产生的影响
20.edge.newFlow=edge.oldFlow*change
21.svcTo=edge.to
22.svcTo.inbound=svcTo.inbound-1
23.ifsvcTo.inbound==0
24.S.add(svcTo)
25.end if
26.end for
27.end for
28.return map(v,newLoadOfV)
举例如图6所示,当入口微服务A的外部请求频率倍增时,微服务A的负载也倍增,但其他各个微服务负载受到的影响各不相同。例如微服务C、E与微服务A联系密切,因此负载均出现较大增长;但是由于微服务A较少调用微服务D,因此微服务D负载增长幅度不大;而微服务B并未参与到增长的请求的处理中去,因此该微服务的负载强度不发生变化。在这种情况下,只有微服务A、C、E需要伸缩,而微服务B、D基本无须伸缩。
图6 微服务的负载强度变化示例
在计算得到各个微服务的负载强度后,下一步即可计算各个微服务的伸缩需求,调用容器编排平台相应接口执行伸缩操作,并等待操作的完成。
计算得到各个微服务在新时段预期负载强度后,即可借此计算各个微服务在新时段的实例需求数,并在微服务容器编排平台上执行自动伸缩操作并等待操作完成。本文假设对于某一个微服务上一系统稳定时段的负载为Lold,上一时段该微服务实例数量为Nold;新时段该微服务预期负载为Lnew,预期新负载强度下的实例数量为Nnew。那么对于Nnew,本文采取如下计算方法:
(3)
通过计算得到在新时段各个微服务的预期的实例数量之后,即可调用容器编排平台提供的相关接口,更新各个微服务的微服务实例数量配置信息,并等待其更新配置数据并部署或删除指定数量的微服务实例。由于各个微服务的自动伸缩是并行进行的,因此最终耗费的时间大致与伸缩耗时最长的微服务所需时间相同。
基于前文所述的负载关系图构建方法和基于负载关系图的微服务自动伸缩方法,本文设计并实现工具LRGAS。该工具的实现架构如图7所示。该工具监控和操作的目标对象为部署于K8S集群上的微服务系统。对于目标微服务系统,微服务系统中还应当配合部署Zipkin调用链追踪平台和Prometheus指标监控平台。目标微服务系统部署在若干台虚拟机组成的集群上,而LRGAS工具相关服务与数据库部署在另一台集群外的独立虚拟机上,以防对微服务系统的运行产生干扰。
图7 LRGAS工具实现
LRGAS工具使用的外部数据共有两种。其一是Zipkin调用链追踪平台收集的微服务系统请求调用链数据。每隔一段时间,本工具将会采集一次最新的调用链数据,用于构建和更新负载关系图中的数据。其二是Prometheus监控平台提供的监控指标数据。这部分数据一方面用于监控入口微服务的请求频率变化,另一方面用于判断各个微服务的监控指标是否处于预设阈值范围内,并最终用于判定是否触发微服务的自动伸缩操作。
该工具主要包括三个Java服务和一个图数据库。三个服务均基于Spring Boot框架完成开发。数据收集与处理服务实现负责关系图的构建和维护,定时从Zipkin平台抽取调用链数据并将更新的负载关系图发送到图数据库API服务。图数据API服务对接neo4j图数据库,负责负载关系图数据的读取和存储。自动伸缩服务从Prometheus平台实时抽取各个微服务的监控指标数据,判断自动伸缩操作是否触发。若自动伸缩操作被触发,自动伸缩服务从图数据库API服务获取最新的微服务负载关系图,计算出各个微服务的伸缩需求后,向K8S平台发出伸缩指令,并等待操作的完成。
本实验使用部署在Kubernetes容器编排平台上的TrainTicket开源微服务基准系统作为实验对象。TrainTicket使用的是在工业界生产环境中使用较多的Spring Cloud Sleuth[22]与Zipkin[23]进行调用链日志的收集,可以很好地模拟微服务系统在真正生产环境中的运行状况。
在TrainTicket的众多功能中,本文选取了三种涉及不同规模微服务数量且在实际使用中使用较为频繁的场景,如表1所示。场景1模拟的是单一微服务需要伸缩的场景,对应于TrainTicket系统的登录场景。该场景模拟用户登录高峰期,涉及验证码请求与核对等操作。在该场景下大量用户登录导致对于验证码服务的请求数量激增。此处涉及一个微服务,即验证码服务。场景2模拟的是有较多微服务需要伸缩的场景,对应于TrainTicket系统出现大量用户订票场景。该场景模拟用户火车票预订高峰期,包括余票查询、价格计算、订单创建和通知发送等请求。该场景是TrainTicket系统中最为复杂的场景,需要9~15个微服务协同完成该功能。场景3模拟的是中等数量微服务需要伸缩的场景。该场景模拟用户退票高峰期,包含订单状态修改、退款等操作。该场景是一个中等复杂的功能,包括订单状态修改、退款等操作,涉及5~7个微服务。由于微服务负载强度会影响请求响应时间,因此本实验以请求响应时间为自动伸缩监控指标。对于三种场景,实验将入口微服务的请求频率变为正常运行下的3倍,以触发自动伸缩操作。
表1 自动伸缩场景列表
为了使负载关系图收集到完整且全面的数据,在触发自动伸缩操作前,应先维持系统正常运行一段时间。本实验实现了一个简单的请求模拟器,以设定频率向微服务系统发送各种类型请求。请求的频率会随着时间产生小幅变化,以模拟微服务系统在实际应用中运行状况。
本文实验主要通过对比微服务系统在不同的场景下使用传统的各个微服务独立自动伸缩的方法以及基于负载关系图的自动伸缩算法最终完全适应新的负载并达到稳态(也就是微服务实例数量不再变化)的时间,来验证本文方法的高效性。实验按照场景分为三组。对于每组实验,使用请求模拟器模拟系统一段时间的正常运行,以确保微服务系统的负载依赖图收集到足够的数据。然后,依照各个场景的描述,逐步增加对应场景功能的请求频率以增加对应微服务的负载。当负载增大到一定程度时,自动伸缩操作将会被触发。本实验将会记录从自动伸缩操作的触发到整个系统最终完成全部操作的时间。对于每种场景,本文将会进行3次实验,并取平均值作为最终结果。
本次实验的最终实验结果数据如表2所示。本实验对三个场景分别使用两种自动伸缩方法的所需平均时间折线图如图8所示。
表2 自动伸缩实验结果统计表
续表2
图8 两种自动伸缩方法使用时间对比
根据图8中的数据可以得出,在微服务数量较少时,本文所述的自动伸缩方法没有优化效果,甚至有时耗时高于各个微服务独立伸缩的方法。但在自动伸缩的微服务数量逐渐增加时,本文方法逐渐体现出时间上的优势。这是由于在微服务规模增大的情况下,微服务之间的依赖关系也变得更加复杂。使用传统的微服务伸缩方法在微服务数量规模较大且调用关系又复杂的情况下,很容易在一个微服务扩容后,流量瓶颈迅速转移到另一个微服务上,进而引发另一轮扩容操作,如此多轮后最终完成扩容。而在仅有少量或一个微服务需要扩容时,无论使用何种方法都只需一轮即可完成所有伸缩操作,而本文方法还需要额外进行各个微服务需求的计算操作,反而会浪费一些时间。根据测算,根据微服务负载依赖图计算各个微服务伸缩需求的时间大约在2~3 s左右。此外,由于本文所提出的自动伸缩方法一并完成所有微服务的伸缩,导致在同一时刻有更多微服务实例处于正在调度和启动状态,在实际系统运行中这将会拖慢单个微服务实例的启动时间,但总体上来说由于伸缩操作的并行性,本文方法仍然相比传统方法能节省大量时间。
目前微服务架构已经在企业实践中得到了广泛的应用。然而,独立地对各个微服务进行监测的自动伸缩方法存在微服务系统内部负载瓶颈转移的问题,使得系统适应新负载所耗费的时间较长。针对上述挑战,本文提出基于负载关系图的微服务故障自动伸缩方法。首先,本文设计和定义微服务负载关系图的结构,并介绍了图谱的数据抽取方式和数据示例。接着,本文借助微服务间负载关系图,在系统外部负载发生变化后,分析各个微服务的负载强度变化,进而计算各个微服务的伸缩需求,最后向容器编排平台发出伸缩指令并完成操作。基于前文所述方法,本文设计并实现了工具LRGAS。最后,本文使用TrainTicket开源微服务基准系统开展了伸缩方法对比实验。实验结果表明,随着微服务数量的增加,本文方法的高效性体现愈加明显。
本文方法目前也存在一些不足。本文自动伸缩方法在监控指标超出门限值范围时才会触发,使得入口微服务的自动伸缩操作存在滞后性。未来可以引入机器学习方法对微服务系统的负载走势进行预测,在请求频率大幅变化前即触发伸缩操作,以更快适应新的负载强度。