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

Linux 容器是由 Linux Kernel 提供的具有特定隔离的进程。Linux 容器技术能够让用户对应用及其整个运行时环境(包括全部所需文件)一起进行打包或隔离。从而让用户在不同环境,之间轻松迁移应用的同时,并保留应用的全部功能。
2019/09/29 11:13 am 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 pm 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 pm 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 pm 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 pm posted in  Docker