Docker容器

Docker容器

1. 运行容器

docker run是启动容器的方法。

可用三种方式指定容器启动时执行的命令

(1)CMD指令。

(2)ENTRYPOINT指令。

(3)在docker run命令行中指定。

例如下面的例子:

[root@0x1e61 ~]# docker run ubuntu pwd
/

容器启动时执行pwd,返回的 / 是容器中的当前目录。

执行docker psdocker container ls可以查看Docker host中当前运行的容器。

[root@0x1e61 ~]# docker ps
CONTAINER ID   IMAGE      COMMAND        CREATED        STATUS        PORTS         NAMES 
[root@0x1e61 ~]	

并没有运行的容器。用docker ps -adocker container ls -a看看

[root@0x1e61 ~]# docker ps -a
CONTAINER ID   IMAGE    COMMAND       CREATED         STATUS             PORTS      NAMES
91fb4c2e9d91   ubuntu    "pwd"       5 minutes ago   Exited (0) 	5 minutes ago   silly_shtern

-a会显示所有状态的容器,可以看到,之前的容器已经退出了,状态为Exited。

1.1 让容器长期运行

如何让容器保存运行呢?

因为容器的生命周期依赖于启动时执行的命令,只要该命令不结束,容器也就不会退出。

理解了这个原理,我们就可以通过执行一个长期运行的命令来保持容器的运行状态。例如执行下面的命令.

[root@0x1e61 ~]# docker run ubuntu /bin/bash -c "while true ; do sleep 1; done"

while语句让bash不会退出。可以打开另一个终端查看容器的状态

[root@0x1e61 ~]# docker ps	
CONTAINER ID   IMAGE     COMMAND      CREATED        STATUS      		 PORTS     				NAMES
6b268bbcfb53   ubuntu    "/bin/bash -c 'while…"   About a minute ago   Up About a minute   serene_noyce

可见容器仍处于运行状态。不过这种方法有个缺点:它占用了一个终端。

可以加上参数 -d 以后台方式启动容器

[root@0x1e61 ~]# docker run -d  ubuntu /bin/bash -c "while true ; do sleep 1; done"
a8adf52984c0a025d0076730f660d495d403ffbceda21d2455614630c0e8711a

容器启动后回到了docker host的终端。这里看到docker返回了一串字符,这是容器的ID。通过docker ps查看容器

image-20230409144752829

这里注意一下容器的CONTAINER ID和NAMES这两个字段。

CONTAINER ID是容器的“短ID”,前面启动容器时返回的是“长ID”。短ID是长ID的前12个字符。

NAMES字段显示容器的名字, 在启动容器时可以通过 --name参数显式地为容器命名,如果不指定,docker会自动为容器分配名字。

对于容器的后续操作,我们需要通过“长ID”“短ID”或者“名称”来指定要操作的容器。比如下面停止一个容器

image-20230409145016009

这里我们就是通过“短ID”指定了要停止的容器。

1.2 两种进入容器的方法

我们经常需要进到容器里去做一些工作,比如查看日志、调试、启动其他进程等。有两种方法进入容器:attach和exec。

1. docker attach

通过docker attach可以attach到容器启动命令的终端。

image-20230409154138195

2. 通过docker exec

通过docker exec进入容器

image-20230409160441958

说明如下:

① -it以交互模式打开pseudo-TTY,执行bash,其结果就是打开了一个bash终端。

​ -i表示交互式的,表示[cmd]是一个有用户输入的程序,比如/bin/bash 和 python 等等。

​ -t 产生一个终端。

② 进入到容器中,容器的hostname就是其“短ID”。

③ 可以像在普通Linux中一样执行命令。ps -elf显示了容器启动进程while以及当前的bash进程。

④ 执行exit退出容器,回到docker host。

docker exec -it <container> bash|sh

这是执行exec最常用的方式。

3. attach VS exec

attach与exec主要区别如下:

(1)attach直接进入容器启动命令的终端,不会启动新的进程。

(2)exec则是在容器中打开新的终端,并且可以启动新的进程。

(3)如果想直接在终端中查看启动命令的输出,用attach;其他情况使用exec。

当然,如果只是为了查看启动命令的输出,可以使用docker logs命令

image-20230409161237543

-f的作用与tail -f类似,能够持续打印输出。

1.3运行容器的最佳实践

按用途容器大致可分为两类:服务类容器工具类的容器

服务类容器以daemon的形式运行,对外提供服务,比如Web Server、数据库等。通过 -d以后台方式启动这类容器是非常合适的。

如果要排查问题,可以通过exec -it进入容器。

工具类容器通常能给我们提供一个临时的工作环境,通常以run -it方式运行

“docker run”通常是在新创建的容器中所使用的命令。 它适用于在没有其他容器运行的情况下,您想要创建一个容器,并且要启动它,然后在其上运行一个进程。

“docker exec”适用于在现有容器中运行命令的情况。如果您已经拥有了一个正在运行的容器,并希望更改该容器或从中获取某些内容,那么使用“docker exec”命令就非常合适了。

2. stop/start/restart容器

通过docker stop可以停止运行的容器。

image-20230409164700862

容器在docker host中实际上是一个进程,docker stop命令本质上是向该进程发送一个SIGTERM信号。

如果想快速停止容器,可使用docker kill命令,其作用是向容器进程发送SIGKILL信号

image-20230409164759686

对于处于停止状态的容器,可以通过docker start重新启动

image-20230409164845526

docker start会保留容器的第一次启动时的所有参数。

docker restart可以重启容器,其作用就是依次执行docker stop和docker start。

容器可能会因某种错误而停止运行。对于服务类容器,我们通常希望在这种情况下容器能够自动重启。启动容器时设置 --restart就可以达到这个效果

image-20230409165259353

--restart=always意味着无论容器因何种原因退出(包括正常退出),都立即重启;该参数的形式还可以是 --restart=on-failure:3,意思是如果启动进程退出代码非0,则重启容器,最多重启3次。

3. pause / unpause容器

有时我们只是希望让容器暂停工作一段时间,比如要对容器的文件系统打个快照,或者dcoker host需要使用CPU,这时可以执行docker pause

image-20230409165626819

处于暂停状态的容器不会占用CPU资源,直到通过docker unpause恢复运行

image-20230409165710511

4. 删除容器

使用docker一段时间后,host上可能会有大量已经退出了的容器

image-20230409165755145

这些容器依然会占用host的文件系统资源,如果确认不会再重启此类容器,可以通过docker rm删除

image-20230409170003992

docker rm一次可以指定多个容器,如果希望批量删除所有已经退出的容器,可以执行如下命令

docker rm -v $(docker ps -aq -f status=exited)

image-20230409170126002

顺便说一句:docker rm是删除容器,而docker rmi是删除镜像。

5. State Machine

前面我们已经讨论了容器的各种操作,对容器的生命周期有了大致的理解,下面这张状态机很好地总结了容器各种状态之间是如何转换的

image-20230409171246151

根据上图右两点可以补充

(1)可以先创建容器,稍后再启动

image-20230409172146647

① docker create创建的容器处于Created状态。

② docker start将以后台方式启动容器。docker run命令实际上是docker create和docker start的组合。

(2)只有当容器的启动进程退出时,--restart才生效

image-20230409172217615

退出包括正常退出或者非正常退出。这里举了两个例子:启动进程正常退出或发生OOM,此时Docker会根据 --restart的策略判断是否需要重启容器。但如果容器是因为执行docker stop或docker kill退出,则不会自动重启。

6. 资源限制

一个docker host上会运行若干容器,每个容器都需要CPU、内存和IO资源。对于KVM、VMware等虚拟化技术,用户可以控制分配多少CPU、内存资源给每个虚拟机。对于容器,Docker也提供了类似的机制避免某个容器因占用太多资源而影响其他容器乃至整个host的性能。

6.1内存限额

与操作系统类似,容器可使用的内存包括两部分:物理内存和swap。

Docker通过下面两组参数来控制容器内存的使用量。

(1)-m或 --memory:设置内存的使用限额,例如100MB,2GB。

(2)--memory-swap:设置内存+swap的使用限额。

注意 主机没有开启swap那么--memory-swap设不设置都是无效的,用不了,那么可用内存就是-m再小一点。

只有主机开启swap,--memory-swap才生效

当我们执行如下命令:

docker run -m 200M --memory-swap=300M ubuntu

其含义是允许该容器最多使用200MB的内存和100MB的swap。

默认情况下,上面两组参数为 -1,即对容器内存和swap的使用没有限制。

下面我们将使用progrium/stress镜像来学习如何为容器分配内存。该镜像可用于对容器执行压力测试。执行如下命令:

docker run -it -m 200M --memory-swap=300M progrium/stress --vm 1 --vm-bytes 280M

● --vm 1:启动1个内存工作线程。

● --vm-bytes 280M:每个线程分配280MB内存。

image-20230409200838494

因为280MB在可分配的范围(300MB)内,所以工作线程能够正常工作,其过程是:

(1)分配280MB内存。

(2)释放280MB内存。

(3)再分配280MB内存。

(4)再释放280MB内存。

(5)一直循环……

如果让工作线程分配的内存超过300MB

image-20230409200937634

分配的内存超过限额,stress线程报错,容器退出。

如果在启动容器时只指定 -m而不指定 --memory-swap,那么 --memory-swap默认为 -m的两倍,比如:

docker run -it -m 200M ubuntu

容器最多使用200MB物理内存和200MB swap。

6.2 CPU限额

默认设置下,所有容器可以平等地使用host CPU资源并且没有限制。

Docker可以通过 -c或 --cpu-shares设置容器使用CPU的权重。如果不指定,默认值为1024。

与内存限额不同,通过 -c设置的cpu share并不是CPU资源的绝对数量,而是一个相对的权重值。某个容器最终能分配到的CPU资源取决于它的cpu share占所有容器cpu share总和的比例。

换句话说:通过cpu share可以设置容器使用CPU的优先级。

比如在host中启动了两个容器:

docker run --name "container_A" -c 1024 ubuntu
docker run --name "container_B" -c 512 ubuntu

containerA的cpu share 1024,是containerB的两倍。

当两个容器都需要CPU资源时,containerA可以得到的CPU是containerB的两倍。

需要特别注意的是,这种按权重分配CPU只会发生在CPU资源紧张的情况下。如果containerA处于空闲状态,这时,为了充分利用CPU资源,containerB也可以分配到全部可用的CPU。

下面我们继续用progrium/stress做实验。

(1)启动container_A, cpu share为1024

image-20230409203049623

--cpu用来设置工作线程的数量。因为当前host只有1颗CPU,所以一个工作线程就能将CPU压满。

如果host有多颗CPU,则需要相应增加 --cpu的数量。

(2)启动container_B, cpu share为512

image-20230409203304077

(3)在host中执行top,查看容器对CPU的使用情况

image-20230409203332766

6.3 Block IO带宽限额

Block IO是另一种可以限制容器使用的资源。

Block IO指的是磁盘的读写,docker可通过设置权重、限制bps和iops的方式控制容器读写磁盘的带宽。

:目前Block IO限额只对direct IO(不使用文件缓存)有效。

1. block IO

权重默认情况下,所有容器能平等地读写磁盘,可以通过设置 --blkio-weight参数来改变容器block IO的优先级。

--blkio-weight与 --cpu-shares类似,设置的是相对权重值,默认为500。

在下面的例子中,containerA读写磁盘的带宽是containerB的两倍。

docker run -it --name container_A --blkio-weight 600 ubuntu
docker run -it --name container_B --blkio-weight 300 ubuntu

2. 限制bps和iops

bps是byte per second,每秒读写的数据量。

iops是io per second,每秒IO的次数。

可通过以下参数控制容器的bps和iops:

​ ● --device-read-bps:限制读某个设备的bps。

​ ● --device-write-bps:限制写某个设备的bps。

​ ● --device-read-iops:限制读某个设备的iops。

​ ● --device-write-iops:限制写某个设备的iops。

下面这个例子限制容器写 /dev/sda的速率为30 MB/s:

docker run -it --device-write-bps /dev/vda:30MB ubuntu

image-20230410083551880

通过dd测试在容器中写磁盘的速度。因为容器的文件系统是在host /dev/sda上的,在容器中写文件相当于对host /dev/sda进行写操作。另外,oflag=direct指定用direct IO方式写文件,这样 --device-write-bps才能生效。

结果表明,bps 31.5MB/s 接近 30 MB/s的限速。

作为对比测试,如果不限速

image-20230410083818194

7. 实现容器的底层技术

cgroup和namespace是最重要的两种技术。cgroup实现资源限额,namespace实现资源隔离。

7.1 cgroup

cgroup全称Control Group。Linux操作系统通过cgroup可以设置进程使用CPU、内存和IO资源的限额。前面我们看到的 --cpu-shares、-m、--device-write-bps实际上就是在配置cgroup。

cgroup到底长什么样子呢?我们可以在 /sys/fs/cgroup中找到它。还是用例子来说明,启动一个容器,设置 --cpu-shares=512

image-20230410084517811查看容器的ID

image-20230410084607735

在 /sys/fs/cgroup/cpu/docker目录中,Linux会为每个容器创建一个cgroup目录,以容器长ID命名

image-20230410085051557

目录中包含所有与cpu相关的cgroup配置,文件cpu.shares保存的就是 --cpu-shares的配置,值为512。

同样的,/sys/fs/cgroup/memory/docker和/sys/fs/cgroup/blkio/docker中保存的是内存以及Block IO的cgroup配置。

7.2 namespace

在每个容器中,我们都可以看到文件系统、网卡等资源,这些资源看上去是容器自己的。

拿网卡来说,每个容器都会认为自己有一块独立的网卡,即使host上只有一块物理网卡。这种方式非常好,它使得容器更像一个独立的计算机。

Linux实现这种方式的技术是namespace。namespace管理着host中全局唯一的资源,并可以让每个容器都觉得只有自己在使用它。换句话说,namespace实现了容器间资源的隔离。

Linux使用了6种namespace,分别对应6种资源:Mount、UTS、IPC、PID、Network和User

1. Mount namespace

Mount namespace让容器看上去拥有整个文件系统。容器有自己的/目录,可以执行mount和umount命令。当然我们知道这些操作只在当前容器中生效,不会影响到host和其他容器。

2. namespace

简单地说,UTS namespace让容器有自己的hostname。

默认情况下,容器的hostname是它的短ID,可以通过 -h或 --hostname参数设置

image-20230410085914251

3. IPC namespace

IPC namespace让容器拥有自己的共享内存和信号量(semaphore)来实现进程间通信,而不会与host和其他容器的IPC混在一起。

4. PID namespace

前面提到过,容器在host中以进程的形式运行。例如当前host中运行了容器

image-20230410090335763

通过ps axf可以查看容器进程

image-20230410091902432

可以看到容器自己的子进程。如果我们进入到某个容器,ps就只能看到自己的进程了

image-20230410092056283

而且进程的PID不同于host中对应进程的PID,容器中PID=1的进程当然也不是host的init进程。

也就是说:容器拥有自己独立的一套PID,这就是PID namespace提供的功能。

5. Network namespace

Network namespace让容器拥有自己独立的网卡、IP、路由等资源

6. User namespace

User namespace让容器能够管理自己的用户,host不能看到容器中创建的用户

image-20230410092353016

在容器中创建了用户cloudman,但host中并不会创建相应的用户。

相关命令

create:创建容器;

run:运行容器;

pause:暂停容器;

unpause:取消暂停继续运行容器;

stop:发送SIGTERM停止容器;

kill:发送SIGKILL快速停止容器;

start:启动容器;

restart:重启容器;

attach:attach到容器启动进程的终端;

exec:在容器中启动新进程,通常使用"-it"参数;

logs:显示容器启动进程的控制台输出,用"-f"持续打印;

rm:从磁盘中删除容器。

posted @ 2023-04-14 17:04  0x1e61  阅读(63)  评论(0编辑  收藏  举报