Serverless是目前比较热门的技术话题,各大云平台以及互联网大厂内部都在积极建设Serverless产品。本文将介绍美团Serverless产品在落地过程中的一些实践经验,其中包括技术选型的考量、系统的详细设计、系统稳定性优化、产品的周边生态建设以及在美团的落地情况。虽然各个公司的背景不尽相同,但总有一些可以相互借鉴的思路或方法,希望能给大家带来一些启发或者帮助。
1 背景
2 快速验证,落地MVP版本
2.1 技术选型
2.2 架构设计
2.3 流程设计
2.4 函数触发
2.5 函数执行
2.6 弹性伸缩
3 优化核心技术,保障业务稳定性
3.1 弹性伸缩优化
3.2 冷启动优化
3.3 高可用保障
3.4 容器稳定性优化
4 完善生态,落实收益
4.1 提供研发工具
4.2 融合技术生态
4.3 开放平台能力
4.4 支持合并部署
5 落地场景、收益
5.1 落地场景
5.2 落地收益
6 未来规划
作者简介
招聘信息
Serverless一词于2012年被提出,2014年由于亚马逊的AWS Lambda无服务器计算服务的兴起,而被大家广泛认知。Serverless通常被直译成“无服务器”,无服务器计算是可以让用户在不考虑服务器的情况下构建并运行应用程序。使用无服务器计算,应用程序仍在服务器上运行,但所有服务器管理工作均由Serverless平台负责。如机器申请、代码发布、机器宕机、实例扩缩容、机房容灾等都由平台帮助自动完成,业务开发只需考虑业务逻辑的实现即可。
回顾计算行业的发展历程,基础设施从物理机到虚拟机,再从虚拟机到容器;服务架构从传统单体应用架构到SOA架构,再从SOA架构到微服务架构。从基础设施和服务架构两条主线来看整体技术发展趋势,大家可能会发现,不论是基础设施还是服务架构,都是从大往小或者由巨到微的方向上演进,这种演变的本质原则无非是解决资源成本或者研发效率的问题。当然,Serverless也不例外,它也是用来解决这两个方面的问题:
虽然AWS在2014年就推出了第一个Serverless产品Lambda,但Serverless技术在国内的应用一直不温不火。不过近两三年,在容器、Kubernetes以及云原生等技术的推动下,Serverless技术迅速发展,国内各大互联网公司都在积极建设Serverless相关产品,探索Serverless技术的落地。在这种背景下,美团也于2019年初开始了Serverless平台的建设,内部项目名称为Nest。
截止到目前,Nest平台已经过两年的建设,回顾整体的建设过程,主要经历了以下三个阶段:
建设Nest平台,首要解决的就是技术选型问题,Nest主要涉及三个关键点的选型:演进路线、基础设施、开发语言。
起初Serverless服务主要包含FaaS(Function as a Service)和BaaS(Backend as a Service),近几年Serverless的产品领域有所扩张,它还包含面向应用的Serverless服务。
面向应用的Serverless服务:如Knative,它提供了从代码包到镜像的构建、部署以及实例弹性伸缩等全面的服务托管能力,公有云产品有Google Cloud Run(基于Knative)、阿里云的SAE(Serverless Application Engine)。
在美团内部,BaaS产品其实就是内部的中间件以及底层服务等,它们经过多年的发展,已经非常丰富且成熟了。因此,在美团的Serverless产品演进主要在函数计算服务和面向应用的Serverless服务两个方向上。那究竟该如何演进呢?当时主要考虑到在业界FaaS函数计算服务相对于面向应用的Serverless服务来说,更加成熟且确定。因此,我们决定“先建设FaaS函数计算服务,再建设面向应用的Serverless服务”这样一条演进路线。
由于弹性伸缩是Serverless平台必备的能力,因此Serverless必然涉及到底层资源的调度和管理。这也是为什么当前业界有很多开源的Serverless产品(如OpenFaaS、Fission、Nuclio、Knative等)是基于Kubernetes来实现的,因为这种选型能够充分利用Kubernetes的基础设施的管理能力。在美团内部基础设施产品是Hulk,虽然Hulk是基于Kubernetes封装后的产品,但Hulk在落地之初考虑到落地难度以及各种原因,最终未按照原生的方式来使用Kubernetes,并且在容器层采用的也是富容器模式。
在这种历史背景下,我们在做基础设施选型时就面临两种选项:一是使用公司的Hulk来作为Nest的基础设施(非原生Kubernetes),二是采用原生Kubernetes基础设施。我们考虑到当前业界使用原生Kubernetes是主流趋势并且使用原生Kubernetes还能充分利用Kubernetes原生能力,可以减少重复开发。因此,最终考量的结果是我们采用了原生Kubernetes作为我们的基础设施。
虽然无论在云原生领域,还是Kubernetes的生态中,Golang都更加主流,但在美团Java才是使用最广泛的语言,相比Golang,Java在公司内部生态比较好。因此,在语言的选型上我们选择了Java语言。在Nest产品开发之初,Kubernetes社区的Java客户端还不够完善,但随着项目的推进,社区的Java客户端也逐渐丰富了起来,目前已经完全够用了。另外,我们也在使用过程中,也贡献了一些Pull Request,反哺了社区。
基于以上的演进路线、基础设施、开发语言的选型,我们进行了Nest产品的架构设计。
在整体的架构上,流量由EventTrigger(事件触发源,如Nginx、应用网关、定时任务、消息队列、RPC调用等)触发到Nest平台,Nest平台内会根据流量的特征路由到具体函数实例,触发函数执行,而函数内部代码逻辑可以调用公司内的各个BaaS服务,最终完成函数的执行,返回结果。
图1 FaaS架构图
在技术实现上,Nest平台使用Kubernetes作为基础底座并适当参考了一些Knative的优秀设计,在其架构内部主要由以下几个核心部分组成:
图2 Nest架构图
在具体的CI/CD流程上,Nest又与传统的模式有何区别呢?为了说明这个问题,我们先来看一看在Nest平台上函数的整体生命周期怎样的?具体有以下四个阶段:构建、版本、部署、伸缩。
就这四个阶段来看,Nest与传统的CI/CD流程本质区别在于部署和伸缩:传统的部署是感知机器的,一般是将代码包发布到确定的机器上,但Serverless是要向用户屏蔽机器的(在部署时,可能函数的实例数还是0);另外,传统的模式一般是不具备动态扩缩容的,而Serverless则不同,Serverless平台会根据业务的自身流量需要,进行动态扩缩容。后续章节会详细讲解弹性伸缩,因此这里我们只探讨部署的设计。
部署的核心点在于如何向用户屏蔽机器?对于这个问题,我们抽象了机器,提出了分组的概念,分组是由SET(单元化架构的标识,机器上会带有该标识)、泳道(测试环境隔离标识,机器上会带有该标识)、区域(上海、北京等)三个信息组成。用户部署只需在相应的分组上进行操作,而不用涉及到具体机器。能够做到这些的背后,是由Nest平台帮助用户管理了机器资源,每次部署会根据分组信息来实时初始化相应的机器实例。
图3 函数生命周期
函数的执行是由事件触发的。完成函数的触发,需要实现以下四个流程:
图4 函数触发
函数不同于传统的服务,传统的服务是个可执行的程序,但函数不同,函数是代码片段,自身是不能单独执行的。那流量触发到函数实例后,函数是如何执行的呢?
函数的执行的首要问题是函数的运行环境:由于Nest平台是基于Kubernetes实现的,因此函数一定是运行在Kubernetes的Pod(实例)内,Pod内部是容器,容器的内部是运行时,运行时是函数流量接收的入口,最终也是由运行时来触发函数的执行。一切看起来是那么的顺利成章,但我们在落地时是还是遇到了一些困难,最主要的困难是让开发同学可以在函数内无缝的使用公司内的组件,如OCTO(服务框架)、Celler(缓存系统)、DB等。
在美团的技术体系中,由于多年的技术沉淀,很难在一个纯粹的容器(没有任何其他依赖)中运行公司的业务逻辑。因为公司的容器中沉淀了很多环境或服务治理等能力,如服务治理的Agent服务以及实例环境配置、网络配置等。
因此,为了业务在函数内无缝的使用公司内的组件,我们复用公司的容器体系来降低业务编写函数的成本。但复用公司的容器体系也没那么简单,因为在公司内没有人试过这条路,Nest是公司第一个基于原生Kubernetes建设的平台,“第一个吃螃蟹的人”总会遇到一些坑。对于这些坑,我们只能在推进过程中“逢山开路,遇水搭桥”,遇到一个解决一个。总结下来,其中最核心的是在容器的启动环节打通的CMDB等技术体系,让运行函数的容器与开发同学平时申请的机器用起来没有任何区别。
图5 函数执行
弹性伸缩的核心问题主要有三个:什么时候伸缩,伸缩多少,伸缩的速度快不快?也就是伸缩时机、伸缩算法、伸缩速度的问题。
除了基本的扩缩容能力,我们还支持了伸缩到0,支持配置最大、最小实例数(最小实例即预留实例)。伸缩到0的具体实现是,我们在事件网关内部增加了激活器模块,当函数无实例时,会将函数的请求流量缓存在激活器内部,然后立即通过流量的Metrics去驱动弹性伸缩组件进行扩容,等扩容的实例启动完成后,激活器再将缓存的请求重试到扩容的实例上触发函数执行。
图6 弹性伸缩
上面提到的伸缩时机、伸缩算法、伸缩速度这三要素都是理想情况下的模型,尤其是伸缩速度,当前技术根本做不到毫秒级别的扩缩容。因此,在线上实际场景中,弹性伸缩会存在一些不符合预期的情况,比如实例伸缩比较频繁或者扩容来不及,导致服务不太稳定的问题。
下图展示的是线上弹性伸缩的真实案例(配置的最小实例数为4,单实例阈值100,阈值使用率0.7),其中上半部分是业务每秒的请求数,下半部分是扩缩实例的决策图,可以看到在成功率100%的情况下,业务完美应对流量高峰。
图7 弹性伸缩案例
冷启动是指在函数调用链路中包含了资源调度、镜像/代码下载、启动容器、运行时初始化、用户代码初始化等环节。当冷启动完成后,函数实例就绪,后续请求就能直接被函数执行。冷启动在Serverless领域至关重要,它的耗时决定了弹性伸缩的速度。
所谓“天下武功,无坚不破,唯快不破”,这句话在Serverless领域也同样受用。试想如果拉起一个实例足够快,快到毫秒级别,那几乎所有的函数实例都可以缩容到0,等有流量时,再扩容实例处理请求,这对于存在高低峰流量的业务将极大的节省机器资源成本。当然,理想很丰满,现实很骨感。做到毫秒级别几乎不可能。但只要冷启动时间越来越短,成本自然就会越来越低,另外,极短的冷启动时间对伸缩时函数的可用性以及稳定性都有莫大的好处。
图8 冷启动的各个阶段
冷启动优化是个循序渐进的过程,我们对冷启动优化主要经历了三个阶段:镜像启动优化、资源池优化、核心路径优化。
图9 镜像启动优化成果
图10 资源池优化成果
说到高可用,对于一般的平台,指的就是平台自身的高可用,但Nest平台有所不同,Nest的高可用还包含托管在Nest平台上的函数。因此,Nest的高可用保障需要从平台和业务函数两个方面着手。
对平台的高可用,Nest主要从架构层、服务层、监控运营层、业务视角层面都做了全面的保障。
图11 部署架构
对于业务高可用,Nest主要从服务层、平台层两个层面做了相关的保障。
图12 业务监控
前文已提到,Serverless与传统模式在CI/CD流程上是不同的,传统模式都是事先准备好机器然后部署程序,而Serverless则是根据流量的高低峰实时弹性扩缩容实例。当新实例扩容出来后,会立即处理业务流量。这听起来貌似没什么毛病,但在富容器生态下是存在一些问题的:我们发现刚扩容的机器负载非常高,导致一些业务请求执行失败,影响业务可用性。
分析后发现主要是因为容器启动后,运维工具会进行Agent升级、配置修改等操作,这些操作非常耗CPU。同在一个富容器中,自然就抢占了函数进程的资源,导致用户进程不稳定。另外,函数实例的资源配置一般比传统服务的机器要小很多,这也加剧了该问题的严重性。基于此,我们参考业界,联合容器设施团队,落地了轻量级容器,将运维的所有Agent放到Sidecar容器中,而业务的进程单独放到App容器中。采用这种容器的隔离机制,保障业务的稳定性。同时,我们也推动了容器裁剪计划,去掉一些不必要的Agent。
图13 轻量级容器
Serverless是个系统工程,在技术上涉及到Kubernetes、容器、操作系统、JVM、运行时等各种技术,在平台能力上涉及到CI/CD各个流程的方方面面。
为了给用户提供极致的开发体验,我们为用户提供了开发工具的支持,如CLI(Command Line Interface)、WebIDE等。为了解决现有上下游技术产品的交互的问题,我们与公司现有的技术生态做了融合打通,方便开发同学使用。为了方便下游的集成平台对接,我们开放了平台的API,实现Nest赋能各下游平台。针对容器过重,系统开销大,导致低频业务函数自身资源利用率不高的问题,我们支持了函数合并部署,成倍提升资源利用率。
开发工具能够降低平台的使用成本,帮助开发同学快速的进行CI/CD流程。目前Nest提供了CLI工具,帮助开发同学快速完成创建应用、本地构建、本地测试、Debug、远程发布等操作。Nest还提供了WebIDE,支持在线一站式完成代码的修改、构建、发布、测试。
仅支持这些研发工具还是不够的,项目推广使用后,我们很快就发现开发同学对平台有了新的需求,如无法在Pipeline流水线、线下服务实例编排平台上完成对函数的操作,这对我们项目的推广也形成了一些阻碍。因此,我们融合这些公司的成熟技术生态,打通了Pipeline流水线等平台,融入到现有的上下游技术体系内,解决用户的后顾之忧。
有很多Nest的下游解决方案平台,如SSR(Server Side Render)、服务编排平台等,通过对接Nest的OpenAPI,实现了生产力的进一步解放。例如,不用让开发同学自己去申请、管理和运维机器资源,就能够让用户非常快速的实现一个SSR项目或者编排程序从0到1的创建、发布与托管。
Nest除了开放了平台的API,还对用户提供了自定义资源池的能力,拥有了该项能力,开发同学可以定制自己的资源池,定制自己的机器环境,甚至可以下沉一些通用的逻辑,实现冷启动的进一步优化。
合并部署指的是将多个函数部署在一个机器实例内。合并部署的背景主要有两个:
基于这两个背景,我们考虑支持合并部署,将一些低频的函数部署到同一个机器实例内,来提升预留实例中业务进程的资源利用率。
在具体实现上,我们参考Kubernetes的设计方案,设计了一套基于Sandbox的函数合并部署体系(每个Sandbox就是一个函数资源),将Pod类比成Kubernetes的Node资源,Sandbox类比成Kubernetes的Pod资源,Nest Sidecar类比成Kubelet。为了实现Sandbox特有的部署、调度等能力,我们还自定义了一些Kubernetes资源(如SandboxDeployment、SandboxReplicaSet、SandboxEndpoints等)来支持函数动态插拔到具体的Pod实例上。
图14 合并部署架
构除此之外,在合并部署的形态下,函数之间的隔离性也是不可回避的问题。为了尽可能的解决函数(合并在同一个实例中)之间的互相干扰问题,在Runtime的实现上,我们针对Node.js和Java语言的特点采取了不同的策略:Node.js语言的函数使用不同的进程来实现隔离,而Java语言的函数,我们采用类加载隔离。采用这种策略的主要原因是由于Java进程占用内存空间相较于Node.js进程会大很多。
目前Nest产品在美团前端Node.js领域非常受欢迎,也是落地最广泛的技术栈。当前Nest产品在美团前端已实现了规模化落地,几乎涵盖了所有业务线,接入了大量的B/C端的核心流量。
具体的落地前端场景有:BFF(Backend For Frontend)、CSR(Client Side Render)/SSR(Server Side Render)、后台管理平台、定时任务、数据处理等。
Serverless的收益是非常明显的,尤其在前端领域,大量的业务接入已是最好的说明。具体收益,从以下两个方面分别来看:
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8