Docker是什么

Docker是一种虚拟化技术,类似虚拟机,这使得安装在其中的程序能够只依赖虚拟机的环境,而不受外部操作系统环境的影响。同虚拟机不同的是,Docker的虚拟容器占用空间更小,使得它比虚拟机更容易分发和多实例安装。

Docker容器化技术的整个开发使用方式非常类似java应用开发,这里同java应用开发做一个类比,帮助有过java开发经验的同学快速掌握其中的核心概念
1554642418878-1

Dockerfile

相当于Java应用开发中的Maven配置文件pom.xml或则gradle的build.gradle文件。java开发中的pom.xml和build.gradle是用来声明java应用依赖的jar包,和应用的构建方式。而Dockerfile是用来声明一个程序依赖的环境和构建运行方式。比如redis的Dockerfile如下:

# 第一部分,声明redis程序依赖系统环境,是使用的debian
    FROM debian:stretch-slim
	
	# 第二部分,配置系统权限,添加新的组和用户,专供redis使用
	RUN groupadd -r redis && useradd -r -g redis redis
	
	# 第三部分,是安装系统更新,环境变量配置,以及下载redis并安装
	ENV GOSU_VERSION 1.10
	RUN set -ex; \
		\
		fetchDeps=" \
			ca-certificates \
			dirmngr \
			gnupg \
			wget \
		"; \
		apt-get update; \
		apt-get install -y --no-install-recommends $fetchDeps; \
		rm -rf /var/lib/apt/lists/*; \
		\
		dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
		wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
		wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
		export GNUPGHOME="$(mktemp -d)"; \
		gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
		gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
		gpgconf --kill all; \
		rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc; \
		chmod +x /usr/local/bin/gosu; \
		gosu nobody true; \
		\
		apt-get purge -y --auto-remove $fetchDeps
	
	ENV REDIS_VERSION 5.0.4
	ENV REDIS_DOWNLOAD_URL http://download.redis.io/releases/redis-5.0.4.tar.gz
	ENV REDIS_DOWNLOAD_SHA 3ce9ceff5a23f60913e1573f6dfcd4aa53b42d4a2789e28fa53ec2bd28c987dd
	
	# for redis-sentinel see: http://redis.io/topics/sentinel
	RUN set -ex; \
		\
		buildDeps=' \
			ca-certificates \
			wget \
			\
			gcc \
			libc6-dev \
			make \
		'; \
		apt-get update; \
		apt-get install -y $buildDeps --no-install-recommends; \
		rm -rf /var/lib/apt/lists/*; \
		\
		wget -O redis.tar.gz "$REDIS_DOWNLOAD_URL"; \
		echo "$REDIS_DOWNLOAD_SHA *redis.tar.gz" | sha256sum -c -; \
		mkdir -p /usr/src/redis; \
		tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1; \
		rm redis.tar.gz; \
		\

		grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 1$' /usr/src/redis/src/server.h; \
		sed -ri 's!^(#define CONFIG_DEFAULT_PROTECTED_MODE) 1$!\1 0!' /usr/src/redis/src/server.h; \
		grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 0$' /usr/src/redis/src/server.h; \
		\
		make -C /usr/src/redis -j "$(nproc)"; \
		make -C /usr/src/redis install; \
		\
		rm -r /usr/src/redis; \
		\
		apt-get purge -y --auto-remove $buildDeps
		
	# 第四部分,设置redis后续命令的工作目录
	RUN mkdir /data && chown redis:redis /data
	VOLUME /data
	WORKDIR /data
	
	#第五部分,启动redis服务,并配置向外暴露的端口
	COPY docker-entrypoint.sh /usr/local/bin/
	ENTRYPOINT ["docker-entrypoint.sh"]
	
	EXPOSE 6379
	CMD ["redis-server"]

可能每个不同的Docker程序,其Dockerfile略有不同,但大致都可以总结为这么几步

  • 声明运行系统环境
  • 安装系统更新,安装程序
  • 配置环境变量
  • 设置向外暴露的端口,并启动程序

image

相当于java应用开发中的jar包。java中基于pom.xml或build.gradle build而成jar。而docker中,基于Dockerfile build出的是image。它可以像jar包一样,提交到Docker的中央仓库,并被下发指其它机器使用。一个使用Dockerfile构建image的demo如下:

  1. 先用python开发一个简单的web服务,名为app.py
 from flask import Flask
		from redis import Redis, RedisError
		import os
		import socket
		
		# Connect to Redis
		redis = Redis(host="redis", db=0, socket_connect_timeout=2, socket_timeout=2)
		
		app = Flask(__name__)
		
		@app.route("/")
		def hello():
		    try:
		        visits = redis.incr("counter")
		    except RedisError:
		        visits = "<i>cannot connect to Redis, counter disabled</i>"
		
		    html = "<h3>Hello {name}!</h3>" \
		           "<b>Hostname:</b> {hostname}<br/>" \
		           "<b>Visits:</b> {visits}"
		    return html.format(name=os.getenv("NAME", "world"), hostname=socket.gethostname(), visits=visits)
		
		if __name__ == "__main__":
		    app.run(host='0.0.0.0', port=80)
  1. 再编写Dockerfile
# 从程序代码中,我们知道使用的python,需要依赖python的环境。python环境的image在docker公共仓库中,可以直接使用,在这个image基础上,添加我们的应用,构建另一个image
		FROM python:2.7-slim
		
		# 把容器看做一个小型操作系统的话,这一步设置后续命令在这个容器操作系统内的路径。名字可以任意。相当于普通linux中的cd命令。路径不存在应该可以直接创建。Dockerfile后续的所有命令,都是在这个文件夹下执行的
		WORKDIR /app1
		
		# 将宿主机的当前路径内容拷贝到app1下
		COPY . /app1
		
		# 从app.py程序代码中,可以看到其依赖FLask库环境和Redis,这里通过pip安装,这一步是在python image的内部执行的,不是外部环境。相当于再给python的image系统镜像安装东西
		RUN pip install --trusted-host pypi.python.org Flask
		RUN pip install --trusted-host pypi.python.org Redis
		
		# 将容器的80端口暴露出来。
		EXPOSE 80
		
		# 在容器内设置一个环境遍历,key为NAME, value为world。就像linux中设置环境变量一样。只不过这里是在容器这个操作系统内设置环境变量,相应的容器中的程序可以读取这个环境变量
		ENV NAME World
		
		# 这一步放在最后,前面的所有命令基本上把程序要求的环境都初始化好了,这里直接执行命令,CMD的第一个参数是程序命令,后面的是参数。这里就是通过python来run app.py。 由于当前路径是/app1(前面WORKDIR设置的),并且其中包含app.py,所以在该路径下执行python app.apy当然找得到程序文件
		CMD ["python", "app.py"]
  1. 构建image
    在宿主机上创建一个文件夹,名字任意,将Dockerfile和app.py 都放置其中(因为Dockerfile中有一个命令COPY . /app1,所以要确保程序跟Dockerfile在同一的路径下,才可以拷贝进去。当然你可以不在一个路径下,那就需要修改Dockerfile命令,将具体app.py的路径写全),然后在该路径下执行构建命令构建image,并将其取名为hellworld
docker build --tag=helloworld .
  1. 发布image
    你可以像发布jar一样,将image发布到docker中央仓库,或公司的私有仓库,具体方式这里就不展开讨论了。

container

类似于java应用中的jar运行。我们基于image运行后,会创建一个运行的实例,即为container,容器。比如我们可以使用以下命令,通过前面build的image,创建一个container

docker run -p 4000:80 helloworld

network

container需要对外进行通信,可能需要网络服务。有5种网络驱动可供docker配置,用来配置docker的联网行为。

  • bridge 桥接模式,通过链路层设备链接host网络,它同host使用不同的ip,一般在单节点的host使用这种方式,默认是这种方式

  • host 模式,container直接跟host公用一个ip,这也意味这container暴露什么端口,通过host的ip可直接访问,不推荐这种方式

  • overlay docker集群的网络连接驱动方式

  • Macvlan 对docker配置mac地址,通过物理地址进行网络通信

  • none 使docker没有任何网络连接

data volumes

container中的程序运行时,可能会产生一些数据,或者需要使用一些数据,甚至希望同其它container共享数据。那么实现这些的方式就是data volumes,它对应docker的存储概念,后续会详细讲解。

docker daemon

类似于Java虚拟机。它负责image构建,分发,获取,执行,以及container、volumes、network等上述核心组件的管理,屏蔽底层操作系统的细节,使得基于docker构建的服务能够跨平台。我们一般通过docker CLI也即docker命令行来向docker deamon发送命令执行上述管理。
1554642870860-1

Docker的基本使用方式

作为普通用户大多数时候,我们只是从中央仓库中获取别人制作好的image,在本地创建container来提供服务,比如获取mysql的image,在本地创建一个mysql的servers。所以下面主要介绍对container的一些核心操作命令。

获取image

使用如下命令去远程仓库中拉取,image文件

 docker pull IMAGE[:TAG]

比如我们想要获取redis的image,在中央仓库中我们可以看到有很多redis的image,他们用不同的tag区分
1554644617846-1
我们可以通过指定tag来拉取特定的image,比如我们拉取tag为5.0.4-alpine的image。docker pull redis:5.0.4-alpine

如何创建一个container

docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]

其命令主干是docker run IMAGE。每一次run,都会创建一个新的container

我们基于前面拉到的redis image启动一个containerdocker run redis:5.0.4-alpine

可以在创建的时候指定许多参数,比如创建container时,指定名字docker run --name test-redis redis:5.0.4-alpine

将容器中的程序以后台形式运行docker run --name some-redis -d redis

查看docker相关组件

我们可以使用ls命令,来查看docker中container,image,network,volume等组件的id和名字,就像linux中的ls命令一样。

docker container ls #查看正在运行的container
docker container ls --all #查看包括已停止和运行中的所有container
docker image ls #查看本地拥有的image
docker network ls#查看当前系统具有的网络驱动
docker volume ls#查看当前系统具有的volume存储

如何停止一个container

docker container stop CONTAINER_ID|CONTAINER_NAME

可以使用container的id或name来将处在run中的container停止

如何启动一个start

通过上述的ls命令,获取到container的名字或id,然后通过命令docker container start CONTAINER_ID|CONTAINER_NAME来启动容器,举例docker container start 48b24d849908

如何查看container中的程序执行日志

我们可以将container当做一个小的linux系统。那启动后如何登入?有两种方式,第一种是attach命令到指定的容器,比如

sudo docker container attach 48b24d849908

但这个命令是只将当前的host终端attach到指定的container中正在运行的进程。并显示其输出。但并不能任意的浏览container的其他系统目录。如果仅仅是为了看当前container中的运行程序日志,大可不必用上述方法,直接用logs命令输出即可(当然这种方式的能看到日志的前提是,container中的程序将日志输出到了STDOUT或STDERR中才行)比如:

sudo docker container logs 48b24d849908

如果嫌输出的日志太多,也可以管道加less慢慢看

sudo docker container logs 48b24d849908 | less

想要正真的直接登录container去浏览其系统文件,需要使用一下命令

sudo docker container exec -it 48b24d849908 /bin/bash

当然这个要容器里确实有bash程序才行。exec还可以run程序中的其他命令

如何启动一个一次性的container

基于image创建一个container后,如果不主动删除,那么该container会一直存在,若以希望container被停止后,自动删除。那么可以在创建命令run中加参数--rm。例如:

docker run --rm --name some-redis -d redis

如何让容器自动重启

有时我们希望宿主机在重启后,或docker deamon重启后,相应的container能自动重启。那么在创建container时,使用参数--restart来控制重启行为。重启策略主要有以下几种

  • no 默认选项,不会自动重启container

  • on-failure 当container非正常退出时,自动重启

  • always 无论什么情况都自动重启。但手动停止容器后,需要docker daemon进程重启时,才会重启container,也即宿主机重启时,会重启container

  • unless-stopped 同always类似,但是手动停止的container不会在自动重启。

举例docker run -dit --restart unless-stopped redis

如何做端口映射

程序运行在container中。container又被docker deamon管理。所以需要将container中的程序暴露的端口,映射到宿主机自己的指定端口,否则外部程序无法直接同container通信。可以在创建时指定参数-p来指定。例如:docker run -p 6379:6379/udp -p6379:6379 redis:5.0.4-alpine

其中冒号左边为宿主机的端口,右边为container中程序暴露的端口。斜杠后面指定暴露的端口类型是UDP还是TCP,如果是TCP可以不写。

如何映射文件系统

container中程序可能需要读或写一些数据,要使得这些数据能够被宿主机可见,需要像端口映射一样,将container中的文件路径映射到外部文件系统中。这些外部的文件系统可以是宿主机的文件系统,也可是docker管理的volume。这里以宿主机的文件系统为例

docker run -v /home/v2ray_proxy:/etc/v2ray -p 1081:1081  v2ray/official  v2ray -config=/etc/v2ray/config.json

将宿主机路径/home/v2ray_proxy映射到container的/etc/v2ray路径,这样宿主机在/home/v2ray_proxy中修改的内容,container可以通过其/etc/v2ray路径获取到。反之亦然。

如何清理所有不使用的container、image、volume、network

可以使用rm命令,删除指定id或name的相关组件。比如:

docker container rm CONTAINER_ID
docker image rm IMAGE_ID
docker volume rm VOLUME_ID
docker network rm NETWOKR_ID

可能上述手动挨个删太麻烦,你可以使用prune命令,直接将符合需求的组件全部删除。比如:

docker image prune#删除未被任何容器使用的image
docker container prune#删除所有未启动的container
docker volume prune#删除所有未被使用的volume
docker network prune#删除所有未被使用的网络
docker system prune#删除所有未被使用的container,image ,volume, network。docker 1.7以上需要显示执行`--volumes`参数,才能一并将volume也删除,之所以这么做是害怕一不小心把数据给删了。多加参数增加了误删数据的门槛

以上所有的删除prune命令,都可以基于过滤条件来删除。加参数--filter即可,比如删除过去24小时未启动的容器

docker container prune --filter "until=24h"

如何查看container的资源使用情况

使用命令docker stats

Layer

一个Dockerfile最终会被构建成image,一个image被run后会生成一个container。为了最大化共享存储文件,减少存储空间的浪费,docker引入了层的概念layer. Docerkfile中RUN, COPY, ADD三个命令会产生layer

一个dockerfile中从上下到下的命令,反应到image上是由下到上的层,每一层都是基于上一层进行构建的。layer又分为image layer和container layer,前者是image构建时,每句dockerfile命令对应生成的layer,后者是通过image 生成一个新的container 时,container所独有的read writer 层。

container的read writer layer是container的程序读写文件时,文件的存储的层,它会随着container的销毁而销毁。通常来说,container运行生成或修改文件内容最好不要放到其read write layer,因为不方便cotnainer间共享,又容易影响container本身的读写性能,所以一般通过volume或bind mount的方式,将container读写的文件内容映射挂载到外部。

比如,Dockerfile

FROM ubuntu:15.04
COPY . /app
RUN make /app
CMD python /app/app.py
  • 第一句是基础层,表示基于ubuntu15的image构建
  • 第二句在ubuntu15的基础上,将宿主机当前路径的内容拷贝到image的/app路径做为新的layer
  • 第三句,使用make命令,将/app中的文件进行编译,生成的内容为新的layer
  • 第四句,使用python命令运行上一步build的可执行文件app.py,其对应container中的R/W layer

其对应的image层的示例为:

1554727787233-1
多个container公用image layer的示例:

1554727796358-1

文件系统

Docker中的任何数据的产生,默认都是存储在了container的write layer,这带来了以下一些问题:

  • 不方便备份和访问,因为数据在容器里面
  • 数据易丢失,当容器被删除后,数据也跟着被删除
  • 不方便程序更新,容器跟数据绑定了,这个时候你想通过更新的image,创建新的容器来达到升级程序的目的变得很难,因为你要丢数据

为了解决这些问题,Docker提出数据更容器分离的理念。以挂载的路径来区分,有以下三种挂载方式

  • volume mount 受docker deamon管理的文件系统
  • bind mount 当前宿主机的文件系统
  • tmpfs mount 内存

1554728019701-1

Volume mount

创建volume的几种方式

  1. 直接用volume命令创建例如docker volume create my-vol
  2. 在创建一个container时或service时,通过参数-v或者--mount挂载volume时,volume不存在,也会自动创建。举例如下:
//volume名为myvol2,挂载到container的指定目录为/app
$ docker run -d \
  --name devtest \
  -v myvol2:/app \
  nginx:latest


//创建四个nginx container组成的service
$ docker service create -d \
  --replicas=4 \
  --name devtest-service \
  --mount source=myvol2,target=/app \
  nginx:latest

-v--mount

这两个都能指定挂载的volume(如果不存在,都会创建),创建service时,只能使用mount命令。-v参数后面直接指定所有的配置value不直观,--mount的配置,则是以key=value的形式体现,能够清楚的知道指定配置项意义。能通过他们配置的信息有:

  • source container外的宿主文件系统(bind mount时,source就是宿主的文件路径)或volume
  • destination path: container 内的指定路径
  • 读写模式:对挂载的宿主文件路径或volume是否有读写的权利
  • driver: 如果挂载到container的是volume时,配置该volume的驱动类型。volume的驱动类型默认是local,也即宿主机所在文件系统。但有些volume对应的存储可能是aws,所以其驱动就不是local.

使用-v参数的大概形式为:

-v <source>:<destination>
//其中source可以忽略,忽略时,默认创建一个匿名的volume

使用--mount参数的大概形式为:

     --mount 'type=volume,src=<VOLUME-NAME>,dst=<CONTAINER-PATH>,volume-driver=local,volume-opt=type=nfs,volume-opt=device=<nfs-server>:<nfs-path>'
     //其中src可以写为source
     //dst 可以写为destination或target

像volume中填充内容

如果一个空的volume挂载到指定的container目录,并且该目录下已经有内容,那么这些内容会自动被复制到volume下。举例如下;

$ docker run -d \
  --name=nginxtest \
  --mount source=nginx-vol,destination=/usr/share/nginx/html \
  nginx:latest
//名为nginx-vol的volume,里面会被拷贝进/usr/share/nginx/html文件

bind mount

volume是挂载一个由docker 守护进程管理的文件系统到container。而bind mount是直接挂载宿主机的任意文件路径到container。这样宿主机其他进程该挂载路径下的文件内容,container也会感受到,反之亦然。其挂载命令跟volume差不读,不再赘述,只是其mount的type为bind。

简单总结来看,希望容器间相互共享内容,使用volume挂载到container
希望容器和宿主机之间相互共享内容,使用bind mount

tmpfs mount

tmpfs是将容器指定路径映射到内存,这样当容器对指定路径写数据时,不会写到容器自己的write layer。并且tmpfs不能被容器共享,即A容器mount 的tmpfs,不能被B容器读到,这就使得tmpfs非常适合存储一些易失的,且容器独有的私密信息。

tmpfs只能在linux的docker中使用

tmpfs的挂载也有两种参数方式,一是--tmpfs,二是--mount,前者不能指定任何参数,后者则可以,后者的功能和工作范围都比较广。

volume和bind在挂载时,需要指定一个source,而tmpfs的挂载不需要,只用指定挂载到对应contaienr的路径即可。

后话

容器化使得部署应用变得简单方便。docker还提供了swarm,使得服务以集群化形式编排和部署同样变得简单。这里不再详述。

使用容器化提供服务时,需要遵循微服务化的原则,保持服务的原子性,即一个container只提供一种服务。这样更加方便后期管理和程序扩展。

参考资料

https://docs.docker.com/