【本文为根据资料整理总结得到,性能分析部分为引用他人结果。】
公用一个内核和某些运行时环境(一些系统命令和系统库),但却彼此看不到,都以为系统中只有自己存在,这种机制就是容器。容器不是虚拟机,而是进程。
Docker是一个能够把开发的应用程序部署到容器的开源引擎,可以轻松的为任何应用创建一个标准化、轻量级、可移植、自给自足的容器。它能把开发环境直接封装成镜像,然后直接部署到生产环境,速度快且不容易出问题。
Docker容器借鉴了“集装箱”的设计理念,Docker在英文中的意思是“码头工人“,码头工人的工作是使用集装箱搬运货物,Docker则是使用容器搬运各种应用程序。
docker容器可以实现虚拟机隔离应用环境的功能,但开销比虚拟机小。同传统的虚拟化及版虚拟技术相比,容器运行不需要模拟层(emulation layer)和管理层(hypervisor layer),而是使用操作系统的系统调用接口。
Docker解决了应用隔离、移植、服务器资源充分利用及高效运维等问题。它的slogan是「一次构建,到处交付」(Docker - Build, Ship, and Run Any App, Anywhere)。
Docker容器为一个软件打包了一个完整的文件系统,该文件系统包含了该软件运行所需的全部可安装内容:代码、运行时环境、系统工具、动态链接库。这样就保证了软件可以忽略它所处的外部系统,始终运行在一致的环境中。
Docker的作用包括:
- Docker使开发运维过程标准化。现有的部署系统从业务程序包的维度操作,开发和运维之间缺乏通用的标准,部署系统往往都与具体的业务绑定。开发、测试、运维之间的交付物没有标准,运行环境没有标准,部署本身也没有标准。
- 很多个应用不隔离直接装在服务器上不但容易出现资源冲突,如依赖环境不同、端口冲突、内存资源抢占等问题,也可能出现不可知的安全问题;如果在一台服务器上装多个虚拟机来部署不同应用,虽能避免问题,但又太耗资源,而Docker在应用隔离安全性与资源使用效率方面取得了较好的平衡。
- Docker还可以屏蔽多种(不同,但相似,如Centos和Ubuntu)操作系统之间的差异性,避免部署问题。
- 支持自动化测试和持续集成、发布;
Docker本身并没有多大的技术创新,它使用的一些技术,比如aufs/dm、cgroup、capability、namespace都是已经存在了很久的技术,它最大的创新就在于它让互联网软件的所有环节都标准化起来。只要你的应用程序遵循这个标准,就可以在任何地方运行,这也是Docker的初衷。docker并不能部署的工作「减少为0」,比较好的情况下是「基本减少为1」。
Docker系统有两个程序:docker服务端(又称Docker引擎)和docker客户端。其中docker服务端是一个服务进程,管理着所有的容器。docker客户端则扮演着docker服务端的远程控制器,可以用来控制docker的服务端进程。大部分情况下,docker服务端和客户端运行在一台机器上。
Docker引擎提供了一组REST API供客户端与之交互,各种docker功能的实现都是以远程调用的形式在服务端Docker引擎完成的。
Doker镜像是由多个文件系统叠加而成的。一个镜像可以放到另一个镜像的顶部,位于下面的镜像称为父镜像,最底部的镜像称为基础镜像。(union mount,一次同时加载多个文件系统,但是外面看起来只能看到一个文件系统,该文件系统包含所有底层的文件和目录。)
当从一个镜像启动时,Docker会在该镜像的最顶部加载一个读写文件系统。每一层构建完的镜像不再改变,增删改操作都只在顶层读写层标记或复制完成,且原始文件会一直跟随镜像。
当Docker第一次启动容器时,初始的读写层是空的,当文件系统发生变化时,这些变化都会应用到这一层(利用写时复制机制,底层镜像当中的文件依然只读,而且被隐藏)。当容器消亡时,读写层的内容也会随之消失。当运行多个容器时,他们可以公用某些基础镜像,以减小容器占用内存。
按照Docker最佳实践的要求,容器不应该向其存储层内写入任何数据,容器的存储层要保持无状态化,所有文件写入操作都应该使用数据卷(Volume)或绑定宿主目录,在这些位置的读写会调过容器的存储层,直接对宿主或网络存储发生读写,性能和稳定性更高。
Registry来保存用户构建的镜像,分为公共和私有两种。Docker公司有DockerHub,允许用户注册分享自己的镜像(类似GitHub),仓库也可以保存私有镜像,但需要付费购买空间。个人和机构也可以建设私有Registry,官方有提供的仓库镜像。
镜像是Docker声明周期中的构建或打包阶段,而容器则是启动和执行阶段。镜像是静态的定义,容器是镜像运行时的实体。容器里的镜像可以进行创建、启动、关闭、重启、销毁,Docker执行上述操作时并不关心容器内部是什么。
卷是在一个或多个容器内被选定的目录,可以绕过分层的联合文件系统(Uinon File System),为Docker提供持久数据或者是共享数据。对卷的修改会直接生效,并且绕过镜像。当提交或创建镜像时,卷不被包含在镜像里。卷可以在容器间共享,即便容器停止,卷里的内容依然存在。
数据卷的使用,类似Linux下对目录或文件进行mount,镜像中被指定为挂载点的目录中的文件会被隐藏掉,能显式看到的是挂在的数据卷。
删除容器时删除数据卷:docker rm -v
卷的用途:
1. 希望同时对代码做开发和测试
2. 代码改动很频繁,不想在开发过程中重构镜像
3. 希望在多个容器间共享代码
如果有一些持续更新的数据需要在容器之间共享,最好创建数据卷容器。数据卷容器其实就是一个正常的容器,专门用来提供数据卷供其他容挂载。
可以利用数据卷容器对数据进行备份、回复、迁移。
一般来说,我们并不是真正完全创建新镜像,而是基于一个已有的基础镜像,如ubuntu或fedora等,构建自己的镜像而已。创建Docker镜像有多种方式,下面主要介绍两种,推荐使用第2种dockerfile方式。
docker commit提交镜像是将容器的存储层保存下来成为镜像,即在原有镜像的基础上,再叠加上容器的存储层,生成一个新的只读文件系统。
docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]
Docker commit的创建过程:
- 启动一个容器,然后在其中进行修改,修改完成后使用exit命令退出。
- 修改完成后,运行docker commit提交修改。
docker commit 容器ID 目标镜像仓库/镜像名
这种方式的缺点:
dokerfile+docker build创建镜像是一种更强大、灵活的镜像构建方式。
Dockerfile是一个文本文件,其中包含了一条条指令,每一条指令会构建一层(commit一次,有最大层数限制,比如aufs限制127层,因此docker指令行数不能过多),因此每一条指令的内容就是描述该层应当如何构建。
Dockerfile示例
FROM docker.io/centos #FROM是必备指令,且必须是第一条,用来指定基础镜像
RUN yum install wget
RUN echo 'I am vacing'
RUN echo "hello world " > ./a.txt
COPY ./a.txt /home/oicq/
Docker build的工作原理:
Docker build命令构建镜像,并非在本地构建,而是在服务端。client在发起build请求时携带用户指定的环境文件(上下文)上传给Docker引擎,Docker引擎在收到上下文包时就会获得镜像所需的一切文件,并根据其中的dockerfile进行镜像构建。
Docker命令只能使用上下文中的文件,也只能引用相对路径引用这些文件。使用.dockerignore文件可以指定哪些文件不作为上下文传递到Docker引擎。
在Dockerfile中连续两行的命令,在两个不同的容器当中,执行环境完全不同。因此,上一条命令指定的目录,对下一条无用,这时需要使用WORKDIR来指定工作目录。
docker push
docker search
docker pull
docker run -ti --name=centos_2 centos /bin/bash
#-t让Docker分配一个伪中端并绑定到容器的标准输入
#-i让容器的标准输入保持打开
#--name可以为容器指定一个名称,允许字符[a-zA-Z0-9_.-]
#/bin/bash是指要在ubuntu容器中执行的命令
docker -ps -a #查看当前系统中的容器列表,默认只能看到正在运行的容器,-a表示看到全部
docker start 指定容器; # 重新启动已经停止的容器
#三种方式可以唯一指定容器:端UUID、长UUID、名称。
docker attach 指定容器; # 附着到容器会话
docker logs --tail -0 -f 容器; # 查看容器的stdout日志
当利用docker run
来创建容器时,Docker后台的标注难操作包括:
- 检查本地是否存在指定的镜像,不存在就从共有仓库下载
- 利用镜像启动并创建一个容器
- 分配一个文件系统,并在只读的竞相层外面挂在一层可读写层。
- 从诉诸主机配置的网桥接口中接一个虚拟接口到容器中去
- 从地址池配置一个IP地址给容器
- 执行用户指定的应用程序
- 执行完毕后容器被终止
容器的核心为所执行的应用程序,所需要的资源都是应用程序运行所必须的,除此之外没有其他资源。
docker exec 命令是从Docker 1.3引入的,早期Docker参考nsenter命令。
容器内运行的进程有两种类型:后台任务和交互式任务。后台任务在容器内运行且没有交互需求,交互式任务则保持 在前台运行。
docker exec -d 容器 命令; #在容器内运行命令,-d标识后台任务
docker exec -t -i 容器 命令; #容器内运行交互式任务命令
docker exec -ti zhiyun /bin/bash; #退出该shell不会影响容器运行。
docker stop/kill 容器; #停止容器,stop发送SIGTERM信号,kill发送SIGKILL信号。
docker run --restart=alway... #重启容器
docker inspect #获取更多容器信息
docker rm 容器 #删除容器,运行中的docker容器需要先停止
docker rm `docker ps -a -q` # 一次删除全部容器
docker images -f dangling=true #显示虚悬镜像
在Docker的世界里,编配用描述一组时间过程,这个过程会管理多个运行在多个Docker容器里的应用,而这些应用有可能运行在多个宿主机上。
前者比较简单,重点是依赖。通常有以下几种依赖:
选择一个目录,创建名为Dockerfile的文本文件。
1. 选定或构建base image
官方的Docker Registry有很多base image,比如centos6/centos7/ubuntu14.04等。这些image里面,包含了对应版本的系统程序,配置文件,库等内容,体积一般一百到两百兆。由于同样的内容只需分发一次,一般来说,为了提高我们的image的分发效率,还是尽量选择这些最常见系统版本的base image。特殊情况下,也可以自己创建特殊的base image。如何从头创建base image。
比如我们选择centos7作为base image,那么在Dockerfile中写下第一行:
FROM docker.oa.com:8080/library/centos7:latest
2. 安装其他依赖包
例如GAIA依赖Java开发环境,需要在我们的Docker image中安装对应的包。
在Dockerfile总增加:
RUN yum -y install java-1.6.0-openjdk-devel.x86_64 wget
ENV JAVA_HOME /usr/lib/jvm/java
ENV PATH $JAVA_HOME/bin:$PATH
其中RUN指令会在docker build过程中下载docker.oa.com:8080/library/centos7:latest
这个image, 启动一个container, 在container中运行RUN指令后面的命令,然后执行docker commit保存这些指令所产生的变化为一个新的layer。
EVN定义的环境变量,不仅在docker build过程中有效,在构建出来的Docker image以后运行起来后也有效。
3.安装设置私有依赖环境
比如我们需要把hadoop-2.4.1.tar.gz解压放到固定的目录/gaia, 然后修改其中的一个文件。在Dockerfile中增加:
ADD hadoop-2.4.1.tar.gz /gaia/
ENV HADOOP_YARN_HOME /gaia/hadoop-2.4.1
RUN rm -rf $HADOOP_YARN_HOME/etc && sed -i "s/null &/null/g" $HADOOP_YARN_HOME/sbin/yarn-daemon.sh
命令为:docker build -t docker.oa.com:8080/library/gaia-base
.
要指定的参数是新构建出来的image的名字(tag), 还有Dockerfile所在的路径。
这里有一点要注意的是:Dockerfile中的ADD指令的源如果是本地文件,这个文件必须事先拷贝到Dockerfile所在目录或者其下级子目录,而不能在Dockerfile所在目录之外的其他地方,也不能是软链接。
docker push docker.oa.com:8080/library/gaia-base
docker pull docker.oa.com:8080/library/gaia-base
至此,我们完成了把一个应用程序及其依赖打包为一个Docker image的全部过程,从而可以非常方便的部署我们的应用程序了。只要一台机器支持docker,并能访问我们的dokcer registry (docker.oa.com:8080)
,无论它上面的操作系统是Suse, CentOS,无论这台机器上面有没有安装Java运行环境,也无论已经安装的Java运行环境是什么版本,都不影响我们的应用程序(Java)在这台机器上直接运行。因为通过打包为Docker image,我们的应用程序为自己带盐,不再依赖机器上现有的软件环境。
测试工具:Linpack,参考这里。
工具介绍:测试单位时间CPU浮点计算能力
测试方法:./xlinpack_xeon64 lininput_xeon64
图1 CPU性能对比,单位:10亿次双精度浮点运算(GFLOPs)/秒
测试工具:STREAM,参考这里。
工具介绍:
测试内存带宽,4种操作:COPY,SCALE,SUM,TRIAD。
Name | Kernel | Bytes/Iteratio | FLOPS/Iteration |
---|---|---|---|
COPY | a(i) = b(i) | 16 | 0 |
SCALE | a(i) = q*b(i) | 16 | 1 |
SUM | a(i) = b(i) + c(i) | 24 | 1 |
TRIAD | a(i) = b(i) + q*c(i) | 24 | 2 |
测试方法:
#gcc -DSTREAM_ARRAY_SIZE=64000000 -O3 -o stream3 stream.c
执行stream 三次,取平均值。
图2 内存性能对比,单位:MB/秒
测试工具:netperf
工具介绍:测试单位时间内传输包的数量
测试方法:
B6运行netserver,4台C1运行netperf,每台C1 100个进程。
bin/netperf -H 10.193.x.x -p 12865 -l 300 -t TCP_RR -- -r 1,1 -P 0,$port
图3 TCP_RR对比,单位:Trans Rate/秒
测试工具 fio
测试方法
A5 Raid1+0
fio --filename=/data1/fio.dat --direct=1 --thread --rw=randrw --rwmixread=70 --ioengine=libaio --runtime=300 --iodepth=64 --size=4G --numjobs=1 -name=test_rw --group_reporting --bs=8k --time_base
可以看到,容器基本可以达到物理机的性能。
Docker在容器的基础上进行了进一步的封装,使镜像成为了一种像集装箱那样的标准货件,极大的简化了容器的创建和维护。Docker的产生对开发的影响不大,但对于部署来说,是场革命。可以极大的减少部署的时间成本和人力成本。
基于一件核心事物「标准化」,可以做更多的事情,比如集装箱的机械自动搬运等。