作者 | 冰蛙、三土、东悦、吕仙
出品 | 酷家乐技术团队
前言
目前,很多公有云厂商都推出了面向应用的 Serverless Application 产品,有 Google Cloud Run、阿里云 Serverless Application Engine、腾讯云 CloudBase Run 、Trigger Mesh 等,
它们都提供了 构建 → 部署→实例管理等全面服务托管能力,支持托管用任意语言和框架编写的容器化应用,简化并加快应用开发和部署工作。
酷家乐 Serverless 团队也在 2020 年中开始探索基于 Knative 开发私有化 Serverless Application,做为现有 Serverless Faas 能力之外的补充产品,满足内部对于无语言限制的容器化应用开发部署的用户诉求。
截止到 2021.06, Serverless Application 在内部已实现了规模化落地,有近 100 个应用服务使用 Serverless Application 做托管,尤其是在非 Java 领域非常受欢迎,是目前其他语言技术栈落地最多的平台选择。
为何要做私有化 Serverless Application
2.1 serverless faas 现状和局限起初,我们简单地认为 serverless 的产品形态就只会是 faas + baas 的组合模式,其 ”专注业务逻辑“ 开发模式可以赋能给 争分夺秒上线 和 服务运维繁琐/困难 的开发者,能带给我们的业务价值是更高的开发效率、更合理的角色分工、更多创新项目的快速落地。所以自 2019 年底起,我们就开始在 serverless faas 方向探索并内部落地主要基于 Node.js 语言的云函数产品,截止到 2021 年初,公司已有超过 60% 内部业务线都有接入并使用 faas 产品,faas 也真实地给业务方带去了开发上的便利并且提高了整体迭代效率。
我们对过去一年多里 faas 落地场景和用户习惯做了数据统计:
创新业务、内部系统的函数占比依旧高达 70%+,真·核心业务接入较少且场景相对简单
faas 在整体网站架构里的中间层作用明显,但是以整块业务形态落地的 SFF 场景并不多
faas 用户大多都是前端开发工程师,对于多函数(聚合部署)使用频率非常高,单逻辑函数使用越来越少
简单总结就是:faas 主要应用于前端领域,落地场景多为可独立业务逻辑,核心业务增长较为疲软。
对此我们也反思了几个 faas 的现状问题:
从落地场景上,逻辑单一的函数业务场景很少,多做为数据聚合或简单后台的场景
从业务维护上,颗粒化的函数开发方式会带来额外的隐性逻辑串联成本,业务不易结块,导致整体项目使用或改造的倾向度低
从基建成本上,提供的 faas 开发语言 runtime 种类基本就圈定未来的用户群体,这也间隔限制了可能接入的业务场景
从架构现状上,中型公司的技术架构往往倾向于稳定,业务开发分配范围广但责任明确,faas 价值发挥相对有限,会很依靠于架构调整的机遇; 而小公司的高速上线模式、大公司的私有化 Bu faas 可能会更适合 faas 生长
那么 Serverless 的价值就只能体现在这了吗?不是的,让我们回归到做 Serverless 初衷上来。
无论是哪种技术/架构的演变,本质上都是为了解决资源成本或者研发效率的问题,Serverless 也是如此。
如果说 faas 做到了消除服务端技术的壁垒,赋能了开发者 散装应用/模块的开发能力,那么整装应用的资源成本或效率又该如何改变呢?历史应用又该何去何从?
2.2 业务诉求2.2.1 技术栈限制每个公司在服务应用开发上,一般都会选有一门主体开发语言,比如常见的 Java,所以内部的基建架构、链路交互等都会围绕这个主体去实现,当然技术栈的统一的确也是项目维护的重要方式。
但是在不同的业务场景、不同的人员配置的情况下,由于存在开发门槛、缺乏可观察性等基建而导致的技术栈约束,同样也是种资源浪费,更直接影响交付效率。
开发者们也希望自己的精力能都聚焦在业务上,以前听过一句很调侃的话:“某个领域的开发专家为了落地产品,经过几个月的时间,努力把自己变成了另一个领域的中级开发者...”
2.2.2 应用资源成本我们在很早期时就开始把应用架构往云原生的方向发展,服务部署也都以容器技术为主,但在现有内部产品里,我们无法规范化地满足实现 “资源弹性为 0” 的业务场景。
假设某个服务的资源配置比较大,工作时间又都是在凌晨,如何解决资源长期占用且利用率低的问题?
再比如,按照常规的软件迭代发布流程,功能发布必须经过多类环境,测试环境、稳定环境、预发环境、线上环境等,如果有些环境不适用或使用频率很低的话,如何做到资源不浪费?
毕竟每个 team 的资源配比都是有限的。
2.2.3 应用全托管有些应用为了快速上线在开发过程中比较粗糙,比如自写 gitlab ci 来做发布控制,又或者是缺乏相应的监控、告警、hook 等基础设施。
开发者们都期望能有一个全托管的服务平台,既帮助他们屏蔽繁琐的服务搭建及运维,又能够提供满足公司要求的服务稳定性等要求,
不用关心代码发布、部署方式、机器宕机、实例扩缩容、机房容灾等问题,而且只需要花费较低的迁移成本就能立刻拥有这些能力。
2.2.4 覆盖更多场景公司内部有很多周期性任务需求,需要定时处理一些事项,常见的有数据同步、日志清理等;这些虽然实现上简单,但大多都需要用户自行处理,例如自建 cronjob,随着任务的不断增多,整体维护性变差。
还有一些离线计算任务,这类任务对返回时间要求较低,但占用的计算资源较大,采用事件驱动可以在更宏观层面做资源调度管理。
2.3 公有云约束我们也考虑过直接购买公有云的 Serverless 产品,但是调研结果显示,私有服务上到公有云的 Serverless 会有很大的难度,难以满足我们对于服务的一些基本要求,
公有云的在线开发工具和云资源联动特性,确实是很诱人,但也同时存在着 “无法复用私有集群基建”、"日志告警依赖平台"、“灰度发布仅支持版本分流”、“单一弹性策略” 、“多环境难以满足” 等致命问题。
综合考虑后,我们决定基于 Knative 开发私有化 Serverless Application 产品来满足我们的内部需求。
我们如何设计和搭建 Serverless Application
我们整个 Serverless 系统都是基于 Knative 打造的,从服务 Serving 上看,其本质是更为简单的容器管理框架,
所以只对平台层而言,无论是 faas 还是 application,实现流程上相差并不大,主要是 服务模板和运行规范 上的差异。
得益于 Serverless faas 可复用基建和内部产品的提前打通,使得 Serverless Application 孵化过程变得更为简单和高效。
3.1 整体架构3.1.1 Serverless 产品大图笔者认为 Serverless 是一种云原生的系统设计思想,是从底层开始变革计算资源的形态。我们除了依赖 Knative 做为容器托管架构方案外,还融入了更多地例如 api Gateway、Service mesh、服务发现、监控告警等内部云原生能力,把更多原本需要用户参与的事情下沉到基础设施中,提供全链路的资源整合,让整个 Serverless 体系变得强大健壮,更为简单的发挥其快速、灵活、弹性、扩展性强、迁移能力强等多种优势。
除了自研 Serverless faas 和 Serverless application 服务级产品外,我们也正在探索基于 knative eventing 做标准化的事件总线,现已支持任意 Serverless 服务去配置并使用 pingsource 做定时任务。
3.1.2 Serverless Application 请求链路Serverless Application 同样依赖 istio gw ingress 做流量管理,通过 gateway-service 做协议转换和服务转发,使得任意语言的应用服务都可以正常访问到 soa 体系服务,打通了 knative 服务与内部服务架构的请求交互。
出于内部域名的规范性,我们对不同类型的 knative service 会自动生成指定的域名尾缀,这也引发了 knative route 在服务生成时会自动添加 virtualService,导致我们想通过 vs 直接基于 custom host 做请求转发方案失败;所以我们先通过把服务转为内部域名来阻止 knative route 自定义域名生成,再设定 virtualService (添加匹配规则) 的方式来做请求转发,实现流量灰度,过程参见下图:
3.1.3 serverless ci/cd架构初期,我们强依赖于 gitlab ci,通过编写相对通用的 gitlab-ci.yaml 去完成服务构建&部署的方式,虽然可以做到流程上的约束,但也有较多的缺点:
每个服务都需要添加 ci yaml,用户操作多余
无法约束用户不去修改 ci yaml 行为,虽然是对内操作,但长期对用户暴露也是件危险的事情
流程上无法自定义执行阶段,如有 stage 或 脚本内容 需要变更,是需要用户自行修改或更新,无法做到统一更新,也不利于引入例如质量卡点等检测机制
无法把用户操作都收聚在 serverless 管理平台,使用 gitlab ci 会使得部署和版本操作是分离的,而且用户需要在 gitlab 额外维护查看及部署权限,不利于整体维护
gitlab ci 机器资源为内部共用,高峰期经常会出现排队现象,无法实现 serverless 资源池私有化
并且由于 Serverless 服务部署逻辑存在较多定制化的需求,经过一段时间的调研及讨论,我们决定放弃使用内部成熟的 devops 工具来做服务部署,选择使用开源的 argo workflow 来满足 Serverless 服务的变更需求。
内置的 ci/cd 默认自动会为用户选择标准流水线模板来实现完整的服务迭代,用户也可以自定义模板(环节)来满足各种特殊的变更要求,除了页面触发外,用户还可以配置 hook api 的方式来达到本地触发部署及 repo 持续部署的目的。
3.1.4 serverless 云平台整个云平台通过 “云产品 & 服务环境 & 服务版本 " 三个维度来定位流量来源,用户可以在平台上轻松实现灰度、上线、回滚等流量版本操作。
上述版本指的是 Knative Revision,每个 revision 记录了某一时刻的代码和 Configuration 的快照,对应着一组 deployment 管理的 pod,通过控制 revision 就可以规范地控制服务迭代,满足灰度、分流等使用场景。
3.2 knative 特性使用3.2.1 virtualService 使用场景目前部署的 knative serving 在流量管理层依赖 istio VirtualService(简称 vs) 实现。原生的 vs 在流量管理方面非常强大和灵活,但是 knative 屏蔽了 vs 的多数能力,比如根据请求路径、header匹配等,只支持流量按比例在不同 revision 之间进行分配。原生的 knative 流量管理策略无法满足业务方的需求,因此需要实现使用自定义 vs 替代(兼容)knative vs。
knative-serving 下有两个 istio 网关,分别是 knative-ingress-gateway 和 cluster-local-gateway,knative 原生的 vs 作用在这两个网关上而生效。当用户使用集群外域名访问 knative 服务时,knative-ingress-gateway 会根据生效的 vs 将请求按比例转发到对应的 revision 上。
在同一个 istio gateway 上,一个域名只能有一个 vs 转发规则,无法在 knative 原生对外域名 vs 已存在的情况下,再发布一份自定义的 vs 策略实现覆盖。knative 支持通过为服务增加一个特定标签,关闭对外域名(及相关的 vs 的生成)。因此对于有特定流量管理需求的服务,关闭对外域名的生成,再使用自定义的 vs 文件来实现对外域名的请求转发。符合自定义匹配规则的请求,可以将请求转发到指定服务版本,其他不符合匹配规则的请求,重写请求 authority 为该服务的内部域名,再将请求转发到 cluster-local-gateway 上,利用 knative 的内部域名转发规则将请求转发到 revision 对应的服务上。自定义 vs 文件如图:
3.2.2 custom domain 域名配置我们对于 Serverless application 域名格式要求是:{custom_name}.sls-{cluster}.qunhe.com (其中 custom_name 即为 ksvc_name),并且希望通过配置 Knative custom domain 来统一生成标准域名。
但是由于 Knative 域名格式受 config-network ConfigMap 中 domainTemplate 控制,默认情况下的域名格式为:name.namespace.domain。其中 domain 字段又收到 config-domain ConfigMap 的控制,用户可以通过不同的 label 为 ksvc 生成特定的 domain。并且域名中的 name 在 revision 中时会加上 tag 信息,因此默认的 revision 的域名格式形如:tag-name.namespace.domain。
所以我们需要解决的问题是:如何删除默认域名中 namespace 字段,并且添加上体现集群信息的 sls-** 字段。
我们可以通过在 config-domain 中增加一个标签选择器,给这些服务加上特定标签,来为这些服务生成一个名为 sls-**.*qunhe.com 的 domain。同时,为了兼容已经以前通过默认域名格式生成域名的服务,不能简单地将 config-network 中的 name.namespace.domain 改为 name.domain。因此我们的解决方法是在服务中增加一个 annotation 表示,并修改 domainTemplate;当判断当服务有该标识时,使用 name.domain 的格式,domainTemplate 的相关片段如图:
3.2.3 eventing 落地探索
knative eventing 架构按流程方向简单地分成 3 块内容:事件源 → 事件处理(转发、存储、过滤等) → 事件消费,通过 Broker/Trigger 事件处理模型对事件进行过滤分发,并且采用 CNCF 定义的标准数据格式 CloudEvents 进行事件传输,确保跨平台和互操作性。
我们实验性地拓展了基于 pingsource 的定时事件触发功能,由于 broker 使用的是默认的 InMemoryChannel, 缺乏对事件的高可用、高可靠保障,所以现在仅对测试环境的所有 serverless 服务开放使用,满足用户 定时推送、数据清理、备份检测等常见需求。
后续我们将继续调研并引入 “分布式消息队列” channel,开发自定义容器源,围绕整个事件链路添加监控和输出查看,使得事件驱动模式可以在生产环境中开始试用。
Serverless Application 在公司内部落地情况
4.1 落地场景具体的落地场景有:对外建站、异步渲染、BFF、后台管理平台、在线搜索引擎、离线数据处理、对内效能工具等。
这里我们挑几个落地较多的场景介绍下:
4.1.1 对外建站通过同构上传一些静态资源就完成部署,非常适合做展示类型的需求,例如 模型展示、虚拟展厅 等。
为此,我们还提供了通用的代码模板,解决了资源获取、转发代理、部署配置 等通用问题,而且模板并不限制您使用何种前端框架。
既能让开发者更关注于自己的业务逻辑,也可以非常便捷地满足老项目实现建站场景,用户还可以按需增加 server 端代码逻辑,不需要关心服务器运维相关的复杂性。
4.1.2 BFF、后台管理平台Backend For Frontend,主要用于向前端提供数据,常用作后端服务接口的聚合裁剪。
与 faas 不同的是,这里的 bff 不受限于语言和框架,用户可以自由选择喜欢的技术栈,而且可以平滑迁移存量业务至 serverless 模式。
公司内有很多的内部系统,前端部分现已基本都交由前端管理平台(PUB),其提供前端应用从构建到发布的整个生命周期管理和维护,使得前后端分离更为简单,
Serverless Application 正好帮助那些内部系统来托管后台服务,帮助用户实现服务快速落地,推进低运维优势的价值转化。
4.1.3 离线数据处理离线数据都是基于服务处理的,每个新场景的上线都需要新增 service,新场景落地成本和管理运维成本非常高,
现在内部业务方通过利用 serverless 的特性高效管理,实现新业务快速接入,离线计算场景也做到了 “按需起服务,计算完后即回收”,不再需要占用固定资源。
模块之间使用云原生标准事件规范 CloudEvent 传输交互,解除平台绑定,加速云原生生态集成。
4.2 落地收益截止到 2021.06, 酷家乐内部已有近 100 个应用服务使用 Serverless Application 做托管,部分核心服务也已接入到 Serverless 体系中,
通过 Serverless 的弹性伸缩能力, 帮助业务方降低 k8s 资源成本达到近 50%,
同时根据 “云平台新场景接入便捷,轻运维” 的特性,部分新服务上线时间也从原来的 2-3 天缩短至 0.5 天之内,整体研发效率提升约 40% 以上。
我们遇到的困难和阻碍
5.1 基建融合问题knative 帮助我们屏蔽了很多 k8s 底层相关的操作,让我们轻松地就能完成服务部署和弹性控制等等。但是,对于生产级产品而言,这是远远不够的。
最近看到陈皓老师在 serverless days 里分享关于 “serverless 需要的基本配套设施”,大致分成四块:资源伸缩编排、全栈可观察性、服务治理、流量管理。
对比以上内容,knative 主要帮我们解决了 “资源伸缩编排” ,那么剩下的三块呢?我们的做法是通过融合公司内部基建来完成。
这时候就会面临一个新的问题:内部基建能否兼容、满足 serverless 架构的技术栈要求?
服务治理:微服务架构是否统一?service mesh or 传统 rpc 框架 ?cmdb 规范?
可观察性:调用链接入?日志是否落盘?监控数据采集方式?告警规则设定?
流量管理:增设多级流量网关?熔断限流黑名单等基础功能满足?定制化需求?
开始时我们也曾陷入过误区,倾向于以快速实现为主,选择更有利于我们自己的技术方向,导致部分功能难以扩展或缺失维护。
后来我们及时做了调整,也得益于酷家乐云原生基建较为完整,通过部门间的不断沟通和讨论,规范化注册接入服务元数据,慢慢地让 knative istio 这套方案与内部基建相结合,合理地分配系统功能模块,找专业的人做专业的事,让整个 Serverless 体系变得更加丰富和健壮。
5.2 业务落地顾虑Serverless 虽然非常有希望成为下一代云计算主流技术,并且 serverless 系统在酷家乐内部稳定地运行已有 2 年,但是部分业务方对新技术和架构还是持观望态度,毕竟对于业务方来说,服务稳定大如天。
我们的 “轻运维” 特性同时也是一把双刃剑。由于封装了很多的底层调度的细节,使得整个系统对于用户来说变得更为黑盒,这也导致问题定位变得更加困难。
所以我们一直持续地在提高系统的可观察性和稳定性 ,除了满足三板斧、故障预警、及时止血等基本要求外,也在努力达到 “让用户不参与过程,但过程对用户透明” 的目标,让用户平滑地放心地将业务迁到 Serverless 上来。
5.3 knative 使用问题5.3.1 日志收集我们内部标准的实时日志方案是基于 FileBeat(收集)+Kafka(消息队列)+ Flink(流处理)+ElasticSearch(查询)实现的,但有以下接入限制:
通过对 pod 添加 volume 信息实现日志挂载(from deployment)
所有被采集的服务都必须要有 cmdbtag(按服务按天分索引,优化查询速度)
根据上述限制并结合 serverless 产品实际情况,我们对不同的产品采取了不同的日志收集策略:
Serverless Application: 服务部署位置在各个服务组分配的 namespace 下,cmdbtag 操作也有明确的审批流程,所以我们采用内部标准方案,通过 “挂载 volume” 方案实现日志收集,期间需要用户主动声明容器内日志文件路径。
serverless faas:服务部署位置及服务命名都有一定的约束,注册函数时也会同步注册 cmdbtag;出于简化整体流程,我们额外采用 “非挂载式日志收集” 方式来达到 “不需要关心日志如何被收集,直接就能在云平台上查看日志” 的用户期望。
5.3.1.1 挂载 volume由于 Knative 自身的限制,无法在 Knative 的 Service 资源中为 user-container 挂载除了 ConfigMap 和 Secret 之外的任何 Volume。为了适配现有的日志收集方式,需要使 Knative 创建的 Pod 最终能挂载其他类型(目前指 HostPath)的volume。
Knative 从 ksvc 到 pod 的控制链路为 ksvc → revision → deployment → pods,其中 ksvc,revision,deployment 资源都受到 knative controller 的约束,因此解决思路即是增加一个自定义的拦截器,在带有特定 label 的 pod 创建时,进行拦截,并通过加 volume 的相关配置保存在 annotation 中,拦截器解析出配置后再为 pod 添加相关 volume 信息。通过此种解决方案,可以以较少的代价将 knative 服务的日志接入到公司原有的日志平台中。
5.3.1.2 非挂载式收集当 docker 作为 kubernetes 容器运行时,容器日志的落盘由 docker 完成,保存在 /var/lib/docker/containers/${container_id} 目录下。同时 kubelet 会在 /var/log/pods 和 /var/log/containers 下建立软链接,指向 /var/lib/docker/containers/${container_id} 下的容器日志文件。/var/log/containers 目录中日志名称格式为:${pod_name}_${namespace}_${container_name}_${container_id}。
目前 severless faas 服务所部署的 namespace 都符合 faas-* 的命名格则,因此通过对 filebeat 配置新增日志收集路径,收集 /var/log/containers 目录下所有满足正则表达式:"*_faas-*.log" 的文件,并发送到新的 topic。同时开发新的日志处理流来处理这个 topic,实现 serverless faas 业务日志接入日志平台。
5.3.2 冷启动时长5.3.2.1 分环境预热配置Serverless 冷启动优化是一个复杂的系统工程。目前主要是在生产环境中通过控制服务实例最小数为1来避免冷启动问题,保证服务的响应时间。
5.3.3 服务灾备迁移服务灾备迁移主要分为两个方面,其一是 knative 自身的相关组件和其依赖的组件(比如istio)灾备迁移;其二就是运行在 serverless 平台之上的服务的灾备迁移。
针对 knative 自身和依赖组件,采用 kubernetes 备份恢复工具 velero,每日定时对相关的组件、配置进行备份。同时搭建一个运行在公有云上仅有少数节点的 knative 集群作为迁移之用的备份集群。当需要启动灾备迁移时,首先通过 velero 根据将备份集群的 knative 相关配置更新到最近一次的备份,然后通过公有云快速扩容节点的能力,将该集群节点数扩容,以承接随之而来的业务迁移的资源需求。
针对 Severless 平台上的服务,由于服务的所有变更都通过平台操作,因此平台能容易的保留每个服务的 manifest 。需要迁移服务时,只需将服务最新的 manifest 文件在备份集群上重新 apply 一次即可。
规划和展望
本节我们会从面向用户的 Serverless 产品角度,粗浅地谈谈提出些笔者自己的看法及方向规划,只期能抛砖引玉,与大家共勉。
Serverless faas 和 Serverless Application 从开发者角度是两个不同服务形态的产品,前者是函数态,后者是应用态。
理论上,所有服务都可以改造成函数态,所有服务也都可以快速地迁移到应用态,那么我们该怎么去看待两者区别?
笔者认为函数态开发难度低,业务嵌套少,适用于无状态、低耦合、逻辑变化快的业务,是快速解决场景落地问题的 “锋利匕首”,短小精悍;
应用态优势是没有技术栈和接入场景限制,可发展潜力巨大,前期可以做到 "量贩式" 产品模式, 同时 “app + mesh” 的链路架构又可以让 Serverless 去承载更多生产级的核心应用。
就目前看来应用态可以极大的发挥 Serverless 带来的价值,毕竟存量项目都是应用服务,流量为王;所以应用态依旧会是我们下一年要持续稳步发展的重点内容。
另外还有一种衍生出来的产品形式也值得去关注:基于 Serverless 服务的工作流。
这两年国内的主流云商们也都把工作流商业化为云产品,用来协调多个分布式任务执行,可以用顺序、分支、并行等方式来编排分布式任务,工作流会按照设定好的步骤可靠地协调任务执行,跟踪每个任务的状态转换,并在必要时执行定义的重试逻辑,以确保工作流顺利完成,很适合处理 事务型业务流程编排和 数据流水线处理 等流式计算任务。
在未来,我们也将继续深入使用 knative eventing,结合 rocketmq channel,建立标准且高可用的 EventBridge 架构。在功能上,先做到 Serverless 产品间可以通过事件驱动做服务交互及工作流编排,再针对不同场景创建对应的自定义容器源,打通 Serverless 服务 和 内部服务 的事件交互,实现 "低成本、松耦合“ 的资源联动,满足更多的内部场景需求,带来符合期望的业务价值。
思考和反思
最后分享几点在实践私有化 Serverless 产品时的想法:
私有化 Serverless 可能不适用于研发体量小或云原生基建缺失较多的公司,前面也有提到,Serverless 是个系统产品,需要融入很多内部基建能力。为了 Serverless 反而需要投入更多甚至几倍的研发资源到缺失的基建开发上,一定程度上来说是背道而驰的做法。企业内私有化 Serverless 的形成是一个自然而然的演进过程,得先有底子,才有可能落地开花。
在做私有化 Serverless 之前,第一步需要先分析 Serverless 能带来的业务价值并且明确受众、调研公有云是否无法达成、预估整体投入产出比,第二步是确定合作目标,推进方需要根据专业的领域寻找专业的团队,不可盲目自研,第三步就是确定 Serverless 产品边界,底层技术革新难免会出现所谓的 “功能重复”,这时候我们需要守住初衷和产品定位,拉开与 “看似重叠” 产品的差异性,体现出价值;这样 Serverless 落地之路会好走很多。
国内外对于 Serverless 产品定义和价值还没有完全统一,各个平台间也有着自己的标准,在当前百家争鸣的阶段,我们可以多关注主流云商的产品走向以及开源社区动态,结合内部业务诉求,适式调整发展路线。