Serverless 场景下 Pod 创建效率优化

头图.png

作者 | 张翼飞  阿里云技术专家
来源|阿里巴巴云原生公众号

导读:众所周知,Kubernetes 是云原生领域的基石,作为容器编排的基础设施,被广泛应用在 Serverless 领域。弹性能力是 Serverless 领域的核心竞争力,本次分享将重点介绍基于 Kubernetes 的 Serverless 服务中,如何优化 Pod 创建效率,提升弹性效率。

Serverless 计算简介

在进入主题之前,先简单回顾下 Serverless 计算的定义。

从维基百科可以了解到,Serverless 计算是云计算的一种形态,由云厂商管理服务器,向用户动态分配机器资源,基于实际使用的资源量计费。

用户构建和运行服务时,不用考虑服务器,降低了用户管理服务器的负担。在业务高峰期通过平台的弹性能力自动扩容实例,在业务低峰期自动缩容实例,降低资源成本。

Serverless 计算平台

下述是当前常见的 Serverless 计算产品的架构。

1.jpg

整个产品架构通常会有管控平面和数据平面两层,管控平面服务开发者,管理应用生命周期,满足开发者对应用管理的需求,数据平面服务应用的访问方,如开发者业务的用户,满足应用的流量管理和访问诉求。

管控平面通常采用 Kubernetes 做资源管理和调度,master 通常是 3 节点,满足对高可用的需求,节点通过内网 SLB 访问 K8s master。

在节点层面,通常会有两种类型的节点:

  • 一种是运行 kubelet 的节点,如裸金属服务器、虚拟机等,这类节点上会运行安全容器作为 Pod 运行时,每个 Pod 拥有独立的 kernel,降低共享宿主机 kernel 带来的安全风险。同时会通过云产品 VPC 网络或其他网络技术,在数据链路层隔离租户的网络访问。通过 安全容器+二层网络隔离,单个节点上可以提供可靠的多租运行环境。
  • 还有一种是虚拟节点,通过 VirtualKubelet 衔接 K8s 和弹性实例。弹性实例是云产品中类似虚拟机的一种轻量资源形态,提供无限资源池的容器组服务,该容器组的概念对应 K8s 中的 Pod 概念。AWS 提供有 Fargate 弹性实例,阿里云提供有 ECI 弹性实例。

Serverless 产品会提供基于 K8s 的 PaaS 层,负责向开发者提供部署、开发等相关的服务,屏蔽 K8s 相关的概念,降低开发者开发、运维应用的成本。

在数据平面,用户可通过 SLB 实现对应用实例的访问。PaaS 层也通常会在该平面提供诸如流量灰度、A/B 测试等流量管理服务,满足开发者对流量管理的需求。

弹性能力是 Serverless 计算平台的核心竞争力,需要满足开发者对 Pod 规模 的诉求,提供类似无限资源池的能力,同时还要满足创建 Pod 效率的诉求,及时响应请求。

Pod 规模可通过增加 IaaS 层资源来满足,接下来重点介绍提升 Pod 创建效率的技术。

Pod 创建相关场景

先了解下 Pod 创建相关的场景,这样可以更有效通过技术满足业务诉求。

业务中会有两种场景涉及到 Pod 创建:

  • 第一种是创建应用,这个过程会先经过调度,决策最适合 Pod 的节点,然后在节点上创建 Pod。
  • 第二种是升级应用,在这个过程中,通常是不断进行 创建新 Pod 和 销毁旧 Pod。

Serverless 服务中,开发者关心的重点在于应用的生命周期,尤其是创建和升级阶段,Pod 创建效率会影响这两个阶段的整体耗时,进而影响开发者的体验。面对突发流量时,创建效率的高低会对开发者服务的响应速度产生重要影响,严重者会使开发者的业务受损。

面对上述业务场景,接下来重点分析如何提升 Pod 创建效率。

创建 Pod 流程

整体分析下 Pod 创建的阶段,按照影响 Pod 创建效率的优先级来依次解决。

这是简化后的创建 Pod 流程:

2.jpg

当有 Pod 创建请求时,先进行调度,为 Pod 选取最合适的节点。在节点上,先进行拉取镜像的操作,镜像在本地准备好后,再进行创建容器组的操作。在拉取镜像阶段,又依次分为下载镜像和解压镜像两个步骤。

我们针对两种类型的镜像进行了测试,结果如下:

3.jpg

从测试结果可看到,解压镜像耗时在整个拉取镜像过程中的占比不容忽视,对于解压前 248MB 左右的 golang:1.10 镜像,解压镜像耗时竟然占到了拉取镜像耗时的 77.02%,对于节解压前 506MB 左右的 hadoop namenode 镜像,解压镜像耗时和下载镜像耗时各占 40% 和 60% 左右,即对于拉取镜像过程的总耗时也不容忽视。

接下来就分别针对上述过程的不同节点进行优化处理,分别从上述整个流程、解压镜像、下载镜像等方面进行探讨。

拉取镜像效率提升

 

镜像预热

可以快速想到的方法是进行镜像预热,在 Pod 调度到节点前预先在节点上准备好镜像,将拉取镜像从创建 Pod 的主链路中移除,如下图:

4.jpg

可以在调度前进行全局预热,在所有节点上行提前拉取镜像。也可以在调度过程中进行预热,在确定调度到的
节点后,在目标节点上拉取镜像。

两种方式无可厚非,可根据集群实际情况进行选择。

社区里 OpenKruise 项目即将推出镜像预热服务,可以关注下。下述是该服务的使用方式:

5.png

通过 ImagePullJob CRD 下发镜像预热任务,指定目标镜像和节点,可配置拉取的并发度、Job 处理的超时时间以及 Job Object 自动回收的时间。若是私有镜像,可指定拉取镜像时的 secret 配置。ImagePullJob 的 Events 会提镜任务的状态信息,可考虑适当增大 Job Object 自动回收的时间,便于通过 ImagePullJob Events 查看任务的处理状态。

提升解压效率

从刚才看到的拉取镜像的数据来看,解压镜像耗时会占拉取镜像总耗时很大的比例,测试的例子最大占比到了 77%,所以需要考虑如何提升解压效率。

先回顾下 docker pull 的技术细节:

6.jpg

在 docker pull 时,整体会进行两个阶段:

  • 并行下载 image 层
  • 拆解 image 层

在解压 image 层时,默认采用的 gunzip。

再简单了解下 docker push 的过程:

  • 先对 image 层进行打包操作,这个过程中会通过 gzip 进行压缩。
  • 然后并行上传。

gzip/gunzip 是单线程的压缩/解压工具,可考虑采用 pigz/unpigz 进行多线程的压缩/解压,充分利用多核优势。

containerd 从 1.2 版本开始支持 pigz,节点上安装 unpigz 工具后,会优先用其进行解压。通过这种方法,可通过节点多核能力提升镜像解压效率。

这个过程也需要关注 下载/上传 的并发度问题,docker daemon 提供了两个参数来控制并发度,控制并行处理的镜像层的数量,–max-concurrent-downloads 和 –max-concurrent-uploads。默认情况下,下载的并发度是 3,上传的并发度是 5,可根据测试结果调整到合适的值。

使用 unpigz 后的解压镜像效率:

7.jpg

在相同环境下,golang:1.10 镜像解压效率提升了 35.88%,hadoop namenode 镜像解压效率提升了 16.41%。

非压缩镜像

通常内网的带宽足够大,是否有可能省去 解压缩/压缩 的逻辑,将拉取镜像的耗时集中在下载镜像方面?即适量增大下载耗时,缩短解压耗时。

再回顾下 docker pull/push 的流程,在 unpack/pack 阶段,可以考虑将 gunzip 和 gzip 的逻辑去掉:

8.jpg

对于 docker 镜像,若 docker push 时的镜像是非压缩的,则 docker pull 时是无需进行解压缩操作,故要实现上述目标,就需要在 docker push 时去掉压缩逻辑。

docker daemon 暂时不支持上述操作,我们对 docker 进行了一番修改,在上传镜像时不进行压缩操作,测试结果如下:

9.jpg

这里重点关注解压镜像耗时,可看到 golang:1.10 镜像解压效率提升了 50% 左右,hadoop namenode 镜像解压效率替身挂了 28% 左右。在拉取镜像总耗时方面,该方案有一定的效果。

镜像分发

小规模集群中,提升拉取镜像效率的重点需要放在提升解压效率方面,下载镜像通常不是瓶颈。而在大规模集群中,由于节点数众多,中心式的 Image Registry 的带宽和稳定性也会影响拉取镜像的效率,如下图:

10.jpg

下载镜像的压力集中在中心式的 Image Registry 上。

这里介绍一种基于 P2P 的镜像分发系统来解决上述问题,以 CNCF 的 DragonFly 项目为例:

11.jpg

这里有几个核心组件:

ClusterManager

它本质上是一个中心式的 SuperNode,在 P2P 网络中作为 tracker 和 scheduler 协调节点的下载任务。同时它还是一个缓存服务,缓存从 Image Registry 中下载的镜像,降低节点的增加对 Image Registry 带来的压力。

Dfget

它既是节点上下载镜像的客户端,同时又充当向其他节点提供数据的能力,可以将本地已有的镜像数据按需提供给其他节点。

Dfdaemon

在每个节点上有个 Dfdaemon 组件,它本质上是一个 proxy,对 docker daemon 的拉取镜像的请求实现透明代理服务,使用 Dfget 下载镜像。

通过 P2P 网络,中心式的 Image Registry 数据被缓存到 ClusterManager 中,ClusterManager 协调节点对镜像的下载需求,将下载镜像的压力分摊到集群节点上,集群节点既是镜像数据的拉取方,又是镜像数据的提供方,充分利用内网带宽的能力进行镜像分发。

按需加载镜像

除了上述介绍到的方法,是否还有其他优化方法?

当前节点上创建容器时,是需要先把镜像全部数据拉取到本地,然后才能启动容器。再考虑下启动虚拟机的过程,即使是几百 GB 的虚拟机镜像,启动虚拟机也通常是在秒级别,几乎感受不到虚拟机镜像大小带来的影响。

那么容器领域是否也可以用到类似的技术?

再看一篇发表在 usenix 上的题为《Slacker: Fast Distribution with Lazy Docker Containers》 的 paper 描述:

Our analysis shows that pulling packages accounts for 76% of container start time, but only 6.4% of
that data is read.

该 paper 分析,在镜像启动耗时中,拉取镜像占比 76%,但是在启动时,仅有 6.4% 的数据被使用到,即镜像启动时需要的镜像数据量很少,需要考虑在镜像启动阶段按需加载镜像,改变对镜像的使用方式。

对于「Image 所有 layers 下载完后才能启动镜像」,需要改为启动容器时按需加载镜像,类似启动虚拟机的方式,仅对启动阶段需要的数据进行网络传输。

但当前镜像格式通常是 tar.gz 或 tar,而 tar 文件没有索引,gzip 文件不能从任意位置读取数据,这样就不能满足按需拉取时拉取指定文件的需求,镜像格式需要改为可索引的文件格式。

Google 提出了一种新的镜像格式,stargz,全称是 seeable tar.gz。它兼容当前的镜像格式,但提供了文件索引,可从指定位置读取数据。

传统的 .tar.gz 文件是这样生成的: Gzip(TarF(file1) + TarF(file2) + TarF(file3) + TarFooter))。分别对每个文件进行打包,然后对文件组进行压缩操作。

stargz 文件做了这样的创新:Gzip(TarF(file1)) + Gzip(TarF(file2)) + Gzip(TarF(file3_chunk1)) + Gzip(F(file3_chunk2)) + Gzip(F(index of earlier files in magic file), TarFooter)。针对每个文件进行打包和压缩操作,同时形成一个索引文件,和 TarFooter 一起进行压缩。

这样就可以通过索引文件快速定位要拉取的文件的位置,然后从指定位置拉取文件。

然后在 containerd 拉取镜像环节,对 containerd 提供一种 remote snapshotter,在创建容器 rootfs 层时,不通过先下载镜像层再构建的方式,而是直接 mount 远程存储层,如下图所示:

12.jpg

要实现这样的能力,一方面需要修改 containerd 当前的逻辑,在 filter 阶段识别远程镜像层,对于这样的镜像层不进行 download 操作,一方面需要实现一个 remote snapshotter,来支持对于远程层的管理。

当 containerd 通过 remote snapshotter 创建容器时,省去了拉取镜像的阶段,对于启动过程中需要的文件,可对 stargz 格式的镜像数据发起 HTTP Range GET 请求,拉取目标数据。

阿里云实现了名为 DADI 的加速器,类似上述的思想,目前应用在了阿里云容器服务,实现了 3.01s 启动
10000 个容器,完美杜绝了冷启动的漫长等待。感兴趣的读者也参考该文章:https://developer.aliyun.com/article/742103

原地升级

上述都是针对创建 Pod 过程提供的技术方案,对于升级场景,在现有的技术下,是否有效率提升的可能性?是否可以达到下述效果,即免去创建 Pod 的过程,实现 Pod 原地升级?

13.jpg

在升级场景中,占比较大的场景是仅升级镜像。针对这种场景,可使用 K8s 自身的 patch 能力。通过 patch image,Pod 不会重建,仅目标 container 重建,这样就不用完整经过 调度+新建 Pod 流程,仅对需要升级的容器进行原地升级。

在原地升级过程中,借助 K8s readinessGates 能力,可以控制 Pod 优雅下线,由 K8s Endpoint Controller 主动摘除即将升级的 Pod,在 Pod 原地升级后加入升级后的 Pod,实现升级过程中流量无损。

OpenKruise 项目中的 CloneSet Controller 提供了上述能力:

14.png

开发者使用 CloneSet 声明应用,用法类似 Deployment。在升级镜像时,由 CloneSet Controller 负责执行 patch 操作,同时确保升级过程中业务流量无损。

小结

从业务场景出发,我们了解了提升 Pod 创建效率带来收益的场景。然后通过分析 Pod 创建的流程,针对不同的阶段做相应的优化,有的放矢。

通过这样的分析处理流程,使得可以有效通过技术满足业务需求。

作者简介

张翼飞,就职于阿里云容器服务团队,主要专注 Serverless 领域的产品研发。

K8S中文社区微信公众号

评论 抢沙发

登录后评论

立即登录