第一章 走进Docker的世界
介绍docker的前世今生,了解docker的实现原理,以Django项目为例,带大家如何编写最佳的Dockerfile构建镜像。通过本章的学习,大家会知道docker的概念及基本操作,并学会构建自己的业务镜像,并通过抓包的方式掌握Docker最常用的bridge网络模式的通信。
认识docker
怎么出现的
- 轻量、高效的虚拟化
Docker 公司位于旧金山,原名dotCloud,底层利用了Linux容器技术(在操作系统中实现资源隔离与限制)。为了方便创建和管理这些容器,dotCloud 开发了一套内部工具,之后被命名为“Docker”。Docker就是这样诞生的。
(思考为啥要用Linux容器技术?)
Hypervisor: 一种运行在基础物理服务器和操作系统之间的中间软件层,可允许多个操作系统和应用共享硬件 。常见的VMware的 Workstation 、ESXi、微软的Hyper-V或者思杰的XenServer。
Container Runtime:通过Linux内核虚拟化能力管理多个容器,多个容器共享一套操作系统内核。因此摘掉了内核占用的空间及运行所需要的耗时,使得容器极其轻量与快速。
- 软件交付过程中的环境依赖
几个知识点
- 可以把应用程序代码及运行依赖环境打包成镜像,作为交付介质,在各环境部署
- 可以将镜像(image)启动成为容器(container),并且提供多容器的生命周期进行管理(启、停、删)
- container容器之间相互隔离,且每个容器可以设置资源限额
- 提供轻量级虚拟化功能,容器就是在宿主机中的一个个的虚拟的空间,彼此相互隔离,完全独立
- CS架构的软件产品
版本管理
- Docker 引擎主要有两个版本:企业版(EE)和社区版(CE)
- 每个季度(1-3,4-6,7-9,10-12),企业版和社区版都会发布一个稳定版本(Stable)。社区版本会提供 4 个月的支持,而企业版本会提供 12 个月的支持
- 每个月社区版还会通过 Edge 方式发布月度版
- 从 2017 年第一季度开始,Docker 版本号遵循 YY.MM-xx 格式,类似于 Ubuntu 等项目。例如,2018 年 6 月第一次发布的社区版本为 18.06.0-ce
发展史
13年成立,15年开始,迎来了飞速发展。
Docker 1.8之前,使用LXC,Docker在上层做了封装, 把LXC复杂的容器创建与使用方式简化为自己的一套命令体系。
之后,为了实现跨平台等复杂的场景,Docker抽出了libcontainer项目,把对namespace、cgroup的操作封装在libcontainer项目里,支持不同的平台类型。
2015年6月,Docker牵头成立了 OCI(Open Container Initiative开放容器计划)组织,这个组织的目的是建立起一个围绕容器的通用标准 。 容器格式标准是一种不受上层结构绑定的协议,即不限于某种特定操作系统、硬件、CPU架构、公有云等 , 允许任何人在遵循该标准的情况下开发应用容器技术,这使得容器技术有了一个更广阔的发展空间。
OCI成立后,libcontainer 交给OCI组织来维护,但是libcontainer中只包含了与kernel交互的库,因此基于libcontainer项目,后面又加入了一个CLI工具,并且项目改名为runC (https://github.com/opencontainers/runc ), 目前runC已经成为一个功能强大的runtime工具。
Docker也做了架构调整。将容器运行时相关的程序从docker daemon剥离出来,形成了containerd。containerd向上为Docker Daemon提供了gRPC接口
,使得Docker Daemon屏蔽下面的结构变化,确保原有接口向下兼容。向下通过containerd-shim
结合runC
,使得引擎可以独立升级,避免之前Docker Daemon升级会导致所有容器不可用的问题。
也就是说
- runC(libcontainer)是符合OCI标准的一个实现,与底层系统交互
- containerd是实现了OCI之上的容器的高级功能,比如镜像管理、容器执行的调用等
- Dockerd目前是最上层与CLI交互的进程,接收cli的请求并与containerd协作
小结
- 为了解决软件交付过程中的环境依赖,同时提供一种更加轻量的虚拟化技术,Docker出现了
- Docker是一种CS架构的软件产品,可以把代码及依赖打包成镜像,作为交付介质,并且把镜像启动成为容器,提供容器生命周期的管理
- docker-ce,每季度发布stable版本。18.06,18.09,19.03
- 发展至今,docker已经通过制定OCI标准对最初的项目做了拆分,其中runC和containerd是docker的核心项目,理解docker整个请求的流程,对我们深入理解docker有很大的帮助
安装
配置宿主机网卡转发
Yum安装配置docker
核心要素及常用操作详解
三大核心要素:镜像(Image)、容器(Container)、仓库(Registry)
(先整体看下流程,再逐个演示)
镜像(Image)
打包了业务代码及运行环境的包,是静态的文件,不能直接对外提供服务。
容器(Container)
镜像的运行时,可以对外提供服务。本质上讲是利用namespace和cgroup等技术在宿主机中创建的独立的虚拟空间。
仓库(Registry)
- 公有仓库,Docker Hub,阿里,网易...
- 私有仓库,企业内部搭建
- Docker Registry,Docker官方提供的镜像仓库存储服务
- Harbor, 是Docker Registry的更高级封装,它除了提供友好的Web UI界面,角色和用户权限管理,用户操作审计等功能
- 镜像访问地址形式 registry.devops.com/demo/hello:latest,若没有前面的url地址,则默认寻找Docker Hub中的镜像,若没有tag标签,则使用latest作为标签
- 公有的仓库中,一般存在这么几类镜像
- 操作系统基础镜像(centos,ubuntu,suse,alpine)
- 中间件(nginx,redis,mysql,tomcat)
- 语言编译环境(python,java,golang)
- 业务镜像(django-demo...)
操作演示
- 解压离线包
为了保证镜像下载的速度,因此提前在一台节点下载了离线镜像包,做解压:
- 查看所有镜像:
- 拉取镜像,从docker hub上:
- 如何唯一确定镜像:
- image_id
- repository:tag
- 导出镜像到文件中
- 从文件中加载本地镜像
- 部署镜像仓库,本地仓库
https://docs.docker.com/registry/
假设启动镜像仓库服务的主机地址为172.21.32.6,该目录中已存在的镜像列表:
现镜像仓库地址 | 原镜像仓库地址 |
172.21.32.6:5000/coreos/flannel:v0.11.0-amd64 | quay.io/coreos/flannel:v0.11.0-amd64 |
172.21.32.6:5000/mysql:5.7 | mysql:5.7 |
172.21.32.6:5000/nginx:alpine | nginx:alpine |
172.21.32.6:5000/centos:centos7.5.1804 | centos:centos7.5.1804 |
172.21.32.6:5000/elasticsearch/elasticsearch:7.4.2 | docker.elastic.co/elasticsearch/elasticsearch:7.4.2 |
172.21.32.6:5000/fluentd-es-root:v1.6.2-1.0 | gcr.io/google_containers/fluentd-elasticsearch:v2.4.0 |
172.21.32.6:5000/kibana/kibana:7.4.2 | docker.elastic.co/kibana/kibana:7.4.2 |
172.21.32.6:5000/kubernetesui/dashboard:v2.0.0-beta5 | kubernetesui/dashboard:v2.0.0-beta5 |
172.21.32.6:5000/kubernetesui/metrics-scraper:v1.0.1 | kubernetesui/metrics-scraper:v1.0.1 |
172.21.32.6:5000/kubernetes-ingress-controller/nginx-ingress-controller:0.30.0 | quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.30.0 |
- 推送本地镜像到镜像仓库中
- 删除镜像
- 查看容器列表
- 启动容器
- 容器数据持久化
- 进入容器或者执行容器内的命令
- 主机与容器之间拷贝数据
- 查看容器日志
- 停止或者删除容器
- 查看容器或者镜像的明细
Django应用容器化实践
django项目介绍
- 项目地址:https://gitee.com/agagin/python-demo.git
- python3 + uwsgi + nginx + mysql
- 内部服务端口8002
构建命令
如何理解构建镜像的过程?
Dockerfile是一堆指令,在docker build的时候,按照该指令进行操作,最终生成我们期望的镜像
- FROM 指定基础镜像,必须为第一个命令
- MAINTAINER 镜像维护者的信息
- COPY|ADD 添加本地文件到镜像中
- WORKDIR 工作目录
- RUN 构建镜像过程中执行命令
- CMD 构建容器后调用,也就是在容器启动时才进行调用
- ENTRYPOINT 设置容器初始化命令,使其可执行化
- ENV
- EXPOSE
Dockerfile
dockerfiles/myblog/Dockerfile
执行构建:
定制化基础镜像
dockerfiles/myblog/Dockerfile-base
简化Dockerfile
dockerfiles/myblog/Dockerfile-optimized
运行mysql
启动Django应用
访问62.234.214.206:8002/admin
构建镜像,替换默认编码:
dockerfiles/mysql/my.cnf
dockerfiles/mysql/Dockerfile
实现原理 录屏!!!
虚拟化核心需要解决的问题:资源隔离与资源限制
- 虚拟机硬件虚拟化技术, 通过一个 hypervisor 层实现对资源的彻底隔离。
- 容器则是操作系统级别的虚拟化,利用的是内核的 Cgroup 和 Namespace 特性,此功能完全通过软件实现。
Namespace 资源隔离
命名空间是全局资源的一种抽象,将资源放到不同的命名空间中,各个命名空间中的资源是相互隔离的。 通俗来讲,就是docker在启动一个容器的时候,会调用Linux Kernel Namespace的接口,来创建一块虚拟空间,创建的时候,可以支持设置下面这几种(可以随意选择),docker默认都设置。
- pid:用于进程隔离(PID:进程ID)
- net:管理网络接口(NET:网络)
- ipc:管理对 IPC 资源的访问(IPC:进程间通信(信号量、消息队列和共享内存))
- mnt:管理文件系统挂载点(MNT:挂载)
- uts:隔离主机名和域名
- user:隔离用户和用户组(3.8以后的内核才支持)
CGroup 资源限制
通过namespace可以保证容器之间的隔离,但是无法控制每个容器可以占用多少资源, 如果其中的某一个容器正在执行 CPU 密集型的任务,那么就会影响其他容器中任务的性能与执行效率,导致多个容器相互影响并且抢占资源。如何对多个容器的资源使用进行限制就成了解决进程虚拟资源隔离之后的主要问题。
Control Groups(简称 CGroups)就是能够隔离宿主机器上的物理资源,例如 CPU、内存、磁盘 I/O 和网络带宽。每一个 CGroup 都是一组被相同的标准和参数限制的进程。而我们需要做的,其实就是把容器这个进程加入到指定的Cgroup中。深入理解CGroup,请点此。
UnionFS 联合文件系统
Linux namespace和cgroup分别解决了容器的资源隔离与资源限制,那么容器是很轻量的,通常每台机器中可以运行几十上百个容器, 这些个容器是共用一个image,还是各自将这个image复制了一份,然后各自独立运行呢? 如果每个容器之间都是全量的文件系统拷贝,那么会导致至少如下问题:
- 运行容器的速度会变慢
- 容器和镜像对宿主机的磁盘空间的压力
怎么解决这个问题------Docker的存储驱动
- 镜像分层存储
- UnionFS
Docker 镜像是由一系列的层组成的,每层代表 Dockerfile 中的一条指令,比如下面的 Dockerfile 文件:
这里的 Dockerfile 包含4条命令,其中每一行就创建了一层,下面显示了上述Dockerfile构建出来的镜像运行的容器层的结构:
镜像就是由这些层一层一层堆叠起来的,镜像中的这些层都是只读的,当我们运行容器的时候,就可以在这些基础层至上添加新的可写层,也就是我们通常说的容器层
,对于运行中的容器所做的所有更改(比如写入新文件、修改现有文件、删除文件)都将写入这个容器层。
对容器层的操作,主要利用了写时复制(CoW)技术。CoW就是copy-on-write,表示只在需要写时才去复制,这个是针对已有文件的修改场景。 CoW技术可以让所有的容器共享image的文件系统,所有数据都从image中读取,只有当要对文件进行写操作时,才从image里把要写的文件复制到自己的文件系统进行修改。所以无论有多少个容器共享同一个image,所做的写操作都是对从image中复制到自己的文件系统中的复本上进行,并不会修改image的源文件,且多个容器操作同一个文件,会在每个容器的文件系统里生成一个复本,每个容器修改的都是自己的复本,相互隔离,相互不影响。使用CoW可以有效的提高磁盘的利用率。
镜像中每一层的文件都是分散在不同的目录中的,如何把这些不同目录的文件整合到一起呢?
UnionFS 其实是一种为 Linux 操作系统设计的用于把多个文件系统联合到同一个挂载点的文件系统服务。 它能够将不同文件夹中的层联合(Union)到了同一个文件夹中,整个联合的过程被称为联合挂载(Union Mount)。
上图是AUFS的实现,AUFS是作为Docker存储驱动的一种实现,Docker 还支持了不同的存储驱动,包括 aufs、devicemapper、overlay2、zfs 和 Btrfs 等等,在最新的 Docker 中,overlay2 取代了 aufs 成为了推荐的存储驱动,但是在没有 overlay2 驱动的机器上仍然会使用 aufs 作为 Docker 的默认驱动。
Docker网络 录屏!!!
docker容器是一块具有隔离性的虚拟系统,容器内可以有自己独立的网络空间,
- 多个容器之间是如何实现通信的呢?
- 容器和宿主机之间又是如何实现的通信呢?
- 使用-p参数是怎么实现的端口映射?
带着我们就这些问题,我们来学习一下docker的网络模型,最后我会通过抓包的方式,给大家演示一下数据包在容器和宿主机之间的转换过程。
网络模式
我们在使用docker run创建Docker容器时,可以用--net选项指定容器的网络模式,Docker有以下4种网络模式:
- bridge模式,使用--net=bridge指定,默认设置
- host模式,使用--net=host指定,容器内部网络空间共享宿主机的空间,效果类似直接在宿主机上启动一个进程,端口信息和宿主机共用。
- container模式,使用--net=container:NAME_or_ID指定
指定容器与特定容器共享网络命名空间 - none模式,使用--net=none指定
网络模式为空,即仅保留网络命名空间,但是不做任何网络相关的配置(网卡、IP、路由等)
bridge模式
那我们之前在演示创建docker容器的时候其实是没有指定的网络模式的,如果不指定的话默认就会使用bridge模式,bridge本意是桥的意思,其实就是网桥模式,那我们怎么理解网桥,如果需要做类比的话,我们可以把网桥看成一个二层的交换机设备,我们来看下这张图:
交换机通信简图
网桥模式示意图
网桥在哪,查看网桥
有了网桥之后,那我们看下docker在启动一个容器的时候做了哪些事情才能实现容器间的互联互通
Docker 创建一个容器的时候,会执行如下操作:
- 创建一对虚拟接口/网卡,也就是veth pair;
- 本地主机一端桥接 到默认的 docker0 或指定网桥上,并具有一个唯一的名字,如 veth9953b75;
- 容器一端放到新启动的容器内部,并修改名字作为 eth0,这个网卡/接口只在容器的命名空间可见;
- 从网桥可用地址段中(也就是与该bridge对应的network)获取一个空闲地址分配给容器的 eth0
- 配置默认路由到网桥
那整个过程其实是docker自动帮我们完成的,清理掉所有容器,来验证。
我们如何知道网桥上的这些虚拟网卡与容器端是如何对应?
通过ifindex,网卡索引号
整理脚本,快速查看对应:
上面我们讲解了容器之间的通信,那么容器与宿主机的通信是如何做的?
添加端口映射:
端口映射如何实现的?先来回顾iptables链表图
访问本机的8088端口,数据包会从流入方向进入本机,因此涉及到PREROUTING和INPUT链,我们是通过做宿主机与容器之间加的端口映射,所以肯定会涉及到端口转换,那哪个表是负责存储端口转换信息的呢,就是nat表,负责维护网络地址转换信息的。因此我们来查看一下PREROUTING链的nat表:
规则利用了iptables的addrtype拓展,匹配网络类型为本地的包,如何确定哪些是匹配本地,
也就是说目标地址类型匹配到这些的,会转发到我们的TARGET中,TARGET是动作,意味着对符合要求的数据包执行什么样的操作,最常见的为ACCEPT或者DROP,此处的TARGET为DOCKER,很明显DOCKER不是标准的动作,那DOCKER是什么呢?我们通常会定义自定义的链,这样把某类对应的规则放在自定义链中,然后把自定义的链绑定到标准的链路中,因此此处DOCKER 是自定义的链。那我们现在就来看一下DOCKER这个自定义链上的规则。
此条规则就是对主机收到的目的端口为8088的tcp流量进行DNAT转换,将流量发往172.17.0.2:80,172.17.0.2地址是不是就是我们上面创建的Docker容器的ip地址,流量走到网桥上了,后面就走网桥的转发就ok了。
所以,外界只需访问192.168.136.133:8088就可以访问到容器中的服务了。
数据包在出口方向走POSTROUTING链,我们查看一下规则:
大家注意MASQUERADE这个动作是什么意思,其实是一种更灵活的SNAT,把源地址转换成主机的出口ip地址,那解释一下这条规则的意思:
这条规则会将源地址为172.17.0.0/16的包(也就是从Docker容器产生的包),并且不是从docker0网卡发出的,进行源地址转换,转换成主机网卡的地址。大概的过程就是ACK的包在容器里面发出来,会路由到网桥docker0,网桥根据宿主机的路由规则会转给宿主机网卡eth0,这时候包就从docker0网卡转到eth0网卡了,并从eth0网卡发出去,这时候这条规则就会生效了,把源地址换成了eth0的ip地址。
注意一下,刚才这个过程涉及到了网卡间包的传递,那一定要打开主机的ip_forward转发服务,要不然包转不了,服务肯定访问不到。
抓包演示
我们先想一下,我们要抓哪个网卡的包
- 首先访问宿主机的8088端口,我们抓一下宿主机的eth0
- 然后最终包会流入容器内,那我们抓一下容器内的eth0网卡
到另一台机器访问一下,
停止抓包,拷贝容器内的包到宿主机
把抓到的内容拷贝到本地,使用wireshark进行分析。
(wireshark合并包进行分析)
进到容器内的包做DNAT,出去的包做SNAT,这样对外面来讲,根本就不知道机器内部是谁提供服务,其实这就和一个内网多个机器公用一个外网IP地址上网的效果是一样的,对吧,那这也属于NAT功能的一个常见的应用场景。
Host模式
容器内部不会创建网络空间,共享宿主机的网络空间
Conatiner模式
这个模式指定新创建的容器和已经存在的一个容器共享一个 Network Namespace,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过 lo 网卡设备通信。
实用技巧
- 清理主机上所有退出的容器
- 调试或者排查容器启动错误
本章小结
- 为了解决软件交付过程中的环境依赖,同时提供一种更加轻量的虚拟化技术,Docker出现了
- 2013年诞生,15年开始迅速发展,从17.03月开始,使用时间日期管理版本,稳定版以每季度为准
- Docker是一种CS架构的软件产品,可以把代码及依赖打包成镜像,作为交付介质,并且把镜像启动成为容器,提供容器生命周期的管理
- 使用yum部署docker,启动后通过操作docker这个命令行,自动调用docker daemon完成容器相关操作
- 常用操作
- systemctl start|stop|restart docker
- docker build | pull -> docker tag -> docker push
- docker run --name my-demo -d -p 8080:80 -v /opt/data:/data demo:v20200327
- docker cp /path/a.txt mycontainer:/opt
- docker exec -ti mycontainer /bin/sh
- docker logs -f mycontainer
- 通过dockerfile构建业务镜像,先使用基础镜像,然后通过一系列的指令把我们的业务应用所需要的运行环境和依赖都打包到镜像中,然后通过CMD或者ENTRYPOINT指令把镜像启动时的入口制定好,完成封装即可。有点类似于,先找来一个空的集装箱(基础镜像),然后把项目依赖的服务都扔到集装箱中,然后设置好服务的启动入口,关闭箱门,即完成了业务镜像的制作。
- 容器的实现依赖于内核模块提供的namespace和control-group的功能,通过namespace创建一块虚拟空间,空间内实现了各类资源(进程、网络、文件系统)的隔离,提供control-group实现了对隔离的空间的资源使用的限制。
- docker镜像使用分层的方式进行存储,根据主机的存储驱动的不同,实现方式会不同,kernel在3.10.0-514以上自动支持overlay2 存储驱动,也是目前Docker推荐的方式。
- 得益于分层存储的模式,多个容器可以通过copy-on-write的策略,在镜像的最上层加一个可写层,实现一个镜像快速启动多个容器的场景
- docker的网络模式分为4种,最常用的为bridge和host模式。bridge模式通过docker0网桥,启动容器的时候通过创建一对虚拟网卡,将容器连接在桥上,同时维护了虚拟网卡与网桥端口的关系,实现容器间的通信。容器与宿主机之间的通信通过iptables端口映射的方式,docker利用iptables的PREROUTING和POSTROUTING的nat功能,实现了SNAT与DNAT,使得容器内部的服务被完美的保护起来。