容器运行时升级:先跑兼容性探针再扩范围

示例中,旧 JVM 因 cgroup 文件路径变化启动失败。cgroup v1/v2 的切换通常由操作系统、内核和 systemd 启动方式决定,并非 Docker Engine 升级本身必然触发;升级前应核对宿主机和运行时的实际配置。

+-----------------------------------------------------------------------------------+
|                  Docker Engine 24.0 升级后 Java 8 崩溃堆栈                         |
+-----------------------------------------------------------------------------------+
| 2026-08-23T04:12:01.442Z [ERROR] Failed to start JVM instance.                     |
| java.lang.InternalError: java.io.FileNotFoundException:                           |
| /sys/fs/cgroup/memory/memory.limit_in_bytes (No such file or directory)          |
|     at sun.management.FileSystemImpl.open(Native Method)                          |
|     at jdk.internal.platform.cgroupv1.Metrics.getInstance(Metrics.java:62)        |
|     at sun.metrics.Launcher.main(Launcher.java:31)                               |
| Fatal: Container exited with status code 1. System systemd-cgroup v2 active.      |
+-----------------------------------------------------------------------------------+

升级踩坑:cgroup v1 到 v2 迁移对应用内存限制的影响。

升级 Docker 之前,应同时评估内核、systemd、containerd、CNI 与应用运行时。在 cgroup v1 中内存控制器通常使用 /sys/fs/cgroup/memory/memory.limit_in_bytes,cgroup v2 则以统一层级中的 memory.max 表示限制;具体挂载路径仍需在目标节点检查。

如果你的应用使用的是较老版本的 JDK(如 Java 8u121 之前),JVM 在启动时会强行去读取 v1 架构的路径来计算 -XX:+UseCGroupMemoryLimitForHeap。当路径不存在时,JVM 就会直接挂掉,或者根本读不到内存限制,错误地将宿主机的 128GB 内存当作容器内存,从而在申请堆空间时瞬间触发宿主机的 OOM Killer。

# 检查宿主机当前生效的 Cgroup 版本
stat -fc %T /sys/fs/cgroup/

# 若输出为 cgroup2fs 则代表系统正运行在 cgroup v2 下
# 查看当前 Docker 引擎详细的版本与 Cgroup Driver 信息
docker info | grep -i cgroup

# 紧急回退 Grub 配置以强制启用 cgroup v1 兼容模式(按需使用)
sudo sed -i 's/GRUB_CMDLINE_LINUX="/GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=0 /' /etc/default/grub
sudo update-grub

存储驱动:overlay2 驱动升级与文件句柄数泄露排查。

除了 Cgroup 的巨坑,Docker 版本升级还会涉及到存储驱动(Storage Driver)和 Storage Quota 的隐性变更。升级过程中,Docker daemon 可能会重新加载 /var/lib/docker/overlay2 目录下的元数据信息。如果老版本 Docker 存在未清理的死容器挂载点,升级后可能会触发 overlay2: mount target does not exist 错误。

此外,Docker 24.0 对默认的 nofile(文件句柄数 limits)做了调整。旧版本中 Docker 默认会给容器赋予巨大的 1048576 文件句柄限制,而新版本可能会回退到 systemd 默认的 1024。这会导致高并发的 Nginx 或 Redis 容器在升级后瞬间打满句柄,吐出 Too many open files 错误。

{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "3"
  },
  "storage-driver": "overlay2",
  "storage-opts": [
    "overlay2.override_kernel_check=true"
  ],
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 65536,
      "Soft": 65536
    }
  }
}

兼容性测试:构建 Docker API 版本兼容层与回归测试脚本。

升级 Docker Engine 还会破坏上层自动化工具与 Docker Engine API 的通信。旧版本的 CI/CD 构建脚本可能还在使用 Docker API v1.40,而升级后的 Docker 守护进程如果开启了 DOCKER_MIN_API_VERSION 限制,拒绝响应低版本 API 请求,就会导致 CI 流水线全面罢工。

在大规模升级节点前,可使用客户端 SDK 编写兼容性探针,在隔离环境模拟关键 API 版本与工作负载,验证镜像构建、容器启动和安全插件的行为是否符合预期。

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/client"
)

func main() {
	// 显式指定客户端使用的 Docker API 版本,避免 Engine 升级后 API 冲突
	cli, err := client.NewClientWithOpts(
		client.WithHost("unix:///var/run/docker.sock"),
		client.WithVersion("1.43"), // 匹配 Docker 24.0 API 版本
	)
	if err != nil {
		log.Fatalf("[Docker API Error] Failed to create client: %v", err)
	}

	ctx := context.Background()
	ping, err := cli.Ping(ctx)
	if err != nil {
		log.Fatalf("[Docker API Error] Daemon unreachable: %v", err)
	}

	fmt.Printf("[Check OK] Connected to Docker Engine. API Version: %s, Min API Version: %s\n",
		ping.APIVersion, ping.MinAPIVersion)

	// 验证旧版 API 创建容器时的 ulimit 属性兼容性
	config := &container.Config{
		Image: "alpine:3.18",
		Cmd:   []string{"/bin/sh", "-c", "ulimit -n"},
	}
	hostConfig := &container.HostConfig{
		Resources: container.Resources{
			Ulimits: []*units.Ulimit{
				{
					Name: "nofile",
					Soft: 65536,
					Hard: 65536,
				},
			},
		},
	}

	resp, err := cli.ContainerCreate(ctx, config, hostConfig, nil, nil, "api-compat-test")
	if err != nil {
		log.Fatalf("[Check Fail] Container creation failed under new Docker API: %v", err)
	}
	fmt.Printf("[Check OK] Successfully created compatibility test container ID: %s\n", resp.ID[:12])
	
	// 清理临时测试容器
	_ = cli.ContainerRemove(ctx, resp.ID, container.RemoveOptions{Force: true})
}

灰度平滑:多节点 Engine 分批升级与 Pod 自动迁移策略。

为了避免单个节点升级失败导致整个物理机房的业务瘫痪,Docker Engine 升级绝对不能用全量脚本一键执行。必须配合 Kubernetes 的 Node 排水机制(kubectl drain),采取分批次、小步快跑的灰度升级策略。

标准的升级流程是:首先将目标 Node 节点标记为不可调度(cordon),然后强制驱逐该节点上的所有 Pod 到其他空闲节点。待 Pod 在其他节点平稳运行后,停止 docker.socket 与 containerd 服务,执行 Engine 二进制包更新,检查 /etc/docker/daemon.json 配置,最后重启服务并解除节点的调度封锁(uncordon)。

#!/usr/bin/env bash
set -eo pipefail

TARGET_NODE="node-prod-08"

echo "[1/5] Cordoning node ${TARGET_NODE}..."
kubectl cordon "${TARGET_NODE}"

echo "[2/5] Draining pods from ${TARGET_NODE}..."
kubectl drain "${TARGET_NODE}" --ignore-daemonsets --delete-emptydir-data --force --grace-period=30

echo "[3/5] Upgrading Docker Engine & Containerd packages..."
ssh "root@${TARGET_NODE}" << 'EOF'
  systemctl stop docker.socket docker containerd
  apt-get update && apt-get install -y --only-upgrade docker-ce docker-ce-cli containerd.io
  systemctl daemon-reload
  systemctl start containerd docker
  docker info | grep "Server Version"
EOF

echo "[4/5] Running compatibility smoke tests..."
ssh "root@${TARGET_NODE}" "docker run --rm alpine:3.18 /bin/echo 'Docker Engine upgrade verified!'"

echo "[5/5] Uncordoning node ${TARGET_NODE}..."
kubectl uncordon "${TARGET_NODE}"
echo "[SUCCESS] Node ${TARGET_NODE} successfully upgraded and ready for traffic!"

兼容性清单、节点 drain 和灰度升级能把影响控制在较小范围。ulimits 管的是进程资源限制,和 cgroup 版本兼容性不是一回事;应用启动、资源识别、网络、存储和运行时 API 都要分别检查。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐