本文介绍了爱奇艺视频生产技术团队针对大镜像的多种优化方案,充分利用现有的 Docker 镜像分层相关技术,在其基础上进行优化和创新,在开发人员无感知的情况下,优化镜像大小,同时提升了构建速度,减少了分发所需要的带宽。
01 背景
爱奇艺视频转码服务通过容器技术进行分布式任务的提交与执行,依赖 Docker 镜像进行版本发布以及部署,然而随着版本的迭代,Docker 镜像内容越来越多,体积越来越大,对镜像的存储、分发、拉取造成了较大的影响。
目前 Docker 镜像的设计是按照 Dockerfile 分层储存,会缓存已有的镜像层,构建新镜像时,如果某镜像层无变动,就直接使用,无需重新构建和分发。在传统的镜像中,可以将频繁变动的代码放入最底层,将环境、工具等无需变动的文件放置在上层,利用 Docker 层缓存机制,提升构建速度。例如,图1所示的镜像在每次更新时,往往前面数层可以保持不变,只需要重新构建最后一层,大小仅在 100MB 左右,构建十分迅速。然而爱奇艺视频转码服务镜像等一些大型镜像无法根据分层结构进行优化,在实际使用中存在诸多问题。
图1 传统 Docker 应用场景
视频转码环境所使用的 Docker 镜像采用 All in One 的方式,包含各种图片、视频编解码工具、业务流程代码、素材文件、推理模型等等,变动均非常频繁;镜像大小达到 18GB,体积较为庞大;版本频繁迭代,每周构建多次,发版部署不止一次;镜像单次构建时间非常长,环境、工具、代码等都在频繁变动,无法按照变动频率对 Dockerfile 分层,底层的占用空间就非常大,每次构建甚至一个小小的文件改动都需要重新构建多层。例如,在图2所示场景中,每次素材文件的改动,均会引发 15GB 的文件更新,构建时间长,分发所需带宽较大;部署节点多,发布的新版镜像需要在全国各个DC上千个节点中部署运行,对 Docker Register 造成巨大压力,例如,新版镜像部署在 5000 个节点上,下载速度约 10MB/s,则峰值带宽可达到 48.8G,成本极高,与此同时,镜像部署会占用昂贵的跨 DC 专线资源,容易对公司其它业务造成影响。
图2 视频转码 Docker 应用场景
02 解决方案
1、差异化构建
按照 Docker 镜像的分层原理,Dockerfile 将在每层文件发生变化时,会重新构建此层与此层之上的所有层。差异化构建抛弃了原有的 Dockerfile 构建方式,不重新构建任何旧层,而是基于已经分发的旧镜像创建新层,将新版本的增量更新文件放入新层中,提交新层后就得到了新的镜像,在这种情况下,只需分发最后一层即可,原理如图3所示。
图3 镜像增量更新原理
具体实现如下:需要一个 Base 镜像,即依赖镜像,包含了项目除了代码与文件之外的全部依赖项。这个 Base 镜像可以是 Docker 官方团队发布的基础系统镜像,也可以是通过 Dockerfile 编译的镜像,甚至可以是团队的历史镜像。接着,通过运行 Base 镜像,创建临时容器,获取代码以及相关文件的部署目录的映射关系,将新版本文件挂载入容器临时目录内。通过递归遍历计算目录的文件 Hash 差异,根据文件差异,将差异文件写入容器中,提交容器新层,完成新版本镜像的构建。构建完成后,需要在构建系统中记录新镜像(新层)的版本号。
在构建系统中记录当前版本之后,下次构建时,可用本次版本的镜像(或一个已被分发的镜像版本)作为 Base 镜像,重复以上构建步骤,每次构建都将叠加属于此版本的仅有差异文件的新层。如图4所示。
在文件比对的步骤中,由于代码、文件管理大部分采用了 Git 等版本管理手段,可以通过记录旧层中代码提交的版本,直接通过版本管理工具计算历史版本与当前版本的文件差异,代替递归计算文件 Hash 的方式获得差异文件列表,进一步提高构建效率,构建流程如图5。
当镜像文件层数大于最大层数(127 层),或者需要更新镜像的相关依赖,可重新构建 Base 镜像。
图 4 差异化构建流程
图 5 Git 差异化构建流程
2、用时下载
镜像越来越大,但是并不是镜像中的每个文件都会被用到,这些文件可以剥离镜像,在使用时再下载。如转码服务可以在 CPU 和 GPU 上运行,但部分工具或者一些依赖库如 CUDA 库只在 GPU 环境中才被使用。一般的解决方案是将 CPU 与 GPU 的 Base 镜像分离,再分别进行构建。然而,随着镜像的成倍增加,在构建、拉取过程中会消耗许多资源,且需维护多个版本。通过用时下载,可在执行容器请求特定资源时,资源再被下载并软链至容器中,从而节省构建镜像所需资源。
图6 用时下载流程
由于容器被随机分配在宿主机上,无法确定宿主机上是否已存在所需资源,因此,当容器索取资源时,需要首先进行资源比对。在宿主机上创建缓存目录并挂载在容器中,当确认宿主机缓存目录没有资源,或资源不完整、版本不对时,将请求远端仓库进行下载。为防止多个任务同时下载造成冗余,进程获取到资源时需加文件锁阻塞。下载结束后,对比 md5 值,保证资源完整即可释放锁。
所有可分离资源的最新版本会上传至远端仓库中进行维护。镜像中将记录资源信息,其中包括资源名称、资源所在路径、md5 值、远端地址。通过 md5 值确认资源可用后,将资源软链至容器中,任务可正常运行。
3、镜像去重
镜像构建所涉及的业务繁杂,有些工具或之前在镜像中用 YUM/APT 安装的库已经不再使用,但由于工具缺乏明确的责任人,很难得到及时清理,久而久之,镜像体积愈加臃肿。镜像去重是指通过运行任务,明确任务所需依赖,从而使镜像所使用资源体积达到最小。具体实现方式为对镜像进行 coverage test,通过 ldd 动态链接,找到执行程序所需的所有动态库,与 Dockerfile 中安装的工具做比较,未使用的动态库可视为该版本已不需要,可删除进行去重,如图7所示。
图7 镜像去重流程
4、节点预热&灰度分发
镜像分发到所有节点,往往需要大量时间,且在分发瞬间会耗费大量带宽。我们首先在 CI/CD 流程中,在版本通过测试后,自动对节点进行预热,提前分发镜像至各节点。分发过程中,按每次 10% 节点的比例拉取最新的 Docker 镜像层,防止分发请求瞬间将专线带宽占满。通过灰度分发和节点预热双措并举,可使带宽峰值降低 85% 以上。
图8 CI/CD 流程
5、Dragonfly
Dragonfly 是开源的镜像分发系统,通过 P2P 的方式加速分发,经过我们的测试,Dragonfly 在实例较多、Docker 镜像较大的场景下,利用 Dragonfly 进行 P2P 下载,可有效提高分发效率,节省网络带宽,但会增大实例所在物理机的 net.out 带宽,且稳定性有待提高。目前我们会在跨专线、跨公有云的场景下进行使用。
03 总结及展望
通过对镜像的多维度优化,镜像大小从 18G 降至 9G,同时,镜像构建时间从 30 分钟降至 5 分钟以内,分发和存储所需成本也随之降低。镜像优化对于开发人员无感知,无需了解本发明的构建原理,因此,可快速应用于各种新旧项目的镜像构建中。未来我们将继续深入研究,持续精简镜像体积。