Serverless并不仅仅是一个概念,很多地方都已经有了它的影子和思想,本文将给大家介绍最近比较火的Serverless。
首先放出官方对Serverless的解释:
Serverless的全称是Serverless computing无服务器运算,又被称为函数即服务(Function-as-a-Service,缩写为 FaaS),是云计算的一种模型。以平台即服务(PaaS)为基础,无服务器运算提供一个微型的架构,终端客户不需要部署、配置或管理服务器服务,代码运行所需要的服务器服务皆由云端平台来提供。国内外比较出名的产品有阿里云Function Compute、AWS Lambda、Microsoft Azure Functions 等。
有人说它是云计算的未来,代表了一种技术趋势、理念和发展方向,因为不是官方科普,说说个人的理解。
早期的软件部署模式是通过采购物理机的形式,有多大规模采购多少台机器,采购多了或者配置差了都会存在比较大的资源浪费。
虚拟化技术出来后缓和了这个问题,允许将物理机切分成一个一个VM实例,一台机器上可以运行多个实例,采购多了那就运行其他服务,配置差的机器多运行一些实例,配置好的少运行一些,但是虚拟化本身还是挺重的,每个VM需要维护自己的OS内核,切的越多浪费越大,并且不利于维护统一基线,安装插件配置agent上传脚本很容易造成应用内一批机器不一致,并且销毁重建也不是那么容易的事情,这个阶段运维的事务性工作还是不少。
接下来云的浪潮铺开,传统的服务器厂商开始转型拥抱云,将自己的服务器搬到云上通过虚拟化技术进行线上售卖提供基础IaaS服务,到这个阶段仅仅是转变了商业模式,上面的问题依然还是存在。
再到后面docker容器技术产生,docker本身还是一种虚拟化技术,但是他是依托于宿主机操作系统之上的虚拟化,仅仅只是一个独立进程共享OS内核,资源碎片和内存占用浪费问题会少很多,并且重建销毁比较容易,非常利于维护应用统一基线,通过一套标准镜像自定义dockerfile进行统一交付,一次构建到处运行,运维可以脱离繁琐重复的工作去做更多工具类的产品。
DevOps概念在这个阶段变得火热,大厂内甚至要求消灭运维,强制运维转型开发,想起来也不无道理,在docker技术出来之前,一次建站往往需要运维们准备一堆脚本和安装顺序手册,搭建操作系统环境,安装网关、DB、软负载等一系列中间件,编译配置安装然后使得服务可用,中间一旦有一个步骤出错还得从头再来,但是运维往往是不了解应用的,启动不起来配置错误报错信息等问题还是需要开发介入,而让开发来负责上线部署,很多服务软件不知道如何部署,两者中间有一道很明显的鸿沟,所以一次建站或者新平台的搭建,需要拉上运维和开发通宵好几个晚上一起攻坚,效率低下,这个时候急需一个了解开发的人把运维的事情干了,而容器技术出现之后,就如上面介绍,docker镜像统一基线使得建站部署变得更加标准化和简单,开发运维一体化这个事情就会变得水到渠成,通过dockerfile、swarm集群编排或者k8s可以很容易的让开发把运维的事情干了,这个时候运维自然会显得多余,当然完全消灭运维也是不可能的,变革是渐进式的,需要有一些人去负责历史包袱的资产系统,同时与开发有更好的协同具备提升系统稳定性和做自动化工具化系统研发能力的SRE出现也代表了运维转型的决心。
到目前为止,还是停留在开发需要关心运维的阶段,但是随着docker技术的普及,很多运维相关的工作已经变得非常容易了,这个时候云厂商开始考虑是否可以把运维这个事情从开发手中收走,让运维自动化,变得对开发更加透明,开发人员只需关注核心业务逻辑的开发,进而精益整个产品开发流程,快速适应市场变化,这个时候Serverless的概念开始产生,所以从这个角度来看,在整个it架构演进中,docker的普及无疑是进一步推动了Serverless的快速发展。
从整个演进过程中来看,一直都在朝着资源切分粒度越来越细(物理机->操作系统->进程->function),资源利用率越来越高,运维工作逐渐减少,开发更聚焦业务的方向发展的,Serverless的产生也是符合历史发展的一般规律。
一个比较典型的应用场景是云上的巡检产品,业务很简单类似于360安全卫士,定期巡检云用户的三方依赖组件,产出包括安全、性能、稳定、成本的巡检报告并且给出建议,由于需要和一些微服务治理高可用等产品做捆绑售卖,本身也是免费产品,以前的ECS部署模式成本较高也不是很灵活,所以需要做改造来适应现在的云原生环境,准备将其托管到云上Function Compute Serverless平台,部署架构如下图:
这是一个比较典型的依托于Serverless平台的SaaS应用交付,即我们提供了一套通用的代码模板,提交给了Serverless平台进行运行,服务开发商也就是我们无需关注承载服务的系统架构和资源运维,不会因为服务的客户越来越多而导致运维负担或者设计重构问题,只需要关心服务业务的实现,然后消费方开箱即用,这套模板可以无限复制给更多的使用方,只要模板是固定的,不管未来新增多少个三方依赖组件要加入巡检,我们都只是新增模板实现然后提交任务给Serverless平台而已,而由采集端负责触发任务执行,对于应用或者业务的创新成本都是极低的。整个Serverless平台就是FaaS和BaaS的组合服务,FaaS是函数即服务,负责运行任务实例,BaaS是后端即服务,是任务依赖的三方依赖组件通过API的形式提供数据,你可以在Serverless平台编写代码(FaaS能力),然后调用一些第三方依赖组件的API进行数据交互(BaaS)。
微服务和Serverless并不冲突,一个微服务应用可以是基于Serverless架构搭建部署的,也可以是传统的先申请资源再进行部署的方式,Serverless本身是技术架构,而微服务是业务架构,经济基础决定上层建筑,底层的技术架构形式会影响上层的业务,当Serverless以function为粒度提供服务的时候,对于上层微服务的架构组织带来了新的契机。
以前的微服务更多是以应用的形式来组织的,一个应用关注一个特定的业务功能集合,很多服务可能都会使用到相同的功能,而这个功能并没有达到分解为一个微服务的程度,这个时候,可能各个服务都会开发这一功能,从而导致代码重复。尽管可以使用共享库来解决这个问题(例如可以将这个功能封装成公共组件,需要该功能的微服务引用该组件),但共享库在多语言环境下就不一定行得通了,而在Serverless架构中以function为最小粒度组织的模式下,你的业务分隔粒度可以无限的小,任何一个模块功能甚至方法都可以独立存在,不再需要通过共享组件代码复用强耦合的方式来组织应用,在Serverless架构中,微服务组件的相互隔离和模块松耦合可以做的更好,应用甚至可以以独立函数的形式存在,当然这不代表你的微服务拆的越细越好,也需要考虑到你的业务边界定义是否有利于不同团队组织之间独立工作,这里不展开讨论,事实上云厂商们已经开始进行了这方面探索,将Serverless和微服务打包做成组合拳产品提供解决方案。
Serverless就是为云而生的东西,天生具有云的基因(免运维弹性伸缩按量计费),用户将服务托管给了云厂商,只需要聚焦业务逻辑不需要去关心和管理资源问题,是对容器技术的封装,代表了云原生的高级阶段。
在我们公司内部其实也有类似和Serverless相似的产品,比如算法推荐平台,他本身提供的是FaaS服务,希望能够让使用者更快更好的实践自己的算法而不用关心机器部署配置的一些细节,但他还不是一个Serverless模式,实现Serverless需要有对应几个能力的建设,首先先介绍下Serverless要解决的几个问题。
一个大型系统因为需要支撑各种业务,往往会拆分集群来进行业务隔离,不同集群由于业务的特性,会出现差距较大的资源利用率,这显然是对资源的一种浪费,当然通过人肉运维调整集群机器配比可以解决这个问题,但是这种重复劳动本身也是对人效的浪费。
每个流量本身就有一定的规律,比如白天流量高晚上流量低,大促流量高常态流量低,如果就为了一天1个小时的高峰流量来准备全天的机器,那剩下来的那23小时也是一种资源的浪费。
微服务之间的数据交互主要是RPC和MQ,常见的RPC框架本身就有比较完善的探活和负载均衡机制能够保证流量在不同机器的的均衡性,而MQ受限于顺序消费、失败重发、消息分区等特性没有办法很好的保证消费端负载均衡,比如我们常用的RocketMQ默认使用了一种分页的算法,即在客户端将消费者和分区分别按照字典序排好,再按照平均分配的原则每个消费者分"一页"的分区,所以这里就会引申出碎片的问题,当一个集群中消费的Metaq消息都是小分区的消息,所有的消息都会被流入集群中的前几个机器中,造成集群整体流量不均,带来集群资源碎片。
对于新接入的用户和业务,我们常见的做法是评估峰值业务量然后为其开启新集群预先分配一批机器,这里就有两个问题,一个是对于资源的预先评估是否能够准确而不造成浪费,另一个是成本结算方式,如果使用机器数来给用户或者业务方结算成本,对于有明显波峰波谷的使用者来说也是一种很大的成本资源浪费。
在每次大促或者压测之前都需要提前扩缩容准备资源、修改集群配置,随着各种大促小促和压测次数越来越密集,这种运维开销成本也变得越来越大。
Serverless的产生就是为了解决上面这些问题,它应该具有的特性有:
FaaS化的目的是为了做任务的抽象标准化和对业务的更细粒度拆分,以函数的方式进行组织,这和我们常说的流程化编排组件化建设其实初衷是一样的,如果有了这一层抽象,任何业务形式都可以在这个标准化平台上像搭积木一样进行构建,并且对于业务组件化和资源拆分的粒度是Function,这个对于上层微服务的架构组织带来了新的契机,上面篇幅已经介绍过这里不再累述。
这里不是真正的没有维护,而是机器帮助人去实现自动化运维进行弹性扩缩容,管理员不必关心应用运行的状态,是否在工作,是否有资源碎片等等,任务不再是固定的跑在某个集群里,而是可以在任意机器里进行迁移。
有了Serverless,应用水位会维持在一个固定的范围,什么时候该扩容什么时候该缩容都由平台按照当前的负载情况来决定。
根据执行次数执行时间来计算费用,只为实际使用的计算资源付费,是一种资源粒度精细化的管理模式。云计算归根结底是一种 IT 服务提供模式,不论是公共云还是专有云(以IT设备的归属不同分类),其本质都是帮助 IT 的最终使用者随时随地,并且简便快速地获取 IT 服务,目前的IaaS、PaaS都已经做到了按需付费,PaaS 甚至做到了按请求付费,如DB、CACHE、MQ等,但是 IaaS 的付费粒度仍然是时间维度,最快按照小时付费,以分钟来交付。因此,当下的云计算场景,应用的开发维护方式相比传统 IDC 时代的开发维护,差别还不是很大,本质上都是做预算然后购买交付,但Serverless平台提供了一种全新的开发维护方式,用户只需要写好业务代码,提交到云上,所有和机器容量、可用性、机器为单位的运维工作可以全部交给云平台,这种模式极大的释放了云的弹性价值,真正做到了按需付费。
Serverless中的function通过事件监控的方式被触发执行,这里的事件监听可以是HTTP API触发、消息监听、定时触发、控制台触发。
结合上面说到的Serverless特性,我们的任务都以函数的方式进行组织,任务可以在任意机器里进行迁移和启动,并且支持自动弹性扩缩容,为了实现这些特性,大前提是我们需要保证函数的无状态。
在Serverless平台中由于执行函数的容器是不确定的、执行顺序也不确定,一个容器可能会被并发的执行多次,也不存在执行顺序,函数运行时根据业务弹性,可能伸缩到 0,所以无法在运行环境中保存状态数据,这就要求我们的函数是无状态的,以便于随时进行水平伸缩,否则可能会出现一些意料之外的结果,比如:
如果连续两次执行handler(3),会得到不一样的结果,因为这里存储了状态变量sum,两次执行间共享了这个状态变量,影响了最终执行的结果,所以无状态也是要求函数开发者在编码过程中保证尽量少的使用外部变量,但是也不代表完全不使用外部变量,对于一些有限资源比如数据库连接,这部分资源是可以被复用的,我们可以提前在函数外进行提前初始化,这样当容器预热之后就可以直接使用;还有一些状态信息比如登录状态,我们可以利用外部服务、产品,例如数据库或缓存,实现状态数据的保存,以便可以用来获取登录信息。
因为按需使用的特性,对于长时间不使用的函数会主动退出以节省内存,当函数调用请求传入时,我们会检查是否已有一个空闲的容器可以使用,如果没有那就需要重新拉起一个容器启动,这个就是"冷启动"问题,一个容器的拉起和函数初始化加载工作需要一定的时间,冷启动会拖慢函数的响应速度。对此业界最常见的做法是准备buffer池,提前申请一部分资源,拉取镜像创建容器加载资源包,这样当有突发流量进入、运行中容器无法承载的时候,会自动进行快上快下,将buffer池中预热好的容器拿出来直接灌入流量,而流量下降后,运行中容器负载下降则会自动进行快下操作,将一部分容器摘流放回buffer池中,那这里有同学可能会问如果提前准备buffer机器那不是违背了Serverless降低成本的初衷了吗,注意buffer池中的资源并不是一个高优先级的绝对资源占用,他实质上并不属于任何一个应用的,预先加载多少buffer资源也是由Serverless平台的算法策略决定,所以在资源紧缺的情况下完全可以挪出去供其他业务使用,一般不超过Serverless平台总机器数的10%,是一种在进程复制技术还不成熟的情况下的折中方案。
引入集群的目的是做分治管理,来提高机器集中运维效率,而在Serverless平台中,所有机器均被统一的部署和管理(同一物理区域内),集群管理的方式已经失去了意义,Serverless平台的机器全部跑在一个大集群中被无差异的执行,机器实际使用量根据实时水位自动进行调整。
标准的Serverless平台应该是一容器一函数的部署方式然后通过容器的资源隔离能力做到完全的隔离,但是实操过程中由于管理的需要,往往是一容器多函数的部署方式,当然也有性能方面的考虑,因为频繁的创建销毁容器做资源隔离也会带来资源消耗(容器资源隔离通过内核cgroup机制实现),过多的cgroup组会对内核造成额外的开销。之前在实践中曾经遇到过一个线上问题,当时发现只要和Serverless平台混部的应用sys CPU都会莫名的高出一截,因为做了线程级别的CPU隔离(通过创建cgroup组进行线程pid绑定的方式),但是在函数退出时没有做cgroup的清理,导致宿主机因为绑定过多cgroup组(上万)进行了内核态空转引起sys CPU飚高,所以控制宿主机上容器的数量以及使用一容器多函数的部署方式还是很有必要的。
混合部署的方式彻底解决了之前说到的碎片化问题,函数可以自由调度,各个机器都得到了充分的利用。虽然是一容器多函数的部署方式,但是也不是意味着就可以随意的混部,混合部署会尽量做到各机器的水位均衡,也就说到下面的自动调度的话题。
我们在第一版测试中,为每台机器都设置了相同的线程池线程数,最后的结果是机器负载非常不均衡,因为不同的函数执行耗时不同,以前可以通过集群来隔离这些不同的业务为每个集群预设不同的线程池配置,而现在Serverless平台统一了集群,不同的函数可以在机器间任意迁移,当机器中存在耗时较高的任务,线程池立马被占满来不及消费,但是机器水位很低,这种情况下再给他分配任务反而会加剧这个问题,当机器中都是耗时较低的任务时,水位会保持的较高。
所以为了规避这个问题,我们将函数进行了分类,分为IO密集型任务和CPU密集型任务,事先分配好两个线程池,IO密集型任务跑在IO线程池中,CPU密集型任务跑在CPU线程池中,由于CPU密集型任务可以近似认为是纯CPU计算型,所以CPU线程池线程数近似等于核数大小,IO密集型任务由于不同的任务耗时也大不相同,所以采用累加的形式,每个IO函数都要设置使用的IO线程数,该容器上IO线程池的线程数等于所有IO函数设置的线程数之和,这是一个动态的数,会随着任务调度而变化。
之前说到buffer池的方案中需要提前预热一批资源,如何设置合理的buffer池资源数量以保证成本和效率的最优解是比较重要的问题。
在第一版的尝试中我们引入过多个指标的数据通过综合判定来进行调度,这里的指标包括系统指标CPU、load和业务SLA指标、队列堆积量、执行RT,但是最后会发现,通过这些指标并不能很好的均衡各机器的水位,因为不同函数的瓶颈并不完全一致,有IO密集型的,有CPU密集型的,一个场景可能在CPU不超过30%的情况下,就已经出现了容量不足的问题,有的场景高水位CPU运行的很好,但是有的场景相对较低的CPU可能就出问题了,或者机器本身的问题导致的load突然飙高,队列堆积增加的问题,其实并不是容量不足导致的。所以通过系统指标和业务指标并不能很好的衡量机器当前的水位。
通过对第一版的分析,我们发现这中间最主要的问题是我们一直在通过外部表象数据来判断机器水位,而这些数据在不同业务场景下表现又不尽相同,还容易受到外部环境的干扰造成数据抖动。所以在第二版中,我们增加了负载率的概念,在讲负载率之前先看一下并行度的概念,并行度(线程数)理论值为:
但是由于混合部署,每个函数脚本都不相同,导致整个机器上总的线程等待时间与线程CPU时间之比是一个不确定值,所以并行度是一个经验值设置,CPU密集型规则可以尽可能靠近核数以提高CPU的资源利用率,IO密集型可以调大,但是不是一味的调大,过多的线程数会导致频繁的上下文切换等损失,反而降低了性能,毕竟CPU时间片有限,太多并行度也执行不过来,并行度和资源利用率近似如下图的关系(正相关的折线,到达最佳并行度后资源利用率开始缓慢下降)。
所以为了达到最佳并行度,每台机器上的线程数都是一个动态变化的值,和这台机器上跑了哪些任务有关系。
负载率是用来衡量线程池利用率的指标,这个指标只和单机的线程池配置和流入的任务量有关,和这台机器的外部表象指标(CPU load 任务堆积量等等)都没有关系,定义为:
负载率是在并行度基础上的概念,当并行度不变时,耗时总和越大负载率越大,但是不会超过1,达到1说明机器的线程资源已经全部使用了,要么提高并行度要么扩机器。负载率还有一个意义在于可以精确的获取每个任务消耗线程资源的比重以便于进行比较精确的任务调度,因为IO密集型和CPU密集型任务已经用不同线程池进行了分隔,可以把这个消耗线程资源的比重,近似认为是消耗机器资源的比重。
自动化调度是第一步,再往下就是基于基线和时间相关性做一些算法智能化调度的探索,因为没有实践过这里不展开讨论。
当容器启动后到灌入第一批流量期间,存在一段时间执行能力较差,负载率偏高的问题,因为对于Java来说当一个新加容器在到达100%服务能力之前需要充分的代码预热(python ruby这类脚本语言可能会好些),为了防止在预热期间负载率偏高造成错误的调度,增加了对于预热期的判定,只要有一台机器正在启动,调度就停止,直到启动完成后一段时间再恢复调度。
消息消费的负载均衡取决于上游消息中间件,比如对于RocketMQ来说订阅者数必须小于消费组队列数,才能保证每个订阅者都有消息可消费,否则会出现部分订阅者无消息消费的问题,如果调度策略不考虑RocketMQ每个topic的队列数情况,无限制的增加订阅者,最后又会出现上面提到的碎片化问题。所以必须保证每个订阅者都有消息消费,每次调度都是有效的调度。
为了解决这个问题,我们增加了一个最大消费者数的概念,对于单个任务来说能够分配的消费机器数必须小于等于这个任务对应的topic的最大消费者数,防止出现无效的调度。对于RocketMQ来说最大消费者数等于该topic队列(分区)数,对于DRC来说,不支持负载均衡所以最大消费者数等于1,其他消息中间件依此类推。
我们的调度策略是把负载率控制在一个稳态区间,然后结合最大消费者数进行调度。比如单机的CPU密集型任务负载率范围设置为5%-30%,大于这个值则给CPU密集型任务中负载率最高的前5个任务扩容器,如果所有在线容器都已经在消费该任务则从buffer池快上容器来给任务扩容,如果已经扩充至最大消费者数,则进行任务迁移,将这台机器上的任务迁移至低负载率的机器上;小于这个值则将该机器快下放进buffer池。IO密集型任务的调度也是类似,最终的目标是将所有在线机器收敛至稳态区间,同时为了避免在任务扩缩容和机器快上快下上出现"扩过了头"或者"缩过了头"的情况,限制了单次扩缩容的机器数和单台机器调度的最小时间间隔,也是防止一台机器刚刚任务扩容又立马任务缩容或者快下。
我们来考虑几种典型场景在这种调度策略下的应用:
混合部署中仅仅以水位为依据进行随机混部对于比较核心的任务不是很友好容易被资源抢占,需要设置不同的任务等级标签和混部策略,保证资源不足的情况下高等级任务被优先执行保证SLA。
如何防止某些实现有问题的任务,消耗大量的资源,不断触发扩容,从而掏空整个集群,影响正常规则的执行,引发不公平,我们可能需要增加一种自动降级机制,针对某些不重要的任务或者问题任务在资源不够的情况下或者在预热的时间段触发降级。
虽然现在针对单次任务调度或者buffer池机器快上可以做到秒级,但是为了避免扩过了头或者缩过了头的问题,我们限制了单次任务扩缩容和快上快下的机器数,所以目前扩缩容对于相对较小的流量猛增可以做到秒级,但是对于几百倍甚至几千倍的流量猛增需要一定次数的扩容迭代周期来达到最终平衡,目前的策略虽然能很好的支持常态期间的秒级调度,但是对于大促场景还需要进行一定的改进。
目前问题函数的发现依赖于线上真实流量,这是一种亡羊补牢的方式,而且任务的线程数配置和CPU密集型IO密集型的划分也是依赖于人工判断,存在预判的误差,如果有一套自动压测机制,针对每一个新上线的规则进行自动压测,获取它的性能数据,自动判断是否存在问题并且划分任务类型设置线程数,对于调度和buffer池容量评估也能提供更加科学准确的依据。
本文介绍了在接触实践Serverless中遇到的一些实际问题和做法,当应用规模扩展到一定程度的时候,势必会要求做更精细化的资源管理,比如上面说到的资源分配不均、碎片化、错峰资源、混合部署、运维等等问题,通过Serverless的架构模式来实现自动化运维调度以及更加灵活的弹性,能让系统资源做到更加合理充分的利用,也让云的红利离我们越来越近。
*文 /Gao
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。