跳到主要内容

5 篇博文 含有标签「nydus」

查看所有标签

· 4 分钟阅读

今年夏天,在四个工程师周的时间里,Trail of Bits 和 OSTIF 合作对 Dragonfly2 进行了安全审计。 作为 CNCF 孵化项目,Dragonfly2 是基于 P2P 技术的文件分发系统。 该范围包括用于镜像分发的 Dragonfly 子项目 Nydus。 此次合作围绕与项目毕业时的安全性和寿命相关的几个目标进行了概述和构造。

Trail of Bits 审计团队通过使用静态和手动测试以及自动化和手动流程来进行审计。 通过引入 semgrep 和 CodeQL 工具,对客户端、调度程序和管理器代码进行手动审查, 并对 gRPC 处理程序进行模糊测试,审计团队能够识别项目的各种结果,以提高其安全性。 通过将工作重点放在高级业务逻辑和外部可访问端上,Trail of Bits 审计团队能够在审计过程中确定重点, 并为 Dragonfly2 未来的工作提供指导和建议。

审计报告记录了 19 项调查结果。其中 5 个调查结果被评为高,1 个为中,4 个为低,5 个为信息性, 4 个被认为是未确定的。其中 9 个调查结果被归类为数据验证,其中 3 个属于高严重性。 Dragonfly2 的代码库成熟度也进行了排名和审查,包括项目代码的 11 个方面,报告中对这些方面进行了单独分析。

这是一个很庞大的项目,由于时间和范围的限制,无法全面审查。由于这些原因, 多个专门功能不在本次审核的范围之内。该项目是在毕业前继续进行审计工作以改进 和提升代码并强化安全性的绝佳机会。持续的安全努力至关重要,因为安全是一个不断变化的目标。

我们要感谢 Trail of Bits 团队,特别是 Dan Guido、Jeff Braswell、Paweł Płatek 和 Sam Alws 在此项目上所做的工作。感谢 Dragonfly2 的维护者和贡献者, 特别是戚文博,他们持续的工作和对本次活动的贡献。 最后,我们感谢 CNCF 为此次审计提供资金并支持开源安全工作。

OSTIF & Trail of Bits

Dragonfly community

Nydus community

· 11 分钟阅读
Gaius

简介

自 17 年开源以来,Dragonfly 被许多大规模互联网公司选用并投入生产使用, 并在 18 年 10 月正式进入 CNCF,成为中国第三个进入 CNCF 沙箱级别的项目。 2020 年 4 月,CNCF 技术监督委员会(TOC)投票决定接受 Dragonfly 作为孵化级别的托管项目。 Dragonfly 多年生产实践经验打磨的下一代产品,它汲取了上一代 Dragonfly1.x 的优点并针对已知问题做了大量的优化。

Nydus 作为 Dragonfly 的子项目优化了 OCIv1 镜像格式,并以此设计了一个镜像文件系统, 使容器可以按需下载镜像,不再需要下载完整镜像即可启动容器。 在最新版本中 Dragonfly 完成了和子项目 Nydus 的集成,让容器启动即可以按需下载镜像,减少下载量。 也可以在传输过程中利用 Dragonfly P2P 的传输方式,降低回源流量并且提升下载速度。

实践

依赖

所需软件版本要求文档
Kubernetes cluster1.20+kubernetes.io
Helm3.8.0+helm.sh
Containerdv1.4.3+containerd.io
Nerdctl0.22+containerd/nerdctl

注意: 如果没有可用的 Kubernetes 集群进行测试,推荐使用 Kind

安装 Dragonfly

基于 Kubernetes cluster 详细安装文档可以参考 quick-start-kubernetes

使用 Kind 安装 Kubernetes 集群

创建 Kind 多节点集群配置文件 kind-config.yaml, 配置如下:

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
extraPortMappings:
- containerPort: 30950
hostPort: 65001
- containerPort: 30951
hostPort: 40901
- role: worker

使用配置文件创建 Kind 集群:

kind create cluster --config kind-config.yaml

切换 Kubectl 的 context 到 Kind 集群:

kubectl config use-context kind-kind

Kind 加载 Dragonfly 镜像

下载 Dragonfly latest 镜像:

docker pull dragonflyoss/scheduler:latest
docker pull dragonflyoss/manager:latest
docker pull dragonflyoss/dfdaemon:latest

Kind 集群加载 Dragonfly latest 镜像:

kind load docker-image dragonflyoss/scheduler:latest
kind load docker-image dragonflyoss/manager:latest
kind load docker-image dragonflyoss/dfdaemon:latest

基于 Helm Charts 创建 Dragonfly P2P 集群

创建 Helm Charts 配置文件 charts-config.yaml 并且开启 Peer 的预取功能, 配置如下:

scheduler:
replicas: 1
metrics:
enable: true
config:
verbose: true
pprofPort: 18066

seedPeer:
replicas: 1
metrics:
enable: true
config:
verbose: true
pprofPort: 18066
download:
prefetch: true

dfdaemon:
hostNetwork: true
metrics:
enable: true
config:
verbose: true
pprofPort: 18066
download:
prefetch: true
proxy:
defaultFilter: 'Expires&Signature&ns'
security:
insecure: true
tcpListen:
listen: 0.0.0.0
port: 65001
registryMirror:
dynamic: true
url: https://index.docker.io
proxies:
- regx: blobs/sha256.*

manager:
replicas: 1
metrics:
enable: true
config:
verbose: true
pprofPort: 18066

使用配置文件部署 Dragonfly Helm Charts:

$ helm repo add dragonfly https://dragonflyoss.github.io/helm-charts/
$ helm install --wait --create-namespace --namespace dragonfly-system dragonfly dragonfly/dragonfly -f charts-config.yaml
NAME: dragonfly
LAST DEPLOYED: Wed Oct 19 04:23:22 2022
NAMESPACE: dragonfly-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
1. Get the scheduler address by running these commands:
export SCHEDULER_POD_NAME=$(kubectl get pods --namespace dragonfly-system -l "app=dragonfly,release=dragonfly,component=scheduler" -o jsonpath={.items[0].metadata.name})
export SCHEDULER_CONTAINER_PORT=$(kubectl get pod --namespace dragonfly-system $SCHEDULER_POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
kubectl --namespace dragonfly-system port-forward $SCHEDULER_POD_NAME 8002:$SCHEDULER_CONTAINER_PORT
echo "Visit http://127.0.0.1:8002 to use your scheduler"

2. Get the dfdaemon port by running these commands:
export DFDAEMON_POD_NAME=$(kubectl get pods --namespace dragonfly-system -l "app=dragonfly,release=dragonfly,component=dfdaemon" -o jsonpath={.items[0].metadata.name})
export DFDAEMON_CONTAINER_PORT=$(kubectl get pod --namespace dragonfly-system $DFDAEMON_POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
You can use $DFDAEMON_CONTAINER_PORT as a proxy port in Node.

3. Configure runtime to use dragonfly:
https://d7y.io/docs/getting-started/quick-start/kubernetes/

检查 Dragonfly 是否部署成功:

$ kubectl get po -n dragonfly-system
NAME READY STATUS RESTARTS AGE
dragonfly-dfdaemon-rhnr6 1/1 Running 4 (101s ago) 3m27s
dragonfly-dfdaemon-s6sv5 1/1 Running 5 (111s ago) 3m27s
dragonfly-manager-67f97d7986-8dgn8 1/1 Running 0 3m27s
dragonfly-mysql-0 1/1 Running 0 3m27s
dragonfly-redis-master-0 1/1 Running 0 3m27s
dragonfly-redis-replicas-0 1/1 Running 1 (115s ago) 3m27s
dragonfly-redis-replicas-1 1/1 Running 0 95s
dragonfly-redis-replicas-2 1/1 Running 0 70s
dragonfly-scheduler-0 1/1 Running 0 3m27s
dragonfly-seed-peer-0 1/1 Running 2 (95s ago) 3m27s

创建 Peer Service 配置文件 peer-service-config.yaml 配置如下:

apiVersion: v1
kind: Service
metadata:
name: peer
namespace: dragonfly-system
spec:
type: NodePort
ports:
- name: http-65001
nodePort: 30950
port: 65001
- name: http-40901
nodePort: 30951
port: 40901
selector:
app: dragonfly
component: dfdaemon
release: dragonfly

使用配置文件部署 Peer Service:

kubectl apply -f peer-service-config.yaml

Containerd 集成 Nydus

生产环境 Containerd 集成 Nydus 详细文档可以参考 nydus-setup-for-containerd-environment。 下面例子使用 Systemd 管理 nydus-snapshotter 服务。

下载安装 Nydus 工具

下载 containerd-nydus-grpc 二进制文件, 下载地址为 nydus-snapshotter/releases:

NYDUS_SNAPSHOTTER_VERSION=0.3.3
wget https://github.com/containerd/nydus-snapshotter/releases/download/v$NYDUS_SNAPSHOTTER_VERSION/nydus-snapshotter-v$NYDUS_SNAPSHOTTER_VERSION-x86_64.tgz
tar zxvf nydus-snapshotter-v$NYDUS_SNAPSHOTTER_VERSION-x86_64.tgz

安装 containerd-nydus-grpc 工具:

sudo cp nydus-snapshotter/containerd-nydus-grpc /usr/local/bin/

下载 nydus-imagenydusd 以及 nydusify 二进制文件, 下载地址为 dragonflyoss/image-service:

NYDUS_VERSION=2.1.1
wget https://github.com/dragonflyoss/image-service/releases/download/v$NYDUS_VERSION/nydus-static-v$NYDUS_VERSION-linux-amd64.tgz
tar zxvf nydus-static-v$NYDUS_VERSION-linux-amd64.tgz

安装 nydus-imagenydusd 以及 nydusify 工具:

sudo cp nydus-static/nydus-image nydus-static/nydusd nydus-static/nydusify /usr/local/bin/

Containerd 集成 Nydus Snapshotter 插件

配置 Containerd 使用 nydus-snapshotter 插件, 详细文档参考 configure-and-start-containerd

127.0.0.1:65001 是 Dragonfly Peer 的 Proxy 地址, X-Dragonfly-Registry 自定义 Header 是提供给 Dragonfly 回源的源站仓库地址。

首先修改 Containerd 配置在 /etc/containerd/config.toml 添加下面内容:

[proxy_plugins]
[proxy_plugins.nydus]
type = "snapshot"
address = "/run/containerd-nydus/containerd-nydus-grpc.sock"

[plugins.cri]
[plugins.cri.containerd]
snapshotter = "nydus"
disable_snapshot_annotations = false

重启 Containerd 服务:

sudo systemctl restart containerd

验证 containerd 是否使用 nydus-snapshotter 插件:

$ ctr -a /run/containerd/containerd.sock plugin ls | grep nydus
io.containerd.snapshotter.v1 nydus - ok

Systemd 启动 Nydus Snapshotter 服务

Nydusd 的 Mirror 模式配置详细文档可以参考 enable-mirrors-for-storage-backend

创建 Nydusd 配置文件 nydusd-config.json, 配置如下:

{
"device": {
"backend": {
"type": "registry",
"config": {
"mirrors": [
{
"host": "http://127.0.0.1:65001",
"auth_through": false,
"headers": {
"X-Dragonfly-Registry": "https://index.docker.io"
},
"ping_url": "http://127.0.0.1:40901/server/ping"
}
],
"scheme": "https",
"skip_verify": false,
"timeout": 10,
"connect_timeout": 10,
"retry_limit": 2
}
},
"cache": {
"type": "blobcache",
"config": {
"work_dir": "/var/lib/nydus/cache/"
}
}
},
"mode": "direct",
"digest_validate": false,
"iostats_files": false,
"enable_xattr": true,
"fs_prefetch": {
"enable": true,
"threads_count": 10,
"merging_size": 131072,
"bandwidth_rate": 1048576
}
}

复制配置文件至 /etc/nydus/config.json 文件:

sudo mkdir /etc/nydus && cp nydusd-config.json /etc/nydus/config.json

创建 Nydus Snapshotter Systemd 配置文件 nydus-snapshotter.service, 配置如下:

[Unit]
Description=nydus snapshotter
After=network.target
Before=containerd.service

[Service]
Type=simple
Environment=HOME=/root
ExecStart=/usr/local/bin/containerd-nydus-grpc --config-path /etc/nydus/config.json
Restart=always
RestartSec=1
KillMode=process
OOMScoreAdjust=-999
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

复制配置文件至 /etc/systemd/system/ 目录:

sudo cp nydus-snapshotter.service /etc/systemd/system/

Systemd 启动 Nydus Snapshotter 服务:

$ sudo systemctl enable nydus-snapshotter
$ sudo systemctl start nydus-snapshotter
$ sudo systemctl status nydus-snapshotter
● nydus-snapshotter.service - nydus snapshotter
Loaded: loaded (/etc/systemd/system/nydus-snapshotter.service; enabled; vendor preset: enabled)
Active: active (running) since Wed 2022-10-19 08:01:00 UTC; 2s ago
Main PID: 2853636 (containerd-nydu)
Tasks: 9 (limit: 37574)
Memory: 4.6M
CPU: 20ms
CGroup: /system.slice/nydus-snapshotter.service
└─2853636 /usr/local/bin/containerd-nydus-grpc --config-path /etc/nydus/config.json

Oct 19 08:01:00 kvm-gaius-0 systemd[1]: Started nydus snapshotter.
Oct 19 08:01:00 kvm-gaius-0 containerd-nydus-grpc[2853636]: time="2022-10-19T08:01:00.493700269Z" level=info msg="gc goroutine start..."
Oct 19 08:01:00 kvm-gaius-0 containerd-nydus-grpc[2853636]: time="2022-10-19T08:01:00.493947264Z" level=info msg="found 0 daemons running"

转换 Nydus 格式镜像

转换 python:3.9.15 镜像为 Nydus 格式镜像, 可以直接使用已经转换好的 dragonflyoss/python:3.9.15-nydus 镜像, 跳过该步骤。 转换工具可以使用 nydusify 也可以使用 acceld

登陆 Dockerhub:

docker login

转换 Nydus 镜像, DOCKERHUB_REPO_NAME 环境变量设置为用户个人的镜像仓库:

DOCKERHUB_REPO_NAME=dragonflyoss
sudo nydusify convert --nydus-image /usr/local/bin/nydus-image --source python:3.9.15 --target $DOCKERHUB_REPO_NAME/python:3.9.15-nydus

Nerdctl 运行 Nydus 镜像

使用 Nerdctl 运行 python:3.9.15-nydus, 过程中即通过 Nydus 和 Dragonfly 下载镜像:

sudo nerdctl --snapshotter nydus run --rm -it $DOCKERHUB_REPO_NAME/python:3.9.15-nydus

搜索日志验证 Nydus 基于 Mirror 模式通过 Dragonfly 分发流量:

$ grep mirrors /var/lib/containerd-nydus/logs/**/*log
[2022-10-19 10:16:13.276548 +00:00] INFO [storage/src/backend/connection.rs:271] backend config: ConnectionConfig { proxy: ProxyConfig { url: "", ping_url: "", fallback: false, check_interval: 5, use_http: false }, mirrors: [MirrorConfig { host: "http://127.0.0.1:65001", headers: {"X-Dragonfly-Registry": "https://index.docker.io"}, auth_through: false }], skip_verify: false, timeout: 10, connect_timeout: 10, retry_limit: 2 }

性能测试

测试 Nydus Mirror 模式与 Dragonfly P2P 集成后的单机镜像下载的性能。 主要测试不同语言镜像运行版本命令的启动时间,例如 python 镜像运行启动命令为 python -V。 测试是在同一台机器上面做不同场景的测试。 由于机器本身网络环境、配置等影响,实际下载时间不具有参考价值, 但是不同场景下载时间所提升的比率是有重要意义的。

nydus-mirror-dragonfly

  • OCIv1: 使用 Containerd 直接拉取镜像并且启动成功的数据。
  • Nydus Cold Boot: 使用 Containerd 通过 Nydus 拉取镜像,没有命中任何缓存并且启动成功的数据。
  • Nydus & Dragonfly Cold Boot: 使用 Containerd 通过 Nydus 拉取镜像,并且基于 Nydus Mirror 模式流量转发至 Dragonfly P2P,在没有命中任何缓存并且启动成功的数据。
  • Hit Dragonfly Remote Peer Cache: 使用 Containerd 通过 Nydus 拉取镜像, 并且基于 Nydus Mirror 模式流量转发至 Dragonfly P2P,在命中 Dragonfly 的远端 Peer 缓存的情况下并且成功启动的数据。
  • Hit Dragonfly Local Peer Cache: 使用 Containerd 通过 Nydus 拉取镜像, 并且基于 Nydus Mirror 模式流量转发至 Dragonfly P2P,在命中 Dragonfly 的本地 Peer 缓存的情况下并且成功启动的数据。
  • Hit Nydus Cache: 使用 Containerd 通过 Nydus 拉取镜像, 并且基于 Nydus Mirror 模式流量转发至 Dragonfly P2P,在命中 Nydus 的本地缓存的情况下并且成功启动的数据。

测试结果表明 Nydus Mirror 模式和 Dragonfly P2P 集成。使用 Nydus 下载镜像对比 OCIv1 的模式, 能够有效减少镜像下载时间。Nydus 冷启动和 Nydus & Dragonfly 冷启动数据基本接近。 其他命中 Dragonfly Cache 的结果均好于只使用 Nydus 的情况。最重要的是如果很大规模集群使用 Nydus 拉取镜像, 会将每个镜像层的下载分解按需产生很多 Range 请求。增加镜像仓库源站 QPS。 而 Dragonfly 可以基于 P2P 技术有效减少回源镜像仓库的请求数量和下载流量。 最优的情况,Dragonfly 可以保证大规模集群中每个下载任务只回源一次。

链接

Dragonfly 社区

Nydus 社区

· 22 分钟阅读

Nydus 镜像加速之内核演进之路

多年来容器化实践告诉我们,优化后的容器镜像搭配 P2P 网络等技术可以有效降低容器部署启动的时间,并可保障容器持续稳定运行。为此我们开发了 Nydus 项目,作为 Dragonfly 下的镜像服务。

而除了启动速度,镜像分层、按需加载等核心特性在容器镜像领域也尤为重要。但是由于没有原生的文件系统支持,大多数都选择了用户态方案,Nydus 最初亦如此。随着方案和需求的不断演进,用户态方案遇到了越来越多的挑战,如性能与原生文件系统相比有较大差距、高密场景下资源开销较大等等。

为此我们设计并实现了兼容内核原生 EROFS 文件系统的 RAFS v6 格式,希望将容器镜像方案下沉到内核态。 同时随着 erofs over fscache 按需加载技术合入 5.19 内核, Nydus 镜像服务的下一代容器镜像分发方案逐渐清晰起来。 同时这也是 Linux 主线内核 首个原生支持、开箱即用的容器镜像分发方案,为容器镜像终态的高密、高性能、高可用和易用性提供坚实保障。

本文将从 Nydus 架构回顾、RAFS v6 镜像格式和 EROFS over Fscache 按需加载技术三个角度来介绍该方案的演变历程。

为了解更多详情,请关注Nydus。 请参考用户手册 以使用 Nydus 的上述特性.

Nydus 架构回顾

一句话总结一下,Nydus 镜像加速服务是一种优化了现有的 OCIv1 容器镜像架构,设计了 RAFS (Registry Acceleration File System) 磁盘格式,最终呈现为一种 文件系统 的容器镜像格式的镜像加速实现。

容器镜像的根本需求,本质上是为了提供容器的根目录 (rootfs),这可以通过文件系统 (file system) 或者是归档格式 (archive format) 来承载,当然也可以在文件系统的基础上二次套娃 (例如通过自定义的块格式来承载), 但本质载体是一个 目录树,体现为文件接口。

先看一下 OCIv1 标准镜像,OCIv1 格式是一种基于 Docker Image Manifest Version 2 Schema 2 格式的镜像格式规范,由 manifest、镜像索引 (optional)、一系列容器镜像层及配置文件组成,细节可以参见相关文档,本文不再赘述。 本质上说 OCI 镜像是一个以层为基本单位的镜像格式,每个层存储了文件级别的 diff data,以 tgz 归档格式存储,如下所示: ociv1

由于 tgz 的限制,OCIv1 存在一些固有问题,例如无法按需加载、较粗的层级的去重粒度、每层 hash 值易变等等。

而一些“二次套娃”方案 (例如基于自定义块格式的容器镜像方案),也存在一些原理性的设计缺陷。例如:

  • 容器镜像最终要体现为一棵目录树,那么就需要相应的文件系统来承载 (例如 ext4),这样整个链路为“自定义块格式 + 用户态块设备 + 文件系统”,相对于文件系统方案其链路更长更复杂,端到端稳定性不可控;
  • 由于块格式对上层的文件系统不感知,无法区分文件系统的元数据和数据并分别处理 (例如压缩);
  • 无法实现基于文件的镜像分析特性例如安全扫描、热点分析和运行时拦截等;
  • 对于多个“二次套娃”容器镜像,无法做到不修改 blob 内容直接 merge 成一个大镜像,而这是文件系统方案的天然能力。

而我们实现的 Nydus 则是一种基于文件系统的容器镜像存储方案。其中将容器镜像文件系统的数据 (blobs) 和元数据 (bootstrap) 分离,让原来的镜像层只存储文件的数据部分。 并且把文件以 chunk 为粒度分割,每层 blob 存储对应的 chunk 数据;因为采用了 chunk 粒度,这细化了去重粒度,chunk 级去重让层与层之间,镜像与镜像之间共享数据更容易,也更容易实现按需加载。 由于元数据被单独分离出来合为一处,因此对于元数据的访问不需拉取对应的 blob 数据,需要拉取的数据量要小很多,I/O 效率更高。Nydus RAFS 镜像格式如下图所示: nydus_rafs

RAFS v6 镜像格式

RAFS 镜像格式演变

在 RAFS v6 格式引入之前,Nydus 使用的是一个完全用户态实现的镜像格式,通过 FUSE 或 virtiofs 接口提供服务。但用户态文件系统方案在设计上存在以下缺陷:

  • 大量系统调用开销不可忽略,例如深度为 1 的随机小 I/O 访问;
  • 当容器镜像中存在大量文件时,频繁的文件操作会产生大量的 fuse 请求,造成内核态/用户态上下文的频繁切换,造成性能瓶颈;
  • 非 FSDAX 场景下,用户态到内核态的 buffer copy 会消耗 CPU 占用;
  • 在 FSDAX (virtiofs 作为接口) 场景下,大量小文件会大量占用 DAX window 资源,存在潜在的性能抖动;频繁切换访问小文件也会产生大量 DAX mapping setup 开销。

这些问题是用户态文件系统方案的天然限制带来的,而如果将容器镜像格式的实现下沉到内核态,就可以从原理上根治上述问题。因而我们引入了 RAFS v6 镜像格式,一个依托于内核 EROFS 文件系统,实现于内核态的容器镜像格式。

EROFS 文件系统介绍

EROFS 文件系统自 Linux 4.19 内核开始存在于 Linux 主线中,过去主要用于嵌入式和移动终端领域,存在于当前各大流行发行版中 (例如 Fedora,Ubuntu,Archlinux,Debian,Gentoo 等等)。 用户态工具 erofs-utils 也已经存在于这些发行版和 OIN Linux system definition 列表中,社区较活跃。

EROFS 文件系统具备如下特征:

  • 适用于多种场景的原生本地只读块文件系统,磁盘格式具备最小 I/O 单位定义;
  • page-sized 块对齐的不压缩元数据;
  • 通过 Tail-packing 内联技术有效节省空间,同时维持高访问性能;
  • 数据均以块为单位寻址 (mmap I/O 友好,不需 I/O 后处理);
  • 随机访问友好的磁盘目录格式;
  • 核心磁盘格式非常简单,且易于增加 payload,扩展性更好;
  • 支持 DIRECT I/O 访问,支持块设备、FSDAX 等多种后端;
  • 同时 EROFS 预留了 boot sector,可支持 bootloader 自启动等需求。

RAFS v6 镜像格式介绍

过去一年,阿里云内核团队对 EROFS 文件系统进行了一系列的改进与增强,拓展其在云原生下的使用场景,使其适应容器镜像存储系统的需求,最终呈现为一个实现于内核态的容器镜像格式,RAFS v6。而除了将镜像格式下沉到内核态,RAFS v6 还在镜像格式上进行了一系列优化,例如块对齐、更加精简的元数据等等。

新的 RAFS v6 镜像格式如下: rafsv6

改进后的 Nydus 镜像服务架构如下图所示,增加了对 (EROFS based) RAFS v6 镜像格式的支持: rafsv6_arch

EROFS over Fscache

erofs over fscache 是阿里云内核团队为 Nydus 开发的下一代容器镜像按需加载技术,同时也是 Linux 内核原生的镜像按需加载特性,于 5.19 版本合入 Linux 内核主线erofs_over_fscache_merge

并于 LWN.net 作为 5.19 合并窗口的 高亮特性erofs_over_fscache_lwn

在此之前业界已有的按需加载几乎都是用户态方案。用户态方案会涉及频繁的内核态/用户态上下文切换,以及内核态/用户态之间的内存拷贝,从而造成性能瓶颈。这一问题在容器镜像已经全部下载到本地的时候尤其突出,此时容器运行过程中涉及的文件访问,都还是会陷出到用户态的服务进程。

事实上我们可以将按需加载的 1) 缓存管理和 2) 缓存未命中的时候,通过各种途径 (例如网络) 获取数据,这两个操作解耦开。缓存管理可以下沉到内核态执行,这样当镜像在本地 ready 的时候,就可以避免内核态/用户态上下文的切换。 而这也正是 erofs over fscache 技术的价值所在。

方案原理

fscache/cachefiles (以下统称 fscache) 是 Linux 系统中相对成熟的文件缓存方案,广泛应用于网络文件系统 (例如 NFS、Ceph 等)。我们的主要工作是,使其支持本地文件系统 (例如 erofs) 的按需加载特性。

此时容器在访问容器镜像的时候,fscache 会检查当前请求的数据是否已经缓存,如果缓存命中 (cache hit),那么直接从缓存文件读取数据。这一过程全程处于内核态之中,并不会陷出到用户态。 erofs_over_fscache_cache_hit

否则 (cache miss) 需要通知用户态的 Nydusd 进程以处理这一访问请求,此时容器进程会陷入睡眠等待状态;Nydusd 通过网络从远端获取数据,通过 fscache 将这些数据写入对应的缓存文件,之后通知之前陷入睡眠等待状态的进程该请求已经处理完成;之后容器进程即可从缓存文件读取到数据。 erofs_over_fscache_cache_miss

方案优势

正如之前所描述的,在镜像数据已经全部下载到本地的情况下,用户态方案会导致访问文件的进程频繁陷出到用户态,并涉及内核态/用户态之间的内存拷贝。 而 erofs over fscache 下则不会再陷出到用户态,让按需加载真的“按需”,从而在提前下载容器镜像的场景下实现几乎无损的性能和稳定性, 最终获得 1) 按需加载与 2) 提前下载容器镜像 这两种场景下真正 统一无损 的方案。

具体来说 erofs over fscache 相对于用户态方案具有以下优势。

1. 异步预取

容器创建之后,当容器进程尚未触发按需加载 (cache miss) 的时候,用户态的 Nydusd 就可以开始从网络下载数据并写入缓存文件,之后当容器访问的文件位置恰好处于预取范围内的时候,就会触发 cache hit 直接从缓存文件读取数据,而不会再陷出到用户态。用户态方案则无法实现该优化。 erofs_over_fscache_prefetch

2. 网络 IO 优化

当触发按需加载 (cache miss) 的时候,Nydusd 可以一次性从网络下载比当前实际请求的数据量更多的数据,并将下载的数据写入缓存文件。 例如容器访问 4K 数据触发的 cache miss,而 Nydusd 实际一次性下载 1MB 数据,以减小单位文件大小的网络传输延时。之后容器访问接下来的这 1MB 数据的时候,就不必再陷出到用户态。 用户态方案则无法实现该优化,因为即使触发 cache miss 的时候,用户态的服务进程同样实现了该优化,下一次容器访问位于读放大范围内的文件数据的时候,同样会陷出到用户态。 erofs_over_fscache_readahead

3. 更佳的性能表现

当镜像数据已经全部下载到本地的时候 (即不考虑按需加载的影响),erofs over fscache 的性能表现显著优于用户态方案,同时与原生文件系统的性能相近,从而实现与原生容器镜像方案 (未实现按需加载) 相近的性能表现。 以下是几个工作负载下的性能测试数据 [1]

read/randread IO

以下是文件 read/randread buffer IO [2] 的性能对比

readIOPSBW性能
native ext4267K1093MB/s1
loop240K982MB/s0.90
fscache227K931MB/s0.85
fuse191K764MB/s0.70
randreadIOPSBW性能
native ext410.1K41.2MB/s1
loop8.7K34.8MB/s0.84
fscache9.5K38.2MB/s0.93
fuse7.6K31.2MB/s0.76
  • "native" 表示测试文件直接位于本地的 ext4 文件系统中
  • "loop" 表示测试文件位于 erofs 镜像内,通过 loop 设备的 DIRECT IO 模式挂载 erofs 镜像
  • "fscache" 表示测试文件位于 erofs 镜像内,通过 erofs over fscache 方案挂载 erofs 镜像
  • "fuse" 表示挂载测试文件位于 fuse 文件系统 [3]
  • "性能" 一栏对各个模式下的性能进行归一化处理,以原生 ext4 文件系统的性能为基准,比较其他模式下的性能

可以看到,fscache 模式下的 read/randread 性能与 loop 模式下的性能基本持平,同时要优于 fuse 模式;但是与原生 ext4 文件系统的性能仍存在一定差距,我们正在进一步分析和优化,理论上该方案可以达到原生文件系统的水平。

文件元数据操作测试

通过对大量小文件执行 tar 操作 [4] 测试文件元数据操作的性能。

Time性能
native ext41.04s1
loop0.550s1.89
fscache0.570s1.82
fuse3.2s0.33

可以看到 erofs 格式的容器镜像的元数据性能甚至优于原生 ext4 文件系统,这是 erofs 特殊的文件系统格式导致的。 由于 erofs 是一个只读 (read-only) 文件系统,因而其所有元数据可以紧密排布在一起,而 ext4 作为可写文件系统,其元数据则分散排布在多个 BG (block group) 中。

典型工作负载测试

测试 linux 源码编译 [5] 这一典型工作负载下的性能表现。

Linux CompilingTime性能
native ext4156s1
loop154s1.0
fscache156s1.0
fuse200s0.78

可以看到,fscache 模式下的 Linux 编译负载性能与 loop 模式、原生 ext4 文件系统的性能基本持平,同时要优于 fuse 模式。

4. 高密部署

由于 erofs over fscache 方案基于文件实现,即每个容器镜像都表现为 fscache 下的一个缓存文件,因而其天然支持高密部署的场景。例如一个典型的 node.js 容器镜像在该方案下对应 ~20 个缓存文件,那么在一个部署有上百个容器的机器中,只需要维护上千个缓存文件。

5. 故障恢复与热升级

当镜像文件全部下载到本地的时候,镜像中文件的访问不再需要用户态服务进程的介入,因而用户态服务进程存在更加充裕的时间窗口来实现故障恢复与热升级功能。这种场景下甚至不再需要用户态进程,从而实现与原生容器镜像方案 (未实现按需加载) 相近的稳定性表现。

6. 统一的容器镜像方案

有了 RAFS v6 镜像格式和 erofs over fscache 按需加载技术,Nydus 同时适用于 runc 与 rund,作为这两种容器场景下的统一的容器镜像分发方案。

另外更重要的,erofs over fscache 是 1) 按需加载与 2) 提前下载容器镜像 这两种场景下真正 统一无损 的方案。一方面,它实现了按需加载特性,在容器启动的时候不需要容器镜像全部下载到本地,从而助力极致的容器启动速度。 另一方面,它又完美兼容容器镜像已经下载到本地的这一场景,在文件访问过程中不再频繁陷出到用户态,从而实现与原生容器镜像方案 (未实现按需加载) 近乎无损的性能和稳定性表现。

展望与感谢

之后我们会对 erofs over fscache 方案进行持续迭代与完善,例如不同容器之间的镜像复用,stargz 和 FSDAX 支持,以及性能优化等。

最后感谢方案开发过程中支持和帮助过我们的所有个人与团队,感谢快手与字节跳动的同学对该方案的大力支持,让我们携手一起构建一个更好的容器镜像生态 :)

  1. 测试环境 ECS ecs.i2ne.4xlarge (16 vCPU, 128 GiB Mem),本地 NVMe 盘
  2. 测试命令 "fio -ioengine=psync -bs=4k -direct=0 -rw=[read|randread] -numjobs=1"
  3. 使用 passthrough_hp 作为 fuse daemon
  4. 测试 "tar -cf /dev/null linux_src_dir" 命令的执行时间
  5. 测试 "time make -j16" 命令的执行时间

· 12 分钟阅读

Dragonfly 发布 Nydus 容器镜像加速服务

镜像对容器部署的挑战

在容器的生产实践中,偏小的容器镜像能够很快的部署启动。当应用的镜像达到几个GB以上的时候,在节点上下载镜像通常会消耗大量的时间。Dragonfly 通过引入 P2P 网络有效的提升了容器镜像大规模分发的效率。然而,用户还是必须等待镜像数据完整下载到本地,然后才能创建自己的容器。我们希望进一步缩减镜像下载的时 间,让用户能够更快地部署容器应用。同时,如何更好的保护用户的数据,也是容器行业近年来的重要关注点。

为此,我们为Dragonfly项目引入了一个容器镜像加速服务 nydus 。nydus 能够极大缩短镜像 下载时间,并提供端到端的镜像数据一致性校验,从而让用户能够更安全快捷地管理容器应用。nydus 由阿里云和蚂蚁集团的工程师合作开发,并大规模部署在内部的 生产环境中。 作为云原生生态的一部分, nydus 在生产环境的优秀表现,让我们有信心现在将项目开源,让更多的容器用户能够体验到容器快速启动和安全加载方面 的能力。

Nydus: Dragonfly 的容器镜像服务

nydus 项目优化了现有的 OCI 镜像标准格式,并以此设计了一个用户态的文件系统。通过这些优化,nydus 能够提供这些特性:

  • 容器镜像按需下载,用户不再需要下载完整镜像就能启动容器
  • 块级别的镜像数据去重,最大限度为用户节省存储资源
  • 镜像只有最终可用的数据,不需要保存和下载过期数据
  • 端到端的数据一致性校验,为用户提供更好的数据保护
  • 兼容 OCI 分发标准和 artifacts 标准,开箱即可用
  • 支持不同的镜像存储后端,镜像数据不只可以存放在镜像仓库,还可以放到 NAS 或者类似 S3 的对象存储上
  • 与 Dragonfly 的良好集成

架构上, nydus 主要包含一个新的镜像格式,和一个负责解析容器镜像的 FUSE 用户态文件系统进程。

nydus-architecture| center | 768x356

nydus 能够解析FUSE, 或者virtiofs协议来支持传统的 runc 容器, 或者Kata容器。容器仓库,OSS 对象存储 ,NAS,以及 Dragonfly 的超级节点和 peer 节点都可以作为 nydus 的镜像数据源。同时, nydus 还可以配置一个本地缓存,从而避免每次启动都从远端数据源拉取数据。

镜像格式方面, nydus 把一个容器镜像分成元数据和数据两层。其中元数据层是一颗自校验的哈希树。 每个文件和目录都是哈希树中的一个附带哈希值的节点。一个文件节点的哈希值是由文件的数据确定,一个目录节点的哈希值则是由该目录下所有文件和目录的哈希值 确定。每个文件的数据被按照固定大小切片并保存到数据层中。数据切片可以在不同文件以及不同镜像中的不同文件共享。

nydus-format| center | 768x356

Nydus 能为用户带来什么?

用户如果部署了 nydus 镜像服务,最直观的一个感受就是,容器启动变快了,从以前的明显时间消耗,变成了几乎瞬间就能启动起来。在我们的测试中, nydus 能够把常见镜像的启动时间,从数分钟缩短到数秒钟。

nydus-performance| center | 768x356

另外一个不那么明显但也很重要的改进,是 nydus 能够为用户提供容器运行时数据一致性校验。在传统的镜像中,镜像数据会先被解压到本地文件系统,再由容器应 用去访问使用。解压前,镜像数据是完整校验的。但是解压之后,镜像数据不再能够被校验。这带来的一个问题就是,如果解压后的镜像数据被无意或者恶意地修改, 用户是无法感知的。而 nydus 镜像不会被解压到本地,同时可以对每一次数据访问进行校验,如果数据被篡改,则可以从远端数据源重新拉取。

nydus-integraty| center | 768x356

未来规划

前面我们介绍了 nydus 的架构和优点。在过去的一年里,我们和内部的产品团队一起致力于让 nydus 项目更稳定,安全和易用。在把 nydus 项目开源之后,我们 将会更关注广泛的云原生容器生态。我们的愿景是,当用户在集群中部署 Dragonfly 和 nydus 服务的时候,无论镜像大小,用户都能够方便快捷地运行他们的容器 应用,同时不需要为容器镜像的数据安全性担忧。

OCI 社区容器镜像标准

虽然我们已经在内部生产环境中大规模部署 nydus,我们坚信对 OCI 镜像标准的改进需要广泛的社区合作。为此,我们积极地参与了 OCI 社区关于下一代镜像标准 的讨论,并发现 nydus 能够广泛地符合 OCI 社区对下一代镜像格式的要求。所以我们提议把 nydus 作为 OCI 社区下一代镜像格式的示例实现,并期待和更多的 云原生行业领导者们一起推进下一代镜像标准的制定和实现。

FAQ

Q: 现有的 OCI 镜像标准有什么问题?

  • SUSE 的 Aleksa Sarai 写过一个 blog (The Road to OCIv2 Images: What's Wrong with Tar?) ,详细地描述了现有 OCI 镜像标准的一系列问题,简单总结就是 OCI 镜像标准使用的 tar 格式太古老并不适合作为容器镜像格式。

Q: nydus 和 CRFS 有什么区别?

  • CRFS 是 GO build team 设计的一个镜像格式。二者在主要设计思想上非常相似。细节上, nydus 支持块级别的数据去重和端到端的数据一致性校验,可以说 是在 CRFS 的 stargz 格式上的进一步改进。

Q: nydus 和 Azure 的 Teleport 有什么区别?

  • Azure Teleport 更像是现有 OCI 镜像标准在基于 SMB 文件共享协议的 snapshotter 上的一个部署实现,能够支持容器镜像数据按需下载,但保留了所有 目前 OCI 镜像 tar 格式的缺陷。相对的, nydus 抛弃了过时的 tar 格式,并使用 merkle tree 格式来提供更多的高级特性。

Q: 如果运行基于 nydus 的容器的时候网络断了怎么办?

  • 使用现有 OCI 镜像的时候,如果在容器镜像还没有完整下载的时候网络断了,容器会一开始就无法启动。 nydus 很大程度上改变了容器启动的流程,用户不需要 再等待镜像数据完整下载就能启动容器。而容器运行时如果网络断了,将无法访问没有下载到本地的镜像数据。 nydus 支持在容器启动后在后台下载容器镜像数据, 所以当容器镜像数据完整下载到本地后,基于 nydus 的容器也不会受到网络中断的影响。

附录:OCIv2 镜像标准

从 2020 年 6 月开始,OCI 社区花了一个多月时间密集讨论了当前 OCI 镜像标准的缺陷,以及 OCIv2 镜像格式需要满足哪些要求。OCIv2 在这里只是一个宣传 命名,实际上 OCIv2 是当前 OCI 镜像标准的改进,而不会是一个全新的镜像标准。

这次镜像格式大讨论从一个邮件和一份共享文档开始,并促成了多次在线的 OCI 社区讨论会议。最后的结论也很鼓舞人心,OCIv2 镜像格式需要满足下列要求:

  • 更少的重复数据
  • 可重建的镜像格式
  • 明确的更少的文件系统元数据
  • 可以 mount 的文件系统格式
  • 镜像内容列表
  • 镜像数据按需加载
  • 可扩展性
  • 可校验和/或可修复
  • 更少的上传数据
  • 可以工作在不可信存储上

在这份共享文档中可以找到每一个要求的详细描述。我们全程参与了整个 OCIv2 镜像格式要求的讨论 ,并发现 nydus 很好地满足了全部的这些要求。这进一步促使我们开源 nydus 项目来为社区讨论提供一个工作的代码基础。