docker基本原理
优势
-
快速移植
- 不需要手动安装依赖
- 比如Java需要jvm的依赖
- 开发和生产环境保持一致
- 不需要手动安装依赖
-
资源隔离
- 保持机器整洁
- 避免一个程序修改的环境变量等影响其他程序
- 减少因为端口等资源冲突导致的错误
- 保持机器整洁
-
安全
- 避免恶意程序影响其与程序
- 限制程序的资源占用(CPU,内存),避免物理机崩溃
缺陷
-
容器只能使用宿主机的kernel,且不能修改
- 如果某一应用只能依赖特定的kernel版本下运行,应该使用虚拟机
- 应用依赖内核是指程序直接进行内核调用,而不是仅仅使用库文件
- 不能在 Windows 宿主机上运行 Linux 容器,或者在低版本的 Linux 宿主机上运行依赖高版本的容器
- 如果某一应用只能依赖特定的kernel版本下运行,应该使用虚拟机
-
容器的安全性比虚拟机低
- 除了Device Mapper,其他文件系统无法限制容器使用的磁盘容量,可能导致一个容器把宿主机的磁盘空间耗尽
- 一个大流量程序容器可能会耗尽宿主机网络带宽
架构
- 在命令行使用的是docker client, 真正提供服务的是docker daemon
- docker client把命令行命令转换为Restful API 通过socks或者**TCP(https)**发送给 daemon
- API Server用于接收来自docker client的请求,然后分发给不同模块
-
docker daemon的工作根目录位于
/var/lib/docker
- 容器的配置信息位于
/var/lib/docker/containers
- 镜像元数据位于
/var/lib/docker/image
- 数据卷元数据位于
/var/lib/docker/volumes
- 容器的配置信息位于
-
命令执行过程
- 创建client实例
- 利用反射从用户命令(run)匹配执行方法(CmdRun函数)
- 解析参数
- 获取与daemon通信的认证配置
- 发送POST, GET等请求给daemon
- 例如
docker run
:POST /containers/create?<containerValues>
POST /containers/<createResponse.ID>/start
- 例如
- daemon通过execdriver模块(封装了对OS资源操作的方法)指挥OS创建进程
- client读取daemon的返回结果并显示
隔离(namespace)
- 使用虚拟化技术作为应用沙盒,就必须要由 Hypervisor 来负责创建虚拟机,这个虚拟机是真实存在的,并且它里面必须运行一个完整的 Guest OS 才能执行用户的应用进程
- 这就不可避免地带来了额外的资源消耗和占用
- 跟真实存在的虚拟机不同,在使用 Docker 的时候,并没有一个真正的Docker 容器运行在宿主机里面
- Docker 项目帮助用户启动的,还是原来的应用进程,只不过在创建这些进程时,Docker 为它们加上了各种各样的 Namespace 参数
-
容器本质上就是一个加了限定参数的进程
- 与其他所有进程之间是平等的关系
- docker daemon 只是启动时用,运行时并不需要,真实进程(容器)是直接跑在宿主机上
- 宿主机可以直接控制容器内的进程,包括杀掉容器(进程)
- 容器化后的用户应用,却依然还是一个宿主机上的普通进程,这就意味着这些因为虚拟化而带来的性能损耗都是不存在的
- 通过namespace技术, 容器中运行的进程其看不到其余进程,所以在容器中会重新计算进程号
- 但是实际上在原来的宿主机中,这个进程仍然有个原本分配的进程号
-
在容器内,除了pid=1的进程(init),其他进程是不受docker控制的
- 通过exec进去之后启动的进程,不受控制
- 控制指的是它们的回收和生命周期管理
- 除了init进程,其他进程挂掉了docker也感知不到
- 通过exec进去之后启动的进程,不受控制
-
使用
clone
函数创建拥有独立namespace的进程-
clone
可以利用flags参数控制使用多少功能- 例如是否与父进程共享虚拟内存等
- 通过位操作设定,例如
CLONE_NETNET|CLONE_NEWPID
-
-
每个进程所对应的各种namespace都在
/proc/<pid>/ns
目录下- namespace以文件描述符的形式存在
- 拥有相同namespace号的进程位于同一个namespace下
- 通过
setns()
可以让进程加入一个namespace-
docker exec
就是通过这种方式工作的
-
namespace
-
UTS namespace提供了主机名和域名的隔离
- 使得容器在网络中被视为独立的节点,而不是宿主机上的一个进程
-
IPC namespace涉及信号量,消息队列和共享内存
- 不同IPC namespace的进程相互不可见
-
PID namespace是树状结构的,创建子进程也就是创建子节点
- 父节点可以看到子节点,并通过信号控制子节点
- 而子节点无法看到父节点,或对父节点产生影响
- 通过监控docker daemon的子节点并筛选,就可以从外部监控docker容器
-
ps aux
或者top
调用了/proc
目录下的文件内容- 因此只隔离PID是不够的,还需要隔离文件系统,重新挂载
/proc
目录
- 因此只隔离PID是不够的,还需要隔离文件系统,重新挂载
-
linux系统中的init进程(pid=1)是所有节点的父进程
-
它维护了一张进程表,不断检查子进程状态,并负责回收孤儿进程的资源
-
因此如果要确实要在容器中运行多个进程,最先启动的进程应该有资源监控和回收的功能
- 例如systemd,bash
-
init进程如果没有编写处理某个信号的逻辑,那么其子进程发送给它的所有信号都会被忽略
- 这样做避免了init进程被误杀
- 父节点发送给其子节点init进程的信号除了SIGSTOP和SIGKILL外也会被忽略
- SIGSTOP和SIGKILL会被强制执行
-
-
mount namespace通过隔离文件挂载点隔离了文件系统
-
挂载对象:
- 共享挂载的目录发生变量时可以自动传播到其他namespace中
- 从属挂载的目录父namespace的变化可以传播到子namespace,反之不行
-
私有挂载的目录相互之间不传播变动
-
挂载对象:
-
network namespace隔离了网络资源,包括IP协议栈,路由表,防火墙,套接字等
- 可以通过创建
veth pair
在不同network namespace创建通道,从而得以相互通信- 容器隔离网络的做法就是创建一个一头在宿主机的docker0网桥,一头在容器中(通常是
ETH0
)的veth pair
- 容器隔离网络的做法就是创建一个一头在宿主机的docker0网桥,一头在容器中(通常是
-
docker daemon
和容器的init
进程通过pipe通信
- 可以通过创建
-
user namespace主要限制了容器的用户权限
- 一个容器进程内的超级用户映射到容器外的普通用户
限制(cgroups)
-
Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等
- cgroups只能限制上限,不能限制下限,所以需要k8s等应用的调度
-
功能
- 资源限制,如内存等
- 优先级分配,例如CPU优先级和IO带宽
- 资源统计,如CPU使用时长,内存用量等
- 任务控制,例如对任务的挂起和恢复
-
API以伪文件系统实现,用户态程序可以通过文件操作实现管理
- 位于
/sys/fs/cgroup
目录 -
docker daemon会在对应的资源目录下创建docker目录,并在其中为每个容器ID创建目录来控制资源
- 例如
/sys/fs/cgroup/cpu/docker/<container-ID>
- 例如
- 位于
文件系统(UnionFS)
镜像
-
镜像是容器的静态视角,容器时镜像的运行状态
-
容器进程在启动前会挂载根目录到镜像提供的文件系统(rootfs)
- 通过修改iNode
- 不同版本的linux OS公用相同的kernel,主要不同在于rootfs文件系统
- 镜像内不包括操作系统内核
-
由于 rootfs 里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起
- 这就赋予了容器所谓的一致性:无论在本地、云端,还是在一台任何地方的机器上,用户只需要解压打包好的容器镜像,那么这个应用运行所需要的完整的执行环境就被重现出来了
-
镜像的元数据与镜像文件是分开储存的
-
repository元数据储存在
/var/lib/docker/image/<graph_driver>/repositories.json
文件中- 该文件中储存了镜像的名字、tag以及对应的镜像ID(采用SHA256算法计算)
-
image元数据储存在
/var/lib/docker/image/<graph_driver>/imagedb/content/sha256/<images-ID>
文件中- 包括镜像架构(如amd64), 创建时间,环境变量等信息
-
layer元数据储存在
/var/lib/docker/image/<graph_driver>/layerdb/sha256/<layer-ID>
文件中- 包括该层的构建信息以及父镜像层ID
-
repository元数据储存在
-
镜像安全
- 通过镜像数字签名验证完整性
分层文件系统
- 当启动一个容器时,docker加载镜像的所有只读层,并在最上层加入init层和读写层
-
init层专门用来存放
/etc/hosts
等信息- 用户往往需要在启动容器时写入一些指定的值比如 hostname,所以就需要在可读写层对它们进行修改
- 这些修改往往只对当前的容器有效,我们并不希望执行commit 时,把这些信息连同可读写层一起提交掉
-
init层专门用来存放
-
使用联合文件系统(unionFS)对rootfs进行增量修改
- 读取: 从最上层找到最下层,直到找到或到底
- 写入: 如果文件不存在则在读写层新建,否则把文件复制到读写层并修改
-
删除: 如果文件仅位于读写层,则直接删除;否则先删除读写层备份,然后创建
writeout
文件标志文件不存在- 不会删除只读层文件,所以反而镜像体积变大
-
新建: 如果只读层存在对应的
writeout
文件,则删除后再新建;否则直接在读写层新建
储存卷
-
环境变量和储存卷实现“多态”
- 密码等配置数据不用插入镜像中,而是通过环境变量或者配置文件动态载入
- 使得镜像可以复用
-
绑定挂载卷
- 把宿主机器上的文件或目录映射到容器中,避免不必要的拷贝
- 可以只挂载单个文件
- 可以设置为只读,避免容器的修改
-
共享储存卷
-
–-volumes-from <container>
参数可以共享储存卷 - 不能更改原来绑定的路径,以及读写权限
- 可以通过在数据卷中使用cp命令复制到指定路径
- 如果从多个容器共享,且他们拥有相同给的挂载点,则只会共享最后一个
- 比如共有相同的配置文件路径
- 如果一个数据卷容器有多个挂载路径,那么某一个路径冲突的概率就会增加,所以最好一个数据卷一个挂载
-
docker run --name devConfig -v /config <image> bash -c "cp /dev/* /config/"
docker run --name prodConfig -v /config <image> bash -c "cp /prod/* /config/"
docker run --name devApp --volumes-from devConfig <image>
docker run --name prodApp --volumes-from prodConfig <image>
网络
-
虽然上图两个joined容器所在的网络和默认bridge网络存在连接,但是仍然无法通信
- 因为iptables DROP掉了网桥 docker0 与自建网络之间双向的流量
- docker在设计时就是想隔离不同网络
- https://yq.aliyun.com/articles/311450?spm=a2c4e.11155435.0.0.7bee216fWZYjAs
- 因为iptables DROP掉了网桥 docker0 与自建网络之间双向的流量
-
容器默认可以访问外网,只是外网默认不能访问容器
- 容器通过-P或者-p参数启动连接时,默认连接地址为
0.0.0.0
,即接受所有地址的流量 - 可以通过显式设置地址来指定允许访问的IP地址
- 容器通过-P或者-p参数启动连接时,默认连接地址为
-
docker服务端会启动一个虚拟网卡(docker 0)
- 这个接口相当于一个网卡,拥有独立的IP地址(ifconfig可以查到),使得容器可以和外部网络通信
- 所有bridge模式的容器都被挂载到了docker0的子网中
- 所有连接到docker 0 的接口都是同一个虚拟子网的一部分,可以通过IP地址互相通信
- 问题在于如何方便得知道对方的IP,这就需要
--link
或者加入同一自定义网络了- docker daemon 实现了一个内嵌的 DNS server,使容器可以直接通过容器名通信
- 使用 docker DNS 有个限制:只能在 user-defined 网络中使用。也就是说,默认的 bridge 网络是无法使用 DNS 的
- 问题在于如何方便得知道对方的IP,这就需要
- 如果
-p 1234:5678
,容器之间访问5678端口,外部服务访问1234端口- 因为容器之间属于同一个局域网中,而外部服务访问是通过NAT转换的
- 可以通过设置
-icc-false
禁止容器间通信
-
每个容器都有一个本地回环接口(localhost或127.0.0.1)
- 这样本机程序可以通过套接字通信
-
docker利用NAT实现与外网的通信
- 容器使用-p指定映射的端口时,docker会通过iptables创建一条nat规则,把宿主机打到映射端口的数据包通过转发到docker0的网关,docker0再通过广播找到对应ip的目标容器,把数据包转发到容器的端口上
- 容器使用-p指定映射的端口时,docker会通过iptables创建一条nat规则,把宿主机打到映射端口的数据包通过转发到docker0的网关,docker0再通过广播找到对应ip的目标容器,把数据包转发到容器的端口上
-
每一个映射的端口,host 都会启动一个 docker-proxy 进程来处理访问容器的流量
-
link原理
- 使用
-link
选项关联容器,不但可以避免容器IP和端口暴露到外部导致的安全问题,还能避免容器在重启后IP地址变动导致的访问失败- 原理类似DNS的IP和域名映射
- 在接受容器(即设置了link参数的容器)中保存了设置了以下信息:
- 设置环境变量:源容器的名称、别名、IP、暴露的端口等
- 如果源容器重启后更换了IP,接受容器的环境变量并不会更新
- 更新
/etc/hosts
文件:添加源容器IP和别名的记录- 源容器重启后会自动更新接受容器的
/etc/hosts
文件
- 源容器重启后会自动更新接受容器的
- 设置环境变量:源容器的名称、别名、IP、暴露的端口等
- 接收容器必须在源容器后启动
- 这只针对位于默认网络中的容器
- 自定义网络中可以先定义接收容器
- 实际上自定义网络中的link不是通过配置
/etc/hosts
文件实现的,而是通过DNS解析
- 实际上自定义网络中的link不是通过配置
- 使用
上一篇: word多个文档替换内容
下一篇: 生活百态图片,大众化搞笑图片
推荐阅读
-
在centos7 中docker info报错docker bridge-nf-call-iptables is disabled 的解决方法
-
docker操作命令大全和后台参数
-
Docker 限制容器可用的CPU的方式
-
详解基于Harbor搭建Docker私有镜像仓库
-
Docker 1分钟搭建DNS服务器的方法
-
docker挂载NVIDIA显卡运行pytorch的方法
-
docker利用WebHook实现持续集成
-
使用docker -v 和 Publish over SSH插件实现war包自动部署到docker的操作步骤
-
Docker如何限制容器可用的内存
-
Centos7安装docker compse踩过的坑及解决方法