分类 Docker 相关 下的文章

导出 docker container 运行时快照并从快照运行

之前讲 docker container 生命周期的时候, 看到 pause container 命令, 突然想到是否能够对一个正在运行的container 做个快照, 把全部文件和内存运行数据都dump出来, 之后根据快照随时继续运行呢?

于是 Google 了一把, 发现也有不少人有同样的疑问, 很多人的回答都提到了 docker commit, 不过这不是我想要的. docker commit 只是把改动的文件内容覆盖到原来的 image, 重新做了一个 image, 然而丢失了正在运行的进程的全部数据, 比如内存和寄存器里面的数据. 而我期望的是能保存这些进程的运行时信息, 等继续运行的时候, 能在进程应该继续执行的下一个指令的地方继续运行.

那么到底有没有神奇的方法, 能做到期望的行为呢? 其实 Docker 已经考虑了这个需求, 做为实验命令, 只是还没有正式发布, 那就是通过docker checkpoint 子命令. 官方链接.

它的主要过程就是: 当一个container 正在运行时, 通过 docker checkpoint create dump一个包含container文件系统和运行时内存数据的快照, 并把它保存到磁盘. 之后通过docker start --checkpoint checkpoint_id container_name 来继续运行. 由于我们保存快照到文件系统, 所以可以复制很多份, 之后可以随时启动它, 随时查看当时的状态.

下面是官方的一个例子:

  1. 安装 CRIU 依赖, Docker 其实是依赖这个包才能完成container冻结和恢复的.

    supra@suprabox:~$ sudo apt install criu -y
  2. 首先启动这个每隔一秒钟打印一个数字的container, 名字是cr

    supra@suprabox:~$ docker run --security-opt=seccomp:unconfined --name cr -d busybox /bin/sh -c 'i=0; while true; do echo $i; i=$(expr $i + 1); sleep 1; done'
    0e09ac6f369f965af18e429ec78fcab8875610fdfd69313d6c7ecb0b86e61d7
  3. 查看打印的数据

    supra@suprabox:~$ docker logs -f cr
    0
    1
    2
  4. 生成快照 --leave-running 表示快照生成完继续运行 --checkpoint-dir表示要保存的文件夹

    supra@suprabox:~$ docker checkpoint create cr checkpoint1 --leave-running --checkpoint-dir /tmp/
    checkpoint1
  5. 查看之前的container 还在运行, 查看我们保存快照的文件夹, 发现里面各种以 .img 结尾的文件

    supra@suprabox:~$ docker ps -l
    CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS          PORTS     NAMES
    ecb0b86e61d7   busybox   "/bin/sh -c 'i=0; wh…"   10 minutes ago   Up 10 minutes             cr
    
    supra@suprabox:~$ sudo ls -alh /tmp/checkpoint1/
    total 224K
    drwx------  2 root root 4.0K Jan 28 18:40 .
    drwxrwxrwt 22 root root  12K Jan 28 18:42 ..
    -rw-r--r--  1 root root  356 Jan 28 18:40 cgroup.img
    -rw-r--r--  1 root root 1.8K Jan 28 18:40 core-1.img
    -rw-r--r--  1 root root 1.8K Jan 28 18:40 core-1034.img
    -rw-------  1 root root   45 Jan 28 18:40 descriptors.json
    -rw-r--r--  1 root root   44 Jan 28 18:40 fdinfo-2.img
    -rw-r--r--  1 root root   44 Jan 28 18:40 fdinfo-3.img
  6. 恢复运行 - 各种失败 - 没有达到预期
    去掉各种参数, 按照官方方法, 在 Ubuntu 22.04 上照样不能成功. 失败

    supra@suprabox:~$ docker start --checkpoint  checkpoint1 cr
    Error response from daemon: OCI runtime restore failed: criu failed: type NOTIFY errno 0
    log file: 
     
     /run/containerd/io.containerd.runtime.v2.task/moby/63263d8b31317dbe1f6cf19eac2198f3e078a12f9d872e4e134eae1a4f465d49/work/restore.log: unknown
    
    supra@suprabox:~$ sudo tail -f 
     
     /run/containerd/io.containerd.runtime.v2.task/moby/63263d8b31317dbe1f6cf19eac2198f3e078a12f9d872e4e134eae1a4f465d49/work/restore.log
    (00.274155) pie: 52:     mmap(0x7fffb69f7000 -> 0x7fffb69fb000, 0x3 0x32 -1)
    (00.274162) pie: 52:     mmap(0x7fffb69fb000 -> 0x7fffb69fd000, 0x7 0x32 -1)
    (00.274170) pie: 52: Preadv 0x1612000:8192... (3 iovs)
    (00.274193) pie: 52: `- returned 20480
    (00.274198) pie: 52:    `- skip pagemap
    (00.274203) pie: 52:    `- skip pagemap
    (00.274208) pie: 52:    `- skip pagemap
    (00.488504)      1: Error (criu/cr-restore.c:1480): 52 killed by signal 11: Segmentation fault
    (00.488867) mnt: Switching to new ns to clean ghosts
    (00.489104) Error (criu/cr-restore.c:2447): Restoring FAILED.

官方 CRIU 最新版本是 3.17.1, 我本地版本是 3.16.1-2 不知道是不是有问题.

upra@suprabox:~$ apt show criu
Package: criu
Version: 3.16.1-2

但是这篇 blog 在2015年当时已经成功了: https://kubernetes.io/blog/2015/07/how-did-quake-demo-from-dockercon-work/

不论如何, 想要做快照, 并且从快照恢复一个进程, 已经有很多研究了, 更多看这里: https://criu.org/

docker container 的生命周期

当我们创建一个 container 的时候, 它的生命过程中都会经历哪几个阶段呢? 有时候明明一个 container 已经死了, 我们去创建一个同名 container 的时候, 它竟然说有一个同名的 container 存在? 明白了 container 的生命周期, 就能理解了.

container 的生命阶段

一个 docker container 从创建到彻底消亡, 可能会经过的阶段:
docker_container_lifecycle.png

  1. created: container 被创建, 但是没有被启动
  2. running: container 被启动, 里面的进程在运行
  3. paused: container 里的所有进程被挂起, 暂停运行
  4. stopped: container 里面的进程被杀死, container 还存在
  5. deleted: container 被彻底删除

下面我们就以 nginx 做container 为例来说明这个过程.

create 创建 container

下面创建一个 nginx container, 并使用 ps -l (--latest) 来查看, 状态是 Created.

supra@suprabox:~$ docker create --name nginxServer nginx
ac301e1f4f3f3ba5631ff5d7f3276d1544610229a9e3727dac6d5f66181bfe05

supra@suprabox:~$ docker ps -l
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS    PORTS     NAMES
ac301e1f4f3f   nginx     "/docker-entrypoint.…"   1 minutes ago   Created             nginxServer

start 启动 container

通过start子命令启动 container, 然后通过ps子命令查看状态. 通过exec子命令写一句话到/text.txt文件, 并且查看写入内容.

supra@suprabox:~$ docker start nginxServer
nginxServer

supra@suprabox:~$ docker ps -l
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS         PORTS     NAMES
ac301e1f4f3f   nginx     "/docker-entrypoint.…"   25 minutes ago   Up 5 seconds   80/tcp    nginxServer

supra@suprabox:~$ docker exec nginxServer bash -c 'echo "this is a test" > /test.txt'
supra@suprabox:~$ docker exec nginxServer cat  /test.txt
this is a test

pause 暂停 container

可以看到经过 pause 之后, 状态变为 Paused. 如果尝试进入这个 container exec, 这个时候会报错.

supra@suprabox:~$ docker pause nginxServer
nginxServer

supra@suprabox:~$ docker ps -l
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS                  PORTS     NAMES
ac301e1f4f3f   nginx     "/docker-entrypoint.…"   29 minutes ago   Up 4 minutes (Paused)   80/tcp    nginxServer

supra@suprabox:~$ docker exec -it nginxServer bash
Error response from daemon: Container nginxServer is paused, unpause the container before exec

unpause 继续 container

supra@suprabox:~$ docker unpause nginxServer
nginxServer

supra@suprabox:~$ docker ps -l
CONTAINER ID   IMAGE     COMMAND                  CREATED             STATUS          PORTS     NAMES
ac301e1f4f3f   nginx     "/docker-entrypoint.…"   About an hour ago   Up 44 minutes   80/tcp    nginxServer

stop 停止 container

stop 之后, 通过docker ps -l 可以看到状态变为 Exited, 这个时候通过docker logs 仍旧能看到之前的log, 并且可以看到主进程被杀死的日志. 但是通过docker exec 已经无法进入 container.

supra@suprabox:~$ docker stop nginxServer
nginxServer

supra@suprabox:~$ docker ps -l
CONTAINER ID   IMAGE     COMMAND                  CREATED             STATUS                     PORTS     NAMES
ac301e1f4f3f   nginx     "/docker-entrypoint.…"   About an hour ago   Exited (0) 8 seconds ago             nginxServer

supra@suprabox:~$ docker logs -f nginxServer
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
...
2023/01/27 13:35:52 [notice] 1#1: signal 17 (SIGCHLD) received from 32
2023/01/27 13:35:52 [notice] 1#1: worker process 32 exited with code 0
2023/01/27 13:35:52 [notice] 1#1: worker process 37 exited with code 0
2023/01/27 13:35:52 [notice] 1#1: exit

supra@suprabox:~$ docker exec -it nginxServer bash
Error response from daemon: Container ac301e1f4f3f3ba5631ff5d7f3276d1544610229a9e3727dac6d5f66181bfe05 is not running

start 重新启动 stop 的 container

通过 start 子命令, 可以重新启动 stop 的container, 通过docker ps -l 可以看到它的状态是刚起来4秒. 这时候去查看之前写入的 /test.txt 文件, 内容还在. 但是这时候里面的进程都是新启动的, 不说 stop 之前的那个(批)进程了.

supra@suprabox:~$ docker start nginxServer
nginxServer

supra@suprabox:~$ docker ps -l
CONTAINER ID   IMAGE     COMMAND                  CREATED             STATUS         PORTS     NAMES
ac301e1f4f3f   nginx     "/docker-entrypoint.…"   About an hour ago   Up 4 seconds   80/tcp    nginxServer

supra@suprabox:~$ docker exec nginxServer cat  /test.txt
this is a test

rm 彻底删除container

可以通过rm子命令删除任何状态的container. 如果一个container 是 stopped 或 created 状态的, 可以直接rm, 否则就必须通过 -f 强制删除.

docker rm nginxServer
docker rm -f nginxServer

run 创建同时启动container

可以通过 run 子命令, 在创建container的同时, 启动container, 相当于同时执行了createstart.

supra@suprabox:~$ docker run --name nginxServer1 --rm  nginx
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/

kill 杀掉一个运行的container

通过kill 杀掉一个运行的container 之后, container 进入 stopped 的状态.

supra@suprabox:~$ docker kill nginxServer
nginxServer

supra@suprabox:~$ docker ps -l
CONTAINER ID   IMAGE     COMMAND                  CREATED       STATUS                        PORTS     NAMES
ac301e1f4f3f   nginx     "/docker-entrypoint.…"   2 hours ago   Exited (137) 18 seconds ago             nginxServer

如何做一个最小的docker image?

当我们想做一个docker image 的时候, 经常想把它做的足够小, 不仅能够节省磁盘空间, 还能减少不必要的依赖, 加快启动, 减少可能出现的漏洞. 那么有哪些最精简的base image 呢?


  1. 如果你的app的所有依赖都是静态编译的, 那么你可以使用 scratch, 顾名思义, 它是一张白纸, 我们只能使用kernel 提供的服务. 它没有shell, 没有libc, 没有各种实用命令, 用户/组, 没有包管理器, 啥都没有.
  2. 如果你 busybox 提供的实用工具已经能满足, 那么你可以使用 busybox. busybox 是嵌入式linux上的瑞士军刀, 它把其它Linux 上常见的一些实用工具在一个很小的可执行文件内全部实现, 虽然选项更少, 但是基本能完成大部分常见功能.
  3. 如果你在 busybox的基础上还需要 libc库和包管理工具(package repo), 那么可以使用 alpine. alpine 在busybox的基础上, 增加了 musl libc 和 包管理器.
  4. 或者你可以尝试一下 https://github.com/GoogleContainerTools/distroless.

exec user process caused: exec format error

今天开发的app 做了一个新的docker image, 发布到K8S之后, 就报下面的错:

standard_init_linux.go:228: exec user process caused: exec format error

还以为自己写的代码配置出错了, 想登上去看看, 发现进程压根就没起来.

Google 一把, 有人说是entrypoint的shell 文件的 shebang 不对, 我这里没有这个问题.

后来看到有人说是可执行格式的错误, 发现还真是.
首先, 我在本地Mac Pro (ARM) 上去执行这个image, 很正常的执行起来了, 然后我在另外一个Ubuntu 上面执行这个image, 就给我报下面的错误了:

WARNING: The requested image's platform (linux/arm64/v8) does not match the detected host platform (linux/amd64) and no specific platform was requested

所以, 很明显, 这个image 是给 ARM64/v8 用的. 用 docker image inspect <img>去查看, 能看到这个image 是给什么平台机构的.
arch.png

我的 docker build 环境是 MAC ARM 机器, 之前一直没有错, 今天为啥出错了, 原因很有可能是我今天重启了 Docker Desktop.

去看 docker 官方文档: https://docs.docker.com/engine/reference/commandline/build/ 发现他们最近(20220914) 对于 docker build 新加了一个参数 (--platform)
platform.png

到它 change 去看, 发现最新的 v141
https://docs.docker.com/engine/api/version-history/#v140-api-changes

所以, 要在 Mac Pro ARM 芯片上做 x86_64/amd64 的image, 要给一个新参数 --platform linux/amd64