无需 Daemon 进程的 Docker 代替品:Podman

3,614 阅读9分钟

《无需 Daemon 进程的 Docker 代替品:Podman》 最早发布在 blog.ihypo.net/15697268296…

什么是容器

Linux 容器技术

Linux 容器是由 Linux Kernel 提供的具有特定隔离的进程。Linux 容器技术能够让用户对应用及其整个运行时环境(包括全部所需文件)一起进行打包或隔离。从而让用户在不同环境,之间轻松迁移应用的同时,并保留应用的全部功能。

Docker 的问题

一提到容器技术,肯定无法绕开 Docker,Docker 是一个著名的开源容器引擎,在容器技术已经在逐步普及的现在,Docker 几乎也成了容器的代名词。

Docker 本身也是 Linux 容器技术的一种封装,通过并向用户提供简易的接口,使用户非常方便的打包和使用容器。

作为目前主流的容器引擎,Docker 有着丰富的使用场景和解决方案,但也有一些问题。

  1. Docker 需要运行一个守护进程,所有容器都是守护进程的子进程
  2. Docker 需要 root 身份运行守护进程

看起来这仿佛没有什么问题,但是如果你尝试大规模使用 Docker 你会发现:

  1. 守护进程并没有想象中的稳定
  2. 一个容器的 OOM 很可能会拖累到父进程从而影响邻居容器
  3. Docker 进程树会有些奇奇怪怪的现象,你无法确定是 Docker 的 bug 还是 Kernel 做了什么
  4. Docker 容器是 root 启动的进程

如果换个方向,守护进程真的有必要吗?

什么是Podman

Podman 曾是 CRI-O project 中的一部分,后来被分离出成为一个独立的项目:libpod( github.com/containers/… )。Podman 的目标是提供一个和 Docker 相似的 Container CLI(甚至官方直接建议使用:alias docker=podman)。

安装

安装 Podman 非常简单,安装文档:github.com/containers/…

MacOS

Using Homebrew:

brew cask install podman

Fedora, CentOS

sudo yum -y install podman

Ubuntu(development versions)

sudo apt-get update -qq
sudo apt-get install -qq -y software-properties-common uidmap
sudo add-apt-repository -y ppa:projectatomic/ppa
sudo apt-get update -qq
sudo apt-get -qq -y install podman

使用

下文实验基于 Podman V1.4.4 进行。

# podman version
Version:            1.4.4
RemoteAPI Version:  1
Go Version:         go1.10.3
OS/Arch:            linux/amd64

拉取镜像

Podman 会默认先拉取 registry.access.redhat.com 的镜像,因为众所周知的原因,国内是无法正常拉取的,但拉取失败之后 Podman 会再尝试 docker.io 的镜像:

# podman pull nginx
Trying to pull registry.access.redhat.com/nginx...ERRO[0001] Error pulling image ref //registry.access.redhat.com/nginx:latest: Error initializing source docker://registry.access.redhat.com/nginx:latest: Error reading manifest latest in registry.access.redhat.com/nginx: name unknown: Repo not found
Failed
Trying to pull docker.io/library/nginx...Getting image source signatures
Copying blob 7acba7289aa3 done
Copying blob b8f262c62ec6 done
Copying blob e9218e8f93b1 done
Copying config f949e7d76d done
Writing manifest to image destination
Storing signatures
f949e7d76d63befffc8eec2cbf8a6f509780f96fb3bacbdc24068d594a77f043

Podman 的数据路径在 /var/lib/containers 下,和 Docker 类似,保存了 layer 等数据。

[root@podman-test-vm lib]# tree /var/lib/containers/ -L 2
/var/lib/containers/
├── cache
│   └── blob-info-cache-v1.boltdb
└── storage
    ├── libpod
    ├── mounts
    ├── overlay
    ├── overlay-containers
    ├── overlay-images
    ├── overlay-layers
    ├── storage.lock
    └── tmp

可以看到刚才拉取的镜像信息:

[root@podman-test-vm lib]# cat /var/lib/containers/storage/overlay-images/images.json | python -m json.tool
[
    {
        "big-data-digests": {
            "manifest": "sha256:066edc156bcada86155fd80ae03667cf3811c499df73815a2b76e43755ebbc76",
            "manifest-sha256:066edc156bcada86155fd80ae03667cf3811c499df73815a2b76e43755ebbc76": "sha256:066edc156bcada86155fd80ae03667cf3811c499df73815a2b76e43755ebbc76",
            "sha256:f949e7d76d63befffc8eec2cbf8a6f509780f96fb3bacbdc24068d594a77f043": "sha256:f949e7d76d63befffc8eec2cbf8a6f509780f96fb3bacbdc24068d594a77f043"
        },
        "big-data-names": [
            "sha256:f949e7d76d63befffc8eec2cbf8a6f509780f96fb3bacbdc24068d594a77f043",
            "manifest-sha256:066edc156bcada86155fd80ae03667cf3811c499df73815a2b76e43755ebbc76",
            "manifest"
        ],
        "big-data-sizes": {
            "manifest": 948,
            "manifest-sha256:066edc156bcada86155fd80ae03667cf3811c499df73815a2b76e43755ebbc76": 948,
            "sha256:f949e7d76d63befffc8eec2cbf8a6f509780f96fb3bacbdc24068d594a77f043": 6669
        },
        "created": "2019-09-24T23:33:17.034191345Z",
        "digest": "sha256:066edc156bcada86155fd80ae03667cf3811c499df73815a2b76e43755ebbc76",
        "id": "f949e7d76d63befffc8eec2cbf8a6f509780f96fb3bacbdc24068d594a77f043",
        "layer": "ea345052c98934e4e4673b2d359b5000a9ff1cc7f0332df0d406980f172deea6",
        "metadata": "{}",
        "names": [
            "docker.io/library/nginx:latest"
        ]
    }
]

启动容器

Podman 绝大多数命令和 Docker 兼容,因此可以使用类似的方式启动容器:

[root@podman-test-vm ~]# podman run -p 80:80 --name=web -d nginx
9d284597eeedbbdfb4df933e063fe1035cbd39f1e712173f7a8a3652773eac02

[root@podman-test-vm ~]# podman ps
CONTAINER ID  IMAGE                           COMMAND               CREATED         STATUS             PORTS               NAMES
9d284597eeed  docker.io/library/nginx:latest  nginx -g daemon o...  48 seconds ago  Up 48 seconds ago  0.0.0.0:80->80/tcp  web

尝试检查 nginx 的进程:

[root@podman-test-vm ~]# ps -ef | grep nginx
root      2518  2508  0 12:01 ?        00:00:00 nginx: master process nginx -g daemon off;
101       2529  2518  0 12:01 ?        00:00:00 nginx: worker process
root      2637  1259  0 12:10 pts/0    00:00:00 grep --color=auto nginx

然后根据 pid 查看进程树:

[root@podman-test-vm ~]# pstree -H 2518
systemd─┬─NetworkManager─┬─2*[dhclient]
        │                └─2*[{NetworkManager}]
        ├─anacron
        ├─auditd───{auditd}
        ├─conmon─┬─nginx───nginx
        │        └─{conmon}
        ├─crond
        ├─dbus-daemon───{dbus-daemon}
        ├─firewalld───{firewalld}
        ├─login───bash
        ├─lvmetad
        ├─master─┬─pickup
        │        └─qmgr
        ├─polkitd───5*[{polkitd}]
        ├─rsyslogd───2*[{rsyslogd}]
        ├─sshd───sshd───bash───pstree
        ├─systemd-journal
        ├─systemd-logind
        ├─systemd-udevd
        └─tuned───4*[{tuned}]

根据 ppid 查看父进程:

[root@podman-test-vm ~]# ps -ef | grep 2508
root      2508     1  0 12:01 ?        00:00:00 /usr/libexec/podman/conmon -s -c 9d284597eeedbbdfb4df933e063fe1035cbd39f1e712173f7a8a3652773eac02 -u 9d284597eeedbbdfb4df933e063fe1035cbd39f1e712173f7a8a3652773eac02 -n web -r /usr/bin/runc -b /var/lib/containers/storage/overlay-containers/9d284597eeedbbdfb4df933e063fe1035cbd39f1e712173f7a8a3652773eac02/userdata -p /var/run/containers/storage/overlay-containers/9d284597eeedbbdfb4df933e063fe1035cbd39f1e712173f7a8a3652773eac02/userdata/pidfile --exit-dir /var/run/libpod/exits --exit-command /usr/bin/podman --exit-command-arg --root --exit-command-arg /var/lib/containers/storage --exit-command-arg --runroot --exit-command-arg /var/run/containers/storage --exit-command-arg --log-level --exit-command-arg error --exit-command-arg --cgroup-manager --exit-command-arg systemd --exit-command-arg --tmpdir --exit-command-arg /var/run/libpod --exit-command-arg --runtime --exit-command-arg runc --exit-command-arg --storage-driver --exit-command-arg overlay --exit-command-arg container --exit-command-arg cleanup --exit-command-arg 9d284597eeedbbdfb4df933e063fe1035cbd39f1e712173f7a8a3652773eac02 --socket-dir-path /var/run/libpod/socket -l k8s-file:/var/lib/containers/storage/overlay-containers/9d284597eeedbbdfb4df933e063fe1035cbd39f1e712173f7a8a3652773eac02/userdata/ctr.log --log-level error
root      2518  2508  0 12:01 ?        00:00:00 nginx: master process nginx -g daemon off;
root      2639  1259  0 12:10 pts/0    00:00:00 grep --color=auto 2508

可以看到,podman 通过 podman/conmon 启动了容器,而且这个进程被挂在到 pid 1 也就是 systemd 下面。

podman/conmon 是 Podman 的启动器,主要负责两个功能,一是监控 runc,利用 runc 的能力管理容器,而是和 Podman 建立通讯,并传递对容器操作的指令。

Podman does not communicate with using the CRI protocol. Instead, Podman creates containers using runc, and manages storage using containers/storage. Technically, Podman launches conmon which launches and monitors the OCI Runtime (runc). Podman can exit and later reconnect to conmon to talk to the container. Runc stops running once the container starts.

《Crictl Vs Podman》:blog.openshift.com/crictl-vs-p…

构建镜像

Podman 可以直接使用 Dockerfile 进行构建:

[root@podman-test-vm ~]# git clone https://github.com/DaoCloud/dao-2048.git
正克隆到 'dao-2048'...
remote: Enumerating objects: 116, done.
remote: Total 116 (delta 0), reused 0 (delta 0), pack-reused 116
接收对象中: 100% (116/116), 304.76 KiB | 60.00 KiB/s, done.
处理 delta 中: 100% (40/40), done.
[root@podman-test-vm ~]# podman build dao-2048/
STEP 1: FROM daocloud.io/nginx:1.11-alpine
Getting image source signatures
Copying blob ed383a1b82df done
Copying blob c92260fe6357 done
Copying blob 4b21d71b440a done
Copying blob 709515475419 done
Copying config bedece1f06 done
Writing manifest to image destination
Storing signatures
STEP 2: MAINTAINER Golfen Guo <golfen.guo@daocloud.io>
711d32f788782528ad36a0c12ae895993474b168f7f2d65158e531a924b3dd55
STEP 3: COPY . /usr/share/nginx/html
b859763deeb7eaab39fd8c34a8c8af18e8de74c80e98f9fcb1e0c694881c5e9c
STEP 4: EXPOSE 80
eedb1a66c8316a309546b21a5c687f3e3611927b54a7fa0f4dbd3f175eeb253c
STEP 5: CMD sed -i "s/ContainerID: /ContainerID: "$(hostname)"/g" /usr/share/nginx/html/index.html && nginx -g "daemon off;"
STEP 6: COMMIT
fda5a2d14a918c3eb088c28dc0d8e89e66b061923a82e8722d9e2a62c994422d

[root@podman-test-vm ~]# podman images
REPOSITORY                TAG           IMAGE ID       CREATED          SIZE
<none>                    <none>        fda5a2d14a91   22 seconds ago   56.9 MB
docker.io/library/nginx   latest        f949e7d76d63   4 days ago       130 MB
daocloud.io/nginx         1.11-alpine   bedece1f06cc   2 years ago      55.9 MB

Dockerfile:

[root@podman-test-vm ~]# cat dao-2048/Dockerfile
# Using a compact OS
FROM daocloud.io/nginx:1.11-alpine

MAINTAINER Golfen Guo <golfen.guo@daocloud.io>

# Add 2048 stuff into Nginx server
COPY . /usr/share/nginx/html

EXPOSE 80

# Start Nginx and keep it running background and start php
CMD sed -i "s/ContainerID: /ContainerID: "$(hostname)"/g" /usr/share/nginx/html/index.html && nginx -g "daemon off;"

问题

Podman 致力于去掉守护进程,这也就意味着需要守护进程完成的任务 Podman 无法做到。

Restart 问题

在 Docker 中,可以通过 --restart 命令指定重启策略,当 node 重启,只要 dockerd 还能起来,有重启策略的容器就会自恢复。

因为 Podman 是将容器的管理托付给了 systemd,因此官方给的建议也是通过 systemd 来解决( podman.io/blogs/2018/… ),可以为需要自启动的容器编写 systemd service 文件,来描述启动方式了重启策略。

$ vim /etc/systemd/system/nginx_container.service 
 
[Unit] 
Description=Podman Nginx Service 
After=network.target 
After=network-online.target 
 
[Service] 
Type=simple 
ExecStart=/usr/bin/podman start -a nginx 
ExecStop=/usr/bin/podman stop -t 10 nginx 
Restart=always 
 
[Install] 
WantedBy=multi-user.target 

虽然有些麻烦(而且感觉有些逆潮流),不过仔细想一想,这不是就是 Linux 比较推荐的服务管理方式么。其次,Docker 虽然支持容器自启,但并不支持按照依赖关系依次启动,但是利用 systemd 的能力,可以通过 After 制定启动依赖,反而可以更好的管理启动顺序。