docker初探

docker’s magic

最早是从我仰慕的Tyr那里听说docker这个词,一年前他就在研究并使用docker、coreOS,没错,我的英文名就是抄的他的。后来docker就很火,连同PAAS、LXC和容器随处可见,不过倒是最近才了解了一下这个技术。

Docker的核心底层技术是LXC(Linux Container),Docker在其上面加了薄薄的一层,添加了许多有用的功能。

容器有效地将由单个操作系统管理的资源划分到孤立的组中,以更好地在孤立的组之间平衡有冲突的资源使用需求。与虚拟化相比,这样既不需要指令级模拟,也不需要即时编译。容器可以在核心 CPU 本地运行指令,而不需要任何专门的解释机制。此外,也避免了准虚拟化(paravirtualization)和系统调用替换中的复杂性。
通过提供一种创建和进入容器的方式,操作系统让应用程序就像在独立的机器上运行一样,但又能共享很多底层的资源。例如,可以有效地共享公共文件(比如 glibc)的页缓存,因为所有容器都使用相同的内核,而且所有容器还常常共享相同的 libc 库(取决于容器配置)。这种共享常常可以扩展到目录中其他不需要写入内容的文件。
容器在提供隔离的同时,还通过共享这些资源节省开销,这意味着容器比真正的虚拟化的开销要小得多。
容器技术早就出现。例如,Solaris Zones 和 BSD jails 就是非 Linux 操作系统上的容器。用于 Linux 的容器技术也有丰富的遗产,例如 Linux-Vserver、OpenVZ 和 FreeVPS。虽然这些技术都已经成熟,但是这些解决方案还没有将它们的容器支持集成到主流 Linux 内核。
相比之下,Linux Resource Containers 项目则通过为主流 Linux 内核作贡献来实现容器。与此同时,这些贡献可能对成熟的 Linux 容器解决方案有用处 — 为更成熟的容器项目提供公共后端。 –摘自IBM

这篇stackoverflow上的问题和答案很好地诠释了Docker和LXC的区别,能够让你更好的了解什么是Docker, 简单翻译下就是以下几点:

  • Docker提供了一种可移植的配置标准化机制,允许你一致性地在不同的机器上运行同一个Container;而LXC本身可能因为不同机器的不同配置而无法方便地移植运行;
  • Docker以App为中心,为应用的部署做了很多优化,而LXC的帮助脚本主要是聚焦于如何机器启动地更快和耗更少的内存;
  • Docker为App提供了一种自动化构建机制(Dockerfile),包括打包,基础设施依赖管理和安装等等;
  • Docker提供了一种类似git的Container版本化的机制,允许你对你创建过的容器进行版本管理,依靠这种机制,你还可以下载别人创建的Container,甚至像git那样进行合并;
  • Docker Container是可重用的,依赖于版本化机制,你很容易重用别人的Container(叫Image),作为基础版本进行扩展;
  • Docker Container是可共享的,有点类似github一样,Docker有自己的INDEX,你可以创建自己的Docker用户并上传和下载Docker Image;
  • Docker提供了很多的工具链,形成了一个生态系统;这些工具的目标是自动化、个性化和集成化,包括对PAAS平台的支持等;

在IBM的研究论文中,结果显示,在每一项测试中,Docker的性能等同于或超出KVM的性能。在CPU和内存性能方面,KVM和Docker都引入了明显的,但可略不计的开销。但是,对于I/O密集型的应用,两者都需要进行调整以减少开销带来的影响。

传统观点(在某种程度上,这种观点存在于年轻的云生态圈中)认为使用虚拟机实现IaaS,使用容器实现PaaS。我们没有找到技术方面的理由来证明必须这么做,尤其是证明容器基于IaaS能提供更好的性能或者更容易部署。由于容器提供了控制手段,并在不使用虚拟机的情况下能达到物理机的性能,所以它能够消除IaaS和非虚拟化的服务器间的差异。

初衷是探究docker在自动化部署和运维方面的使用意义。但是粗略使用下来和预想的本不太一样。

  • docker用来封装应用,推荐做法是每个应用封装在一个container中,相比于vmware等虚拟机,docker可以提供轻量级虚拟化方案,适用于多租户隔离环境,一个contaner的基本大小大约100-300M。
  • 为了构建docker 镜像,需要编写Dockerfile,在运维中类似使用shell脚本来实现,而不需要占用多余的内存、CPU、磁盘等资源。Dockerfile的构建过程即相当于shell脚本的编写过程。并且,container中的前台进程结束时这个container就会退出,因此必须让进程出去前台工作。比如让nginx工作在前台的指令十分非主流和生僻。
  • 在docker中修改配置文件需要使用环境变量、挂载等方式。配置文件生效一般需要重新启动docker,重启docker会向contaniner发送SIGTERM信号,如果container没有响应则使用SIGKILL,因此重启往往十分缓慢。
  • 如果container中集成多个服务,比如我想搭建基于UDP转发的DNS服务器,那么需要dns server和socat,socat单独作为container运行似乎没有必要,为了满足只有一个进程处于前台运行,必须编写脚本。因此docker的意义可能更多的在于快速部署。
  • container中可能会重复安装应用,占据空间。
  • 对网络IO性能支持不好,存在docker-proxy进程实现NAT(iptables)。当然好处是可以做到内外部iptables隔离。如果使用--net=host那么相关进程将直接处于宿主host上运行。
  • docker对磁盘密集型和大流量网络IO应用的支持并不好。
  • 如果不采用coreOS大规模部署服务器集群,在线上环境下并不能做到真正的快速部署。
  • 在我的VPS上(centos 6.6 x64)使用docker时反复出现运行某些container导致linux 崩溃的情况,甚至在停止docker、重启VPS的过程中出现linux panic等字样。

根据调查,docker的早期用户中,63%的用于QA/Test,53%的用于开发,并且31%的用户计划在生产环境中使用docker。阻碍企业使用dokcer的最大因素在于其安全性以及缺少生产环境下的运维工具(两个原因各占49%左右)。

docker 技术架构

要使用docker,首先需要有一个docker registry。这里简要介绍一下什么是image。在Docker的世界里,Image是指一个只读的层(Layer),这里的层是AUFS里的概念,最直观的方式就是看一下docker官方给出的图:

Docker使用了一种叫AUFS的文件系统,这种文件系统可以让你一层一层地叠加修改你的文件,最底下的文件系统是只读的,如果需要修改文件,AUFS会增加一个可写的层(Layer),这样有很多好处,例如不同的Container可以共享底层的只读文件系统(同一个Kernel),使得你可以跑N多个Container而不至于你的硬盘被挤爆了!这个只读的层就是Image!而如你所看到的,一个可写的层就是Container。

那Image和Container的区别是什么?很简单,他们的区别仅仅是一个是只读的层,一个是可写的层,你可以使用docker commit 命令,将你的Container变成一个Image,也就是提交你所运行的Container的修改内容,变成一个新的只读的Image,这非常类似于git commit命令。

install

安装部分参考official tutorial,Docker官方的文档非常详细明了。

Hello-world

docker run ubuntu:14.04 /bin/echo "hello,world"

docker将先查询本地image,如果没有发现ubuntu:14.04,则在docker hub中查找。此为非交互式运行,当echo命令运行完,docker退出。

interactive

docker run -it ubuntu:14.04 /bin/bash

-i interactive,交互式运行,允许STDIN
-t 在新container中开启一个终端

daemonize

1
2
$ docker run -d ubuntu:14.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
1e5535038e285177d5214659a068137486f96ee5c2e85a4ac52dc83f2ebe4147

-d daemonize,放入后台以守护进程运行
返回container ID。

containers information

1
2
3
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1e5535038e28 ubuntu:14.04 /bin/sh -c 'while tr 2 minutes ago Up 1 minute insane_babbage

返回正在运行的container,最后container name随机分配。

logs
docker logs insane_babbage 可以看到daemonize container的STDOUT。

stop
docker stop insane_babbage 结束container

attach
docker attach insane_babbage 连接一个正在运行的container

web app

docker run -d -P traning/webapp python app.py

其中-P用于docker侦听端口到本地host的映射。

docker ps -l 显示上次运行的container的详细信息

1
2
3
$ docker ps -l
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9775101fda3c training/webapp "python app.py" 3 minutes ago Up 3 minutes 0.0.0.0:32768->5000/tcp stupefied_goldstine

netstat

1
2
$ sudo netstat -tunlp
tcp6 0 0 :::32768 :::* LISTEN 3640/docker-proxy

-P是将container需要的端口随机映射到localhost的高端口,也可以使用-p指定
docker run -d -p 80:5000 traning/webapp python app.py
将container中的5000端口映射到host的80端口。在local port前可以指定监听地址,比如-p 127.0.0.1:80:5000,也可以绑定UDP端口-p 127.0.0.1:80:5000/udp

获取端口映射关系

docker port stupefied_goldstine 5000 返回stupefied_goldstine实例5000端口映射的本地主机端口。

1
2
ubuntu@aws:~$ docker port stupefied_goldstine 5000
0.0.0.0:32768

logs -f
docker logs -f stupefied_goldstine 类似于tail -f log

top
docker top stupefied_goldstine 监视进程信息

inspect
docker inspect stupefied_goldstine 返回JSON配置信息和状态信息

start
docker start stupefied_goldstine 使用container name启动container

rm
docker rm stupefied_goldstine 删除container

Images

pull

docker pull centos 来下载镜像,镜像由”ubuntu:14.04”这种形式来表示。

search
docker search vpn 搜索镜像

创造自己的image

commit
docker commit -m "install htop" -a "conan" f49b71100671 conan/debian:v1

依次由这几部分组成

  • m : commit message 类似于git commit中的-m
  • a : author
  • f49b71100671 : image id
  • conan/debian : user/image name
  • v1 : version tag

可以用docker images看到我们创造的image,通过docker run -it conan/debian:v1来运行

build

使用Dockerfile build一个image

touch Dockerfile 写入以下内容

1
2
3
4
5
FROM ubuntu:14.04 # source image
MAINTAINER Tyr Chen <i@tyr.so> # 维护者
RUN apt-get update && apt-get install -y htop # docker中需要执行的指令
RUN apt-get install nethogs
RUN touch /home/mydocker

每一个指令都会创造一个层级,最多不能超过127层一次来鼓励优化image的大小。

docker build -t conan/debian:v2 .
build完成后docker images中看到 conan/debian:v2的image id会改变,之后可以docker run来运行。

tag
docker tag 5db5f8471261 conan/debian:v2 更改tag

digest

docker images --digests 镜像散列值

push
docker push conan/debian push到docker hub
需要注意的是这里的conan应该为你的docker账号用户名,否则会出现认证失败的信息

remove image
docker rmi conan/debian 删除镜像

总结一下

docker run -d --name web conan/debian 指定container name

通过使用--link 指令在container间创建连接,如
docker run -d -P --name web --link db:db traning/webapp python app.py

格式为 --link <name>[:alias]

连接使用环境变量和/etc/hosts两种方式来工作,并不通过网络连接。docker会在指定–link的contaner中创建一系列的环境变量,如

1
2
3
4
5
6
7
8
9
10
11
ubuntu@aws:~$ docker run --rm --name web2 --link db training/webapp env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=f7d96f8fa894
DB_PORT=tcp://172.17.0.28:5432
DB_PORT_5432_TCP=tcp://172.17.0.28:5432
DB_PORT_5432_TCP_ADDR=172.17.0.28
DB_PORT_5432_TCP_PORT=5432
DB_PORT_5432_TCP_PROTO=tcp
DB_NAME=/web2/db
DB_ENV_PG_VERSION=9.3
HOME=/root

其中--rm 表示运行完立即删除container

contaniner也在/etc/hosts中增加了对被连接容器的主机条目

1
2
3
4
5
6
7
8
9
$ cat /etc/hosts
172.17.0.34 f4808ba3024a
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
172.17.0.28 db 75f103ff4a5d

1
2
3
4
5
$ ping db
PING db (172.17.0.28) 56(84) bytes of data.
64 bytes from db (172.17.0.28): icmp_seq=1 ttl=64 time=0.084 ms
64 bytes from db (172.17.0.28): icmp_seq=2 ttl=64 time=0.056 ms
64 bytes from db (172.17.0.28): icmp_seq=3 ttl=64 time=0.070 ms

使用ping -f db后观察到

1
2
3
4
5
6
7
8
9
$ ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:ac:11:00:22
inet addr:172.17.0.34 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::42:acff:fe11:22/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:9001 Metric:1
RX packets:156742 errors:0 dropped:0 overruns:0 frame:0
TX packets:156743 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:15360412 (15.3 MB) TX bytes:15360502 (15.3 MB)

从1k 增加到15.3M,在contaniner内核中依然是通过网络接口来通信。
宿主host能也能看到虚拟网卡的流量相应增加

1
2
3
4
5
6
7
vethb94bf95 Link encap:Ethernet HWaddr 12:80:dc:18:b5:e1
inet6 addr: fe80::1080:dcff:fe18:b5e1/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:9001 Metric:1
RX packets:1289004 errors:0 dropped:0 overruns:0 frame:0
TX packets:1289003 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:126322024 (126.3 MB) TX bytes:126321934 (126.3 MB)

再看路由表

1
2
3
4
5
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.31.16.1 0.0.0.0 UG 0 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
172.31.16.0 0.0.0.0 255.255.240.0 U 0 0 0 eth0

对应网段从docker0接口发出去。

因为是NAT,考虑是不是使用了iptables:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ iptables -t nat -nL
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
DOCKER all -- 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
DOCKER all -- 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0
MASQUERADE tcp -- 172.17.0.26 172.17.0.26 tcp dpt:443
MASQUERADE tcp -- 172.17.0.26 172.17.0.26 tcp dpt:80
Chain DOCKER (2 references)
target prot opt source destination
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:32770 to:172.17.0.26:443
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:32771 to:172.17.0.26:80

确实是用的iptables来实现NAT和转发。所以流量大时会影响网络性能。

Data volume

可以使用-v 这个参数来挂载数据卷。如
docker run -it --name web -v /mydata tyrchen/debian:v1

在container中可以看到

1
2
3
4
5
6
root@287e45297bac:/# df -h
Filesystem Size Used Avail Use% Mounted on
none 30G 4.5G 24G 16% /
tmpfs 497M 0 497M 0% /dev
shm 64M 0 64M 0% /dev/shm
/dev/disk/by-uuid/c70a26ec-1dda-455c-acfd-792015b2bb6f 30G 4.5G 24G 16% /mydata

/mydata的文件系统为/dev/disk/by-uuid/c70a26ec-1dda-455c-acfd-792015b2bb6f

docker inspect web 可以看到

1
2
3
4
5
6
"Volumes": {
"/mydata": "/var/lib/docker/volumes/71220874b72d28b470e463c872cd91a39e23f271d7a415fb24d41b57504fa227/_data"
},
"VolumesRW": {
"/mydata": true
},

/mydata在宿主host中的实际文件位置,以及/mydata可读可写。

如果在container中的/home目录下dd if=/dev/zero of=/home/file bs=1024k count=10,在宿主host中ls -lh /var/lib/docker/volumes/71220874b72d28b470e463c872cd91a39e23f271d7a415fb24d41b57504fa227/_data并不能看到文件,但是如果在container的/mydata目录下dd,在host的/var/lib/docker/volumes/71220874b72d28b470e463c872cd91a39e23f271d7a415fb24d41b57504fa227/_data就能看到该文件。相当于这个挂载点/文件 在host和container中共享。

可以指定挂载文件和位置 docker run -it --name web -v /home/docker:/data[:ro] tyrchen/debian:v1
将host的/home/docker挂载到container的/data,并且可以指定read-only模式,除了挂载目录也可以挂载单独的文件。

也可以创建专门的data volume container来供其他container挂载。

1
2
$ docker create --name sharedata -v /home/ubuntu:/mydata tyrchen/debian:v1
$ docker run -it --name usedata --volumes-from sharedata tyrchen/debian:v1

如果需要删除挂载了卷的容器,需要docker rm -v,包括初始的sharedata以及后续的usedata,如果不加-v,那么将会存在一个泄露的volume并不再被容器使用。

Dockerfile

具体内容可以参考官方文档

  • FROM 容器运行的base image
  • MAINTAINER 维护者的信息
  • RUN 需要运行的指令
  • CMD
    CMD指定在dockerfile中只能存在一条,如果有多条将只有最后一条生效,当docker run命令没有指定command时将会执行CMD指令指定的command。
  • LABEL image的键值对信息,可以使用docker inspect命令查看
  • EXPOSE 需要监听的端口
  • ENV 环境变量
  • COPY 向image中复制文件
  • ENTRYPOINT
    docker run中指定的command将作为传递给ENTRYPOINT的参数。ENTRYPOINT有2种写法,这两种写法的行为是不一样的。
    ENTRYPOINT ["executable", "param1", "param2"] 这是推荐的形式
    ENTRYPOINT command param1 param2 shell形式
    这里如果使用shell形式写一个ENTRYPOINT 为 ENTRYPOINT cat,那么docker run test/test /etc/passwd运行后docker ps看到的COMMAND为/bin/sh -c cat /etc/passwd,即ENTRYPOINT后的部分将作为/bin/sh -c的参数,并且也将不会传递信号。比如docker stop将不会向容器中的进程传递SIGTERM。
    如果使用第一种方式,那么docker ps就能看到COMMAND为cat /etc/passwd
    如果同时指定ENTRYPOINT和CMD,那么CMD将作为ENTRYPOINT的参数。
    可以阅读stackoverflow上的解释
  • VOLUME 挂载共享卷
  • USER 指定daemon的运行身份
  • WORKDIR 指定工作目录

Advanced

network

docker run -it --net=host conan/debian host使得容器拥有宿主机网卡的完全访问权限,不存在bridge方式的开销。

性能消耗

在bridge模式下,由于需要进行NAT转换,流量大时会出现比较明显的性能消耗。

另一个是磁盘IO密集型的应用,因为docker的文件系统是aufs多层文件系统。

docker实例构建

Nginx

参考官方Dockerfile

shadowsocks

这里尝试搭建一个shadowsocks container

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# shadowsocks
FROM debian
MAINTAINER Tyr Chen <i@tyr.so>
RUN apt-get update && apt-get install -y python-pip
RUN pip install shadowsocks
RUN rm -rf /var/lib/apt/lists/*
RUN echo ' \
{ \
"server":"0.0.0.0", \
"port_password": { \
"8080": "password" \
}, \
"local_address": "127.0.0.1", \
"local_port":1080, \
"timeout":300, \
"method":"rc4-md5" \
}' > /etc/shadowsocks.json
RUN useradd -M -s /usr/sbin/nologin ssuser
EXPOSE 8080
CMD ["ssserver","-c","/etc/shadowsocks.json","--user","ssuser"]

运行: docker run --name ss -d --net=host tyrchen/shadowsocks:v1

为了避免NAT带来的性能损失,这里直接绑定到物理网卡上。

dnsmasq+udp forward

十分钟架设DNS转发缓存服务器 中的UDP转发和dnsmasq打包进container

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
46
47
48
49
50
51
52
53
54
55
56
FROM debian
MAINTAINER Tyr Chen <i@tyr.so>
RUN apt-get update && apt-get -y install dnsmasq && apt-get install -y socat
RUN echo ' \
no-resolv \
no-poll \
server=223.5.5.5 \
server=223.6.6.6 \
conf-dir=/etc/dnsmasq.d \
' >> /etc/dnsmasq.conf
RUN echo ' \
# Google and Youtube \
server=/.google.com/127.0.0.1# 5353 \
server=/.google.com.hk/127.0.0.1# 5353 \
server=/.gstatic.com/127.0.0.1# 5353 \
server=/.ggpht.com/127.0.0.1# 5353 \
server=/.googleusercontent.com/127.0.0.1# 5353 \
server=/.appspot.com/127.0.0.1# 5353 \
server=/.googlecode.com/127.0.0.1# 5353 \
server=/.googleapis.com/127.0.0.1# 5353 \
server=/.gmail.com/127.0.0.1# 5353 \
server=/.google-analytics.com/127.0.0.1# 5353 \
server=/.youtube.com/127.0.0.1# 5353 \
server=/.googlevideo.com/127.0.0.1# 5353 \
server=/.youtube-nocookie.com/127.0.0.1# 5353 \
server=/.ytimg.com/127.0.0.1# 5353 \
server=/.blogspot.com/127.0.0.1# 5353 \
server=/.blogger.com/127.0.0.1# 5353 \
\
# FaceBook \
server=/.facebook.com/127.0.0.1# 5353 \
server=/.thefacebook.com/127.0.0.1# 5353 \
server=/.facebook.net/127.0.0.1# 5353 \
server=/.fbcdn.net/127.0.0.1# 5353 \
server=/.akamaihd.net/127.0.0.1# 5353 \
\
# Twitter \
server=/.twitter.com/127.0.0.1# 5353 \
server=/.t.co/127.0.0.1# 5353 \
server=/.bitly.com/127.0.0.1# 5353 \
server=/.twimg.com/127.0.0.1# 5353 \
server=/.tinypic.com/127.0.0.1# 5353 \
server=/.yfrog.com/127.0.0.1# 5353 \
' > /etc/dnsmasq.d/domain
RUN echo ' \
# !/bin/bash \
socat -ly udp4-listen:5353,reuseaddr,fork tcp:157.7.7.7:5353 & \
dnsmasq -d \
' > /start.sh
RUN chmod +x /start.sh
EXPOSE 53
ENTRYPOINT ["/bin/bash"]
CMD ["/start.sh"]

运行: `docker run -d -p 53:53/udp –name dns tyrchen/dnsmasq

搭建本地docker registry

需要https证书

1
docker run -d -p 5000:5000 -v /home/certs/tyr.so/certs -e REGISTRY_HTTP_TLS_CERTIFICATE=/home/certs/i.tyr.so/2.crt -e REGISTRY_HTTP_TLS_KEY=/home/certs/tyr.so/2.key --restart=always --name registry registry:2
如果您觉得这篇文章对您有帮助,不妨支持我一下!