gVisor Checkpoint Gofer 源码解析:将容器检查点文件直接保存到 GCS 的完整机制
【免费下载链接】gvisorApplication Kernel for Containers项目地址: https://gitcode.com/GitHub_Trending/gv/gvisor
Checkpoint gofer 是 gVisor 中负责把容器检查点(checkpoint)文件直接读写到 Google Cloud Storage(GCS)的独立侧车二进制。它由 runsc 在 save/restore 时按需 fork 出来,通过 Unix 域套接字向 sentry 暴露一个 URPCAsyncFileServer,使容器状态不再必须先落盘到本地文件系统。读完本文,你将理解它为什么必须是一个独立二进制、gcs_opts.json各配置项如何生效、五类检查点文件如何映射为 GCS 对象,以及并行复合上传(parallel composite upload)的递归组合树实现。
1. 为什么 checkpoint gofer 是一个独立二进制
官方说明见 runsc/checkpointgofer/README.md,其原文给出了两条关键设计动机:
- 该目录实现了一个 checkpoint gofer,提供将检查点文件保存到 GCS、以及从 GCS 恢复检查点文件的能力;
- 它被构建为独立二进制,目的是避免把
net/http拉进主 runsc 二进制——否则会导致 fsgofer 中的 netpoll 因找不到/etc/hosts而失败。
这是一个很典型的“为了解耦依赖而拆分进程”的工程决策:GCS 客户端栈依赖 HTTP 网络库,而 gVisor 的 fsgofer(文件系统 gofer,负责把宿主文件系统安全地暴露给 sandbox)运行在受限环境中,不希望其进程镜像携带完整的 net/http 运行时。因此 GCS 访问逻辑被整体移出主二进制,单独编译。从构建系统看,runsc/checkpointgofer/BUILD 中checkpointgofer_binary使用了pure = True属性,并通过embedded_binary_go_library打包成内嵌二进制(checkpointgofertarget),最终由 runsc/gvisorbinaries/gvisorbinaries.go 声明为CheckpointGofer侧车二进制,与普通安装中的runsc-metric-server、runsc-sentry等并列。
2. 整体架构:sandbox 拉 gofer,sentry 拿套接字
从源码结构看,一次经由 checkpoint gofer 的 save/restore 的数据面是:
runsc (sandbox) checkpoint gofer 进程 sentry fork + socketpair → urpc.Server + gcs.FileServer 客户端 FD 通过 FilePayload ──────────────────────────────→ Restore/Save关键入口在 runsc/sandbox/sandbox.go 的maybeStartCheckpointGoferAndGetSocket:
- 在检查点目录(
imagePath,即注解中指定的 checkpoint path)中查找名为gcs_opts.json的文件(常量checkpointGCSOptsFileName定义于 sandbox.go)。该文件是否存在,就是是否启用 checkpoint gofer 的开关:不存在则静默回退到本地文件路径; - 创建一个
socketpair(Unix 域、SOCK_STREAM),客户端一端(fd 3)留给 sentry,服务端一端(fd 4)传给 gofer; - 将标准输入/输出/错误重定向到
/dev/null,避免 gofer 的日志污染 runsc 日志(日志可通过-log-fd/--debug-log-fd另行落盘); - 通过
cgroup.RunInCgroup在沙箱的 cgroup 中执行gvisorbinaries.CheckpointGofer.ForkExec,并以Setsid: true脱离会话,防止子进程被重新挂起父进程后收到 SIGHUP/SIGCONT; - 进程参数固定为
checkpointgofer -sock-fd=3 -gcs-opts-fd=4加一个权限标志,且显式剔除环境中的GOMAXPROCS,避免 containerd-shim 传入的GOMAXPROCS=2等值被错误继承。
四个场景各自传入不同的权限标志(见 sandbox.go):
| 场景 | 函数 | 权限标志 |
|---|---|---|
| 从检查点恢复内核状态 | setRestoreOpts(#L606) | -allow-checkpoint-reads |
| 保存内核检查点 | setCheckpointOptsFiles(#L1704) | -allow-checkpoint-writes |
| 恢复文件系统检查点 | openFSRestoreFiles(#L1769) | -allow-fscheckpoint-reads |
| 保存文件系统检查点 | setFSSaveArgs(#L1824) | -allow-fscheckpoint-writes |
如果 gofer 启动成功,客户端套接字 FD 被追加进FilePayload.Files(且是唯一一个文件),同时置UseCheckpointGofer = true;sentry 侧收到该标志后,就从套接字而不是本地文件读取检查点内容。
3. gofer 进程命令行与 GCSOptions 配置
3.1 命令行参数
gofer 是一个subcommands风格的 CLI,子命令名为checkpointgofer,参数定义见 runsc/checkpointgofer/main.go:
[-allow-checkpoint-reads|-allow-checkpoint-writes| -allow-fscheckpoint-reads|-allow-fscheckpoint-writes] -sock-fd=<socket fd> -gcs-opts-fd=<options fd>| 参数 | 含义 |
|---|---|
-allow-checkpoint-reads | 允许读取内核检查点文件(checkpoint.img、pages.img、pages_meta.img) |
-allow-checkpoint-writes | 允许写入内核检查点文件 |
-allow-fscheckpoint-reads | 允许读取文件系统检查点文件(fscheckpoint.pb、multitar.img) |
-allow-fscheckpoint-writes | 允许写入文件系统检查点文件 |
-sock-fd | 已连接到 sentry 的 Unix 域套接字 FD |
-gcs-opts-fd | 以 JSON 编码的GCSOptions文件 FD(即gcs_opts.json) |
Execute要求至少一个权限标志和两个 FD 都为有效值,否则打印用法并退出。启动后,main从-gcs-opts-fd解码GCSOptions,构造gcs.FileServer,再包一层stateipc.NewAsyncFileServer注册到urpc.Server上Handle(sock)服务请求。
3.2 gcs_opts.json 字段
GCSOptions结构体(main.go)就是gcs_opts.json的 JSON 模式:
{ "token": null, "bucket": "my-checkpoint-bucket", "object_prefix": "sandbox-abc/", "parallel_composite_upload": "safe" }| 字段 | JSON key | 说明 |
|---|---|---|
Token | token | OAuth2 token。非空时 gofer 用该 token 认证;否则回退到应用默认凭据(ADC)。注意omitzero标签——零值可省略 |
Bucket | bucket | 存放检查点文件的 GCS bucket,必填,runGCS与gcs.NewFileServer都会校验非空 |
ObjectPrefix | object_prefix | 以纯字符串前缀(不是路径拼接)加到每个检查点文件名之前构成 GCS 对象名;若前缀不含尾随/,实现不会替你补/,需要自己写对 |
ParallelCompositeUpload | parallel_composite_upload | safe(缺省,安全时启用)、disable(无条件禁用)、force(无条件启用);其他取值会直接报错退出 |
4. 检查点文件模型:五个文件名与权限门控
gVisor 一个完整检查点由若干固定命名的文件组成,定义在 pkg/sentry/state/checkpointfiles/checkpointfiles.go:
| 常量 | 文件名 | 内容 | 读权限 | 写权限 |
|---|---|---|---|---|
StateFileName | checkpoint.img | 内核状态 | allowCheckpointReads | allowCheckpointWrites |
PagesMetadataFileName | pages_meta.img | 页元数据 | 内核或 FS 读权限任一 | 内核或 FS 写权限任一 |
PagesFileName | pages.img | 内存页数据(最大文件) | 内核或 FS 读权限任一 | 内核或 FS 写权限任一 |
FSCheckpointManifestFileName | fscheckpoint.pb | 文件系统检查点 manifest | allowFSCheckpointReads | allowFSCheckpointWrites |
FSCheckpointMultiTarFileName | multitar.img | 文件系统检查点 tar 数据 | allowFSCheckpointReads | allowFSCheckpointWrites |
gcs.FileServer.OpenRead/OpenWrite(gcs/gcs.go)是一个switch path的白名单:不在上表中的路径一律拒绝并记 warning 日志。被拒绝的打开会返回fs.ErrPermission,并打一条 “attempted to open ... with allowXxx disabled” 的日志,便于排查权限标志漏配的问题。
5. GCS 客户端:认证、传输与 HTTP 细节
NewFileServer(gcs.go)在构建客户端时有几处值得注意的实现细节:
认证。未提供token时走credentials.DetectDefault(应用默认凭据),并按是否允许写选择 OAuth scope:只读场景请求devstorage.read_only,只要开启任一写权限就升级为devstorage.read_write。凭据还会查询UniverseDomain并传给客户端,保证与凭据所在宇宙域一致。
为什么用 HTTP 客户端而不是 gRPC。代码注释(gcs.go#L199-L215)说明:gRPC 客户端不支持storage.Writer.ChunkSize == 0,会强制最小 256 KiB chunk,这对ParallelWriter是致命的(其重试与内存控制依赖禁用 chunk 缓冲)。因此必须使用 HTTP(JSON) 客户端,并显式:
- 设置
MaxIdleConnsPerHost为读/写两侧计算出的最大并行总数; - 通过空
TLSNextProtomap禁用 HTTP/2(与 gcsfuse 的性能取舍一致); - 调用
storage.WithJSONReads(),这也是storage.NewClient文档推荐做法。
并行度预算。读写的MaxIdleConnsPerHost由一组调优常量推导:
| 常量(gcs.go#L37-L59) | 值 | 用途 |
|---|---|---|
manifestFileMaxReadBytes/manifestFileMaxReadParallel | 1 MiB / 2 | 读fscheckpoint.pb |
miscFileMaxReadBytes/miscFileMaxReadParallel | 1 MiB / 8 | 读checkpoint.img、multitar.img、pages_meta.img(numMiscFiles = 2) |
pagesFileMaxReadBytes/pagesFileMaxReadParallel | 16 MiB / 160 | 读pages.img |
manifestFileMaxWriteBytes/manifestFileMaxWriteParallel | 2 MiB / 2 | 写 manifest |
miscFileMaxWriteBytes/miscFileMaxWriteParallel | 2 MiB / 4 | 写其他小文件 |
pagesFileMaxWriteBytes/pagesFileMaxWriteParallel | 32 MiB / 4 | 写pages.img(非 PCU) |
pagesFileMaxPCUWriteParallel | 160 | 写pages.img(PCU 模式) |
读pages.img时的maxRanges取pagesFileMaxReadBytes / os.Getpagesize(),即“每个页一个 range”,这是pgalloc.MemoryFile恢复时可能需要的上限;注释同时指出 gofer 侧的Reader不使用 readv,因此不受内核UIO_MAXIOV限制。
6. 读取路径:range reader 池化复用
reader.go 的Reader实现stateio.AsyncReader:
- 构造函数启动
maxParallel个workerMaingoroutine,通过subs通道接收读请求(AddRead/AddReadv提交、Wait收割Completion); - 每个 worker 用
obj.NewRangeReader(ctx, off, total)发起单范围HTTP GET(GCS 客户端不支持一次请求多个 range),读满后通过rr.ReadHandle()复用底层读句柄给下一次读,减少重复建立读取状态; - 错误映射很讲究:
RangeNotSatisfiable归一为io.EOF;HTTP 401/403 归一为unix.EACCES,保证 sentry 侧看到的 errno 与本地文件语义一致(错误码解析在 gcs/errors.go)。
7. 写入路径:普通 Writer 与并行复合上传(PCU)
写pages.img时FileServer.OpenWrite先查询getParallelCompositeUploadEnabled():PCU 可用则创建ParallelWriter,否则(或创建失败时)回退到普通Writer,并打一条 warning。
7.1 Safe 模式的安全检查
ParallelCompositeUploadSafe(缺省)会拉取 bucket 属性逐项检查(gcs.go#L379-L426),任何一项不满足就禁用PCU 并记录原因:
- bucket 默认存储类必须为
STANDARD(否则临时对象会产生 early deletion 费用或落入非默认存储类); - bucket 不能有 retention policy;
- 不能启用默认 event-based hold;
- 不能启用 soft delete(
SoftDeletePolicy.RetentionDuration != 0)——代码注释特别指出,这一条比gcloud storage cp的兼容性检查更严格,因为 gVisor 的写入是流式的,可能需要递归组合,临时存储开销随文件大小超线性增长。
检查只在首次需要时执行一次(sync.Once),且FileServer构造时若开启了写权限,会在后台 goroutine 里预取该结果。所有临时对象与最终对象都被强制StorageClass = "STANDARD"和ContentType = "application/octet-stream",避免存储类不一致与 Content-Type 探测开销。
7.2 ParallelWriter:递归组合树
parallelwriter.go 是这部分最有深度的实现。GCS 官方描述的并行复合上传假设“写之前知道全部数据、固定 32 chunk”,而检查点写入是流式的(页数据边产出边写),无法预先切块。ParallelWriter的解法:
- 每次
AddWrite都生成一个 level 0 的临时对象,对象名格式为<目标对象名>_<随机hex>_part_<随机hex>_<level>_<partIndex>——前缀与目标对象一致,是为了兼容基于对象名前缀的 IAM/managed folder 授权; - 当某一层连续攒满 32 个(
composeMax = 32,GCS 单次 compose 的源对象上限)就绪对象,启动 composer goroutine 将其组合成上一层对象;composer 会递归向上组合,直到不再凑满 32; - 每组合成功一个中间对象,立即把被消费的 32 个源对象交给 32 个 deleter goroutine 异步删除,把临时对象数量维持在 O(log) 量级;
Finalize()时丢弃不满 32 的残缺分支,重新构建一棵最小化组合次数的最终组合树:让下标最小(体积最大)的对象尽量推迟到最终 compose,用恰好一棵“次满”(sub-maximal)分支吸收(n-1) % 31的余数。若只剩 1 个对象,优先用ObjectHandle.Move(原子重命名)完成落名,失败则回退为 copy + delete。
写入重试方面,writerMain把storage.Writer.ChunkSize设为 0 关闭库内缓冲(否则默认 16 MiB chunk × 160 并行会吃掉大量内存),改用自实现的指数退避:初始 50 ms、倍率 2、上限 15 s、总超时 32 s;权限类错误直接映射为EACCES。临时对象写入成功后会记录其 generation,保证即使 bucket 开了版本化也能精确删除对应版本。Close()(异常/取消路径)则取消写与组合、递归删除所有未落名的临时对象并等待删除完成,不留垃圾。parallelwriter_test.go对这套组合逻辑有专门的单元测试覆盖。
8. sentry 侧对接:URPC 异步文件接口与 boot 参数
gofer 服务的是 pkg/sentry/state/stateipc 定义的AsyncFileServerURPC 接口(AsyncFileServer.Open/AsyncFileServer.Close等方法),sentry 端拿到套接字后构造AsyncFileClient,把checkpoint.img等逻辑文件名的每次读异步代理到 gofer。契约在多处一致声明,例如 runsc/boot/controller.go#L569-L574:“若UseCheckpointGofer为 true,FilePayload的第一个(也是唯一一个)文件是连接到stateipc.AsyncFileServerURPC 服务器的 Unix 域套接字”,此时RestoreOpts.HavePagesFile未知,需由容器管理器在 restore 时确定。
对应的内核态开关经 sentry boot 命令行传入(runsc/cmd/sentry/sentrycmd/boot.go#L293-L298):
| 参数 | 含义 |
|---|---|
-save-fds | 保存检查点使用的 FD 有序列表(本地模式:kernel state、page metadata、page file) |
-save-checkpoint-gofer | 为 true 时-save-fds只有一个 FD,即连到 checkpoint gofer 的套接字 |
-fs-save-fds/-fs-save-checkpoint-gofer | 文件系统检查点保存的对偶参数 |
-fs-restore-fds/-fs-restore-checkpoint-gofer | 文件系统检查点恢复的对偶参数 |
runsc/boot中各路径按此分流:getRestoreReaders→getRestoreReadersForCheckpointGofer(dup 客户端 FD 后建 URPC 客户端,见 controller.go#L700-L705);FSSave走setKernelFSSaveOptsFilesForCheckpointGofer(fscheckpoint.go#L161-L166);restore 的UseCheckpointGofer还会在压缩级别为 None 时置HavePagesFile = true(restore.go#L202-L206)。
9. 使用方式小结与适用前提
综合上述源码,启用 GCS 检查点的操作面是:
- 在 runsc 指定的检查点目录(checkpoint path)中放置
gcs_opts.json,至少包含bucket;token可省略(回退 ADC),object_prefix决定对象名前缀,parallel_composite_upload三选一; - 按普通流程触发 save/restore(内核检查点经由 spec 注解给出的 checkpoint path,文件系统检查点经由对应注解/命令)。
gcs_opts.json存在即自动走 gofer 路径,日志中会出现 “Saving to GCS via checkpoint gofer” / “Restoring from GCS via checkpoint gofer”;删除该文件则回退到本地目录文件; - 权限按需最小化:只做 restore 就只给
-allow-checkpoint-reads,由 sandbox 按场景自动传入,使用者无需手工指定。
适用前提与限制:gofer 仅实现 GCS 一种远端后端(runGCS是唯一的run*分支);GCS 认证依赖 OAuth2/ADC 体系,因此实际部署环境需要可达的 GCS 与合规凭据;object_prefix是字符串前缀而非路径拼接,漏写/会导致对象名粘连;PCU 在safe模式下可能被 bucket 属性静默禁用(以Disabling parallel composite upload: ...日志为准),此时大页文件退化为 4 路并行的普通写入。相关源码入口汇总:入口 runsc/checkpointgofer/main.go、GCS 文件系统服务 runsc/checkpointgofer/gcs/gcs.go、读取器 runsc/checkpointgofer/gcs/reader.go、并行复合写入器 runsc/checkpointgofer/gcs/parallelwriter.go、fork 与启动逻辑 runsc/sandbox/sandbox.go。
【免费下载链接】gvisorApplication Kernel for Containers项目地址: https://gitcode.com/GitHub_Trending/gv/gvisor
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考