Skip to content

容器 Exited 无挂载数据抢救

01 前置理论与核心机制

1. 容器 Exited 不等于数据销毁

  • 读写层与数据卷保留机制:容器状态变为 Exited (1),仅代表容器内部的 1 号主进程(如 MySQL)因异常退出。只要未执行 docker rm 删除容器,容器专属的可写层(Writable Layer)及其关联的匿名数据卷(Anonymous Volumes)依然完整保存在宿主机的磁盘上。
  • 离线提取原理:Docker 引擎支持在容器处于停止(Exited)状态时,直接定位并读取其底层的文件系统与数据卷。因此无需强行修复并拉起崩溃的容器,可直接进行离线数据提取。

2. 两种提取路径的适用场景

  • docker cp:直接通过 Docker Daemon 读取目标容器的文件系统并拷贝至宿主机。适合文件数量较少、路径明确的快速提取场景。
  • --volumes-from 数据卷继承:适合对数据卷(Volume)进行复杂操作。例如文件数量达到百万级需要先在容器内打包压缩再导出,或需要严格保留文件的属主、属组与读写权限属性。该方式通过启动一个挂载了相同数据卷的临时容器作为中转环境来执行操作。

02 故障排查与恢复闭环 SOP

当发现线上有状态容器异常停止,且前期未配置宿主机目录绑定挂载(Bind Mount)时,严禁执行 docker rm,按以下方案提取数据:

步骤一:确认容器状态并保护现场

查询崩溃容器的准确名称或 ID,确认容器实例未被删除:

docker ps -a | grep user-center-db
  • 定性:确认状态为 Exited (1) 或其他非零退出码,表明容器实体及底层存储仍然存在。

步骤二:方案 A - 直接物理拷贝 (适用于常规提取)

在宿主机上创建恢复目录,利用 docker cp 直接将容器内的核心数据目录拷贝至本地:

# 1. 创建本地恢复目录
mkdir -p rescue_plan_1_cp

# 2. 跨容器边界拷贝数据 (无需容器处于运行状态)
docker cp user-center-db:/var/lib/mysql ./rescue_plan_1_cp/

# 3. 验证数据体积与完整性
ls -lh ./rescue_plan_1_cp/mysql

步骤三:方案 B - 数据卷继承拷贝 (适用于需打包或保留权限)

若目标目录属于容器数据卷(如官方 MySQL 镜像默认声明的 VOLUME /var/lib/mysql),且内部包含大量小文件,直接执行 docker cp 效率较低且可能改变文件属主,可改用数据卷继承模式:

# 1. 准备宿主机接收目录
mkdir -p rescue_plan_2_inherit

# 2. 启动一个轻量的 alpine 临时容器
# --volumes-from 核心参数:直接继承故障容器挂载的所有数据卷
# -v 挂载本地接收目录到临时容器的 /export
# 以交互模式进入 sh
docker run -it --rm --volumes-from user-center-db -v $(pwd)/rescue_plan_2_inherit:/export alpine sh
进入临时容器后,执行打包或带权限归档拷贝:
# 确认原容器的数据卷内容可见
ls -lh /var/lib/mysql

# 使用 cp -a 归档模式拷贝,严格保留原始权限与属主属性
cp -a /var/lib/mysql/* /export/

# 拷贝完成后退出临时容器,容器会自动销毁 (--rm)
exit
返回宿主机检查 ./rescue_plan_2_inherit 目录,验证数据已完整导出。

03 真实生产故障现象与案例库

案例 1:修改 my.cnf 语法错误导致容器无法启动

  • 故障现象:运维人员通过 docker exec 进入容器修改了 MySQL 配置文件,因存在语法拼写错误,重启容器后 MySQL 进程立即退出,状态变为 Exited (1)。该容器未挂载外部配置目录。
  • 排查与处置:由于容器处于停止状态,无法再次通过 exec 进入修改。直接在宿主机执行 docker cp user-center-db:/etc/mysql/my.cnf ./ 将配置文件提取到本地,修正语法错误后,再执行 docker cp ./my.cnf user-center-db:/etc/mysql/my.cnf 覆盖回容器内部。随后执行 docker start,服务正常拉起。

案例 2:海量小文件导致 docker cp 响应缓慢

  • 故障现象:某容器化部署的文件服务异常退出,数据存储在匿名卷中。目录内包含 300 多万张缩略图,直接执行 docker cp 导致 I/O 居高不下,命令长时间无响应。
  • 排查与处置:终止 docker cp 操作,采用方案 B。使用 --volumes-from 拉起一个临时容器,在临时容器内部执行 tar -czpf /export/images_backup.tar.gz /data/images/,将数百万个小文件顺序打包为单个压缩文件并直接输出至宿主机挂载目录,大幅缩短了恢复时间。

04 生产排障速查表(离线数据提取)

提取场景适用方案 / 核心命令优势与特点局限性少量文件 / 快速修复配置docker cp <cid>:<源> <目标>操作直接,支持读取可写层与数据卷,无需额外镜像海量小文件场景下效率低,拷贝至宿主机时可能改变属主映射海量小文件 / 严格保留权限docker run --volumes-from <cid> ...支持在容器内先压缩打包再通过挂载卷导出,完整保留文件属性仅对声明为 Volume 的目录生效,且需依赖基础镜像Docker Daemon 服务瘫痪定位宿主机 /var/lib/docker/overlay2 或 volumes完全绕过 Docker 守护进程,直接从宿主机文件系统提取overlay2 目录名为随机 Hash 字符串,需配合元数据或 find 检索

05 面试应答参考话术

面试官问:“线上数据库的 Docker 容器突然崩溃,状态变成了 Exited,而且之前没有配置宿主机目录挂载,里面的数据还能不能恢复出来?怎么操作?”

可以这样回答:只要没有执行 docker rm 删除容器,数据完全可以恢复。因为 Exited 状态仅代表容器内的 1 号进程退出了,容器自身的文件系统可写层以及关联的匿名数据卷依然完整保存在宿主机磁盘上。常用的离线提取方案有两种:第一种是直接使用 docker cp 命令。

该命令不要求容器处于运行状态。可以直接在宿主机创建备份目录,执行 docker cp <容器名>:/var/lib/mysql ./,即可将容器内部的数据直接拷贝到宿主机上。如果只是改错了容器内的配置文件导致起不来,也可以用 docker cp 把配置拷出来改好,再拷回去覆盖,随后重新启动容器。第二种是使用 --volumes-from 继承数据卷。

如果数据量极大、包含大量碎小文件,或者需要严格保留数据库文件原本的 uid/gid 权限属性,直接使用 docker cp 效率较低。 此时可以启动一个轻量的 alpine 临时容器,加上 --volumes-from <故障容器名> 参数直接继承原容器的数据卷,同时用 -v 挂载一个宿主机目录作为导出通道。进入临时容器后,使用 cp -a 保留权限拷贝,或者直接用 tar 命令将数据打包压缩到宿主机挂载目录中。此外,如果遇到极端情况,连宿主机的 Docker Daemon 服务都崩溃无法响应命令,上述两个命令将无法使用。此时可以直接前往宿主机的 /var/lib/docker/volumes/ 或 /var/lib/docker/overlay2/ 目录下,直接在物理磁盘层面检索并拷贝底层数据文件。

06 面试高频追问与应答预案

追问 1:如果有人误操作已经执行了 docker rm user-center-db,数据还能找回吗?

  • 回答要点:
  • 取决于数据的存储位置。如果该数据库镜像(如官方 MySQL 镜像)在 Dockerfile 中通过 VOLUME 指令声明了数据目录,Docker 会在启动时自动为其创建匿名卷(Anonymous Volume)。在不加 -v 参数的情况下,执行 docker rm 默认不会清理匿名卷。此时可通过 docker volume ls 结合 /var/lib/docker/volumes/ 下的文件修改时间,找到遗留的卷目录将数据找回。
  • 如果数据是直接写入在容器自身的可写层(Writable Layer)且未声明为 Volume,执行 docker rm 后对应的 overlay2 层会被立即销毁,常规手段将无法恢复。

追问 2:为什么直接在 /var/lib/docker/overlay2 目录下找文件比较困难?如何快速定位?

  • 回答要点:
  • Docker 采用 OverlayFS 联合文件系统,容器的镜像层与可写层在 /var/lib/docker/overlay2/ 下均以 64 位随机 Hash 字符串命名,无法直接从目录名看出对应哪个容器。
  • 如果 Docker 服务尚可用,可通过 docker inspect -f '{{.GraphDriver.Data.UpperDir}}' <容器名> 直接打印出该容器可写层在宿主机上的真实物理路径;如果 Docker 服务已瘫痪,则只能使用 find /var/lib/docker/overlay2 -name "<特征文件名>" 进行全局文件检索。

07 自动化离线数据提取脚本(docker_rescue_data.sh)

该脚本封装了临时容器挂载与 tar 归档打包逻辑,可将处于 Exited 状态的容器数据卷目录安全提取为宿主机上的压缩包。

#!/usr/bin/env bash
# ==============================================================================
# 脚本名称: docker_rescue_data.sh
# 功能描述: 自动利用数据卷继承机制提取 Exited 容器内部数据 (纯只读提取)
# 使用方式: ./docker_rescue_data.sh
# ==============================================================================

set -o pipefail

TARGET_CONTAINER=$1
TARGET_PATH=$2

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
PLAIN='\033[0m'

# 这里修复了渲染乱码 \vert{}\vert{} 为 ||
if [ -z "$TARGET_CONTAINER" ] || [ -z "$TARGET_PATH" ]; then
    echo -e "${RED}用法: $0  ${PLAIN}"
    exit 1
fi

echo -e "${BLUE}================================================================${PLAIN}"
echo -e "${BLUE}           容器 Exited 离线数据自动提取脚本                     ${PLAIN}"
echo -e "${BLUE}================================================================${PLAIN}"

# 1. 检查容器是否存在
if ! docker ps -a --format '{{.Names}}' | grep -q -x "$TARGET_CONTAINER"; then
    # 尝试按 ID 匹配
    if ! docker ps -a --format '{{.ID}}' | grep -q "^${TARGET_CONTAINER}"; then
         echo -e "${RED}[FATAL] 未找到名为 $TARGET_CONTAINER 的容器,请确认名称或是否已被删除。${PLAIN}"
         exit 1
    fi
fi

CONTAINER_STATE=$(docker inspect --format '{{.State.Status}}' "$TARGET_CONTAINER")
echo -e "${YELLOW}[1/3] 锁定目标容器: ${TARGET_CONTAINER} (当前状态: ${CONTAINER_STATE})${PLAIN}"

# 2. 准备宿主机接收目录
HOST_BACKUP_DIR="$(pwd)/rescue_export_${TARGET_CONTAINER}_$(date +%Y%m%d%H%M)"
mkdir -p "$HOST_BACKUP_DIR"
echo -e "${YELLOW}[2/3] 创建宿主机数据接收目录: ${HOST_BACKUP_DIR}${PLAIN}"

# 3. 启动临时容器执行底层打包拷贝
echo -e "${YELLOW}[3/3] 启动临时 Alpine 容器进行数据卷挂载与归档提取...${PLAIN}"
echo -e "  -> 目标路径: ${TARGET_PATH}"

# 使用 tar 打包避免大量小文件导致 I/O 阻塞,同时保留属主权限 (-p)
docker run --rm \
    --volumes-from "$TARGET_CONTAINER" \
    -v "${HOST_BACKUP_DIR}:/export" \
    alpine \
    sh -c "if [ -e '${TARGET_PATH}' ]; then tar -czpf /export/rescue_data.tar.gz -C \$(dirname '${TARGET_PATH}') \$(basename '${TARGET_PATH}'); echo 'SUCCESS'; else echo 'NOT_FOUND'; fi" > /tmp/rescue_cmd.out 2>&1

RESULT=$(tail -n 1 /tmp/rescue_cmd.out)

echo -e "${BLUE}----------------------------------------------------------------${PLAIN}"
if [ "$RESULT" == "SUCCESS" ]; then
    echo -e "${GREEN}[OK] 数据提取成功。${PLAIN}"
    echo -e "数据已打包保存至: ${GREEN}${HOST_BACKUP_DIR}/rescue_data.tar.gz${PLAIN}"
    ls -lh "${HOST_BACKUP_DIR}/rescue_data.tar.gz"
else
    echo -e "${RED}[ERROR] 数据卷继承提取未找到目标路径 ${TARGET_PATH}(该路径可能未声明为 Volume,而处于容器可写层)。${PLAIN}"
    echo -e "${YELLOW}[INFO] 正在自动降级使用 docker cp 从容器可写层提取...${PLAIN}"
    if docker cp "${TARGET_CONTAINER}:${TARGET_PATH}" "${HOST_BACKUP_DIR}/"; then
        echo -e "${GREEN}[OK] docker cp 降级提取成功,数据已保存至: ${HOST_BACKUP_DIR}${PLAIN}"
        ls -lh "${HOST_BACKUP_DIR}"
    else
        echo -e "${RED}[FATAL] 提取失败,请确认容器内是否存在路径 ${TARGET_PATH}。${PLAIN}"
        rm -rf "$HOST_BACKUP_DIR"
    fi
fi

echo -e "${BLUE}================================================================${PLAIN}"