You need to enable JavaScript to run this app.

字节跳动函数计算大规模实践及Serverless展望

最近更新时间2022.12.09 12:10:11

首次发布时间2022.12.09 12:10:11

6月14日,CSDN云原生系列在线峰会第9期“Serverless峰会”正式举行,本期峰会出品人、火山引擎副总经理张鑫携手火山引擎基础架构函数计算负责人杨华辉等业界专家,共同分享了关于Serverless的核心技术与典型应用。

在张鑫看来,Serverless本质上是通过对云计算底层的计算资源、存储资源甚至应用架构进行更高层次的抽象,让用户以应用为中心,提升业务创新的敏捷性。

随后,杨华辉讲解了《字节跳动函数计算大规模实践及Serverless展望》。分享字节FaaS应用场景与规模、大规模函数计算架构,并展望了Serverless未来发展。本文基于演讲内容整理。

背景介绍

Severless

Severless 包含两层含义:一是出于节约成本的考虑,通过构建高弹性的架构,使得服务器的使用规模变小;二是应用该架构研发可以不用关注底层Sever,能够多关注自己的业务逻辑,实现业务架构,以敏捷开发的形态去应对新的业务需求。

“Less”也意味着“More”,平台需要处理的事务会增加。一方面,平台需要帮助用户屏蔽更多的底层细节,用户又会需求拥有更加透明的开发体验,因此平台还需要帮用户去暴露一些 Tracing、 Monitoring 等接口,尽量让用户可以保持原始的开发体验,避免带来比较大的开发体验损失,以及线上运维体验的损失。

IaaS/PaaS/FaaS

  • IaaS:基础设施即服务场景可以理解为买车,购买一个虚拟机或者裸机时,需要支付比较高昂的成本去独占计算实例、存储实例。但是实例可以拥有很强的自定义权利,用户可以对它进行定制,适用于自己的应用场景,更好地为业务服务。
  • PaaS:PaaS 是基于 IaaS 的进一步抽象,用户不用关心机器这一层面,只需要了解框架代码包。PaaS 也是通过平台层面去为用户提供服务,可以理解为一个租车场景。在这个场景下,手续会比较复杂,但无法对车进行运维和改造,好处是租用成本更低。
  • FaaS:可以将其理解为打车的场景,即不需要花成本租一辆车。打车只需要占用车的一段时间,其好处在于:时间片这种分片复用的架构可以让大家共享资源,由此会带来整个共享经济的极大提升,整体经济运作也会变得更加高效。但是需要注意的是,比如用户想打车时,车并不会立马能够被使用,需要花费时间等待。因此它引申了开箱即用的能力,这会带来一个隐患,即冷启动的效果会极大影响落地场景体验,如何优化冷启动成为 FaaS 场景值得关注的话题。

字节 FaaS 场景

字节 FaaS 场景的一个特点是规模特别大,基本已经到达业界前三,甚至在有些场景,规模已经达到第一水平。分维度去看一下字节产品的规模:

  • 9600万 QPS:前段时间的统计显示,字节跳动的函数计算在承载高峰期承载了9600万的 QPS,这个场景应该是业内最大的场景。该场景主要包含一些消费任务,比如 MQ 场景、Kafka 消费、对象存储消费以及 Binlog DB 消费等。
  • 10w+ 函数:10w+ 函数代表了10w+ 的微服务,其主要承载在线流量。字节内部的 load balance 通过一个7层的负载均衡器进入到 FaaS,FaaS 可以承载一些微服务框架, 比如 CloudWeGo 开源的 Kitex、Hertz 等框架。
  • 毫秒级别冷启动:传统的函数计算冷启动基本能做到100毫秒级别,在云边一体的场景承载一些 BFF、SSR 等场景是可以达到亚毫秒级别的冷启动效果。

架构解析

FaaS 在字节跳动架构中的布局,如下图所示:

字节跳动的云原生基础设施基本上都搭建在 Kubernetes 上,所以 FaaS 整体也是基于 Kubernetes 构建的。图中蓝色的部分是 FaaS 控制面、数据面以及 FaaS 运行时的布局图。图中黑色的部分,字节跳动目前采用 Kubernetes 去管理服务器,形成了很多的 Cluster ,然后封装一个 PaaS 的引擎 TCE。

FaaS 整体的控制面和数据面的组件是通过 PaaS 平台托管、运维和升级发布;用户的函数运行时,是被放在一个独立的 Kubernetes 的集群上进行承载;Function Pods 是放在原生的Kubernetes 上面去运行的。未来让用户 Pods 有更好的冷启动体验,FaaS 对 Kubernetes 有强管控,可以直接在云原生的 Kubernetes 机制上去构建承载运行。

字节特色

消费任务

字节跳动的消费任务规模是非常庞大的,消息触发器流量高峰达 9600万 QPS,调用量和计算资源规模在业界处于全球领先水平。那么消费体系的架构是如何搭建的呢?先来看看下面这个架构图:

图中最左边是 Message Queue(Kafka/RocketMQ),中间是 FaaS 写的Consumer 去消费对应 MQ。架构图的重点是右边的部分,整体上将一个 Consumer 组件抽象出来,把消息从 MQ 中拉取过来,通过 Dispatcher 组件拿到路由,然后 AutoScale 反向对它进行扩缩容。最后承载起Consumer 组件,获取消息之后,就把消息打入函数的 Instance 中,以上就是一个最简单的消费体系架构。

同时,消费体系中 Consumer 组件是被掌握在 FaaS 平台开发者手中,所以FaaS平台开发者可以帮助用户解决很多问题。其实,在 MQ场景,写一个高可用的、多机房的、 Rebalance 无感的Consumer 是非常复杂的,需要比较强的工程能力以及试错,才能做到很稳定的 Consumer 体验。特别是在Consumer 变化场景下,通过统一的平台方在 MQ 场景中把 rebalance 变得足够无感知是有好处的。在内部,Consumer 也能做很多事情,比如并发控制、反压机制、限流控制等。

FaaS 高效过滤,filter上推

挑战:从上述场景中可以看到,触发器解耦会导致触发器与函数之间多了一跳,这会导致双端都有序列化、反序列化的开销,如果从纯消费的场景中转移到 FaaS 场景中,如何考虑大规模场景中的更多资源需求?

解决思路:在大规模场景中,FaaS 对于 MQ 的消费其实更多是一种 map only 清洗机制或者过滤机制。其实很多消息是不需要到函数下游的,所以函数本身也是处理一些 filter 和清洗的逻辑,此时可以抽象逻辑,做成一个 filter 组件,然后上推到 Consumer 侧。如下图所示:

图中的 filter 组件可以和触发器放在一个运行时中。如果是 Go 的场景,Filter 可以由 Golang 去写,编译成 Golang 的 plugin,然后与触发器做一个绑定 。用户写 filter,平台提供触发器,两者一起运行。在大规模的场景中,它可以过滤 80%-90% 的消息,从而节约 8到9 倍的成本,优化效果十分明显。

另外,假设 filter 无法承担所有的业务逻辑,函数计算团队正在研发一种触发器,可以做一些进程间通信方面的优化。这能在用户不感知的情况下,帮用户节省更多的成本。

FaaS 连接数分片优化

挑战:在大规模场景中,触发器、消费者、函数都特别多。如果要进行通信需要建立连接,可能会发生爆炸。如下图左边所示,如果采用笛卡尔的 n对n 方式,连接数可能会爆炸,这会对 CPU 造成负担。

解决思路:让触发器和函数在一个 sharding 中是 n对n 的形式,剩余的就不是 n对n 形式 。可以让水平扩缩能力达到极致。理论情况下,Sharding 机制可以让整套体系在消费测试做到无限水平扩展。

微服务

在微服务场景中, FaaS 并没有出现之前,大家都是通过 PaaS 或者虚拟机、物理机承载微服务的。此时存在两个疑问?一是用户能否在微服务场景中享受 FaaS 的低成本、开发敏捷、以及触发器支持等便利,同时无需重新学习一个编码框架?二是能否支持原生HTTP,RPC 框架在 FaaS 直接运行?答案是可以,一共有以下几种解决思路。

FaaS HTTP 应用支持

首先在 HTTP 场景中的做法如下图所示:

挑战:左边的框架中是一个经典的 FaaS 在单机层面上的架构,其中 HTTP Server 承载用户的请求,负责请求的治理和管控,然后传到用户的 handler 中。在经典的 FaaS 中,用户写的函数其实是一个 handler ,中间会有一个入参,为大家提供了函数的概念。在传统的 FaaS 场景中,用户其实是一个HTTP 的应用。用户不能将一个完整的应用拆分成一个个函数放在 FaaS 上运行,管理和经营成本会很高。

解决思路:用户不改代码,HTTP 框架仍然可以直接运行,此时需要打通请求。HTTP 应用本身是监听在一个端口上,需要做一个 7层的 Proxy 导流,其余的东西可以沿用 FaaS 之前的基础架构。它的区别是:FaaS 原先让用户去写 handler 的本质做法是在 FaaS 提供一个 HTTP 或者一个 wrapper 去 wrap handler ,而我们的做法是去掉了 wrapper,直接使用用户 HTTP 框架去进行承载。

FaaS Thrift 支持

其实服务端微服务的场景,大多是RPC。在 RPC 场景中,由于字节跳动内部的 Thrift based RPC 场景规模巨大,我们以 Thrift 为例,看看 FaaS 是如何支持 Thrift 的。

挑战:在消息协议上,Thrift、 Protobuf 都提供了序列化和反序列化能力。在传输协议上,封装消息协议进行 RPC 互通,传输协议可以额外透传元信息,帮助进行服务治理和 FaaS 的控制调用。因此目标不感知用户 IDL 、不解析用户实际请求。
解决思路:采用 THeader transport,它有消息边界和长度、可以携带元信息 (字节内部称之为 TTHeader)。如下图所示,采取 Header 存储 FaaS 需要的函数和服务的元信息,进行 FaaS 场景治理。其中有关用户的代码保持不变,原先的架构并没有遭到破坏。

如果想要在 runtime 层面达到上述基本思路的效果,如下图所示,需要从左边场景切换成右边场景。

左边场景是代表一个单 Pod 层面的通信协议,所有的控制信令、数据信令通过一个端口进来,然后通过 Header 执行控制分流就能解决问题。而在右边新的架构中,既存在 HTTP 协议,又有 Thrift,因此数据请求端口和流量调度端口是分开的,让一个走数据端口,一个走流量调度端口,这就是数据链路和流量调度链路解耦的思路。

FaaS gRPC,Thrift 支持

基于上述解耦思路,采用了支持 gRPC 或者 Thrift 的 RPC 框架。由于 gRPC 本质是HTTP/2 协议,其可以和 HTTP 走同一条链路。具体的架构思路如下图所示:

为什么会有 Gateway?对于 FaaS 来说,它需要一个统一的网关。假设没有流量时,需要将流量先导到 Gateway 上进行一定时间的承载;当 Pod 拉起时再将流量导入过去,此时就需要一个统一的网关,保证每一个请求都不丢失,做到无流量时缩 0,有流量时瞬时拉起的能力。因此在 Thrift 的 RPC 场景中,可以提供了一个额外的 TTHeader Gateway,用于承载以 TTHeader based 的 RPC 框架所有语言的流量。对于这种 gRPC 或者 Thrift RPC的层面,Gateway 只是一个加法,并没有对原先的架构做过多的干预和改变。

FaaS 自定义镜像支持

代码框架的问题已经得到了解决,那么依赖怎么办?目前主要是通过自定义镜像加以解决。

FaaS 中支持了自定镜像,用户的自定镜像未必包含 FaaS 的 sidecar ,因此我们利用了一个 Init 容器的能力。Init 容器分发 FaaS 产品的中的系统二进制,它启动时通过 share volumn 的方式与应用容器进行一个 Bind mount,将 FaaS sidecar copy 到 volumn 中;应用容器启动时,它的share volumn 就有sidecar,然后它可以注入一些 FaaS 的 sidecar 的二进制,进行一个 supervisor 方式的拉起,通过这样的方式可以支持用户任意自定义镜像,承载用户的请求。

当框架和自定义镜像都支持的情况下,几乎所有的微服务场景都可以放在 FaaS 上运行,达到自动扩缩容收益。

FaaS mesh生态支持

关于 Mesh 场景下的微服务治理,这边有两个例子,分别是通过 Mesh 将流量打入到 FaaS 和 FaaS Mesh 出流量,如下图所示:

左图是通过 Mesh 把流量打进 FaaS,该思路是:用户侧出流量会有一个 Mesh 的代理,当流量打过来,在流量比较少的情况下,它会到达一个网关;在流量比较多的情况下,会到达函数实例,从而能兼顾小规模和大规模场景中的各种优势。右图是 FaaS 出流量,该思路是:函数侧会有个出流量代理,出流量代理需要在冷启动池上进行 Mesh 出流量的支持。

云边一体

FaaS 能否承载精简架构,做到云边一体?字节的解决方案是 WebAssembly,如下图所示:

由于 WebAssembly 本身是开源组织的一个方案,因此简单介绍一下它在方案中的位置。WebAssembly 可以支持跨平台的运行时,WebAssembly 虚拟机可以被放在 X86 和 ARM 上。前端语言(Rust、Golang、C++)到 WebAssembly 可以由特定的编译器编译到 WebAssembly 中。WebAssembly 能够将各种语言编译到一个模块,然后运行在一个WebAssembly 虚拟机上。优势在于不仅可以跨平台,还能实现冷启动,memory footprint 特别低。

WebAssembly 本质是一种沙箱机制,无论希望拥有什么能力,都需要自己开发。WebAssembly 虚拟机会根据 WebAssembly 标准做一些实现。但这会带来一个问题:即无论在里面实现 Golang 的 API,还是需要读写文件,读写 socket ,都需要在虚拟机层面制定一些标准和规范,才能带来相应的能力。

WebAssembly 扩展

上述的一些能力对于真正的业务是不够的,因此需要扩展 WebAssembly ,通过 WASI + Hostcalls 的方式去让用户的业务逻辑在 WebAssembly 中承载。如下图所示,通过 Hostcalls 承载,包括 HTTP、KV、日志、监控、服务发现等,这都需要外部服务的帮助,因此需要打通通道。

用户侧提供了 Guest SDK,用户只要用 Guest SDK 进行业务逻辑表达,就可以使用 Hostcalls 打通到外部的服务。

精简架构

从整体的层面上来说,其可以做到一个精简的架构。如下图所示,左图是经典 FaaS ,有 Dispatcher 和 Gateway 进行并发控制;右图架构更为简单,只要 Gateway 进来,在单机上通过一个 runtime 将它进行拉起,然后对它进行编排即可。

Worker的冷启动效果

在冷启动效果方面,先看下方左图。Java、Golang 的冷启动可以做到几十毫秒到100毫秒左右,JavaScript v8、WebAssembly 可以做到亚毫秒级别。右图显示的是端到端的冷启动效果,能做到亚毫秒级别。亚毫秒级别效果非常适用于边缘场景,边缘场景一般是单节点不到10台机器的规模,中心场景与边缘场景可以用一套代码,这种情形认为是云边一体。

云边通信

云边一体的场景下,架构还能打通云边通信。如下图所示,主要提供了双端的 Proxy,用户通过一个7层的方式去做一个重定项,只要指定一些目标,它可以通过双端 Proxy 直接把流量打到云端。其优势在于:在边缘端用户不需要自己做权限控制,网络打通等繁琐的工作。

总结与展望

首先,字节跳动的 FaaS 规模十分庞大,在进行基建时一定要考虑高可用,高扩展性,爆炸半径控制,合并部署,成本优化,智能扩缩容等。同时,FaaS 也正在支持 PaaS 演进,比如微服务高弹性和 BaaS 建设。

其次,再来看看下面这篇关于通用 Severless 的文章,其思路是:右边的 Serverless 是 FaaS + BaaS 形式,左边的 Serverless 是 general-purpose serverless。

可以看到,它整体的思路是把 Cloud Provider 抽象成两个组件:FaaS 和 BaaS,即计算与存储。希望通过两个基础设施去泛化一些接口,然后支持上层的 Software Layer 进行特定领域上的表达,这样可以支持一些融合场景。假设有很多的应用,它们都是通过下面的统一基础设施进行承载。这些应用之间的数据交换、融合打通,都可以在底层做一些更好的调度和封装,因此需要打造一个通用的Serverless。

另外,关于 Multi-runtime Architecture,如下图所示:

从 Monolithic -> Microsevices -> FaaS/Serverless -> Multi-runtime/Mecha 的架构演进十分符合上文中“字节跳动特色”云边一体中 WebAssembly 的思路。从图中最右边可以看到,它是将 Business domain 和基础架构的 Software 能力做一个抽象,将能力下沉到下方, 让用户侧只需处理纯计算逻辑和业务逻辑,从繁重的调度编排、合并部署等任务中解放出来。其优势在于可以在上层实现简单的解耦、调度编排以及合并部署等。

以上便是字节跳动在函数计算领域的部分实践,相信随着 Serverless 理念逐渐被市场接受和认可,充分的弹性和按需使用将是大势所趋,也期待 Serverless 形态可以逐步开启云计算的下半场。