Docker容器详解(上)

一、Docker核心概念

Docker使用客户端-服务器(C/S)架构模式,使用远程API管理和创建Docker容器

Docker容器详解(上)

  • Docker客户端(Client)
  • Docker服务器(Docker daemon):负责创建、运行、监控容器,构建、存储镜像
  • Docker镜像(Image):可以看成只读模板,通过它创建Docker容器
  • Registry:存放Docker镜像的仓库,分为私有和公有两种
  • Docker容器(Container):Docker镜像的运行实例

2)、安装Docker CE

1)设置存储库

安装所需的包。yum-utils提供了yum-config-manager 效用,并device-mapper-persistent-datalvm2由需要 devicemapper存储驱动程序

[[email protected] ~]# yum install -y yum-utils device-mapper-persistent-data lvm2

设置稳定存储库

[[email protected] ~]# yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

2)安装Docker CE

[[email protected] ~]# yum install docker-ce docker-ce-cli containerd.io

3)启动Docker

[[email protected] ~]# systemctl start docker

参考:https://docs.docker.com/install/linux/docker-ce/centos/

二、从进程说起

容器其实是一种沙盒技术。沙盒就是能够像一个集装箱一样,把应用装起来的技术。这样,应用与应用之间,就因为有了边界而不至于相互干扰;而被装进集装箱的应用,也可以被方便地搬来搬去

对于进程来说,它的静态表现就是程序,平常都安安静静地待在磁盘上;而一旦运行起来,它就变成了计算机里的数据和状态的总和,这就是它的动态表现

容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个边界

对于Docker等大多数Linux容器来说,Cgroups技术是用来制造约束的主要手段,而Namespace技术则是用来修改进程视图的主要方法

Linux里面的Namespace机制其实就是对被隔离应用的进程空间做了手脚,使得这些进程只能看到重新计算过的进程编号,可实际上,他们在宿主机的操作系统还是原来的进程编号。而Namespace其实只是Linux创建新进程的一个可选参数

Linux系统中创建线程的系统调用是clone(),比如:

int pid = clone(main_function, stack_size, SIGCHLD, NULL); 

这个系统调用就会为我们创建一个新的进程,并且返回它的进程号pid

而当我们用clone()系统调用创建一个新进程时,就可以在参数中指定CLONE_NEWPID参数,比如:

int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL); 

这时,新创建的这个进程将会看到一个全新的进程空间,在这个进程空间里,它的PID是1

namespace 系统调用参数 隔离内容
UTS CLONE_NEWUTS 主机名与域名
IPC CLONE_NEWIPC 信号量、消息队列和共享内存
PID CLONE_NEWPID 进程编号
Network CLONE_NEWNET 网络设备、网络栈、端口等
Mount CLONE_NEWS 文件系统
User CLONE_NEWUSER 用户和用户组

容器其实是一种特殊的进程而已

三、隔离与限制

Namespace技术实际上修改了应用进程看待整个计算机视图,即它的视线被操作系统做了限制,只能看到某些指定的内容。但对于宿主机来说,这些被隔离了的进程跟其他线程并没有太大区别

虚拟机和容器的对比图:

Docker容器详解(上)

左边为虚拟机的工作原理,名为Hypervisor的软件是虚拟机最主要的部分,它通过硬件虚拟化功能,模拟出了运行一个操作系统需要的各种硬件,比如CPU、内存、I/O设备等等

使用虚拟化技术作为应用沙盒,就必须要由Hypervisor来负责创建虚拟机,这个虚拟机是真实存在的,并且它里面必须运行一个完整的Guest OS才能执行用户的应用进程。这就不可避免地带来了额外的资源消耗和占用。此外,用户应用运行在虚拟机里面,它对宿主机操作系统的调用就不可避免地要经过虚拟化软件的拦截和处理,这本身又是一层性能损耗,尤其对计算资源、网络和磁盘I/O的损耗非常大

相比之下,容器化后的用户应用却依然还是一个宿主机上的普通进程,这就意味着这些因为虚拟化而带来的性能损耗都是不存在的;而另一方面,使用Namespace作为隔离手段的容器并不需要单独的Guest OS,这就使得容器额外的资源占用几乎可以忽略不计

敏捷和高性能是容器相较于虚拟机最大的优势

但是基于Linux Namespace的隔离机制相比于虚拟化技术也有很多不足之处,其中最主要的问题就是:隔离得不彻底

既然容器只是运行在宿主机上的一种特殊的进程,那么多个容器之间使用的就还是同一个宿主机的操作系统内核

在Linux内核中,有很多资源和对象是不能被Namespace化的,最典型的例子就是:时间

Linux Cgroups是Linux内核中用来为进程设置资源限制的一个重要功能,它最主要的作用就是限制一个进程组能够使用的资源上限,包括CPU、内存、磁盘、网络带宽等等

  • blkio:这个子系统为块设备设定输入/输出限制,比如物理设备(磁盘,固态硬盘,USB 等等)
  • cpu:这个子系统使用调度程序提供对 CPU 的 cgroup 任务访问
  • cpuacct:这个子系统自动生成 cgroup 中任务所使用的 CPU 报告
  • cpuset:这个子系统为 cgroup 中的任务分配独立 CPU(在多核系统)和内存节点
  • devices:这个子系统可允许或者拒绝 cgroup 中的任务访问设备
  • freezer:这个子系统挂起或者恢复 cgroup 中的任务
  • memory:这个子系统设定 cgroup 中任务使用的内存限制,并自动生成内存资源使用报告
  • net_cls:这个子系统使用等级识别符(classid)标记网络数据包,可允许 Linux 流量控制程序(tc)识别从具体 cgroup 中生成的数据包
  • net_prio :这个子系统用来设计网络流量的优先级
  • hugetlb:这个子系统主要针对于HugeTLB系统进行限制,这是一个大页文件系统

四、深入理解容器镜像

Mount Namespace修改的,是容器进程对文件系统挂载点的认知。Mount Namespace对容器进程试图的改变,一定是伴随着挂载操作才能生效

容器镜像就是挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,也叫作rootfs(根文件系统)

对于Docker项目来说,它最核心的原理实际上就是为待创建的用户进程:

1)启用Linux Namespace 配置

2)设置指定的Cgroups参数

3)切换进程的根目录

rootfs只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在Linux操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像

同一台机器上的所有容器,都共享宿主机操作系统的内核

由于rootfs里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。对一个应用来说,操作系统本身才是它运行所需要的最完整的依赖库。这种深入到操作系统级别的运行环境一致性,打通了应用在本地开发和远端执行环境之间难以逾越的鸿沟

Docker在镜像的设计中,引入了层的概念。也就是,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量rootfs

Docker容器详解(上)

1)、只读层

它是这个容器的rootfs最下面的五层,对应的正是ubuntu:latest镜像的五层

2)、可读写层

它是这个容器的rootfs最上面的一层,在没有写入文件之前,这个目录是空的。而一旦在容器里做了写操作,修改的内容就会以增量的方式出现在这个层中

如果删除只读层里的一个文件,会在可读写层创建一个whileout文件,把只读层里的文件遮挡起来

最上面这个可读写层的作用,就是专门用来存放修改rootfs后产生的增量,无论是增、删、改都发生在这里。而当我们使用完了这个被修改过的容器之后,还可以使用docker commit和push指令,保存这个被修改过的可读写层,并上传到Docker Hub,供其他人使用;而与此同时,原先的只读层里的内容则不会有任何变化

3)、Iint层

它是一个以-init结尾的层,夹在只读层和读写层之间。Init层是Docker项目单独生成的一个内部层,专门用来存放/etc/hosts、/etc/resolv.conf等信息

需要这样一层的原因是,这些文件本来属于只读的Ubuntu镜像的一部分,但是用户往往需要在启动容器时写入一些指定的值比如hostname,所以就需要在可读写层对它们进行修改。可是,这些修改往往只对当前的容器有效,我们并不希望docker commit时,把这些信息连同可读写层一起提交掉。Docker的做法是在修改了这些文件之后,以一个单独的层挂载出来,而用户执行docker commit只会提交可读写层,所以是不会包含这些内容的