无需 Daemon 进程的容器工具:Podman

什么是容器

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( https://github.com/containers/libpod )。Podman 的目标是提供一个和 Docker 相似的 Container CLI(甚至官方直接建议使用:alias docker=podman)。

安装

安装 Podman 非常简单,安装文档:https://github.com/containers/libpod/blob/master/install.md

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》:https://blog.openshift.com/crictl-vs-podman/

构建镜像

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 来解决( https://podman.io/blogs/2018/09/13/systemd.html ),可以为需要自启动的容器编写 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 制定启动依赖,反而可以更好的管理启动顺序。

2019/09/29 11:13 上午 posted in  Docker

容器的健康状态检查

docker 1.12新加了不少命令,而健康检查相关的命令,让容器的健康检查变得十分的简单。

可以通过 docker run --help 找到和健康检查相关的命令以及介绍:

--health-cmd string           Command to run to check health
--health-interval duration    Time between running the check
--health-retries int          Consecutive failures needed to report unhealthy
--health-timeout duration     Maximum time to allow one check to run

因此,在docker run的时候,可以通过添加 health-cmd 来明确健康检查的命令;可以通过 health-interval 命令来确定两次检查的间隔时间;health-retries 可以设定一个上限,当检查失败次数超过上限之后便会报告费健康状态;health-timeout 可以设定一次健康检查的时间上限,如果超过这个时间,便认为是检查失败的。

使用这些命令也十分简单,比如:

Hypo-MBP:~ hypo$ docker run -d --health-cmd="curl stat /etc/passwd || exit 1" --health-interval=5s --health-retries=3  --health-timeout=5s busybox sleep 10000
05e80cb5daba9521dc6b3745f1618c2ac695e0ba565957134dd0b9016f06125b
Hypo-MBP:~ hypo$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS                            PORTS                    NAMES
05e80cb5daba        busybox             "sleep 10000"            3 seconds ago        Up 2 seconds (health: starting)                            kickass_wilson

如果容器在健康状态,在docker ps的时候就会发现被注明健康。如果不在健康状态,便会显示:

2e533723454c        busybox             "sleep 10000"            About a minute ago   Up About a minute (unhealthy)                              desperate_agnesi

不仅可以通过docker cli来设定健康检查的方式,可以把健康检查的方式写到dockerfile里。还是上文的例子,写成dockerfile如下:

HEALTHCHECK --interval=5s --timeout=ss --retries 3 CMD curl stat /etc/passwd || exit 1

通过包含 HEALTHCHECK 的dockerfile构建出来的镜像,在实例化容器的时候,就具备了健康状态检查的功能。

2016/11/19 12:28 下午 posted in  Docker

容器的运行状态获取

获得运行状态的两种方式

获得容器的运行状态主要有两种方法,一种是通过docker cli提供的stats命令查看容器的状态。另一种就是通过docker api

docker stats

通过docker stats可以获得所有的容器的状态:

docker api

默认情况下,Docker daemon监听unix://var/run/docker.sock,并且客户端必须有root权限用来与daemon交互。

为了使用Docker REST API,可以修改docker配置,添加-H标记开启远程访问:

DOCKER_OPTS="$DOCKER_OPTS -H=0.0.0.0:4232"

此处开启远程访问只是方便测试,但会因为没有添加权限验证留下安全隐患

然后就可以用过docker api与docker daemon交互:

root@ubuntu:~# curl localhost:4232/version | python -mjson.tool
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   199  100   199    0     0  19638      0 --:--:-- --:--:-- --:--:-- 19900
{
    "ApiVersion": "1.23",
    "Arch": "amd64",
    "BuildTime": "2016-06-01T21:47:50.269346868+00:00",
    "GitCommit": "b9f10c9",
    "GoVersion": "go1.5.4",
    "KernelVersion": "3.16.0-30-generic",
    "Os": "linux",
    "Version": "1.11.2"
}

而通过remote api获得容器状态比较简单,只需要通过 /containers/<id or name>/stats 便可以获得容器状态。而且这个api会每秒一次的返回最新状态。

当然,如果只是想获得一次状态也是很简单的,这个api支持一个stream参数,支持 True/true/1False/false/0 两种只,前者会让这个api每秒返回一次状态,后者只会让这个api返回一次。默认是前者。

root@ubuntu:~# curl localhost:4232/containers/d81984a3c60d/stats?stream=0 | python -mjson.tool
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1592  100  1592    0     0   1018      0  0:00:01  0:00:01 --:--:--  1018
{
    "blkio_stats": {
        "io_merged_recursive": [],
        "io_queue_recursive": [],
        "io_service_bytes_recursive": [],
        "io_service_time_recursive": [],
        "io_serviced_recursive": [],
        "io_time_recursive": [],
        "io_wait_time_recursive": [],
        "sectors_recursive": []
    },
    "cpu_stats": {
        "cpu_usage": {
            "percpu_usage": [
                348564780
            ],
            "total_usage": 348564780,
            "usage_in_kernelmode": 10000000,
            "usage_in_usermode": 0
        },
        "system_cpu_usage": 3000540000000,
        "throttling_data": {
            "periods": 0,
            "throttled_periods": 0,
            "throttled_time": 0
        }
    },
    "memory_stats": {
        "failcnt": 0,
        "limit": 1041981440,
        "max_usage": 6668288,
        "stats": {
            "active_anon": 6610944,
            "active_file": 4096,
            "cache": 32768,
            "hierarchical_memory_limit": 18446744073709551615,
            "inactive_anon": 12288,
            "inactive_file": 0,
            "mapped_file": 0,
            "pgfault": 1133,
            "pgmajfault": 0,
            "pgpgin": 669,
            "pgpgout": 584,
            "rss": 6594560,
            "rss_huge": 6291456,
            "total_active_anon": 6610944,
            "total_active_file": 4096,
            "total_cache": 32768,
            "total_inactive_anon": 12288,
            "total_inactive_file": 0,
            "total_mapped_file": 0,
            "total_pgfault": 1133,
            "total_pgmajfault": 0,
            "total_pgpgin": 669,
            "total_pgpgout": 584,
            "total_rss": 6594560,
            "total_rss_huge": 6291456,
            "total_unevictable": 0,
            "total_writeback": 0,
            "unevictable": 0,
            "writeback": 0
        },
        "usage": 6627328
    },
    "networks": {
        "eth0": {
            "rx_bytes": 2592,
            "rx_dropped": 0,
            "rx_errors": 0,
            "rx_packets": 32,
            "tx_bytes": 648,
            "tx_dropped": 0,
            "tx_errors": 0,
            "tx_packets": 8
        }
    },
    "pids_stats": {},
    "precpu_stats": {
        "cpu_usage": {
            "percpu_usage": [
                347925623
            ],
            "total_usage": 347925623,
            "usage_in_kernelmode": 10000000,
            "usage_in_usermode": 0
        },
        "system_cpu_usage": 2999540000000,
        "throttling_data": {
            "periods": 0,
            "throttled_periods": 0,
            "throttled_time": 0
        }
    },
    "read": "2016-10-08T00:05:40+08:00"
}
2016/10/26 12:28 下午 posted in  Docker

容器端口映射到主机端口探究

容器的网络

在说端口之前,先明确下docker 容器的网络,可以用过docker network命令常看docker的网络:

# docker network ls
NETWORK ID          NAME                DRIVER
33b01b58a9a2        bridge              bridge
ffe4299ed1a9        host                host
fc93a0e85b75        none                null

可以看到,docker中包含bridge,host,none三种网络,其实除了这三种,还有container第四种网络。

其中,bridge是默认的网络,也就是在不指定网络模式的时候docker run使用的的网络模式,而none,正如其名,是不分配任何网络。host模式和container模式不是那么常用,前者是使用宿主机的网络,而后者是和指定的容器共享网络。

而这所讨论的容器端口与主机端口映射的原理,就是docker默认的bridge网络模式。

bridge网络

docker run一个容器,可以使用-p参数指定一个开放端口,甚至可以把这个端口映射的主机的某一端口上。比如,可以通过以下命令将nginx容器的80端口映射到主机的8080端口上:

# docker run -d -p 8080:80 --name nginx daocloud.io/library/nginx
81ea762a6bb99c1ee8c72d0fcc91f5151f8f65cf7c26747dd1f64d260845e21c
# docker ps
CONTAINER ID        IMAGE                       COMMAND                  CREATED             STATUS              PORTS                           NAMES
81ea762a6bb9        daocloud.io/library/nginx   "nginx -g 'daemon off"   13 seconds ago      Up 12 seconds       443/tcp, 0.0.0.0:8080->80/tcp   nginx

可以尝试访问宿主机的8080端口,可以看到是一个运行的ngxin。

使用docker inspect命令可以看到nginx容器网络相关的配置:

"NetworkSettings": {
    "Bridge": "",
    "SandboxID": "0af9e9bf575206c247c0c0e1915dfec533dc173ae55917662e8c08df19441fe3",
    "HairpinMode": false,
    "LinkLocalIPv6Address": "",
    "LinkLocalIPv6PrefixLen": 0,
    "Ports": {
        "443/tcp": null,
        "80/tcp": [
            {
                "HostIp": "0.0.0.0",
                "HostPort": "8080"
            }
        ]
    },
    "SandboxKey": "/var/run/docker/netns/0af9e9bf5752",
    "SecondaryIPAddresses": null,
    "SecondaryIPv6Addresses": null,
    "EndpointID": "16ab10c9a32ea277f3f78b589ceee0f8698feafdfe6061c9ca3ac9f9633d1af8",
    "Gateway": "172.17.0.1",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "IPAddress": "172.17.0.2",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "MacAddress": "02:42:ac:11:00:02",
    "Networks": {
        "bridge": {
            "IPAMConfig": null,
            "Links": null,
            "Aliases": null,
            "NetworkID": "33b01b58a9a2ac92011051f1f75aa94ec5eb29bdbac06580aef462d700e527ef",
            "EndpointID": "16ab10c9a32ea277f3f78b589ceee0f8698feafdfe6061c9ca3ac9f9633d1af8",
            "Gateway": "172.17.0.1",
            "IPAddress": "172.17.0.2",
            "IPPrefixLen": 16,
            "IPv6Gateway": "",
            "GlobalIPv6Address": "",
            "GlobalIPv6PrefixLen": 0,
            "MacAddress": "02:42:ac:11:00:02"
        }
    }
}

docker也提供了直接对网络进行管理的docker network命令,可以通过docker network inspect命令查看docker网络的详细信息:

# docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "33b01b58a9a2ac92011051f1f75aa94ec5eb29bdbac06580aef462d700e527ef",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16"
                }
            ]
        },
        "Internal": false,
        "Containers": {
            "81ea762a6bb99c1ee8c72d0fcc91f5151f8f65cf7c26747dd1f64d260845e21c": {
                "Name": "nginx",
                "EndpointID": "16ab10c9a32ea277f3f78b589ceee0f8698feafdfe6061c9ca3ac9f9633d1af8",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

端口映射

通过上面可以发现,bridge只不过是ip域为172.17.0.0/16子网,通过ifconfig命令也可以获得一些信息:

# ifconfig
docker0   Link encap:Ethernet  HWaddr 02:42:72:b4:25:d9
          inet addr:172.17.0.1  Bcast:0.0.0.0  Mask:255.255.0.0
          inet6 addr: fe80::42:72ff:feb4:25d9/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:37 errors:0 dropped:0 overruns:0 frame:0
          TX packets:39 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:3684 (3.6 KB)  TX bytes:3399 (3.3 KB)

docker是通过iptables将流量打到docker0的子网上的,因此,可以尝试找下iptables的配置:

# iptables-save |grep 172.17.0.*
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A POSTROUTING -s 172.17.0.2/32 -d 172.17.0.2/32 -p tcp -m tcp --dport 80 -j MASQUERADE
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT

在上面已经得知nginx容器的ip正是172.17.0.2,iptables会将来自8080端口的流量打在80端口上。
其实在宿主机直接访问子网172.17.0.2的80端口就可以获得nginx正确的返回:

# curl 172.17.0.2:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
2016/09/19 12:28 下午 posted in  Docker

容器的持久化方式

容器作为镜像的实例,是镜像中无状态应用的运行时。所谓没有状态,因为容器的生命周期非常灵活。当一个容器的生命周期结束,期间产生的数据不会持久化,而是随着容器的删除而被删除。但是,大多数应用都是为数据服务了,那么,这里就探讨下容器的持久化问题。

容器的存储

在讨论容器的持久化之前,先探索下,没有持久化,也就是一个普通的容器的数据存储是什么样子的。
我们先run一个容器:

# docker run -d ubuntu sleep 100000
# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS              PORTS               NAMES
3a8fed62029c        ubuntu              "sleep 100000"      About a minute ago   Up About a minute                       hopeful_kilby

因为容器的本质也就是一个进程,那么,容器内产生的所有的文件肯定也是通过某种方式存储到宿主机上的,我们可以exec进入这个容器,创建一个文件出来:

# docker exec -it 3a8fed62029c bash
root@3a8fed62029c:/# cd home/
root@3a8fed62029c:/home# echo "dao" > hello
root@3a8fed62029c:/home# ls
hello
root@3a8fed62029c:/home# cat hello
dao

创建完成后,可以在/var/lib/docker/aufs/mnt下找到这个文件:

root@ubuntu:/var/lib/docker/aufs/mnt# find -name hello
./24c7a79f3a3bcb9320e028dab081eec7f55a5a8fb8eb2201d9cfe7b1d9d0e7bf/home/hello

而进入找到这个路径/var/lib/docker/aufs/mnt/24c7a79f3a3bcb9320e028dab081eec7f55a5a8fb8eb2201d9cfe7b1d9d0e7bf就会发现,这就是容器下的整个文件系统。

如果尝试在宿主机下的这个目录中创建若干文件,则在容器中也同样是可以找到的。

其中的原理也很简单,我们可以在/var/lib/docker/aufs/layers下找到答案:

root@ubuntu:/var/lib/docker/aufs/layers# cat 24c7a79f3a3bcb9320e028dab081eec7f55a5a8fb8eb2201d9cfe7b1d9d0e7bf
24c7a79f3a3bcb9320e028dab081eec7f55a5a8fb8eb2201d9cfe7b1d9d0e7bf-init
d7b377dff0a5d7b84ae6f30cab5d94b668406e30e8f1584e0eab567b45e27a60
e00588b1d53b6376da8ac07a5176bf44c246a5c1077fd56cf41979565ab8e290
5c05060aac0e7a11db24402fc2639d3fd47dc3ab8a996d279a28ac6d73d1217b
3cba5f639a5bca0a7d7d4b28b68151d8a10101583f1b59f48328ade9f7c668dd
ac5fe0af9b19fe9bb88448932b3c7866e942dbdc9666457195183a2af7caf9f9

正如所知道的,镜像是分层存储的,而容器只不过是以镜像的若干层为基础,在上面有增加了一可读可写层作为数据的存储。

2016-09-11_21:41:49.jpg

因此,很明白,容器的存储只不过就是把容器内的文件映射到主机的某一个目录上,而这个目录就是这一层可读可写的layer。不过,当容器的生命周期结束,这个宿主机的文件夹也就不复存在。

也因为这个原因,我们需要考虑如何把容器产生的数据持久化。

持久化与Volume

把容器产生的数据持久化有两个中方法,基本就是两个完全不同的思路。第一种就是通过docker commit,把和容器的生命周期一样的临时的layer持久化:

# docker commit 3a8fed62029c
sha256:c82ef37ba95e8cc6b03a487294385e76d774f46bb225496f9278b50c0137175c
# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
<none>              <none>              c82ef37ba95e        3 seconds ago       126.6 MB

这样,已经存有数据的layer便永久的存储到新生成的镜像中,想再使用数据时可以通过实例一个新的镜像就可以。

但是,这样的局限性很明显,也更不能满足运行时的需求,因此,我们可以考虑下第二种的持久化方式,Volume。

Volume使用起来也非常简答,在run的时候加一个-v参数就可以了,我们从使用-v参数的三种方式探究下volume。

第一种方式,也就是最简单的,直接挂载一个volume到容器。

我们run一个容器,并挂在一个volume:

# docker run -d -v /volume ubuntu sleep 100000

通过inspect命令,我们可以看到这个容器的volume信息:

"Mounts": [
  {
    "Name": "8aa697c21e65e104dcb9f8b8507c905715d457ad88ee3c2e79a0c72ef07fff0e",
    "Source": "/var/lib/docker/volumes/8aa697c21e65e104dcb9f8b8507c905715d457ad88ee3c2e79a0c72ef07fff0e/_data",
    "Destination": "/volume",
    "Driver": "local",
    "Mode": "",
    "RW": true,
    "Propagation": ""
  }
],

我们可以看到,docker把本地/var/lib/docker/volumes下的一个文件件挂载到了容器的/volume,这个目录上。

当我们再次去/var/lib/docker/aufs/mnt下找打容器对应的目录,并在/volume这个目录下进行修改时,容器里的文件并不会发生什么变化,然而,在/var/lib/docker/volumes/8aa697c21e65e104dcb9f8b8507c905715d457ad88ee3c2e79a0c72ef07fff0e/_data这个目录下进行修改,在容器内就可以看到相应的变化。

但是,这次虽然使用了volume,但是到底和没有使用有什么区别呢?

区别就在于volume这个文件夹并不会和容器一样有相同的生命周期,而是在容器的生命周期结束后保留在宿主机上:

# docker volume ls
DRIVER              VOLUME NAME
local               8aa697c21e65e104dcb9f8b8507c905715d457ad88ee3c2e79a0c72ef07fff0e
# docker rm -f f5390aec14bc
f5390aec14bc
# docker volume ls
DRIVER              VOLUME NAME
local               8aa697c21e65e104dcb9f8b8507c905715d457ad88ee3c2e79a0c72ef07fff0e

那么这样就满足了对于没有状态的容器,持久化在他生命周期期间产生的数据的问题。不过这样还是有一些不方便,比如不方便管理volume,以及再run一个容器来继承使用前面的容器产生的数据。

而第二种方式便满足了这个问题。第二种方式就是把宿主机的一个目录挂在到容器。

我们可以run一个容器,把本地的/root/data这个目录挂载到容器的/volume下:

# docker run -d -v /root/data:/volume ubuntu sleep 1000000
9548d507859b6a331a167a8f60d6d96ccdf2b0244c6bdd24033ccf277b89d162
# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
9548d507859b        ubuntu              "sleep 1000000"     54 seconds ago      Up 53 seconds                           drunk_swanson

我们依然可以通过inspect命令获得volume相关的信息:

"Mounts": [
  {
    "Source": "/root/data",
    "Destination": "/volume",
    "Mode": "",
    "RW": true,
    "Propagation": "rprivate"
  }
],

这时候就发现,docker十分省事的并没有在volume下创建一个文件夹,而是直接创建了/root/data,然后,直接把这个文件夹挂载到了容器内的/volume。

第三种方式,实例化一个数据卷容器,然后把这个数据卷容器挂载到新建的容器上。

而实例化一个数据卷容器,就有上面提到的两种方式,我们可以分别看下两种不同的数据卷对新创建的容器有什么影响。

我们先run一个容器,并以包含普通volume的容器作为数据卷容器(见上面第一种方式):

# docker run -d --volumes-from f5390aec14bc ubuntu sleep 100000
8cfe0eb466e1935cafe353195deebd0debd677433625d05770b2458755c1a32b
# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
8cfe0eb466e1        ubuntu              "sleep 100000"      8 seconds ago       Up 8 seconds                            evil_nobel

然后,带这个容器进行inspect:

"Mounts": [
  {
    "Name": "8aa697c21e65e104dcb9f8b8507c905715d457ad88ee3c2e79a0c72ef07fff0e",
    "Source": "/var/lib/docker/volumes/8aa697c21e65e104dcb9f8b8507c905715d457ad88ee3c2e79a0c72ef07fff0e/_data",
    "Destination": "/volume",
    "Driver": "local",
    "Mode": "",
    "RW": true,
    "Propagation": ""
  }
],

我们可以看到和前面一样的信息,这个容器并没有创建任何volume,只不过是复用了数据卷容器的volume。

相似的,from一个第二种方式创建的数据卷容器,也会得到类似的结果:

"Mounts": [
  {
    "Source": "/root/data",
    "Destination": "/volume",
    "Mode": "",
    "RW": true,
    "Propagation": "rprivate"
  }
],

新创建的容器并没有创建volume,依然是直接使用了原有容器的volume,因此,如果多个容器需要共享一个文件目录,完全可以由一个容器创建一个volume,然后其他容器通过volume-from关联这个volume进行使用。

2016/09/09 12:28 下午 posted in  Docker