Docker

简介(与虚拟机区别)

Docker其实是在LXC的基础上(1.5之后不再默认使用LXC)提供了一个高层的容器引擎,具有以下优势:

  • 轻量级资源使用、资源隔离。容器在进程级别隔离并使用宿主机的内核,而不需要虚拟化整个操作系统。容器可以使用自己的cpu、memory等。
  • 可移植性。跨主机部署,Docker镜像屏蔽了不同OS差异,它可以将环境配置进行抽象和应用打包,保证在不同的硬件机器上都可以部署。【代码+配置+OS】
  • 以应用为中心、自动构建、版本管理、组件重用、镜像共享。


    传统VM的镜像只是基础镜像,docker镜像是基础加应用,是一个软件从最顶层一直到最底层系统库的完整依赖栈。有了这样一个完整的依赖栈,再加上容器技术的隔离性,就能在任何地方把应用启动起来,且保证行为一致,只要内核在docker要求的最低版本之上就行【并不是模拟一个完整的OS】。
    虚拟机的缺点:需要Hypervisor实现硬件资源虚拟化,内存资源占用高;启动慢,需要加载OS;
    image.png
    容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的命名空间。
    容器销毁,数据随之被删除。任何保存于容器存储层的信息都会随容器删除而丢失。

    架构

    Docker是一个C/S架构的程序,Docker Client是客户端,Docker daemon是服务端。

    组成

    Rootfs:文件系统隔离
    容器引擎:生命周期管理
    Cgroup:资源控制 control group;CPU、内存、block I/O和网络带宽
    Namespace:访问隔离;将内核的全局资源做封装,使得每个Namespace都有一份独立的资源,因此不同的进程在各自的Namespace内对同一资源的使用不会互相干扰
    Namespace内对同一资源的使用不会互相干扰。目前Linux内核总共实现了6中Namespace:

  • IPC Namespace:隔离型号量、消息队列和共享内存

  • Network Namespace:隔离网络资源
  • Mount Namespace:隔离文件系统挂载点(每个容器看到不同的文件系统层次结构)
  • PID Namespace:隔离进程ID
  • UTS Namespace:隔离主机名与域名(每个容器有自己的hostname和domain name)
  • User Namespace:隔离用户和用户组


    通过Cgroup和Namespace实现了资源的隔离,从而实现了轻量级的虚拟化;
    主机系统通过Cgroup和Namespace将整个服务器划分为多个bins/libs,每个app运行在独立的bins/libs中,每个bins/libs相当于从服务器中划分出的独立的资源
    Docker/libcontainer:前者基于后者,前者处理上层业务,后者与内核交互,管理namespaces、cgroups、capabilities以及文件系统,为前者提供处理容器的创建(Factory)、容器生命周期管理(Container)、进程生命周期管理(Process)等一系列接口

    当需要为Docker创建网络环境时,通过网络管理驱动networkdriver创建并配置Docker容器网络环境;当需要限制Docker容器运行资源或执行用户指令等操作时,则通过execdriver来完成。而libcontainer是一项独立的容器管理包,networkdriver以及execdriver都是通过libcontainer来实现具体对容器进行的操作。

    镜像(分层)

    Docker通过把应用的运行时环境和应用打包在一起,解决了部署环境依赖的问题;
    通过引入分层文件系统这种概念,解决了空间利用率的问题;
    不同的容器可以共享底层的只读镜像,通过写入自己特有的内容后添加新的镜像层,新增的镜像层和下层的镜像一起又可以作为基础镜像被更上层的镜像使用。这种特性可以极大地提高磁盘利用率。

    Docker镜像是由多个文件系统(只读层)叠加而成。当我们启动一个容器的时候,Docker会加载只读镜像层并在其上添加一个读写层。如果运行中的容器修改了现有的一个已经存在的文件,那该文件将会从读写层下面的只读层复制到读写层,该文件的只读版本仍然存在,只是已经被读写层中该文件的副本所隐藏。当删除Docker容器,并通过该镜像重新启动时,之前的更改将会丢失。在Docker中,只读层及在顶部的读写层的组合被称为Union File System(联合文件系统)。 【对文件系统的修改作为一次commit来一层层地叠加】
    Union FS包括bootfs(引导文件系统)和rootfs。前者是linux内核,后者是linux发行版。
    image.png
    而一个linux发行版,如centos镜像很小的原因(完整版4G,镜像200M),即与宿主机共用了bootfs,仅自己打包了rootfs部分。

Dockerfile

  1. FROM reg.docker.alibaba-inc.com/infrastructure/infrastructure-base-6u2
  2. MAINTAINER dongkai.dk <dongkai.dk@alibaba-inc.com>
  3. RUN yum install -y curl
  4. COPY ./start.sh /home/admin/
  5. WORKDIR /home/admin
  6. CMD [ "/bin/bash", "start.sh" ]


从FROM命令开始,Docker就已经运行了一个容器来执行后续的所有操作,所有非FROM命令的构建都是在容器里面执行的,和宿主机的环境基本没有关系。并且完成一条构建,就会产生一个新的镜像,通过docker commit进行提交。Dockerfile中的每一条命令都会构建出一个新的镜像层。

网络

1)Host网络类型
和宿主机共用一个Network Namespace,不会虚拟出自己的网卡,配置自己的IP。也就是说启动的容器和宿主机共用同一网络,访问容器里面的应用就和访问宿主机里面的应用一样。

2)Container网络类型
指定新创建的容器和已有的容器共用同一个Network Namespace。在启动容器的时候可以指定和已有的容器共用同一网络。

3)None网络类型
关闭了容器的网络功能。

4)Brideg网络类型
也是Docker默认的网络类型,使用独立的Network Namespace,并连接到docker0虚拟网卡
我们在安装完Docker之后,宿主机就会默认出现一个Docker0的虚拟网卡,用于Docker和宿主机之前的通信。这是可以通过在启动容器的时候设置端口映射,就可以通过宿主机IP加Port访问到端口所映射的应用。

Docker Compose

Docker compose是服务编排的模板,可以批量部署应用、多应用部署。

一个service对应一个镜像,可以使用Dockerfile来描述,也可以直接用官方镜像;

  1. version: '2'
  2. services:
  3. web:
  4. build: .
  5. ports:
  6. - "5000:5000"
  7. redis:
  8. image: "redis:alpine"


sudo docker-compose up —scale web=3 -d 扩容

Docker-Compose只能管理当前主机上的Docker,也就是说不能去启动其他主机上的Docker容器。
Docker Swarm 是一款用来管理多主机上的Docker容器的工具,可以负责帮你启动容器,监控容器状态,如果容器的状态不正常它会帮你重新帮你启动一个新的容器,来提供服务,同时也提供服务之间的负载均衡,而这些东西Docker-Compose 是做不到的。

CI/CD

开发者向Github提交代码,Github通过webhook通知Jenkins有更新,Jenkins从Github下拉取最新代码Dockerfile以及其他文档,在slave构建节点构建Docker镜像,然后启动容器执行测试代码,如果测试通过就可以把Docker镜像推送到Docker镜像仓库供生成环境使用。

Volumn

为了能够保存(持久化)数据以及共享容器间的数据,Docker提出了Volume的概念。简单来说,Volume就是目录或者文件,它可以绕过默认的联合文件系统,而以正常的文件或者目录的形式存在于宿主机上。

AliDocker

Alidocker是在官方Docker代码的基础上加入了少量功能构建而成,所以Alidocker完全可以拿来当官方Docker一样使用,不会有任何问题。T4和Docker一样,都建立在cgroup、namespace等相同的内核特性上。其中磁盘空间隔离和资源可见性是T4所独有的。除了T4所具有的优势外,Alidocker还做了一件非常重要的事情,Alidocker把Docker daemon和容器生命周期进行解耦。官方的Docker如果Docker daemon进程挂掉了,所有容器都将会退出。Alidocker修正这个缺陷,给Docker daemon的升级以及容器的稳定性带来很大的优势。Alidocker也和Aone进行了无缝集成,使得应用的部署发布流程更加的灵活简单。有关Alidocker更多的知识大家可以参考阿里人,请用AliDocker(现在的名字是Pouch)。

Link

https://www.jianshu.com/p/21d66ca6115e

有一种方式是使用link。link可以不必直接指定容器IP,而是指定容器名/ID,即使容器重建,也是可以重连的。
—link oracle_instance:oracle -p 1521:1521
—link :alias
其中,name和id是源容器的name和id,alias是源容器在link下的别名。

站在应用容器的角度,oracle_instance和oracle和hub都是该容器的名字,
并且作为容器的hostname,应用容器用这2个名字中的哪一个都可以访问到该容器并与之通信(docker通过DNS自动解析)。

源容器和接收容器之间传递数据是通过以下2种方式:

  • 设置环境变量
  • 更新/etc/hosts文件

docker run -d --name app --link oracle_instance:oracle app镜像名

环境变量

当使用—link时,docker会自动在接收容器内创建基于—link参数的环境变量:

_NAME

docker会在接收容器中设置名为_NAME的环境变量,该环境变量的值为:
_NAME=/接收容器名/源容器alias

_PORT

另外,docker还在接收容器中创建1个名为_PORT的环境变量,值为源容器的URL:源容器暴露的端口号中最小的那个端口号。

  1. env | grep -i ORACLE_PORT=
  2. ORACLE_PORT=tcp://172.17.0.2:4444

接收容器环境变量中存储的源容器的IP,不会自动更新,即,若源容器重启,则接收容器环境变量中存储的源容器的IP很可能就失效了。所以,docker官方建议使用/etc/hosts来解决上述的IP失效问题。

更新/etc/hosts文件

docker会将源容器的host更新到目标容器的/etc/hosts中。
查看目标容器的/etc/hosts:

  1. 172.17.0.2 oracle 1cbbf6f07804 oracle_instance
  2. 172.17.0.3 c4cc05d832e0

后者是目标容器的容器ID
前者是源容器别名,源容器ID,源容器名

如果重启了源容器,接收容器的/etc/hosts会自动更新源容器的新ip。

Dockerfile详解

https://juejin.im/post/5a1bd8a36fb9a0450f21a966
https://juejin.im/post/5922e07cda2f60005d602dcd

Dockefile示例:

  1. # Base images 基础镜像
  2. FROM centos
  3. #MAINTAINER 维护者信息
  4. MAINTAINER lorenwe
  5. #ENV 设置环境变量
  6. ENV PATH /usr/local/nginx/sbin:$PATH
  7. #ADD 文件放在当前目录下,拷过去会自动解压
  8. ADD nginx-1.13.7.tar.gz /tmp/
  9. #RUN 执行以下命令
  10. RUN rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 \
  11. && yum update -y \
  12. && yum install -y vim less wget curl gcc automake autoconf libtool make gcc-c++ zlib zlib-devel openssl openssl-devel perl perl-devel pcre pcre-devel libxslt libxslt-devel \
  13. && yum clean all \
  14. && rm -rf /usr/local/src/*
  15. RUN useradd -s /sbin/nologin -M www
  16. #WORKDIR 相当于cd
  17. WORKDIR /tmp/nginx-1.13.7
  18. RUN ./configure --prefix=/usr/local/nginx --user=www --group=www --with-http_ssl_module --with-pcre && make && make install
  19. RUN cd / && rm -rf /tmp/
  20. COPY nginx.conf /usr/local/nginx/conf/
  21. #EXPOSE 映射端口
  22. EXPOSE 80 443
  23. #ENTRYPOINT 运行以下命令
  24. ENTRYPOINT ["nginx"]
  25. #CMD 运行以下命令
  26. CMD ["-h"]

FROM

FROM 表示的是这个 dockerfile 构建镜像的基础镜像是什么,有点像代码里面类的继承那样的关系,基础镜像所拥有的功能在新构建出来的镜像中也是存在的,一般用作于基础镜像都是最干净的没有经过任何三方修改过的,比如我用的就是最基础的 centos。

MAINTAINER

MAINTAINER 就是维护者信息了,填自己名字就可了,不用说什么了

ENV

ENV 设置环境变量,简单点说就是设置这个能够帮助系统找到所需要运行的软件,比如 “ENV PATH /usr/local/nginx/sbin:$PATH”,这句话的意思就是告诉系统如果运行一个没有指定路径的程序时可以从 /usr/local/nginx/sbin 这个路径里面找,只有设置了这个,后面才可以直接使用 ngixn 命令来启动 nginx,不然的话系统会提示找不到应用。

ADD

ADD 顾名思义,就是添加文件的功能了,但是他比普通的添加做的事情多一点,源文件可以是一个文件,或者是一个 URL 都行,如果源文件是一个压缩包,在构建镜像的时候会自动的把压缩包解压开来,示例我写的是 ‘ADD nginx-1.13.7.tar.gz /tmp/’ 其中 nginx-1.13.7.tar.gz 这个压缩包是必须要在 dockefile 文件目录内的,不在 dockerfile 文件目录内的 比如你写完整路径 D:test/nginx-1.13.7.tar.gz 都是会提示找不到文件的。

RUN

RUN 就是执行命令的意思了,RUN 可以执行多条命令, 用 && 隔开就行,如果命令太长要换行的话在末尾加上 ‘\’ 就可以换行命令,RUN 的含义非常简单,就是执行命令,但其中有一些细节还是需要注意的,现在就通过上面的示例来看看需要注意的地方有哪些吧。其中 RUN rpm —import /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 的作用就是导入软件包签名来验证软件包是否被修改过了,为做到安全除了系统要官方的之外软件也要保证是可信的。yum update -y 升级所有包,改变软件设置和系统设置,系统版本内核都升级,我们知道 linux 的软件存在依赖关系,有时我们安装新的软件他所依赖的工具软件也需要是最新的,如果没有用这个命令去更新原来的软件包,就很容易造成我们新安装上的软件出问题,报错提示不明显的情况下我们更是难找到问题了,为避免此类情况发生我们还是先更新一下软件包和系统,虽然这会使 docker 构建镜像时变慢但也是值得的,至于后面的命令自然是安装各种工具库了,接着来看这句 yum clean all ,把所有的 yum 缓存清掉,这可以减少构建出来的镜像大小,rm -rf /usr/local/src/ 清除用户源码文件,都是起到减少构建镜像大小的作用。RUN 指令是可以分步写的。
因为 dockerfile 构建镜像时每执行一个关键指令都会去创建一个镜像版本,这有点像 git 的版本管理,比如执行完第一个 RUN 命令后在执行第二个 RUN 命令时是会在一个新的镜像版本中执行,这会导致 yum clean all 这个命令失效,没有起到精简镜像的作用,虽然不推荐多写几个 RUN,但也不是说把所有的操作都放在一个 RUN 里面,这里有个原则就是把所有相关的操作都放在同一个 RUN 里面,就比如我把 yum 更新,安装工具库,清除缓存放在一个 RUN 里面,后面的编译安装 nginx 放在另外一个 RUN 里面。

WORKDIR

WORKDIR 表示镜像活动目录变换到指定目录,就相当于 linux 里面 cd 到指定目录一样,其实完全没有必要使用这个指令的,在需要时可以直接使用 cd 命令就行,因为这里使用了 WORKDIR,所以后面的 RUN 编译安装 nginx 不用切换目录。也就是运行RUN / CMD / ENTRYPOINT指令的地方。

也是终端登录进来的默认路径。

COPY

COPY 这个指令很简单,就是把文件拷贝到镜像中的某个目录,注意源文件也是需要在 dockerfile 所在目录的,示例的意思是拷贝一份 nginx 配置文件,现在就在 dockerfile 所在目录创建这个文件。

EXPOSE

EXPOSE 示例注释写的是映射端口,但我觉得用暴露端口来形容更合适,因为在使用 dockerfile 创建容器的时候不会映射任何端口,映射端口是在用 docker run 的时候来指定映射的端口,比如我把容器的 80 端口映射到本机的 8080 端口,要映射成功就要先把端口暴露出来,有点类似于防火墙的功能,把部分端口打开。

ENTRYPOINT&CMD(不可覆盖,可接CMD;可被覆盖)

ENTRYPOINT 和 CMD 要放在一起来说,这两者的功能都类似,但又有相对独特的地方,他们的作用都是让镜像在创建容器时运行里面的命令。在执行 docker run 时 ENTRYPOINT 和 CMD 里面的命令是会执行的,两者是可以单独使用,并不一定要同时存在,当然这两者还是有区别的。
先从 CMD 说吧,CMD 的一个特点就是可被覆盖,比如CMD 填写[“nginx”],构建好镜像后直接使用 docker run nginx 命令执行的话通过 docker ps 可以看到容器正常运行了,启动命令也是 “nginx”,但是我们使用 docker run nginx bin/bash 来启动的话通过 docker ps 查看到启动命令变成了 bin/bash,这就说明了 dockerfile 的 CMD 指令是可被覆盖的,也可以把它看做是容器启动的一个默认命令,可以手动修改的。
而 ENTRYPOINT 恰恰相反,他是不能被覆盖,也就是说指定了值后再启动容器时不管你后面写的什么 ENTRYPOINT 里面的命令一定会执行,通常 ENTRYPOINT 用法是为某个镜像指定必须运行的应用,例如我这里构建的是一个 nginx 镜像,也就是说这个镜像只运行 nginx,那么我就可以在 ENTRYPOINT 写上[“nginx”],有些人在构建自己的基础镜像时(基础镜像只安装了一些必要的库)就只有 CMD 并写上 [‘bin/bash’],当 ENTRYPOINT 和 CMD 都存在时 CMD 中的命令会以 ENTRYPOINT 中命令的参数形式来启动容器(当然也可以在docker run时附加参数,相当于CMD)

脚本

ENTRYPOINT是一个脚本,它会默认执行,并且将指定的命令错误其参数。它通常用于构建可执行的Docker镜像。entrypoint.sh如下:

  1. #!/usr/bin/env sh
  2. # $0 is a script name,
  3. # $1, $2, $3 etc are passed arguments
  4. # $1 is our command
  5. CMD=$1
  6. case "$CMD" in
  7. "dev" )
  8. npm install
  9. export NODE_ENV=development
  10. exec npm run dev
  11. ;;
  12. "start" )
  13. # we can modify files here, using ENV variables passed in
  14. # "docker create" command. It can't be done during build process.
  15. echo "db: $DATABASE_ADDRESS" >> /app/config.yml
  16. export NODE_ENV=production
  17. exec npm start
  18. ;;
  19. * )
  20. # Run custom command. Thanks to this line we can still use
  21. # "docker run our_image /bin/bash" and it will work
  22. exec $CMD ${@:2}
  23. ;;
  24. esac

Dockerfile:

  1. FROM node:7-alpine
  2. WORKDIR /app
  3. ADD . /app
  4. RUN npm install
  5. ENTRYPOINT ["./entrypoint.sh"]
  6. CMD ["start"]

示例:
运行开发版本 docker run our-app dev
运行生产版本 docker run our-app start

在前文的entrypoint脚本中,我使用了exec命令运行node应用。不使用exec的话,我们则不能顺利地关闭容器,因为SIGTERM信号会被bash脚本进程吞没。exec命令启动的进程可以取代脚本进程,因此所有的信号都会正常工作。

VOLUMN

VOLUME,VOLUME指令创建一个可以从本地主机或其他容器挂载的挂载点,用法是比较多的,都知道 docker 做应用容器比较方便,其实 docker 也可做数据容器,创建数据容器镜像的 dockerfile 就主要是用 VOLUME 指令。
VOLUMN [“dir1”, “dir2”]
使用-v 宿主机目录:容器目录的挂载方式是无法使用VOLUMN实现的,因为宿主机目录因宿主机而异(EXPOSE同理)。
当docker run时没有指定宿主机目录,则会使用/var/lib/docker/volumns下的随机目录(端口映射则是随机端口),可以使用docker insepct查看映射到的宿主机目录。

LABEL

LABEL = = =
一个LABEL指定一层,尽量合并为一个指令,同名覆盖。

ARG

用来定义可以在docker build命令运行时传递给构建运行时的便利,在build时使用—build-arg即可。

  1. ARG build
  2. ARG webapp_user=user

第二条设置了一个默认值。
如docker build —build-arg build=1234
但是不要把秘钥等传入,会在构建过程以及构建历史中被暴露。
预定义ARG变量:
HTTP_PROXY
HTTPS_PROXY
FTP_PROXY
NO_PROXY

ONBUILD

为镜像添加触发器,当一个镜像被用作其他镜像的基础镜像时(比如用户镜像需要从某未准备好的位置添加源代码,或者用户需要执行特定于构建镜像的环境的构建脚本),该镜像中的触发器将会被执行。

  1. ONBUILD ADD . /app/src
  2. ONBUILD RUN cd /app/src && make

这些命令将在FROM之后被执行。
注意,触发器只能被继承一次。。

最佳实践

  • 编写.dockerignore文件
  • 容器只运行单个应用
  • 将多个RUN指令合并为一个
  • 基础镜像的标签不要用latest
  • 每个RUN指令后删除多余文件
    • 假设我们更新了apt-get源,下载,解压并安装了一些软件包,它们都保存在/var/lib/apt/lists/目录中。但是,运行应用时Docker镜像中并不需要这些文件。我们最好将它们删除,因为它会使Docker镜像变大。
  • 选择合适的基础镜像(alpine版本最好)
  • 设置WORKDIR和CMD
  • 使用ENTRYPOINT (可选)
  • 在entrypoint脚本中使用exec
  • COPY与ADD优先使用前者
  • 合理调整COPY与RUN的顺序
  • 设置默认的环境变量,映射端口和数据卷
  • 使用LABEL设置镜像元数据
  • 添加HEALTHCHECK
    • 运行容器时,可以指定--restart always选项。这样的话,容器崩溃时,Docker守护进程(docker daemon)会重启容器。对于需要长时间运行的容器,这个选项非常有用。但是,如果容器的确在运行,但是不可(陷入死循环,配置错误)用怎么办?使用HEALTHCHECK指令可以让Docker周期性的检查容器的健康状况。我们只需要指定一个命令,如果一切正常的话返回0,否则返回1。
    • HEALTHCHECK CMD curl —fail http://localhost:$APP_PORT || exit 1

应用部署:云游的编排

1、创建LB,返回地址,记录到数据库里
2、创建RDB,返回地址,记录到数据库里
3、创建xxx,返回地址,记录到数据库里
4、渲染应用的表达式,表达式的key是自己定义的,value是一个占位符,这个占位符是有规则的,如产品码.db_url
5、应用启动时将其注入到环境参数
6、应用需要编写一个配置文件,其key与环境参数相同,渲染value至占位符。这个由中间件来完成。