在使用docker过程中,我们经常发现管理维护是一个很复杂过程,因为我们在使用docker commands的过程中,我们只会去使用我们认为简单并且熟悉的命令,然而docker本身其实是提供给我们很多便捷且人性化的工具的,如果掌握这些使用技巧,也许你的维护管理工作将会事半功倍,并且给人看起来会很牛逼的样子。

创建容器时传入环境变量

在实际应用场景中,不论是从安全还是可配置方面去考虑,很多参数是比较适合用环境变量加载进去的,比如数据库的连接信息,时区,还有字体支持等等,在创建容器的时候其实都可以使用-e 指定key/value进行传递环境变量进去。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
sh-4.2# docker run -itd --name test-env -e TZ='Asia/Shanghai' 172.25.46.9:5001/centos6.8-jdjr-test-app 
ee20b44301e27c16eae63dab243d293054178dd5f819c23d44bd9e534208bb42
sh-4.2# docker exec -it test-env date
2017年 01月 17日 星期二 10:35:17 CST
sh-4.2# date
Tue Jan 17 10:35:21 CST 2017
可以看到加了时区环境变量的容器已经和宿主机在同一个时区(CST),并且时间和宿主机基本同步

sh-4.2# docker run -itd --name test  172.25.46.9:5001/centos6.8-jdjr-test-app
d6a02874b999ff4eea79e3b302148b42043af01c89a5d31e5d858e0806f9077a
sh-4.2# docker exec -it test date
2017年 01月 20日 星期五 01:43:48 Asia
默认没有加时区环境变量的容器还是Asia

调整宿主机和容器的时间差异

首先我们需要弄清几个概念:在类unix系统中有硬件时钟与系统时钟,硬件时钟是指主机板上的时钟设备,也就是通常可在BIOS画面设定的时钟,系统时钟则是指kernel中的时钟。unix以及linux系统时间是从格林威治时间到当前的秒数,即1970年1月1日凌晨零点零分零秒到当前的时间,全球都一样,这是绝对值;而时区则是由于地理位置差异、行政区划导致各地显示时间的差异,为了克服时间上的混乱,规定将全球划分为24个时区,我们国家属于东八区标识为CST。

因此,对于 Docker 容器而言,根本不存在宿主和容器的时间差异问题,因为他们使用的是同一个内核、同一个时钟,二者完全一样,所以根本不存在同步问题。一般来说这个问题是由时区导致的,可以使用date命令查看下容器当前的时间时区是啥。UTC(通用协调时)表示使用的是国际标准0时区,UTC与格林尼治平均时(GMT, Greenwich Mean Time)一样,都与英国伦敦的本地时相同。CST表示中国标准时间时区一般是中国上海"Aisa/Shanghai",也就是说UTC和CST相差了8个小时。

1
2
3
解决办法:
创建容器的时候,使用-e 将时区信息传入到容器内部。
sh-4.2# docker run -itd --name test-env -e TZ='Asia/Shanghai' images

注意:其实使用单纯的环境变量来改变容器内部的TIME ZONE,只会影响当前容器用户的时区,一旦切换到真正的root用户就会发现时区依然是不正确的,比如以下栗子:

1
2
3
4
5
6
7
$ docker run -itd --name test-env -e TZ='Asia/Shanghai' images
$ docker exec -it test-env bash
bash-4.1# date
2017年 09月 20日 星期三 20:45:54 CST
bash-4.1# sudo su -c date
2017年 09月 20日 星期三 08:46:02 EDT
bash-4.1# 

那么如何真正解决时区这个问题呢?其实是/etc/localtime在作怪,用户只需要将容器内部的localtime改成你想要的时区就行了。

1
2
3
4
5
6
bash-4.1# ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 
bash-4.1# date
2017年 09月 20日 星期三 20:54:35 CST
bash-4.1# sudo su -c date
2017年 09月 20日 星期三 20:54:39 CST
bash-4.1# 

So,在使用Dockerfile构建镜像的时候将/usr/share/zoneinfo/Asia/Shanghai强制软连接到/etc/localtime就可以永久修复时区的问题了。

####指定容器的rootfs的大小 在使用docker的过程中,会发现cpu和memory可以很随意的动态调整,但是默认的rootfs却是不能随意调整的,默认是10g大小,当然如果对于数据有需求,可以通过挂载voulme进行扩展存储。如果用户执意想要调整rootfs的大小,在docker1.12版本默认提供了两种方式:在启动docker 的时候加载参数--storage-opt dm.basesize=40G用来调整默认容器的rootfs大小;在创建容器的时候使用参数--storage-opt size=70G 来设置改容器的rootfs大小。

喜讯:在docker最近发布的1.13版本中,支持了磁盘的配额,不过还未测试

1
2
3
4
5
6
7
sh-4.2# docker run -itd --name volume-test --storage-opt size=70G 172.25.46.9:5001/centos6.8-jdjr-test-app
18d47e69802aa84df00182885b256c50ebc56e15d8e6990fc1e187ffe254171e

sh-4.2# docker exec -it volume-test df -H | grep rootfs
rootfs                 76G  1.5G   74G   2% /
sh-4.2# docker exec -it test-env df -H | grep rootfs
rootfs                 11G  1.5G  9.3G  14% /

快速管理容器和镜像

在docker中删除容器需要指定容器名或者容器id,但是在容器比较多,并且状态不一的情况下删除容器还是需要走下心的。不过好处是docker ps默认提供了很多好用的功能,可以很方便地管理容器(创建容器的时候如果加上label后更方便哦)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
原理:先用docker ps -a -q 输出所有容器的container id(-f 表示过滤参数或者输出格式),然后作为docker rm 的参数进行批量删除
输出所有容器的name:
sh-4.2# docker ps --format='{{.Names}}'
test-env
test-args
test-run
输出所有容器名包含test的容器,并打印容器名
sh-4.2# docker ps -f name=test --format='{{.Names}}'
test-env
test-args
test-run
查看退出状态的容器,并打印容器名
sh-4.2# docker ps -f status=exited --format="{{.Names}}"
thirsty_brahmagupta
clever_mestorf
hopeful_morse
stoic_morse
elated_williams
tender_jepsen
reverent_mirzakhani

删除所有容器:
sh-4.2# docker rm -f -v $(docker ps -a -q)
删除/启动所有退出的容器:
sh-4.2# docker rm/start $(docker ps -qf status=exited)
删除所有镜像:
sh-4.2# docker rmi $(docker images -q)

查看悬挂镜像:
sh-4.1# docker  images -qf dangling=true

只查看镜像或者容器指定的信息(在docker1.10之后才支持的)

只列出镜像的id以及仓库名称:
sh-4.2# docker images --format "{{.ID}}: {{.Repository}}"
67591570dd29: centos
0a18f1c0ead2: rancher/server

只列出容器的相关id,image,status和name
sh-4.2# docker ps --format "{{.ID}}: {{.Image}} : {{.Status}} : {{.Names}}"
66b60b72f00e: centos : Up 7 days : pensive_poincare
或者自己重新定义列,就和原生差不多:
sh-4.2# docker ps --format "table {{.ID}}\t{{.Image}}\t{{.Status}}\t{{.Names}}"
CONTAINER ID        IMAGE                                         STATUS              NAMES
66b60b72f00e        centos                                        Up 7 days           pensive_poincare

注意:其实上面的–format利用的就是go语言中的模版语法,所有容器的组织信息都在结构体中:

*formatter.containerContext

容器label的使用

在实际运维过程中,大量的容器可能会一些运维上的挑战,通过使用label,可以很好的将容器分类。label贯穿于docker的整个过程。 这个label可以作为你区分业务,区分模板各种区分容器的标识,通过标识,可以将容器更好的进行分组

1
2
3
4
5
6
7
8
sh-4.2# docker run -itd --name volume-test --storage-opt size=70G --label zone=test 172.25.46.9:5001/centos6.8-jdjr-test-app
c3772397e58e663095c2c0fd8d688b3d41b494097999ec2b6d6b7c509d23a138
创建容器的时候定义一个label,表示该容器在test这个区域
使用定义的label进行快速检索容器,并进行下一步操作(比如删除啦,更新啦)
sh-4.2# docker ps -qf label=zone=test
c3772397e58e
sh-4.2# docker ps -f label=zone=test --format='{{.Names}}'
volume-test

快速查看容器的相关配置信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
查看容器的devicemapper设备:
sh-4.2# docker inspect -f '{{.GraphDriver.Data.DeviceName}}' nginx 
docker-8:1-67411759-7c9d6d3327b02659c81bcb70bf6a4c7a45df6a589af2a2d42a387dc0e90d4913
查看容器的PID:
sh-4.2# docker inspect -f '{{.State.Pid}}' nginx 
27521
查看容器name:
sh-4.2# docker inspect -f '{{.Name}}' nginx 
/nginx
获取容器的ID:
sh-4.2# docker inspect --format {{.Id}} nginx
53214bc9cd001f2c548edcce0c42fe51f1a118c08941406d43122a8348055843

使用alias来预定义常用的命令

docker管理命令经常需要指定各种参数,通过linux的alias命令将默认的参数预定义起来,可以很方便的进行管理容器。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
sh-4.2# alias dockerrm='docker rm -f -v'
sh-4.2# alias dockerexec='docker exec -it'
sh-4.2# alias dockerrmimage='docker rmi'

sh-4.2# dockerrm volume-test
volume-test

sh-4.2# dockerexec volume-test ls
bin   dev  export  lib	  media  opt   root  selinux  sys  usr
boot  etc  home    lib64  mnt	 proc  sbin  srv      tmp  var
sh-4.2# dockerexec volume-test bash
bash-4.1# 

使容器随着docker daemon的启动一同启动

1
docker run 的时候加参数--restart=always

如何动态修改容器的内存和cpu限制docker1.10之后才支持的动态调整

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
sh-4.2# dockerexec test-env cat /sys/fs/cgroup/memory/memory.limit_in_bytes
9223372036854775807
sh-4.2# cat /sys/fs/cgroup/memory/memory.limit_in_bytes 
9223372036854775807
可以看到,默认没有给容器限制内存,它会共享宿主机的所有内存
动态调整内存为2014M:
sh-4.2# docker update -m 2014M test-env
test-env
sh-4.2# dockerexec test-env cat /sys/fs/cgroup/memory/memory.limit_in_bytes
2111832064

docker容器中真实用户的隔离

注意:默认docker容器内部的用户会继承宿主机的用户id,也就是说容器外部有一个uid为500的用户test,容器内部有一个uid为500的用户admin,容器内部运行的程序如果在宿主机上查看的时候会发现程序的启动用户会是外部宿主机的test用户。 这是因为默认情况下容器的 user namespace 并未开启,所以容器内的用户和宿主用户共享 uid 空间。容器内的 uid 为 0 的 root,就被系统视为 uid=0 的宿主 root,因此磁盘读写时,具有宿主 root 同等读写权限。

1
2
3
开启user namespace:
启动docker的时候加参数--userns-remap=default
https://docs.docker.com/engine/reference/commandline/dockerd/#/daemon-user-namespace-options

在docker container和物理机中双向拷贝文件

1
2
3
4
5
6
7
8
容器内部文件拷贝到宿主机:
sh-4.2# docker cp jupyter-70002111:/home/70002111/教程-研究功能介绍.ipynb .
sh-4.2# ls
Dockerfile  教程-研究功能介绍.ipynb
宿主机文件拷贝到容器:
sh-4.2# docker cp Dockerfile jupyter-70002111:/home/70002111/
sh-4.2# docker exec -it jupyter-70002188 ls 
Dockerfile

向容器内部程序发送signal

注意:在给容器进程发送SIGTERM信号时只会发给主进程,也就是容器内 PID 为 1 的进程。至于说主进程启动的那些子进程,完全看主进程是否愿意转发SIGTERM 给子进程了。所以那些把 Docker当做虚拟机用的,主进程跑了个bash,然后exec 进去启动程序的,或者来个&让程序跑后台的情况,应用进程必然无法收到SIGTERM。 还有一种可能是在Dockerfile中的CMD那行用的是 shell 格式写的命令,而不是 exec 格式。在镜像中使用CMD启动的容器会加一个 sh -c 来去执行,因此使用 shell 格式写 CMD 的时候,PID 为 1 的进程是 sh,而它不转发信号,所以主程序收不到。

所以在写CMD哪行命令的时候,最好按照exec格式去写。

划重点: 由于在容器内部是没有init进程的,所以容器的整个生命周期会和容器内部PID为1的进程紧密相连,用户在使用过程中经常会发现容器更新版本之后,业务调用方经常会有一些请求异常,这其实也是因为容器内部的1号进程的设置有关,导致容器在停止时可能直接发送SIGKILL信号,导致容器当前正在处理中的业务也会立即断开连接,这样可能会导致一些业务异常

总而言之,向容器内部程序发送合适的信号是非常有必要的,这样可以使你的容器很优雅的退出。docker stop操作会让容器在10s后进行优雅的退出

如何优雅的关闭容器

容器的cache不释放

1
$ echo 1 > /proc/sys/vm/drop_caches

桥接网络连入下层网络并使用IPAM (没有NAT/端口映射)

注意:docker network是1.12版本加进来的,支持了多种网络插件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ docker network create \
	-d bridge \
	--subnet=192.168.57.0/24 \
	--ip-range=192.168.57.32/28 \
	--gateway=192.168.57.11 \
	--aux-address DefaultGatewayIPv4=192.168.57.1 \
	-o com.docker.network.bridge.name=brnet \
	brnet
$ brctl addif brnet eth2
$ docker run --net=brnet -it busybox ifconfig

注意其它主机的 --ip-range 和 --gateway 需要做对应调整。

这种拓扑是,容器内 eth0 连接 brnet 接口,该接口直接通过 eth2 访问交换。

Docker查看某个容器绑定的cpu内核

容器内部第一个进程编号一般为1

1
2
$ docker exec -it container-name taskset -c -p 1 
pid 1's current affinity list:0-3

给docker配置hosts

1
2
3
4
5
6
7
8
9
$ docker run --add-host biaoge-ops:192.168.0.1 centos cat /etc/hosts
127.0.0.1	localhost
::1	localhost ip6-localhost ip6-loopback
fe00::0	ip6-localnet
ff00::0	ip6-mcastprefix
ff02::1	ip6-allnodes
ff02::2	ip6-allrouters
192.168.0.1	biaoge-ops
10.0.0.3	6ff3ea7114b4

个人博客:https://my.oschina.net/xxbAndy/blog 微信公众号:

wechat.png