一、docker 介绍
容器是一种基础工具,指任何可以用于容纳其他物品的工具。而 docker 是一个开源的应用容器引擎。docker 公司位于旧金山,原名叫 dotcloud,底层使用了 Linux 容器技术(LXC:在操作系统里实现资源隔离与限制)。
1.虚拟化的演变:
Hypervisor:一种运行在基础物理服务器和操作系统之间的中间软件层,可允许多个操作系统和应用共享硬件,常见有 vmare、微软的 Hyper-V、思杰的 Xenserver 等。通过上图可以看到虚拟化的部署可以看到在 APP 的使用必须在实现虚拟化的同时需要安装操作系统。例如我不想在宿主机安装 tomcat 这个服务器,想通过虚拟化的技术将 tomcat 安装在虚拟化的 centos 操作系统中,但是需要安装整个 centos 系统。大大的浪费了我们内存、硬盘、CPU 资源。
Container Runtime:通过 linux 内核虚拟化能力管理多个容器,多个容器共享一个操作系统。所以去除了内核的占用空间及运行所需要的耗时,使得容器轻量和极速!而容器化在安装 tomcat 的时候只需要在 centos 极小化的镜像基础上安装 tomcat 依赖和 tomcat 等。极大的节约了硬盘和内存的占用。
比较了上面的两张图我们可以看到,传统的虚拟机必须携带操作系统,本身很小的应用程序却因为携带了操作系统而变得很大,很笨重。Docker是不携带操作系统的,所以docker的应用就非常轻巧。另外在调用宿主机的CPU、内存、磁盘等等资源的时候,拿内存举例。虚拟机是利用Hypervisor去虚拟化内存,整个调用的过程是虚拟内存--->虚拟物理内存---->物理机内存。但是Docker是利用Docker Engine去调用宿主机的资源,这时候过程是虚拟内存---->物理内存。
2.VM 虚拟化技术和 Docker 的区别
2.1、本质上的区别:
VM 是在宿主机的操作系统的基础上创建虚拟层、通过虚拟化出来资源进行安装操作系统,然后在虚拟化的操作系统上安装软件。
Docker 容器:在宿主机的操作系统上创建 Docker 引擎,然后在引擎的基础上安装应用。其实并不是没有操作系统,而是操作系统的提供商会提供一个专门为 docker 发布的极小镜像。
2.2、使用上的区别:
docker 的优势有:一致的运行环境,更轻松的迁移。对进程进行封装隔离,容器与容器之间互不影响,更高效的利用系统资源。docker 将程序以及程序使用的环境直接打包到一起,无论在哪个机器上保持了环境的一致。
3.Docker架构及组件剖析
docker整体结构采用C/S(客户机/服务器)模式,主要由客户端和服务端两大部分组成,客户端负责发送操作指令,服务端负责接收和处理指令。客户端和服务端通信有很多种方式,既可以在同一台机器上通过UNIX套接字进行通信,也可以通过网络连接远程通信。
Docker客户端
Docker客户端其实是一种泛称。其中Docker命令是Docker用户与Docker服务端交互的主要方式。除了使用docker命令的方式,还可以使用直接请求REST API的方式与Docker服务端交互,甚至可以使用各种语言的SDK与Docker服务端进行交互。
Docker服务端
docker服务端是Docker所有后台服务的统称,其中dockerd是一个非常重要的后台管理进程,他负责响应和处理来自Docekr客户端的请求,然后将客户端的请求转换为Docker具体的操作。例如镜像、容器、网络和挂载卷等具体对象的操作管理。
Docker从诞生到现在,服务端历经了多次架构重构。期初,服务端的组件是全部集成在Docker二进制里。但是从1.11版本开始,dockerd已经成为独立的二进制,此时的容器也不是直接由dockerd来启动了,而是集成了containerd、runC等多个组件。
虽然Docker的架构不停重构,但是各个模块的基本功能和定位并没有变化。他和一般的C/S架构系统一样,Docker服务端模块负责和Docker客户端交互,并管理Docker的容器、镜像、网络资源。
Docker组件剖析:
下面,以Docker的20.10.9版本为例,看下Docker都有哪些工具和组件。
1、下载docker二进制文件
wget https://download.docker.com/linux/static/stable/x86_64/docker-20.10.6.tgz
2、解压
tar -zxvf docker-20.10.6.tgz
3、将Docker可执行文件复制到/usr/bin目录下
cp docker/ /usr/bin/
4、查看有关docker的二进制文件
[root@localhost ~]# ll /usr/bin/
总用量 229716
-rwxr-xr-x. 1 root root 39602024 4月 22 14:21 containerd
-rwxr-xr-x. 1 root root 7270400 4月 22 14:21 containerd-shim
-rwxr-xr-x. 1 root root 9953280 4月 22 14:21 containerd-shim-runc-v2
-rwxr-xr-x. 1 root root 21504072 4月 22 14:21 ctr
-rwxr-xr-x. 1 root root 60074792 4月 22 14:21 docker
-rwxr-xr-x. 1 root root 78940616 4月 22 14:21 dockerd
-rwxr-xr-x. 1 root root 708616 4月 22 14:21 docker-init
-rwxr-xr-x. 1 root root 2928566 4月 22 14:21 docker-proxy
-rwxr-xr-x. 1 root root 14233296 4月 22 14:21 runc
可以看到,Docker目前已经有了非常多的组件和工具。这些组件可以分为三大类:
- docker相关的组件:docker、dockerd、docker-init、docker-proxy
- containerd相关的组件:containerd、contarinerd-shim和ctr
- 容器运行时相关的组件:runc
下面我们进行逐一讲解:
(1)docker
docker是Docker客户端的一个完整实现,他是一个二进制文件,对用户可见的操作形式为docker命令,通过docker命令可以完成所有的docker客户端与服务端的通信(还可以通过REST API、SDK等多种形式与Docker服务端通信)。
Docker客户端与服务端的交互过程:docker组件向服务端发送请求后,服务端根据请求执行具体的动作并将结果返回给docker客户端,docke客户端解析服务端的返回结果,并将结果通过命令行标准输出展示给用户。这样一次完整的客户端对服务端的请求就完成了。
(2)dockerd
dockerd是docker服务端的后台常驻进程,用来接收客户端发送的请求,执行具体的处理任务,处理完成后将结果返回给客户端。
Docker客户端可以通过多种方式向dockerd发送请求,我们常用的docker客户端与dockerd的交互方式有三种。
- 通过UNIX套接字与服务端通信:配置格式为unix://socket_path,默认dockerd生成的socket文件路径为/var/run/docker.sock,该文件只有root用户或者docker用户组的用户才可以访问。这就是为什么Docker刚安装完成后只有root用户才能使用docker的原因。
- 通过TCP与服务端通信:配置格式为TCP://host:port,通过这种方式可以实现客户端远程连接服务端,但是在方便的同时也带有安全隐患,所以在生产环境中如果你要使用TCP的方式与Docker服务端通信,推荐使用TLS认证,可以通过设置Docker的TLS相关参数,来保证数据传输的安全。
- 通过文件描述符的方式与服务端通信:配置格式为:fd://这种格式一般用于systemd管理的系统中。
DOcker客户端和服务端的通信方式必须保持一致,否则无法通信,只有当dockerd监听了UNIX套接字客户端才可以使用UNIX套接字的方式与服务端通信,UNIX套接字也是docker默认的通信方式,如果你想要通过远程的方式访问dockerd,可以在dockerd启动的时候添加-H参数指定监听的HOST和PORT。
(3)docker-init
如果你熟悉Linux系统,你应该知道在linux系统中,1号进程是init进程,是所有进程的父进程。主机上的进程出现问题时,init进程可以帮助我们回收这些问题进程。同样的,在容器内部,当我们自己的业务进程没有回收子进程的能力时,在执行docker run启动容器时可以添加--init参数,此时Docker会使用docker-init作为1号子进程,帮你管理容器内子进程。例如回收僵尸进程等。
下面我们通过启动一个busybox容器来演示下:
[root@localhost ~]# docker run -it --name busybox001 busybox sh
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 sh
7 root 0:00 ps aux
/ #
可以看到容器启动时如果没有添加--init参数,1号进程就是sh进程,我们使用ctrl+D退出当前容器,重新启动一个容器并添加--init参数,然后看下进程:
[root@localhost ~]# docker run -it --name busybox002 --init busybox
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 /sbin/docker-init -- sh
7 root 0:00 sh
8 root 0:00 ps aux
可以看到此时容器内的1号进程已经变为了/sbin/docker-init,而不是sh了。
(4)docker-proxy
docker-proxy主要是用来做端口映射的。当我们使用docker run命令启动容器时,如果使用了-p参数,docker-proxy组件就会把容器内相应的端口映射到主机上来,底层时依赖于iptables实现的。
下面我们通过一个实例演示:
使用以下命令启动一个nginx容器并将容器的80端口映射到主机的8080端口。
docker run -dit --name nginx002 -p 8080:80 nginx
使用以下命令查看下容器的IP地址:
[root@localhost ~]# docker inspect --format '{{ .NetworkSettings.IPAddress }}' nginx002
172.17.0.2
此时,使用ps命令查看一下主机上是否有docker-proxy进程:
[root@localhost ~]# ps aux | grep docker-proxy
root 4664 0.0 0.0 899228 2644 ? Sl 15:20 0:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.2 -container-port 80
root 4672 0.0 0.0 898972 2388 ? Sl 15:20 0:00 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 8080 -container-ip 172.17.0.2 -container-port 80
root 4803 0.0 0.0 112676 988 pts/0 S+ 15:22 0:00 grep --color=auto docker-proxy
可以看到当我们启动一个容器时需要端口映射时, Docker 为我们创建了一个 docker-proxy 进程,并且通过参数把我们的容器 IP 和端口传递给 docker-proxy 进程,然后 docker-proxy 通过 iptables 实现了 nat 转发。
我们通过以下命令查看一下主机上 iptables nat 表的规则:
[root@localhost ~]# iptables -L -nv -t nat | grep docker
0 0 MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0
0 0 RETURN all -- docker0 * 0.0.0.0/0 0.0.0.0/0
0 0 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80
1 177 POST_docker all -- * docker0 0.0.0.0/0 0.0.0.0/0
Chain POST_docker (1 references)
3 357 POST_docker_log all -- * * 0.0.0.0/0 0.0.0.0/0
3 357 POST_docker_deny all -- * * 0.0.0.0/0 0.0.0.0/0
3 357 POST_docker_allow all -- * * 0.0.0.0/0 0.0.0.0/0
Chain POST_docker_allow (1 references)
Chain POST_docker_deny (1 references)
Chain POST_docker_log (1 references)
0 0 PRE_docker all -- docker0 * 0.0.0.0/0 0.0.0.0/0
Chain PRE_docker (1 references)
0 0 PRE_docker_log all -- * * 0.0.0.0/0 0.0.0.0/0
0 0 PRE_docker_deny all -- * * 0.0.0.0/0 0.0.0.0/0
0 0 PRE_docker_allow all -- * * 0.0.0.0/0 0.0.0.0/0
Chain PRE_docker_allow (1 references)
Chain PRE_docker_deny (1 references)
可以看到第三条:当我们在访问主机的8080端口的时候,iptables会把流量转发到172.17.0.2的80端口,从而实现了我们从主机上可以直接访问到容器内的业务。
我们通过curl命令访问一下nginx容器:
[root@localhost ~]# curl http://127.0.0.1:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
通过上面的输出可以得知我们已经成功的访问到了nginx服务器。
总体来说,docker是官方实现的标准客户端,dockerd是docker服务端的入口,负责接收客户端发送的指令并返回相应的结果,而docker-init在业务主进程没有进程回收功能时则十分有用,docker-proxy组件则是实现docker网络访问的重要组件。
containerd相关的组件
(1)containerd
- containerd不仅负责容器生命周期的管理,同时还负责一些其他的功能:
- 镜像的管理,例如容器运行前从镜像仓库拉取镜像到本地。
- 接收dockerd的请求,通过适当的参数调用runc启动容器。
- 管理存储相关的资源。
- 管理网络相关资源。
containerd包含一个后台常驻进程,默认的socket路径为/run/containerd/containerd.sock,dockerd通过UNIX套接字向containerd发送请求,containerd接收到请求后负责执行相关的动作并把执行结果返回给dockerd。如果不想使用dockerd,也可以直接使用containerd来管理容器,由于containerd更加简单和轻量,生产环境中越来越多的人开始直接使用containerd来管理容器。
(2)containerd-shim
containerd-shim的意思是垫片,类似于拧螺丝时夹在螺丝和螺母中之间的垫片。containerd-shim的主要作用是将containerd和真正的容器进程解耦,使用containerd-shim作为容器进程的父进程,从而实现重启dockerd或containerd不影响已经启动的容器进程。
(3)ctr
ctr实际上是containerd-ctr,他是containerd的客户端,主要用来开发和调试,在没有dockerd的环境中,ctr可以充当docker客户端的部分角色,直接向containerd守护进程发送操作容器的请求。
容器运行时组件runc
runc 是一个标准的 OCI 容器运行时的实现,它是一个命令行工具,可以直接用来创建和运行容器。下面我们通过一个实例来演示一下 runc 的神奇之处。
第一步,准备容器运行时文件:进入 /root 目录下,创建 runc 文件夹,并导入 busybox 镜像文件。
[root@localhost ~]# mkdir /root/runc
导入 rootfs 镜像文件
mkdir rootfs && docker export $(docker create busybox) | tar -C rootfs -xvf -
第二步,生成 runc config 文件。我们可以使用 runc spec 命令根据文件系统生成对应的 config.json 文件。命令如下:
runc spec
此时会在当前目录下生成 config.json 文件,我们可以使用 cat 命令查看一下 config.json 的内容:
$ cat config.json
{
"ociVersion": "1.0.1-dev",
"process": {
"terminal": true,
"user": {
"uid": 0,
"gid": 0
},
"args": [
"sh"
],
"env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=xterm"
],
//后面部分省略...
config.json 文件定义了 runc 启动容器时的一些配置,如根目录的路径,文件挂载路径等配置。
第三步,使用 runc 启动容器。我们可以使用 runc run 命令直接启动 busybox 容器。
[root@localhost ~]# runc run busybox
/ #
此时,我们已经创建并启动了一个 busybox 容器。
我们新打开一个命令行窗口,可以使用 run list 命令看到刚才启动的容器。
[root@localhost ~]# runc list
ID PID STATUS BUNDLE CREATED OWNER
busybox 5355 running /root 2022-04-22T07:55:26.622779845Z root
通过上面的输出,我们可以看到,当前已经有一个 busybox 容器处于运行状态。
总体来说,Docker 的组件虽然很多,但每个组件都有自己清晰的工作职责,Docker 相关的组件负责发送和接受 Docker 请求,contianerd 相关的组件负责管理容器的生命周期,而 runc 负责真正意义上创建和启动容器。这些组件相互配合,才使得 Docker 顺利完成了容器的管理工作。
Docker 各组件之间的关系#
首先通过以下命令来启动一个 busybox 容器:
$ docker run -d busybox sleep 3600
容器启动后,通过以下命令查看一下 dockerd 的 PID:
$ sudo ps aux |grep dockerd
root 4147 0.3 0.2 1447892 83236 ? Ssl Jul09 245:59 /usr/bin/dockerd
通过上面的输出结果可以得知 dockerd 的 PID 为 4147。为了验证图 3 中 Docker 各组件之间的调用关系,下面使用 pstree 命令查看一下进程父子关系:
$ sudo pstree -l -a -A 4147
dockerd
|-containerd --config /var/run/docker/containerd/containerd.toml --log-level info
| |-containerd-shim -namespace moby -workdir /var/lib/docker/containerd/daemon/io.containerd.runtime.v1.linux/moby/d14d20507073e5743e607efd616571c834f1a914f903db6279b8de4b5ba3a45a -address /var/run/docker/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc
| | |-sleep 3600
事实上,dockerd 启动的时候, containerd 就随之启动了,dockerd 与 containerd 一直存在。当执行 docker run 命令(通过 busybox 镜像创建并启动容器)时,containerd 会创建 containerd-shim 充当 “垫片”进程,然后启动容器的真正进程 sleep 3600 。(containerd-shim 的主要作用是将 containerd 和真正的容器进程解耦,使用 containerd-shim 作为容器进程的父进程,从而实现重启 dockerd或containerd 不影响已经启动的容器进程)。这个过程和架构图是完全一致的。
注意: docker 19.03.12 版本的 dockerd 和containerd 组件已经不是父子关系,可以使用以下命令查看,sudo ps aux |grep containerd , 然后使用 pstree 查看 containerd 的 PID。
4.docker 核心理念
docker 的三大理念:build(构建)、ship(运输)、run(运行)。docker 采用 c/s 架构。
docker 的组成部分:
Docker 主机(Host):一个物理机或虚拟主机,用于运行 Docker 服务进程和容器。
Docker 服务端(Server):Docker 守护进程,运行 docker 容器。
Docker 客户端(Client):客户端使用 docker 命令或其他工具调用 docker API。
Docker 仓库(Registry):保存镜像的仓库。分为远程仓库(docker在全世界范围维护一个唯一的远程仓库)和本地仓库(当前自己安装有docker下载的镜像)。
Docker 镜像(Images):镜像可以理解为常见实例使用的模板。只有有了镜像才会有多个容器。一个镜像代表一个软件(如:mysql镜像,redis镜像,nginx镜像)。而镜像启动后就称为一个容器(服务),一个镜像可以启动N个容器(服务)。镜像的特点是只读。
Docker 容器(Container):容器时从镜像生成对外提供服务的一个或一组服务。容器的特点是可读可写。而不会修改镜像。
5.docker镜像的原理
我们一直以来,使用vmare虚拟机安装的系统,是一个完整的系统文件,包括2个部分:
- liunx内核:作用是提供操作系统的基本功能,和硬件机器交互。
- centos7发行版:作用是提供软件功能,例如yum安装包管理等。
所以,linux内核+centos发行版就组成了一个系统,让用户使用。
那么,是否有一个办法,可以灵活的替换发行版,让我们使用不同的系统?
于是docker利用的技术就是共用宿主机的内核实现发行版本的替换。
快速实践,docker下载多个发行版的linux操作系统,查看宿主机系统内核是否和下载的发行版的内核版本一致:
宿主机的内核版本:
root@wtdata-virtual-machine:~# uname -r
5.11.0-27-generic
docker下载的发行版镜像:
ubuntu:
root@47462c73e5b7:/# uname -r
5.11.0-27-generic
debian:
root@7e17d2b83aae:/# uname -r
5.11.0-27-generic
centos:
[root@678680aba2af /]# uname -r
5.11.0-27-generic
docker镜像分层原理:
我们在获取redis的时候。发现下载了多行的信息,最终得到了一个完整的镜像。
root@localhost:~# docker pull redis
Using default tag: latest
latest: Pulling from library/redis
a2abf6c4d29d: Already exists
c7a4e4382001: Pull complete
4044b9ba67c9: Pull complete
c8388a79482f: Pull complete
413c8bb60be2: Pull complete
1abfd3011519: Pull complete
docker通过联合文件系统,将上述的不同的每一层,整合为一个文件系统,为用户隐藏了多层的视角。
第一层:在我们输入docker pull redis的时候,第一步先加载宿主机的linux内核【linux在刚启动的时候会加载bootfs(包含bootloader和kernel。bootloader主要的作用是引导加载kernel)文件系统】。
第二层:当内核加载完成后,docker进行拉取基础镜像也就是linux发行版(Rootfs:在bootfs之上包含的就是典型linux系统中的/dev、/proc、/bin、/etc等标准目录和文件。)。例如这里选择的是centos。
第三、四层:使用centos提供的软件管理(yum)这个镜像有什么作用就利用yum安装什么软件。例如安装tomcat。
前四层是只读镜像,不能修改。且技术是通过联合文件系统整合成一个文件系统
第五层:运行容器后可读可写。
这一过程就是我们在dockerfile构建镜像的时候的样子。
总结:
1.当通过一个image启动容器时,docker会在该images最顶层添加一个读写文件系统作为容器,然后运行该容器。
2.docker镜像本质是基于UnionFS管理的分层文件系统。
3.docker镜像小的原因是docker镜像只有rootfs和其他镜像层,共用宿主机的linux内核(bootfs),所以很小。
4.为什么下载一个docker的nginx镜像,需要133MB?nginx安装包不是才几兆?
因为docker的nginx镜像是分层的,nginx安装包的确就几M,但是一个运行nginx的镜像文件,依赖于父镜像(上一层)和基础镜像(发行版),所以下载nginx镜像有100多MB。
docker镜像的定义:
如果我们定义一个mysql的镜像,我们会这样做:
- 获取基础镜像,选择一个发行平台(ubuntu,centos等)
- 在centos中安装mysql5.6软件
- 导出镜像并且命名为mysql:5.6镜像文件。
从上面这个过程中,我们可以感觉到这是一层一层添加的,docker镜像的层级概念就出来了,底层是centos镜像,上层是mysql镜像。centos镜像层属于父镜像。
Docker为什么叫分层镜像?
镜像分享一大好处就是共享资源,例如有多个镜像都来自于同一个base镜像,那么在docker host只需要存储一份base镜像。内存里也只需要加载一份host,即可为多个容器服务的。即多个容器共用一个镜像。即使多个容器共享一个base镜像,某个容器修改了base镜像的内容,例如修改了/etc/下的配置文件,其他容器下的/etc/文件时不会被修改的,修改的动作只在单容器内。所有的修改动作,都只会发生在容器层里。
标签:容器,0.0,介绍,containerd,docker,root,Docker From: https://www.cnblogs.com/wtdata123/p/16908623.html