Docker是一个开源的应用容器引擎,基于Go语言并遵循Apatch2.0协议。主要的作用是将应用程序和基础设施进行分离,方便快速进行交付软件。
其中docker包含了三个基本的概念:
镜像(Image) : 镜像是一个只读的模板,相当于root文件系统。官方提供了许多镜像,如Ubuntu16.04 镜像包含了Ubuntu16.04 最小系统的 root 文件系统。
容器(Container) :容器是镜像运行的实体,可以被创建、启动、停止、暂停以及删除。它和镜像的关系类似于类和实例的关系。容器是实例,镜像是静态的定义。
仓库(Repository) :仓库用于代码控制,保存镜像。
此外,Docker使用客户端-服务端架构(C/S) ,Docker 客户端是Docker用户与Docker交互的主要方式。客户端将我们的命令,类似于:docker run 发送到Docker守护进程。守护进程侦听Docker API请求并管理Docker对象。同时,守护进程还可以与其他守护进行进行通信来管理Docker服务。Docker 客户端和守护进程可以在同一系统上运行,也可以将 Docker 客户端连接到远程 Docker 守护进程。
总的来说,Docker 技术使用 Linux 内核和内核功能来分隔进程,以便各进程相互独立运行。它可以独立运行多种进程、多个应用,更加充分地发挥基础设施的作用,同时保持各个独立系统的安全性。此外提供基于镜像的部署模式,这使得它能够轻松跨多种环境,与其依赖程序共享应用或服务组。一个docker 容器,是一个运行时环境,可以简单理解为进程运行的集装箱。
Docker的出现一定意义上是为了解决开发环境和生产环境一致性的问题,通过Docker可以将环境也纳入到版本控制中,排除环境不同导致的差异。因此,如何的虚拟化技术解决这个问题是实现Docker的一个核心技术。
docker实现原理
docker和kvm都是虚拟化技术,但是docker具有更少的抽象层,简单来说,就是VM是在宿主机器、宿主机器的操作系统之上创建虚拟层、虚拟化操作系统、虚拟化仓库,再安装应用。而docker是在宿主机器和宿主操作系统之上创建Docker引擎,在引擎的基础上安装应用。docker是利用宿主机的操作系统,省略了重新加载操作系统的过程,启动速度是妙级的,同时单机上支持上千容器。而虚拟机加载的Guest OS是分钟级的。
Namespace
当我们在服务器上启动多个服务的时候,服务会相互影响,没有办法做到完全隔离。而Docker可以通过namespace对不同容器进行隔离。Linux的命名空间提供了分离进程树、网络接口、挂载点以及进程通信等资源的方法。Linux Namespace是一种内核级别的资源隔离机制,它可以使得一组进程可以看到一组资源,而无法看到其他组的资源。其原理就是让一组资源使用相同的namespace,而不同的namespace引用的是不同的资源。目前的namespace类型如下:
| Namespace | 系统调用参数 | 隔离内容 |
| UTS | CLONE_NEWUTS | 主机名与域名 |
| IPC | CLONE_NEWIPC | 信号量、消息队列和共享内存 |
| PID | CLONE_NEWPID | 进程编号 |
| Network | CLONE_NEWNET | 网络设备、网络栈、端口等等 |
| Mount | CLONE_NEWNS | 挂载点(文件系统) |
| User | CLONE_NEWUSER | 用户和用户组 |
| Cgroup | CLONE_NEWCGROUP | Cgroup的根目录 |
| Time | CLONE_NEWTIME | 时钟 |
进程隔离
每个进程都有一个关于namespace的属性nsproxy表示所属的namespace,其中nsproxy就是指向各种namespace的代理。当新进程被创建之后会继承父进程的namespace,这就是为啥一个容器里所有的进程可以共享namespace的原因。
在所有的进程中有两个特殊的进程,一个是 pid 为 1 的 /user/lib/system 进程,另一个是 pid 为 2 的 kthreadd 进程,这两个进程都是被 Linux 中的上帝进程 idle创建出来的,其中前者负责执行内核的一部分初始化工作和系统配置,也会创建一些类似 getty 的注册进程,而后者负责管理和调度其他的内核进程。 注意:在PID=1的进程是Linux 系统中的 init 进程,它是系统的第一个进程,在现代 Linux 发行版中,init 进程通常由 systemd 代替。
当我们创建了一个新的Docker,并通过 exec 进入其内部的 bash 并打印其中的全部进程,会发现容器内部进程列表不包含宿主机器的其他进程。
使用 Linux 的命名空间实现进程的隔离,Docker 容器内部的任意进程都对宿主机器的进程一无所知。通过进程可以构成如下进程树。
其中,containerd 进程是容器的管理器,负责整个容器的生命周期和资源管理,而 containerd-shim 进程是容器运行时的一部分,负责在容器运行时环境中管理容器的各个方面,包括启动、监控、事件传递等。这两个组件协同工作,实现了容器的高效运行和管理。
当使用Linux的clone创建一个新进程的时候,会传入CLONE_NEWPID,通过命名空间进行隔离,使得Docker 容器内部的任意进程都对宿主机器的进程一无所知。
网络隔离
当Docker通过namespace完成了与宿主机的进程的网络隔离之后,导致无法通过宿主机的网络连接整个互联网,这里Docker为我们提供了四种不同的网络模式:Host、Container、None、Bridge。
其中,**Bridge模式除了分配网络命名空间之外,会为所有的容器设置IP地址。**其具体流程:
- 当 Docker 服务器在主机上启动之后会创建新的虚拟网桥 docker0,随后在该主机上启动的全部服务在默认情况下都与该网桥相连。
- 每一个容器创建时,会创建一对虚拟网卡,一个放在创建的容器中,一个放在docker0网桥中。
- docker0会为每一个容器分配一个新的IP地址,而并将docker0的ip地址设置成默认网关。
- 网桥通过iptables的配置与宿主机器的网卡连接,所有符合条件的请求都会通过 iptables 转发到 docker0 并由网桥分发给对应的机器。
Docker 是如何将容器的内部的端口暴露出来并对数据包进行转发的呢?
当Docker的容器需要将服务暴露给宿主机器的时候,为容器分配一个IP地址,向iptable中追加新的规则,通过 iptables 进行数据包转发,让 Docker 容器能够优雅地为宿主机器或者其他容器提供服务。
Cgroups实现资源限制
虽然namespace为我们提供了进程、文件系统等之间的相互隔离,但没办法实现物理层面的隔离。如果这些容器占用了相同的物理资源,而一个容器在执行CPU密集型的任务,会对其他容器中任务的性能和执行效率造成影响。而 Control Groups(简称 CGroups)就是能够隔离宿主机器上的物理资源,例如 CPU、内存、磁盘 I/O 和网络带宽。
在Linux中,CGroup以文件和目录的形式存在操作系统的/sys/fs/cgroup路径下(mount -t cgroup),具体的子系统如下:
这些就是当前机器可以被cgroup进行限制的资源种类,Cgroup都是被相同标准和参数限制的进程,不同的Cgroup之间有层级关系,他们可以从父类继承一些用于资源使用和标准的参数。每个进程都可以加入一个Cgroup也可以随时退出一个Cgroup。
以cpu限制举例,我们可以查看CPU的限制情况:
两个参数cfs_quota_us和cfs_period_us,它表示的意思是:限制进程在长度为**cfs_period_us的一段时间内,只能被分配到总量为cfs_quota_us**的CPU时间。-1代表没有限制,100000代表100ms。也就是说,默认情况下,任何一个线程在 100ms 的时间周期内,「最多」能被分配到的CPU时间就是 100ms,不会被限制。
Linux 使用文件系统来实现 CGroup,如果我们想要创建一个新的 group 只需要在想要分配或者限制资源的子系统下面创建一个新的文件夹,然后这个文件夹下就会自动出现很多的内容。 (新建文件为test_cpu_cgroup)
如果linux上已经安装了Docker,会发现所有的子系统目录下都存在一个docker文件夹。这里就有关于CPU资源的各种限制。我们创建一个进程死循环的进程 while : ; do : ; done &,这是他对CPU的使用是没有限制的。
通过修改cfs_quota_us的限制,一**个进程「最多」只能使用到 的CPU限制在20% **,并将被限制的PID写入tasks文件。即可控制对资源的使用情况。
echo 20000 > ./cpu.cfs_quota_us
echo 20101 > ./tasks
总的来说,启动容器的时候,docker会为这个容器创建一个和容器标识符一样的cgroup,而每个cgroup下都有一个tasks文件,这个文件中存储着属于当前Cgroup的所有进程的 pid。当我们关闭掉正在运行的容器时,Docker 的子控制组对应的文件夹也会被 Docker 进程移除,Docker 在使用 CGroup 时其实也只是做了一些创建文件夹改变文件内容的文件操作。
UnionFS
联合文件系统是一种分层、轻量级并高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下。Docker是怎么使用联合文件系统的呢?
首先,我们理解一下Docker的存储驱动。
Docker中每一个镜像都是一系列的只读的层,Dockerfile中每一个命令都会在已有的只读的层上创建一个新的层。如果这个时候我们执行了**docker run**** ,会在最上面的层上添加一个可写的层,也就是容器层。所有对于运行时容器的修改其实都是对这个容器读写层的修改。**
镜像其实本质上是一个文件,联合文件系统是docker镜像的基础**。因为镜像是通过分层来实现继承,基于父镜像可以制作各种新的镜像**。另外,不同 Docker 容器就可以共享一些基础的文件系统层,同时再加上自己独有的改动层,大大提高了存储的效率。
~~AUFS,AUFS支持为每一个成员目录设置只读、读写和写出权限,同时AUFS中有一个类似分层的概念,对只读权限的分支可以逻辑上进行增量地修改(不影响只读部分的)。每一个镜像层或者容器层都是 ~~~~/var/lib/docker/~~~~ 目录下的一个子文件夹。在 Docker 中,所有镜像层和容器层的内容都存储在 ~~~~/var/lib/docker/aufs/diff/~~~~ 目录中 ~~
docker的基本使用
使用镜像
- 获取: (镜像是由多层存储所构成。下载也是一层层的去下载,并非单一文件)
docker pull ubuntu:18.04
- 查看:
docker image ls
docker system df // 查看镜像、容器、数据卷所占用的空间
由于新旧镜像同名,旧镜像的名字被取消,从而出现的名字为的镜像称为虚悬镜像,可以通过docker image prune删除
- 删除
docker image rm <镜像ID、镜像名、镜像摘要>
-
commit:适用于入侵之后保存现场
-
我们可以启动一个nignx容器并进入该容器,对内容进行修改。随后使用commit该容器保存为新的镜像。
docker run --name web-commit -d -p 80:80 nginx docker exec -it web-commit bash echo '<h1>bakabaka</h1>' > /usr/share/nginx/html/index.html docker diff web-commit docker commit --author "fox" --message "change" web-commit nginx:v2
-
-
我们运行一个容器的时候(如果不使用卷的话),我们做的任何文件修改都会被记录于容器存储层里。而 Docker 提供了一个
docker commit命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。 -
不推荐使用
docker commit制作镜像,每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,即使根本无法访问到。 -
使用Dockerfile构建镜像
首先我们需要在项目根目录下创建Dockerfile文件
# syntax=docker/dockerfile:1
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]
EXPOSE 3000
- 构建镜像
docker build -t <image_name> .
- 启动容器,
-p标志采用HOST:CONTAINER格式的字符串值
docker run -dp 127.0.0.1:3000:3000 <image_name>
使用容器
- 新建启动
docker run ubuntu:18.04 - 终止
docker container stop - 进入容器
docker attach <container_id>或docker exec -it <container_id> bash - 导入导出
docker export container_id > ubuntu.tar&cat ubuntu.tar | docker import - test/ubuntu:v1.0 - 删除
docker rm <the-container-id>
dockerfile的基本使用
一个基础的Dockerfile(FROM& RUN)
docker镜像由只读层组成,每一层是一个Dockefile指令,这些层是堆叠在一起的,每一层都是与前一层的变化的增量。
# syntax=docker/dockerfile:1
FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
每条指令创建一层:
-
FROM从 Docker 镜像创建一个层。FROM指定了一个基础的镜像,且必须是第一条指令。在Docker Hub中又许多的官方镜像可以供我们选择,如node、nginx、redis等。特殊的,Docker 存在一个名为scratch的空白镜像。 -
RUN使用make构建您的应用程序。RUN指令是用来执行命令行命令的,其格式有两种:- shell格式,即RUN <命令>,如:
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html - exec 格式,即
RUN ["可执行文件", "参数1", "参数2"],类似于函数调用。如:RUN yum install gcc
值得注意的是:
Dockerfile 中每一个指令都会建立一层,每一个
RUN的行为,也会新建立一层,在这个层上执行命令,执行结束后,commit这一层的修改,构成新的镜像。如果存在多个RUN的操作(如下),结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。同时UnionFS还有最大层数限制,比如AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。 - shell格式,即RUN <命令>,如:
FROM debian:stretch
RUN apt-get update
RUN apt-get install -y gcc libc6-dev make wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
// after
FROM debian:stretch
RUN set -x; buildDeps='gcc libc6-dev make wget' \
&& apt-get update \
&& apt-get install -y $buildDeps \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
&& rm redis.tar.gz
这个时候我们基于上面的Dockerfile构建一个镜像,
启动容器docker run -dp 0.0.0.1:82:80 <image_name>
镜像构建上下文
Docker 引擎(也就是服务端守护进程)提供了一组 API, docker 客户端通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 docker 功能,但实际上,一切都是使用的远程调用形式在**服务端(Docker 引擎)**完成。当我们进行镜像构建的时候,经常会需要将一些本地文件复制进镜像,比如通过 COPY 指令、ADD 指令等。而 docker build 命令构建镜像,其实并非在本地构建,而是在 Docker 引擎中构建的。
这里就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
Dockerfile指令基本使用
-
COPY :
-
格式:
COPY <源路径>... <目标路径> -
原路径可以是多个甚至是通配符,目标路径可以是容器内的绝对路径也可以是相对于WORKDIR置顶目录的相对路径。使用
COPY指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。 -
在dockerfile中添加COPY:
COPY index.html /usr/share/nginx/html/index.html,构建镜像并运行容器效果如下
-
-
ADD
- 格式:
ADD <源路径>... <目标路径> - ADD可以看成一个更高级的复制文件,在COPY的基础上添加了一些功能,例如当源路径是一个连接,这是Docker引擎会试图去下载这个连接的文件放到目标路径。如果原路径是一个压缩包,ADD会自动解压文件。但是ADD会导致镜像构建的缓存失效,肯呢个会导致镜像构建变得缓慢。
- 一般情况下可以遵循这样的原则,所有的文件复制均使用
COPY指令,仅在需要自动解压缩的场合使用ADD。
- 格式:
-
CMD
- CMD指令和RUN很类似,也是两种格式shell和 exec。在启动容器的时候,需要指定所运行的程序及参数。
CMD指令就是用于指定默认的容器主进程的启动命令的。 - 例如:
CMD ["nginx", "-g", "daemon off;"]
- CMD指令和RUN很类似,也是两种格式shell和 exec。在启动容器的时候,需要指定所运行的程序及参数。
-
ENTRYPOINT
ENTRYPOINT的格式和RUN指令格式一样,分为exec格式和shell格式。ENTRYPOINT的目的和CMD一样,都是在指定容器启动程序及参数。指定了ENTRYPOINT后,CMD的含义就发生了改变,将变为:<ENTRYPOINT> "<CMD>"
-
ENV
- 设置环境变量,格式:
ENV <key> <value>或ENV <key1>=<value1> <key2>=<value2>...
- 设置环境变量,格式:
-
VOLUME
- 定义匿名卷:
VOLUME <路径> - 容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,数据库文件应该保存于卷(volume)中。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在
Dockerfile中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,也不会向容器存储层写入大量数据。
- 定义匿名卷:
-
EXPOSE
- 暴露端口:
EXPOSE <端口1> [<端口2>...] - 容器运行时,并不会因为这个声明就开启这个端口的服务。这里只是在运行时使用随机端口映射时,也就是
docker run -P时,会自动随机映射EXPOSE的端口。 - 要将
EXPOSE和在运行时使用-p <宿主端口>:<容器端口>区分开来。-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而EXPOSE仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。
- 暴露端口:
-
WORKDIR
- 指定工作目录,如该目录不存在,
WORKDIR会帮你建立目录。
- 指定工作目录,如该目录不存在,
docker-compose的基本使用
Compose的定位是定义和运行多个Docker容器的应用。它允许用户通过一个单独的docker-compose.yml文件来定义一组相关联的应用容器。Compose实现上调用了 Docker 服务提供的 API 来对容器进行管理。Compose 适用于生产、登台、开发、测试以及 CI 工作流程。它还具有用于管理应用程序整个生命周期的命令。
基本的docker-compose的使用需要编写Dockefile文件和docker-compose.yml,并通过docker-compose up来运行。
compose常用命令
- build: 构建项目中的服务容器,
docker-compose build
选项包括:
--pull始终尝试通过 pull 来获取更新版本的镜像。--no-cache构建镜像过程中不使用 cache(这将加长构建过程)。--force-rm删除构建过程中的临时容器。
- 启动服务:
docker-compose up -d // 后台运行
- 停止服务
docker-compose down
- 停止已运行的服务
docker-compose stop
- 启动服务
docker-compose start
- 查看容器
docker-compose ps
- 查看日志
docker-compose logs
compose模版文件
默认的模版文件名称docker-compose.yml,每个服务通过image执行镜像。Docker Compose 的 YAML 文件包含 4 个一级 key:version、services、networks、volumes
- version 是必须指定的,而且总是位于文件的第一行。它定义了 Compose 文件格式(主要是 API)的版本。注意,version 并非定义 Docker Compose 或 Docker 引擎的版本号。
- services 用于定义不同的应用服务。上边的例子定义了两个服务:一个名为 lagou-mysql数据库服 务以及一个名为lagou-eureka的微服。Docker Compose 会将每个服务部署在各自的容器中。
- networks 用于指引 Docker 创建新的网络。默认情况下,Docker Compose 会创建 bridge 网络。 这是一种单主机网络,只能够实现同一主机上容器的连接。当然,也可以使用 driver 属性来指定不 同的网络类型。
- volumes 用于指引 Docker 来创建新的卷。
这里,我们使用一个常见的docker-compose文件举例部分配置
version: '3'
services:
app:
image: node:latest
container_name: app_main
restart: always
command: sh -c "yarn install && yarn start"
ports:
- 8000:8000
working_dir: /app
volumes:
- ./:/app
environment:
MYSQL_HOST: localhost
MYSQL_USER: root
MYSQL_PASSWORD:
MYSQL_DB: test
mongo:
image: mongo
container_name: app_mongo
restart: always
ports:
- 27017:27017
volumes:
- ~/mongo:/data/db
volumes:
mongodb:
其中:
version指的是 docker-compose 的版本(最新的是 3)services定义了我们需要运行的服务app是你的一个容器的自定义名称image指的是我们要拉取的镜像,这里我们使用的是node:latest和mongocontainer_name是每个容器的名称restart启动/重启一个服务容器port定义了运行该容器的自定义端口working_dir是服务容器的当前工作目录environment定义了环境变量,如 DB 凭证等command是运行该服务的命令