01 初识容器:万事开头难
你好,我是Chrono。
在课前准备里,我们使用VirtualBox/VMWare搭建了Linux虚拟机环境,有了这个基础,今天我们就开始正式的学习。
俗话说:"万事开头难",对于Kubernetes这个庞大而陌生的领域来说更是如此,如何迈出学习的第一步非常关键,所以,今天我们先从最简单、最基本的知识入手,聊聊最流行的容器技术Docker,先搭建实验环境,再动手操作一下,进而破除它的神秘感。
Docker的诞生
现在我们都已经对Container、Kubernetes这些技术名词耳熟能详了,但你知道这一切的开端------Docker,第一次在世界上的亮相是什么样子的吗?
九年前,也就是2013年3月15日,在北美的圣克拉拉市召开了一场Python开发者社区的主题会议PyCon,研究和探讨各种Python开发技术和应用,与我们常说的"云""PaaS""SaaS"根本毫不相关。
在当天的会议日程快结束时,有一个"闪电演讲"(lighting talk)的小环节。其中有一位开发者,用了5分钟的时间,做了题为 "The future of Linux Containers" 的演讲,不过临近末尾因为超时而被主持人赶下了台,场面略显尴尬(你可以在这里回看这段具有历史意义的视频)。

相信你一定猜到了,这个只有短短5分钟的技术演示,就是我们目前所看到的、席卷整个业界的云原生大潮的开端。正是在这段演讲里,Solomon Hykes(dotCloud公司,也就是Docker公司的创始人)首次向全世界展示了Docker技术。
5分钟的时间非常短,但演讲里却包含了几个现在已经普及,但当时却非常新奇的概念,比如容器、镜像、隔离运行进程等,信息量非常大。
PyCon2013大会之后,许多人都意识到了容器的价值和重要性,发现它能够解决困扰了云厂商多年的打包、部署、管理、运维等问题,Docker也就迅速流行起来,成为了GitHub上的明星项目。然后在几个月的时间里,Docker更是吸引了Amazon、Google、Red Hat等大公司的关注,这些公司利用自身的技术背景,纷纷在容器概念上大做文章,最终成就了我们今天所看到的至尊王者Kubernetes的出现。
Docker的形态
好了,下面我们就要来一个"情境再现",在我们的Linux虚拟机上搭建一个容器运行环境,模拟一下当年Solomon Hykes初次展示Docker的场景。
当然,如今的Docker经过了九年的发展,已经远不是当初的"吴下阿蒙"了,不过最核心的那些概念和操作还是保持了一贯性,没有太大的变化。
首先,我们需要对Docker的形态有所了解。目前使用Docker基本上有两个选择:Docker Desktop 和Docker Engine。

Docker Desktop是专门针对个人使用而设计的,支持Mac和Windows快速安装,具有直观的图形界面,还集成了许多周边工具,方便易用。
不过,我个人不是太推荐使用Docker Desktop,原因有两个。第一个,它是商业产品,难免会带有Docker公司的"私人气息",有一些自己的、非通用的东西,不利于我们后续的Kubernetes学习。第二个,它只是对个人学习免费,受条款限制不能商用,我们在日常工作中难免会"踩到雷区"。
Docker Engine则和Docker Desktop正好相反,完全免费,但只能在Linux上运行,只能使用命令行操作,缺乏辅助工具,需要我们自己动手DIY运行环境。不过要是较起真来,它才是Docker当初的真正形态,"血脉"最纯正,也是现在各个公司在生产环境中实际使用的Docker产品,毕竟机房里99%的服务器跑的都是Linux。
所以,在接下来的学习过程里,我推荐使用Docker Engine,之后在本专栏内,如果没有什么特别的声明,Docker这个词通常指的就是Docker Engine。
Docker的安装
在课前准备里,我们已经在Linux虚拟机里安装了一些常用软件,用的是Ubuntu的包管理工具apt,所以,我们仍然可以使用同样的方式来安装Docker。
先让我们尝试输入命令 docker ,会得到"命令未找到"的提示,还有如何安装的建议:
Command 'docker' not found, but can be installed with: sudo apt install docker.io
所以,你只需要按照系统的提示,"照葫芦画瓢"输入命令,安装 docker.io 就可以了。为了方便,你还可以使用 -y 参数来避免确认,实现自动化操作:
sudo apt install -y docker.io #安装Docker Engine
刚才说过,Docker Engine不像Docker Desktop那样可以安装后就直接使用,必须要做一些手工调整才能用起来,所以你还要在安装完毕后执行下面的两条命令:
sudo service docker start #启动docker服务 sudo usermod -aG docker ${USER} #当前用户加入docker组
第一个 service docker start 是启动Docker的后台服务,第二个 usermod -aG 是把当前的用户加入Docker的用户组。这是因为操作Docker必须要有root权限,而直接使用root用户不够安全,加入Docker用户组是一个比较好的选择,这也是Docker官方推荐的做法。当然,如果只是为了图省事,你也可以直接切换到root用户来操作Docker。
上面的三条命令执行完之后,我们还需要退出系统(命令 exit ),再重新登录一次,这样才能让修改用户组的命令 usermod 生效。
现在我们就可以来验证Docker是否安装成功了,使用的命令是 docker version 和 docker info。
docker version 会输出Docker客户端和服务器各自的版本信息:

下面是我从中摘出的比较关键的版本号和系统信息。可以看到,我使用的是Docker Engine 20.10.12,系统是Linux,硬件架构是arm64,也就是Apple M1:
Client: Version: 20.10.12 OS/Arch: linux/arm64 Server: Engine: Version: 20.10.12 OS/Arch: linux/arm64
docker info 会显示当前Docker系统相关的信息,例如CPU、内存、容器数量、镜像数量、容器运行时、存储文件系统等等,这里我也摘录了一部分:
Server: Containers: 1 Running: 0 Paused: 0 Stopped: 1 Images: 8 Server Version: 20.10.12 Storage Driver: overlay2 Backing Filesystem: extfs Cgroup Driver: systemd Default Runtime: runc Kernel Version: 5.13.0-19-generic Operating System: Ubuntu Jammy Jellyfish (development branch) OSType: linux Architecture: aarch64 CPUs: 2 Total Memory: 3.822GiB Docker Root Dir: /var/lib/docker
docker info 显示的这些信息,对于我们了解Docker的内部运行状态非常有用,比如在这里,你就能够看到当前有一个容器处于停止状态,有8个镜像,存储用的文件系统是overlay2,Linux内核是5.13,操作系统是Ubuntu 22.04 Jammy Jellyfish,硬件是aarch64,两个CPU,内存4G。
Docker的使用
现在,我们已经有了可用的Docker运行环境,就可以来重现9年前Solomon Hykes的那场简短的技术演示了。
首先,我们使用命令 docker ps,它会列出当前系统里运行的容器,就像我们在Linux系统里使用 ps 命令列出运行的进程一样。
注意,所有的Docker操作都是这种形式:以 docker 开始,然后是一个具体的子命令,之前的 docker version 和 docker info 也遵循了这样的规则。你还可以用 help 或者 --help 来获取帮助信息,查看命令清单和更详细的说明。
因为我们刚刚安装好Docker环境,这个时候还没有运行任何容器,所以列表显然是空的。

接下来,让我们尝试另一个非常重要的命令 docker pull ,从外部的镜像仓库(Registry)拉取一个busybox镜像(image),你可以把它类比成是Ubuntu里的"apt install"下载软件包:
docker pull busybox #拉取busybox镜像

docker pull 会有一些看起来比较奇怪的输出信息,现在我们暂时不用管,后续的课程会有详细解释。
我们再执行命令 docker images ,它会列出当前Docker所存储的所有镜像:

可以看到,命令会显示有一个叫busybox的镜像,镜像的ID号是一串16进制数字,大小是1.41MB。
现在,我们就要从这个镜像启动容器了,命令是 docker run ,执行 echo 输出字符串,这也正是Solomon Hykes在大会上所展示的最精彩的那部分:
docker run busybox echo hello world
这条命令会在我们的终端上,输出计算机世界最著名的语句"hello world":

然后我们再用 docker ps 命令,加上一个参数 -a ,就可以看到这个已经运行完毕的容器:

以上的这些,基本上就是Solomon Hykes闪电演讲的全部内容了。
初次接触容器的你可能会感到很困惑,这些命令都做了什么?看起来并没有展示出什么特别神奇的本领啊?可能还不如直接写一个Shell脚本来得省事。
有同样感想的不止你一个,也许PyCon2013当时绝大部分的现场观众也都有这样的疑问。不要着急,我们在后续的课程再逐步讲解这其中的奥妙。
Docker的架构
这里我再稍微讲一下Docker Engine的架构,让你有个初步的印象,也为之后的学习做一个铺垫。
下面的这张图来自Docker官网(What is Docker? | Docker Docs),精准地描述了Docker Engine的内部角色和工作流程,对我们的学习研究非常有指导意义。

刚才我们敲的命令行 docker 实际上是一个客户端client ,它会与Docker Engine里的后台服务Docker daemon通信,而镜像则存储在远端的仓库Registry里,客户端并不能直接访问镜像仓库。
Docker client可以通过 build、pull、run等命令向Docker daemon发送请求,而Docker daemon则是容器和镜像的"大管家",负责从远端拉取镜像、在本地存储镜像,还有从镜像生成容器、管理容器等所有功能。
所以,在Docker Engine里,真正干活的其实是默默运行在后台的Docker daemon,而我们实际操作的命令行工具"docker"只是个"传声筒"的角色。
Docker官方还提供一个"hello-world"示例,可以为你展示Docker client到Docker daemon再到Registry的详细工作流程,你只需要执行这样一个命令:
docker run hello-world
它会先检查本地镜像,如果没有就从远程仓库拉取,再运行容器,最后输出运行信息:

小结
好了,今天我们初步了解了容器技术,再简单小结一下主要的内容:
- 容器技术起源于Docker,它目前有两个产品:Docker Desktop和Docker Engine,我们的课程里推荐使用免费的Docker Engine,它可以在Ubuntu系统里直接用apt命令安装。
- Docker Engine需要使用命令行操作,主命令是
docker,后面再接各种子命令。 - 查看Docker的基本信息的命令是
docker version和docker info,其他常用的命令有docker ps、docker pull、docker images、docker run。 - Docker Engine是典型的客户端/服务器(C/S)架构,命令行工具Docker直接面对用户,后面的Docker daemon和Registry协作完成各种功能。
课下作业
最后是课下作业时间,给你留两个思考题:
- 学完了这节课,你对容器技术和Docker有什么样的认识和感受?
- Docker Engine为什么要设计成客户端/服务器(C/S)架构?它有什么样的好处?
欢迎在留言区发言参与讨论,如果觉得有收获,也欢迎你转发给身边的朋友一起学习。我们下节课见。

02 被隔离的进程:一起来看看容器的本质
你好,我是Chrono。
在上一次课里,我们初步了解了容器技术,在Linux虚拟机里安装了当前最流行的容器Docker,还使用 docker ps、docker run等命令简单操作了容器。
广义上来说,容器技术是动态的容器、静态的镜像和远端的仓库这三者的组合。不过,"容器"这个术语作为容器技术里的核心概念,不仅是大多数初次接触这个领域的人,即使是一些已经有使用经验的人,想要准确地把握它们的内涵、本质都是比较困难的。
那么今天,我们就一起来看看究竟什么是容器(即狭义的、动态的容器)。
容器到底是什么
从字面上来看,容器就是Container,一般把它形象地比喻成现实世界里的集装箱,它也正好和Docker的现实含义相对应,因为码头工人(那只可爱的小鲸鱼)就是不停地在搬运集装箱。

集装箱的作用是标准化封装各种货物,一旦打包完成之后,就可以从一个地方迁移到任意的其他地方。相比散装形式而言,集装箱隔离了箱内箱外两个世界,保持了货物的原始形态,避免了内外部相互干扰,极大地简化了商品的存储、运输、管理等工作。
再回到我们的计算机世界,容器也发挥着同样的作用,不过它封装的货物是运行中的应用程序,也就是进程,同样它也会把进程与外界隔离开,让进程与外部系统互不影响。
我们还是来实际操作一下吧,来看看在容器里运行的进程是个什么样子。
首先,我们使用 docker pull 命令,拉取一个新的镜像------操作系统Alpine:
docker pull alpine
然后我们使用 docker run 命令运行它的Shell程序:
docker run -it alpine sh
注意我们在这里多加了一个 -it 参数,这样我们就会暂时离开当前的Ubuntu操作系统,进入容器内部。
现在,让我们执行 cat /etc/os-release ,还有 ps 这两个命令,最后再使用 exit 退出,看看容器里与容器外有什么不同:

就像这张截图里所显示的,在容器里查看系统信息,会发现已经不再是外面的Ubuntu系统了,而是变成了Alpine Linux 3.15,使用 ps 命令也只会看到一个完全"干净"的运行环境,除了Shell(即sh)没有其他的进程存在。
也就是说,在容器内部是一个全新的Alpine操作系统,在这里运行的应用程序完全看不到外面的Ubuntu系统,两个系统被互相"隔离"了,就像是一个"世外桃源"。
我们还可以再拉取一个Ubuntu 18.04的镜像,用同样的方式进入容器内部,然后执行 apt update、apt install 等命令来看看:
docker pull ubuntu:18.04 docker run -it ubuntu:18.04 sh # 下面的命令都是在容器内执行 cat /etc/os-release apt update apt install -y wget redis redis-server &
这里我就不截图了,具体的结果留给你课下去实际操作体会。可以看到的是,容器里是另一个完整的Ubuntu 18.04 系统,我们可以在这个"世外桃源"做任意的事情,比如安装应用、运行Redis服务等。但无论我们在容器里做什么,都不会影响外面的Ubuntu系统(当然不是绝对的)。
到这里,我们就可以得到一个初步的结论:容器,就是一个特殊的隔离环境,它能够让进程只看到这个环境里的有限信息,不能对外界环境施加影响。
那么,很自然地,我们会产生另外一个问题:为什么需要创建这样的一个隔离环境,直接让进程在系统里运行不好吗?
为什么要隔离
相信因为这两年疫情,你对"隔离"这个词不会感觉到太陌生。为了防止疫情蔓延,我们需要建立方舱、定点医院,把患病人群控制在特定的区域内,更进一步还会实施封闭小区、关停商场等行动。虽然这些措施带来了一些不便,但都是为了整个社会更大范围的正常运转。
同样的,在计算机世界里的隔离也是出于同样的考虑,也就是系统安全。
对于Linux操作系统来说,一个不受任何限制的应用程序是十分危险的。这个进程能够看到系统里所有的文件、所有的进程、所有的网络流量,访问内存里的任何数据,那么恶意程序很容易就会把系统搞瘫痪,正常程序也可能会因为无意的Bug导致信息泄漏或者其他安全事故。虽然Linux提供了用户权限控制,能够限制进程只访问某些资源,但这个机制还是比较薄弱的,和真正的"隔离"需求相差得很远。
而现在,使用容器技术,我们就可以让应用程序运行在一个有严密防护的"沙盒"(Sandbox)环境之内,就好像是把进程请进了"隔离酒店",它可以在这个环境里自由活动,但绝不允许"越界",从而保证了容器外系统的安全。

另外,在计算机里有各种各样的资源,CPU、内存、硬盘、网卡,虽然目前的高性能服务器都是几十核CPU、上百GB的内存、数TB的硬盘、万兆网卡,但这些资源终究是有限的,而且考虑到成本,也不允许某个应用程序无限制地占用。
容器技术的另一个本领就是为应用程序加上资源隔离,在系统里切分出一部分资源,让它只能使用指定的配额,比如只能使用一个CPU,只能使用1GB内存等等,就好像在隔离酒店里保证一日三餐,但想要吃山珍海味那是不行的。这样就可以避免容器内进程的过度系统消耗,充分利用计算机硬件,让有限的资源能够提供稳定可靠的服务。
所以,虽然进程被"关"在了容器里,损失了一些自由,但却保证了整个系统的安全。而且只要进程遵守隔离规定,不做什么出格的事情,也完全是可以正常运行的。
与虚拟机的区别是什么
你也许会说,这么看来,容器不过就是常见的"沙盒"技术中的一种,和虚拟机差不了多少,那么它与虚拟机的区别在哪里呢?又有什么样的优势呢?
在我看来,其实容器和虚拟机面对的都是相同的问题,使用的也都是虚拟化技术,只是所在的层次不同,我们可以参考Docker官网上的两张图,把这两者对比起来会更利于学习理解。

(Docker官网的图示其实并不太准确,容器并不直接运行在Docker上,Docker只是辅助建立隔离环境,让容器基于Linux操作系统运行)
首先,容器和虚拟机的目的都是隔离资源,保证系统安全,然后是尽量提高资源的利用率。
之前在使用VirtualBox/VMware创建虚拟机的时候,你也应该看到了,它们能够在宿主机系统里完整虚拟化出一套计算机硬件,在里面还能够安装任意的操作系统,这内外两个系统也同样是完全隔离,互不干扰。
而在数据中心的服务器上,虚拟机软件(即图中的Hypervisor)同样可以把一台物理服务器虚拟成多台逻辑服务器,这些逻辑服务器彼此独立,可以按需分隔物理服务器的资源,为不同的用户所使用。
从实现的角度来看,虚拟机虚拟化出来的是硬件,需要在上面再安装一个操作系统后才能够运行应用程序,而硬件虚拟化和操作系统都比较"重",会消耗大量的CPU、内存、硬盘等系统资源,但这些消耗其实并没有带来什么价值,属于"重复劳动"和"无用功",不过好处就是隔离程度非常高,每个虚拟机之间可以做到完全无干扰。
我们再来看容器(即图中的Docker),它直接利用了下层的计算机硬件和操作系统,因为比虚拟机少了一层,所以自然就会节约CPU和内存,显得非常轻量级,能够更高效地利用硬件资源。不过,因为多个容器共用操作系统内核,应用程序的隔离程度就没有虚拟机那么高了。
运行效率,可以说是容器相比于虚拟机最大的优势,在这个对比图中就可以看到,同样的系统资源,虚拟机只能跑3个应用,其他的资源都用来支持虚拟机运行了,而容器则能够把这部分资源释放出来,同时运行6个应用。

当然,这个对比图只是一个形象的展示,不是严谨的数值比较,不过我们还可以用手里现有的VirtualBox/VMware虚拟机与Docker容器做个简单对比。
一个普通的Ubuntu虚拟机安装完成之后,体积都是GB级别的,再安装一些应用很容易就会上到10GB,启动的时间通常需要几分钟,我们的电脑上同时运行十来个虚拟机可能就是极限了。而一个Ubuntu镜像大小则只有几十MB,启动起来更是非常快,基本上不超过一秒钟,同时跑上百个容器也毫无问题。
不过,虚拟机和容器这两种技术也不是互相排斥的,它们完全可以结合起来使用,就像我们的课程里一样,用虚拟机实现与宿主机的强隔离,然后在虚拟机里使用Docker容器来快速运行应用程序。
隔离是怎么实现的
我们知道虚拟机使用的是Hypervisor(KVM、Xen等),那么,容器是怎么实现和下层计算机硬件和操作系统交互的呢?为什么它会具有高效轻便的隔离特性呢?
其实奥秘就在于Linux操作系统内核之中,为资源隔离提供了三种技术:namespace、cgroup、chroot,虽然这三种技术的初衷并不是为了实现容器,但它们三个结合在一起就会发生奇妙的"化学反应"。
namespace是2002年从Linux 2.4.19开始出现的,和编程语言里的namespace有点类似,它可以创建出独立的文件系统、主机名、进程号、网络等资源空间,相当于给进程盖了一间小板房,这样就实现了系统全局资源和进程局部资源的隔离。
cgroup是2008年从Linux 2.6.24开始出现的,它的全称是Linux Control Group,用来实现对进程的CPU、内存等资源的优先级和配额限制,相当于给进程的小板房加了一个天花板。
chroot的历史则要比前面的namespace、cgroup要古老得多,早在1979年的UNIX V7就已经出现了,它可以更改进程的根目录,也就是限制访问文件系统,相当于给进程的小板房铺上了地砖。
你看,综合运用这三种技术,一个四四方方、具有完善的隔离特性的容器就此出现了,进程就可以搬进这个小房间,过它的"快乐生活"了。我觉得用鲁迅先生的一句诗来描述这个情景最为恰当:躲进小楼成一统,管他冬夏与春秋。
小结
好了,今天我们一起学习了容器技术中最关键的概念:动态的容器,再简单小结一下课程的要点:
- 容器就是操作系统里一个特殊的"沙盒"环境,里面运行的进程只能看到受限的信息,与外部系统实现了隔离。
- 容器隔离的目的是为了系统安全,限制了进程能够访问的各种资源。
- 相比虚拟机技术,容器更加轻巧、更加高效,消耗的系统资源非常少,在云计算时代极具优势。
- 容器的基本实现技术是Linux系统里的namespace、cgroup、chroot。
课下作业
最后是课下作业时间,给你留两个思考题:
- 你能够对比现实中的集装箱,说出容器技术更多的优点吗?
- 有一种说法:容器就是轻量级的虚拟机,你认为这种说法正确吗?
欢迎在留言区发言参与讨论,如果你觉得有收获,也欢迎转发给身边的朋友一起学习。我们下节课见。

精选留言(15)
- Geek_b213f7 👍(82) 💬(1) chroot 是在 Unix 和 Linux 系统的一个操作,针对正在运作的软件行程和它的子进程,改变它外显的根目录。一个运行在这个环境下,经由 chroot 设置根目录的程序,它不能够对这个指定根目录之外的文件进行访问动作,不能读取,也不能更改它的内容。 首先创建一个目录rootfs mkdir rootfs 然后在rootfs目录下新建三个文件 cd rootfs touch a b c 此时执行一条命令:chroot /home/centos/rootfs /bin/sh 这条命令意思是启动一个 sh 进程,并且把 /home/centos/rootfs 作为 sh 进程的根目录 此时执行命令:/bin/ls / 就可以显示a b c三个文件,说明当前进程的根目录已经变成了主机上的 /home/centos/rootfs 目录。这样就实现了当前进程与主机的隔离 Namespace Namespace 是 Linux 内核的一项功能,该功能对内核资源进行隔离,使得容器中的进程都可以在单独的命名空间中运行,并且只可以访问当前容器命名空间的资源。Namespace 可以隔离进程 ID、主机名、用户 ID、文件名、网络访问和进程间通信等相关资源。 Docker 主要用到以下五种命名空间。 pid namespace:用于隔离进程 ID。 net namespace:隔离网络接口,在虚拟的 net namespace 内用户可以拥有自己独立的 IP、路由、端口等。 mnt namespace:文件系统挂载点隔离。 ipc namespace:信号量,消息队列和共享内存的隔离。 uts namespace:主机名和域名的隔离。 Cgroups Cgroups 是一种 Linux 内核功能,可以限制和隔离进程的资源使用情况(CPU、内存、磁盘 I/O、网络等)。在容器的实现中,Cgroups 通常用来限制容器的 CPU 和内存等资源的使用。 联合文件系统 联合文件系统,又叫 UnionFS,是一种通过创建文件层进程操作的文件系统,因此,联合文件系统非常轻快。Docker 使用联合文件系统为容器提供构建层,使得容器可以实现写时复制以及镜像的分层构建和存储。常用的联合文件系统有 AUFS、Overlay 和 Devicemapper 等。