gVisor Checkpoint Gofer 源码解析:将容器检查点文件直接保存到 GCS 的完整机制
2026/9/13 21:10:17 网站建设 项目流程

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,其原文给出了两条关键设计动机:

  1. 该目录实现了一个 checkpoint gofer,提供将检查点文件保存到 GCS、以及从 GCS 恢复检查点文件的能力;
  2. 它被构建为独立二进制,目的是避免把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-serverrunsc-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.imgpages.imgpages_meta.img
-allow-checkpoint-writes允许写入内核检查点文件
-allow-fscheckpoint-reads允许读取文件系统检查点文件(fscheckpoint.pbmultitar.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.ServerHandle(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说明
TokentokenOAuth2 token。非空时 gofer 用该 token 认证;否则回退到应用默认凭据(ADC)。注意omitzero标签——零值可省略
Bucketbucket存放检查点文件的 GCS bucket,必填runGCSgcs.NewFileServer都会校验非空
ObjectPrefixobject_prefix纯字符串前缀(不是路径拼接)加到每个检查点文件名之前构成 GCS 对象名;若前缀不含尾随/,实现不会替你补/,需要自己写对
ParallelCompositeUploadparallel_composite_uploadsafe(缺省,安全时启用)、disable(无条件禁用)、force(无条件启用);其他取值会直接报错退出

4. 检查点文件模型:五个文件名与权限门控

gVisor 一个完整检查点由若干固定命名的文件组成,定义在 pkg/sentry/state/checkpointfiles/checkpointfiles.go:

常量文件名内容读权限写权限
StateFileNamecheckpoint.img内核状态allowCheckpointReadsallowCheckpointWrites
PagesMetadataFileNamepages_meta.img页元数据内核或 FS 读权限任一内核或 FS 写权限任一
PagesFileNamepages.img内存页数据(最大文件)内核或 FS 读权限任一内核或 FS 写权限任一
FSCheckpointManifestFileNamefscheckpoint.pb文件系统检查点 manifestallowFSCheckpointReadsallowFSCheckpointWrites
FSCheckpointMultiTarFileNamemultitar.img文件系统检查点 tar 数据allowFSCheckpointReadsallowFSCheckpointWrites

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/manifestFileMaxReadParallel1 MiB / 2fscheckpoint.pb
miscFileMaxReadBytes/miscFileMaxReadParallel1 MiB / 8checkpoint.imgmultitar.imgpages_meta.imgnumMiscFiles = 2
pagesFileMaxReadBytes/pagesFileMaxReadParallel16 MiB / 160pages.img
manifestFileMaxWriteBytes/manifestFileMaxWriteParallel2 MiB / 2写 manifest
miscFileMaxWriteBytes/miscFileMaxWriteParallel2 MiB / 4写其他小文件
pagesFileMaxWriteBytes/pagesFileMaxWriteParallel32 MiB / 4pages.img(非 PCU)
pagesFileMaxPCUWriteParallel160pages.img(PCU 模式)

pages.img时的maxRangespagesFileMaxReadBytes / os.Getpagesize(),即“每个页一个 range”,这是pgalloc.MemoryFile恢复时可能需要的上限;注释同时指出 gofer 侧的Reader不使用 readv,因此不受内核UIO_MAXIOV限制。

6. 读取路径:range reader 池化复用

reader.go 的Reader实现stateio.AsyncReader

  • 构造函数启动maxParallelworkerMaingoroutine,通过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.imgFileServer.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的解法:

  1. 每次AddWrite都生成一个 level 0 的临时对象,对象名格式为<目标对象名>_<随机hex>_part_<随机hex>_<level>_<partIndex>——前缀与目标对象一致,是为了兼容基于对象名前缀的 IAM/managed folder 授权;
  2. 当某一层连续攒满 32 个(composeMax = 32,GCS 单次 compose 的源对象上限)就绪对象,启动 composer goroutine 将其组合成上一层对象;composer 会递归向上组合,直到不再凑满 32;
  3. 每组合成功一个中间对象,立即把被消费的 32 个源对象交给 32 个 deleter goroutine 异步删除,把临时对象数量维持在 O(log) 量级;
  4. Finalize()时丢弃不满 32 的残缺分支,重新构建一棵最小化组合次数的最终组合树:让下标最小(体积最大)的对象尽量推迟到最终 compose,用恰好一棵“次满”(sub-maximal)分支吸收(n-1) % 31的余数。若只剩 1 个对象,优先用ObjectHandle.Move(原子重命名)完成落名,失败则回退为 copy + delete。

写入重试方面,writerMainstorage.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中各路径按此分流:getRestoreReadersgetRestoreReadersForCheckpointGofer(dup 客户端 FD 后建 URPC 客户端,见 controller.go#L700-L705);FSSavesetKernelFSSaveOptsFilesForCheckpointGofer(fscheckpoint.go#L161-L166);restore 的UseCheckpointGofer还会在压缩级别为 None 时置HavePagesFile = true(restore.go#L202-L206)。

9. 使用方式小结与适用前提

综合上述源码,启用 GCS 检查点的操作面是:

  1. 在 runsc 指定的检查点目录(checkpoint path)中放置gcs_opts.json,至少包含buckettoken可省略(回退 ADC),object_prefix决定对象名前缀,parallel_composite_upload三选一;
  2. 按普通流程触发 save/restore(内核检查点经由 spec 注解给出的 checkpoint path,文件系统检查点经由对应注解/命令)。gcs_opts.json存在即自动走 gofer 路径,日志中会出现 “Saving to GCS via checkpoint gofer” / “Restoring from GCS via checkpoint gofer”;删除该文件则回退到本地目录文件;
  3. 权限按需最小化:只做 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询