CubeSandbox 组件日志排查指南:定位各组件日志路径、使用 cubecli logs 与查看 guest kernel 日志
【免费下载链接】CubeSandboxInstant, Concurrent, Secure & Lightweight Sandbox for AI Agents.项目地址: https://gitcode.com/GitHub_Trending/cu/CubeSandbox
CubeSandbox 由 CubeAPI、CubeMaster、Cubelet、CubeShim、Hypervisor(VMM)、Cubelet 内置 network runtime 以及 cube-proxy 等多个组件组成,排障时最大的困惑往往在于"这条报错该去哪个日志文件里找"。本文以一键部署(systemd 托管)场景为准,系统汇总各组件业务日志的确切路径与查看方式,并深入讲解cubecli logs读取沙箱/模板日志的原理、在 CubeShim 日志中直接查看 guest kernel(虚拟机内核)启动输出的方法,以及两种无需重启即可将 Cubelet 切换到 debug 级别日志的途径。读完本文,你可以在一分钟内根据报错定位到对应日志文件,并在复现问题时拿到足够详细的调试信息。
适用环境与前提
本文内容面向 CubeSandbox v0.4.0 及以上版本的一键部署环境(systemd 托管),all-in-one 单机与多机集群部署方式均适用。涉及组件包括 CubeAPI、CubeMaster、Cubelet、CubeShim、Hypervisor(VMM)、Cubelet 内置 network runtime 与 cube-proxy。宿主机为运行 Cubelet 的 Linux 主机,以下命令中的路径均指宿主机文件系统路径。
根因分析:CubeSandbox 的三类日志
CubeSandbox 的日志在来源和查看方式上分成三类,三者互不重叠,排查时必须先判断报错属于哪一类:
- 业务行为日志:请求、调度、审计、VMM 创建过程等运行时行为日志,各组件直接写入
/data/log/<Module>/目录下的文件中,不会进入journalctl。 - 启动期日志:进程被 systemd 拉起到稳定运行(或失败退出)期间的 stdout/stderr,只能通过
journalctl -u <unit>查看。这部分内容量很小,仅覆盖启动/退出窗口。 - 容器沙箱/模板日志:沙箱内 init 进程(容器 PID 1)的 stdout/stderr。沙箱日志由 CubeShim 写入 Cubelet 的私有挂载命名空间内,必须用
cubecli logs读取;模板构建日志则直接落在宿主机文件系统,无需进入命名空间。
guest kernel(虚拟机内核)的启动日志本质上属于"容器 init 进程输出"的范畴:CubeShim 通过内核参数console=hvc0接管 guest 内核的控制台输出(包括Linux version ...之类的启动信息),并转发进同一条cube-shim-req.log,因此guest kernel log 直接在 CubeShim 的日志文件中就能看到,不需要任何额外工具或挂载。
此外,Cubelet 默认日志级别较低(warn),常规运行时不会打印详细的调试信息。打开 debug 级别有两种方式:修改动态配置文件热更新,或者调用内置 HTTP 接口临时切换(无需重启进程)。
各组件日志速查表
下表汇总了一键部署场景下各组件日志的确切路径与查看方式:
| 组件 | 日志类型 | 路径 | 查看方式 |
|---|---|---|---|
| CubeAPI | 业务请求日志(按天滚动) | /data/log/CubeAPI/cube-api-YYYY-MM-DD.log | tail -F |
| CubeMaster | 业务请求日志 | /data/log/CubeMaster/cubemaster-req.log | tail -F |
| CubeMaster | 启动期日志 | /data/log/CubeMaster/cubemaster.log | tail -F |
| Cubelet | 业务请求日志 | /data/log/Cubelet/Cubelet-req.log | tail -F |
| Cubelet | 统计/指标日志 | /data/log/Cubelet/Cubelet-stat.log | tail -F |
| Cubelet | 启动期日志 | — | journalctl -u cube-sandbox-cubelet.service |
| CubeShim | 业务请求日志(含 guest kernel 输出) | /data/log/CubeShim/cube-shim-req.log | tail -F |
| CubeShim | 统计日志 | /data/log/CubeShim/cube-shim-stat.log | tail -F |
| Hypervisor (VMM) | VMM 创建过程日志 | /data/log/CubeVmm/vmm.log | tail -F |
| cube-proxy | 访问/错误日志 | /data/log/cube-proxy/{access,error}.log | tail -F(见下文) |
| 沙箱容器 | init 进程 stdout/stderr | /data/cubelet/log/<sandbox-id>/{stdout,stderr}(新版本,宿主机直读);旧版本在/data/cubelet/state/io.containerd.runtime.v2.task/default/<sandbox-id>/{stdout,stderr}(Cubelet 挂载命名空间内) | cubecli logs <sandbox-id>(见下文) |
| 模板构建 | 构建容器 stdout/stderr | /data/log/template/<template-id>_0/{stdout,stderr}(宿主机文件系统) | cubecli logs --tpl <template-id> |
提示:业务日志不在 journalctl 与 服务管理与日志 中说明的一致:以上业务/请求类日志全部直接写到
/data/log/<Module>/目录,journalctl里只能看到进程启动/退出期间很少量的 stdout/stderr。
其中沙箱容器日志的路径值得单独说明。从 Cubelet/cmd/cubecli/commands/cubebox/logs.go 的源码注释与实现可以看到,新版本 CubeShim 会把日志转发到宿主机路径/data/cubelet/log/<sandboxID>/stdout|stderr(对应 Cubelet/pkg/sandboxlog/sandboxlog.go 中定义的Dir = "/data/cubelet/log");仅当该路径不存在时才回退到旧的 containerd bundle 路径(Cubelet 挂载命名空间内)。cubecli会优先尝试宿主机路径直读,命中旧路径时才通过CUBEMNT=1重入挂载命名空间读取,这也是cubecli logs必须直接在计算节点上执行的原因。
用 cubecli logs 查看沙箱 / 模板日志
cubecli随 Cubelet 一同构建,一键部署时自动安装。沙箱日志文件位于 Cubelet 的私有挂载命名空间内(或由 CubeShim 转发到宿主机路径),因此cubecli logs必须直接在计算节点上执行,无法远程调用。
# 沙箱 stdout,最后 100 行(默认) cubecli logs <sandbox-id> # 沙箱 stderr,最后 100 行 cubecli logs --stderr <sandbox-id> # 沙箱完整日志 cubecli logs --all <sandbox-id> # 沙箱最后 N 行 / 前 N 行 cubecli logs --tail 50 <sandbox-id> cubecli logs --head 20 <sandbox-id> # 模板构建日志:直接读宿主机文件系统,跳过命名空间切换 cubecli logs --tpl <template-id> cubecli logs --tpl --all --stderr <template-id>参数说明:
| 参数 | 简写 | 说明 |
|---|---|---|
--tpl | — | 把 id 当作模板 ID,从/data/log/template/<id>_0/读取,无需进命名空间 |
--stderr | -e | 读取 stderr,默认读取 stdout |
--all | -a | 输出全部行;不可与--tail/--head同时使用 |
--tail N | -t N | 输出最后 N 行(默认 100) |
--head N | -H N | 输出前 N 行 |
上述命令与参数与 logs.go 中的 CLI 定义完全一致:默认行为是当--all、--tail、--head均未显式指定时输出最后 100 行;--all与--tail/--head互斥、--tail与--head互斥,同时指定会直接报错退出。实现上,--tail采用环形缓冲区只保留末尾 N 行,--head用 Scanner 顺序输出前 N 行,读取时使用了 256 KiB 的缓冲,能应对单行较长的日志内容。
几个值得注意的工程细节:
- 短 ID 前缀解析:传入的 id 如果不是完整的 32 位 sandbox ID,
cubecli logs会先通过 Cubelet 的 gRPCList接口拉取沙箱列表,把短前缀解析为完整 ID 后再去读日志文件。 - 路径安全校验:日志文件通过
O_NOFOLLOW(拒绝打开符号链接)打开,并校验解析后的真实路径必须落在日志根目录之内,防止目录穿越攻击。 - 模板日志的
_0后缀:_0是沙箱内容器索引,单容器模板恒为 0,构建日志路径即为/data/log/template/<template-id>_0/{stdout,stderr},直接读取宿主机文件系统即可,无需进入命名空间。
cubecli logs只覆盖**容器 init 进程(PID 1)**的输出。通过 E2Bexec接口在沙箱内启动的子任务,其 stdout/stderr 需要用 E2B SDK 的on_stdout/on_stderr回调获取,不在本文范围内,详见 沙箱日志。
沙箱删除后,对应日志文件会一并清除;日志转发需要 v0.4.0 及以上版本的 CubeShim。
Guest kernel 日志:直接在 CubeShim log 里看
CubeShim 在启动虚拟机时,通过内核参数console=hvc0把 guest 内核的控制台输出接管过来,和沙箱容器 init 进程日志一样,统一写进 CubeShim 的请求日志文件:
LC_ALL=C sudo grep -a -E "(<sandbox-id 或 InstanceId>|Linux version)" /data/log/CubeShim/cube-shim-req.logcube-shim-req.log是逐行 JSON,日志本体在LogContent字段里。典型的 guest 内核启动记录形如:
{"Module":"Shim","InstanceId":"<sandbox-id>","ContainerId":"<sandbox-id>","Timestamp":"...","LogContent":"[ 0.000000] Linux version 6.12.33-cube.sandbox.pvm.guest-... #2 SMP PREEMPT_DYNAMIC ...","FunctionType":""}按InstanceId(即 sandbox ID)过滤即可拿到对应沙箱完整的内核启动输出,包括设备探测、EXT4-fs、rootfsmount 等信息,无需额外工具或进入命名空间。
JSON 行格式可以在 CubeShim/shim/src/log/mod.rs 中得到印证:LogItem结构体使用serde(rename_all = "PascalCase")序列化,字段依次为Module、InstanceId、ContainerId、Timestamp、LogContent、FunctionType;日志目录与文件名常量分别定义为/data/log/CubeShim/、cube-shim-req.log与cube-shim-stat.log。
为什么在这里,不在别处
console=hvc0是 x86_64 虚拟机的串口(virtio console)控制台,内核所有printk输出都会走这条通路。CubeShim 在 hypervisor/config.rs 中为 x86_64 追加console=hvc0(arm64 平台则使用console=ttyAMA0,115200),并把这条控制台流量直接转发进它自己的日志管道(与普通请求日志共用同一个 writer),所以查 guest kernel 日志不需要额外挂载或调试工具,直接 grepcube-shim-req.log即可。
cube-proxy 宿主机日志
cube-proxy是基于 OpenResty 的 nginx 容器。一键部署会将宿主机目录/data/log/cube-proxy/bind mount 到容器内同一路径,可以直接从宿主机读取日志:
tail -F /data/log/cube-proxy/error.log tail -F /data/log/cube-proxy/access.log容器内仍使用同一个/data/log/cube-proxy/路径,底层由宿主机目录提供。该路径配置可以在 CubeProxy/nginx.conf 中找到:error_log /data/log/cube-proxy/error.log notice;与access_log /data/log/cube-proxy/access.log access;。
| 文件 | 内容 |
|---|---|
access.log | nginx 访问日志 |
error.log | nginx 错误日志 |
开启 Cubelet debug 级别日志
Cubelet 默认日志级别是warn,排障时往往需要临时调高到debug才能看到足够的调试信息。有两种方式,二者都不需要重启 Cubelet 进程。
方式一:改动态配置文件(持久生效,热加载)
Cubelet 会以 10 秒为周期轮询动态配置文件dynamicconf/conf.yaml,检测到变化后自动热加载并调用日志级别更新逻辑。在common:段落下加一行log_level:
common: enable_pf_mode: false log_level: debug # 新增这一行 sandbox_exec_cmd_time_out: 5s ...一键部署环境下该文件的默认路径是:
/usr/local/services/cubetoolbox/Cubelet/dynamicconf/conf.yaml保存后等待约 10 秒(配置轮询周期),Cubelet 会自动切到 debug 级别,此后/data/log/Cubelet/Cubelet-req.log会打出更细粒度的记录。
源码层面,这一机制的链路清晰可见:配置结构体在 Cubelet/pkg/config/config.go 中定义了LogLevel string \yaml:"log_level"`字段,并通过hotswap.NewWatcher(configPath, 10, &Config{})创建 10 秒轮询的 watcher;[Cubelet/pkg/hotswap/file.go](https://link.gitcode.com/i/e7157f8be0800d5df10f2c1c5bbc667b) 的实现同时使用 fsnotify 事件与 10 秒 ticker 做双重保障——即使文件被原子替换(remove + create)也能在下个 tick 捕获到;配置变化后触发 [Cubelet/services/server/server.go](https://link.gitcode.com/i/c7d5e568c62d0825b1f4340323212525) 中的OnEvent,将conf.Common.LogLevel通过CubeLog.SetLevel应用到日志框架。dynamicconf/conf.yaml的完整内容可参考 [Cubelet/dynamicconf/conf.yaml](https://link.gitcode.com/i/4f8c1d6219244c0053bf7a115d4d96f9),其中common:段还包含enable_pf_mode、sandbox_exec_cmd_time_out、default_dns_servers` 等其余运行参数。
debug 级别可能使单次请求的日志量放大一个数量级,高吞吐场景下会带来磁盘 I/O 争用和 CPU 开销——开启 debug 期间建议关注/data/log/磁盘占用。排障结束后记得把这一行删掉或改回info/warn,避免长期产生大量日志。
方式二:调试端口临时切换(不改文件,进程重启后失效)
Cubelet 内置了一个 debug HTTP 端口(默认监听:9966,对应 Cubelet/config/config.toml 里的[debug] address配置),暴露了/debug/loglevel接口,可以直接调用来临时切换级别:
# 切到 debug curl -X POST 'http://127.0.0.1:9966/debug/loglevel?level=debug' # 排障结束后切回 info curl -X POST 'http://127.0.0.1:9966/debug/loglevel?level=info'这种方式立即生效、无需等待轮询周期,但只在当前进程生命周期内有效——Cubelet 重启后会恢复到配置文件里写的级别(默认warn)。适合临时抓一段现场,而不想改配置文件的场景。
该接口的实现位于 Cubelet/services/server/server.go 的ServeDebug中:/debug/loglevel处理器读取请求中的level参数并同时调用CubeLog.SetLevel与logrus.SetLevel完成切换。注意该接口不返回 JSON,只接受POST请求携带level表单参数。
同一个 debug 端口下还挂载了pprof相关的 profiling 接口(/debug/pprof/*,以及expvar的/debug/vars),性能问题排查时也可以使用。安全提示:pprof 会暴露 goroutine 栈、堆内存 profile 和源码片段等信息——请确保:9966仅监听 localhost,不要通过反向代理暴露,也不要绑定到0.0.0.0。
警告:只对新启动的沙箱生效 无论用哪种方式切到 debug,已经处于运行中的沙箱不会补出更详细的日志——它们创建/启动阶段的日志早已按之前的级别打完,不会因为后来调高了级别而重新打印。要看到完整的 debug 级别日志,需要在切换级别之后重新创建/启动沙箱。因此排障时建议的顺序是:先切到 debug,再复现问题(新建沙箱触发问题场景),而不是对着一个已经在跑的沙箱切日志级别等日志变详细。
参考资料
- 相关文档:
- 沙箱日志
- 服务管理与日志
- 相关代码:
- Cubelet/cmd/cubecli/commands/cubebox/logs.go(
cubecli logs实现) - Cubelet/services/server/server.go(
/debug/loglevelHTTP 接口) - Cubelet/pkg/config/config.go、Cubelet/pkg/hotswap/file.go(动态配置热加载)
- CubeShim/shim/src/log/mod.rs(CubeShim 日志与 console 转发实现)
- CubeShim/shim/src/hypervisor/config.rs(
console=hvc0内核参数配置) - Cubelet/config/config.toml(debug 端口、日志滚动等运行配置)
- CubeProxy/nginx.conf(cube-proxy 访问/错误日志路径)
- Cubelet/cmd/cubecli/commands/cubebox/logs.go(
【免费下载链接】CubeSandboxInstant, Concurrent, Secure & Lightweight Sandbox for AI Agents.项目地址: https://gitcode.com/GitHub_Trending/cu/CubeSandbox
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考