资料来源:痴者工良-Kubernetes
一、Docker基础
1.1 什么是容器化应用
containerized applications
指容器化的应用,我们常常说使用镜像打包应用程序,使用Docker发布、部署应用程序,那么当你的应用成功在Docker上运行时,称这个应用是containerized applications
。
1.2 应用怎么打包
容器化应用的最主要特征是使用镜像打包应用的运行环境以及应用程序,可以通过 Docker 启动这个镜像,进而将 应用程序启动起来。将一个应用程序打包为镜像,大约分为以下过程:
- 编写 Dockerfile 文件 -- 定义构建镜像的流程
- 选择一个基础镜像(操作系统) -- 操作系统
- 安装应用的需要的环境 -- 运行环境
- 复制程序文件 -- 应用程序
- 启动 Dockerfile -- 生成镜像
1.3 Docker镜像组成
在Docker镜像中,操作系统是高度精简的,可能只有一个精简的Shell,甚至没有Shell。而且镜像中的操作系统还不包含内核,容器都是共享所在的宿主机的内核。所以有时会说容器仅包含必要的操作系统(通常只有操作系统文件和文件系统对象),容器中查看到的Linux内核版本与宿主机一致。Docker镜像的是由一系统文件组成的。
1.4 联合文件系统
Linux又名为Unionfs的文件系统服务,可以将不同文件夹中的文件联合到一个文件夹中。Unionfs有称为分支的概念,一个分支包含了多个目录和文件,多个分支可以挂载在一起,在挂载时,可以指定一个分支优先级大于另一个分支,这样当两个分支都包含相同的文件名时,一个分支会优先于另一个分支,在合并的目录中,会看到高优先级分支的文件。
Docker中,层层组成镜像的技术也是联合文件系统,Union File System
。Docker镜像中的操作系统是根文件系统,有bin、boot 等目录。我们都知道Docker镜像是由多层文件组成的,由三层组成:根文件系统、环境依赖包、应用程序文件。当镜像层生成后,便不能被修改,如果再进行操作,则会在原来的基础上生成新的镜像层,层层联合,最终生成镜像。当然生成的镜像可能会因为层数太多或者操作过多,导致出现大量冗余,镜像臃肿。
Docker的镜像分层是受Linux Unionfs
启发而开发的,Docker 支持多种文件联合系统,如 AUFS、OverlayFS、VFS等。
1.5 Linux内核
既然Docker容器需要与Linux内核结合才能使用,那么我们看一下Linux内核的功能,稍微了解一下Linux内核在支撑Docker容器运作中起到什么作用。
Linux内核主要包含以下功能:
-
内存管理:追踪记录有多少内存存储了什么以及存储在哪里;
-
进程管理:确定哪些进程可以使用中央处理器(CPU)、何时使用以及持续多长时间;
-
设备驱动程序:充当硬件与进程之间的调解程序/解释程序;
-
系统调用和安全防护:接受程序请求调用系统服务;
-
文件系统:操作系统中负责管理持久数据的子系统,在 Linux 中,一切皆文件。
1.6 Docker服务与客户端
Docker由Service和Client两部分组成,在服务器上可以不安装Docker Client,可以通过Http Api等方式与Docker Servie通讯。在安装了Docker的主机上执行命令docker version
查看版本号。
1.7 Docker客户端
要想跟Docker Server通讯,可以使用Restful API、UNIX 套接字或网络接口(Socket)。Docker 官方的客户端是一个二进制命令行程序,使用Go语言编写,我们也可以使用C#、Java等语言写一个类似的程序,Docker 客户端不需要安装到Docker Server所在的主机,Client跟Server可以远程通讯。
Docker的客户端是许多Docker用户与Docker交互的主要方式,当我们使用docker run
之类的命令时,客户端会将这些命令发送到Docker Server,由Docker Server解析并执行命令。
Docker for Linux中最为常见的同主机通讯方式是Unix域套接字。很多软件都支持使用域套接字与Docker通讯,例如CI/CD软件Jenkins,使用域套接字连接Docker,能够利用Docker启动容器构建应用程序。
1.8 容器运行时
容器运行时是提供运行环境并启动容器的软件,我们最常听说的是Docker,此外还有containerd、CRI-O等。可以毫不夸张的说,整个Kubernetes建立在容器之上。
默认情况下,Kubernetes 使用容器运行时接口(Container Runtime Interface,CRI) 来与服务器中容器运行时交互。所以Kubernetes支持多种容器软件,但只能使用一种容器运行时进行工作,在有多个容器运行时的情况下,我们需要指定使用何种运行时,如果你不指定运行时,则kubeadm会自动尝试检测到系统上已经安装的运行时,方法是扫描一组众所周知的Unix域套接字。
Linux是多进程操作系统,为了让多个系统中的多个进程能够进行高效的通讯,出现和很多方法,其中一种是域套接字(Unix domain socket),只能用于在同一计算机中的进程间通讯,但是其效率高于网络套接字(socket),域套接字不需要经过网络协议处理,通过系统调用将数据从一个进程复制到另一个进程中。
1.9 Docker引擎
Docker引擎也可以说是Docker Server,它由Docker守护进程(Docker daemon)、containerd 以及runc组成。当使用Docker client输入命令时,命令会被发送到Docker daemon ,daemon会侦听请求并管理Docker对象,daemon可以管理镜像、容器、网络和存储卷等。
1.10 Docker引擎变化
Docker首次发布时,Docker引擎由两个核心组件构成:LXC和Docker daemon,这也是很多文章中称Docker是基于LXC的原因,旧版本的Docker利用了 LXC、cgroups、Linux 内核编写。接下来我们了解一下 LXC 。
LXC (Linux Container)是 Linux 提供的一种内核虚拟化技术,可以提供轻量级的虚拟化,以便隔离进程和资源,它是操作系统层面上的虚拟化技术。LXC提供了对诸如命名空间(namespace) 和控制组(cgroups) 等基础工具的操作能力,它们是基于 Linux 内核的容器虚拟化技术。我们不需要深入了解这个东西。
Docker一开始是使用 LXC做的,LXC是一个很牛逼的开源项目,但是随着Docker的成熟,Docker开始抛弃LXC,自己动手手撕容器引擎。
为什么Docker要抛弃LXC 呢?首先,LXC是基于Linux 的。这对于一个立志于跨平台的Docker来说是个问题,离开LXC,怎么在 MAC、Windows 下运行?其次,如此核心的组件依赖于外部工具,这会给项目带来巨大风险,甚至影响其发展。
1.11 Docker引擎的架构
1.11.1 containerd
containerd是一个开源容器引擎,是 Docker开源出去的。之前有新闻说Kubernetes不再支持 Docker,只支持containerd,很多人以为Docker不行了。
一开始 Docker 是一个 “大单体”,随着Docker的成长,Docker开始进行模块化,Docker中的许多模块都是可替换的,如Docker网络。支持容器运行的核心代码自然也抽出来,单独做一个模块,便是 containerd。Kubernetes不再支持Docker,只不过是降低依赖程度,减少对其他模块的依赖,只集中在containerd上。当我们安装Docker时,自然会包含containerd。如果我们不需要Docker太多组件,那么我们可以仅仅安装containerd,由Kubernetes调度,只不过我们不能使用Docker client了。因此可以说,Kubernetes不再支持Docker,并不代表会排斥Docker。
containerd的主要任务是容器的生命周期管理,如启动容器、暂停容器、停止容器等。containerd 位于 daemon 和 runc 所在的 OCI 层之间。
1.11.2 shim
shim它的作用非常单一,那就是实现 CRI 规定的每个接口,然后把具体的 CRI 请求“翻译”成对后端容器项目的请求或者操作。这里要区别一下,dockershim和containerd-shim,dockershim是一个临时性的方案,dockershim 会在Kubernetes v1.24中删除(2022年),这也是Kubernetes不再支持Docker的另一组件。
1.11.3 runc
runc实质上是一个轻量级的、针对Libcontainer进行了包装的命令行交互工具,runc生来只有一个作用——创建容器,即runc是一个由于运行容器的命令行工具。
如果主机安装了Docker,我们可以使用runc --help
来查看使用说明。我们可以这样来理解runc,runc是在隔离环境生成新的进程的工具,在这个隔离环境中有一个专用的根文件系统(ubuntu、centos等)和新的进程树,这个进程树的根进程 PID=1。
二、Docker虚拟化
2.1 传统虚拟化部署方式
传统虚拟化方式的优点是:
1、虚拟机之间通过虚拟化技术隔离互不影响。
2、物理机上可部署多台虚拟机,提升资源利用率。
3、应用资源分配、扩容通过虚拟管理器直接可配置。
4、支持快照、虚拟机克隆多种技术,快速部署、容灾减灾。
传统虚拟化部署方式的缺点:
1、资源占用高,需要额外的操作系统镜像,需要占用GB级别的内存以及数十GB存储空间。
2、启动速度慢,虚拟机启动需要先启动虚拟机内操作系统,然后才能启动应用。
3、性能影响大,应用 => 虚拟机操作系统=> 物理机操作系统=> 硬件资源。
2.2 Linux虚拟化
我们知道,操作系统是以一个进程为单位进行资源调度的,现代操作系统为进程设置了资源边界,每个进程使用自己的内存区域等,进程之间不会出现内存混用。Linux 内核中,有 cgroups 和 namespaces 可以为进程定义边界,使得进程彼此隔离。
2.2.1 Linux-Namespace
在容器中,当我们使用 top 命令或 ps 命令查看机器的进程时,可以看到进程的 Pid,每个进程都有一个 Pid,而机器的所有容器都具有一个 Pid = 1 的基础,但是为什么不会发生冲突?容器中的进程可以任意使用所有端口,而不同容器可以使用相同的端口,为什么不会发生冲突?这些都是资源可以设定边界的表现。
在 Linux 中,namespace 是 Linux 内核提供的一种资源隔离技术,可以将系统中的网络、进程环境等进行隔离,使得每个 namespace 中的系统资源不再是全局性的。目前有以下 6 种资源隔离,Docker 也基本在这 6 种资源上对容器环境进行隔离。
2.2.2 unshare
Linux 中,unshare 命令行程序可以创建一个 namespace,并且根据参数创建在 namespace 中隔离各种资源,在这里我们可以用使用这个工具简单地创建一个 namespace。
为了深刻理解 Linux 中的 namespace,我们可以在 Linux 中执行:
unshare --pid /bin/sh
这命令类似于docker run -it {image}:{tag} /bin/sh
。当我们执行命令后,终端会进入一个 namespace 中,执行 top 命令查看进程列表。
可以看到,进程 PID 是从 1 开始的,说明在这个 namespace 中,与主机的进程是隔离开来的。这个命令中,只隔离了进程,因为并没有隔离网络,因此当我们执行netstat --tlap
命令时,这个命名空间的网络跟其它命名空间的网络是相通的。
2.3 cgroups硬件资源隔离
前面提到的 namepace 是逻辑形式使得进程之间相互不可见,形成环境隔离,这跟 Docker 容器的日常使用是一样的,隔离根目录,隔离网络,隔离进程 PID 等。当然,Docker 处理环境隔离外,还能限制每个容器使用的物理资源,如 CPU 、内存等,这种硬件资源的限制是基于 Linux 内核的 cgroups 的。
groups 是 control groups 的缩写,是 Linux 内核提供的一种可以进程所使用的物理资源的机制。cgroups 可以控制多种资源,在 cgroups 中每种资源限制功能对应一个子系统,可以使用命令查看
mount | grep cgroup
2.4 不同层次的虚拟化
我们应该在很多书籍、文章中,了解到虚拟机跟 Docker 的比较,了解到 Docker 的优点,通过 Docker 打包镜像后可以随时在别的地方运行而不需要担心机器的兼容问题。但是 Docker 的虚拟化并不能让 Linux 跑 Windows 容器,也不能让 Windows 跑 Linux 容器,更不可能让 x86 机器跑 arm 指令集的二进制程序。但是 VMware 可以在 Windows 运行 Linux 、Mac 的镜像,但 WMWare 也不能由 MIPS 指令构建的 Linux 系统。
Docker 和 VMware 都可以实现不同程度的虚拟化,但也不是随心所欲的,它们虚拟化的程度相差很大,因为它们是在不同层次进行虚拟化的。
在指令集级别虚拟化中,从指令系统上看,就是要在一种机器上实现另一种机器的指令系统。例如,QEMU 可以实现在 X64 机器上模拟 ARM32/64、龙芯、MIPS 等处理器。
虚拟化程度在于使用硬件实现与软件实现的比例,硬件部分比例越多一般来说性能就会越强,软件部分比例越多灵活性会更强,但是性能会下降,不同层次的实现也会影响性能、兼容性等。随着现在计算机性能越来越猛,很大程度上产生了性能过剩;加之硬件研发的难度越来越高,越来越难突破,非硬件程度的虚拟化将会越来越广泛。
三、Docker网络
3.1 Docker的四种网络模式
Docker 有 bridge、none、host、container 四种网络模式,提供网络隔离、端口映射、容器间互通网络等各种支持,下面开门见山地直接介绍这四种网络模式。这四种网络模式可以通过启动容器的时候指定,其命令或参数个数如下:
-
host模式 -–net=host 容器和宿主机共享 Network namespace。
-
container模式 –-net={id} 容器和另外一个容器共享 Network namespace。 kubernetes 中的pod就是多个容器共享一个 Network namespace。
-
none模式 –-net=none 容器有独立的Network namespace,但并没有对其进行任何网络设置,如分配 veth pair 和网桥连接,配置IP等。
-
bridge模式 -–net=bridge 默认为该模式,通过 -p 指定端口映射。
3.2 bridge模式
使用 Docker 创建一个 bridge 模式的容器命令格式如下:
docker run -itd -p 8080:80 nginx:latest
bridge 模式称为网桥模式,首先 Docker 会在主机上创建一个名为 docker0 的虚拟网桥,这个虚拟网络处于七层网络模型的数据链路层,每当创建一个新的容器时,容器都会通过 docker0 与主机的网络连接,docker0 相当于网桥。
使用 bridge 模式新创建的容器,其内部都有一个虚拟网卡,名为 eth0,容器之间可以通过 172.17.x.x 相互访问。
一般情况下,网桥默认 IP 范围是 172.17.x.x ,可以在宿主机执行 ifpconfig 命令查看所有网卡,里面会包含 Docker 容器的虚拟网卡,可以查看某个容器的 ip。在容器中,也可以使用 ifconfig 命令查看自身的容器 ip
使用了 bride 创建的容器,其网络与主机以及其他容器隔离,以太网接口、端口、路由表以及 DNS配置 都是独立的。每个容器都好像是一个独立的主机 ,这便是 bridge(网桥)的作用。但是因为 docker0 的存在,对于容器来说,可以通过 ip 访问别的容器。
容器1 可以通过 172.17.0.3 访问容器2,同样,主机也可以使用这个 ip 访问容器2 中的服务。
3.3 none 模式
这种网络模式下容器只有 lo 回环网络,没有其他网卡,这种类型的网络没有办法联网,外界也无法访问它,封闭的网络能很好地保证容器的安全性。创建 none 网络的容器:
docker run -itd --net=none nginx:latest
3.4 host 模式
host 模式会让容器与主机共享网络,此时映射的端口可能会生产冲突,但是容器的其余部分(文件系统、进程等)依然是隔离的,此时容器与宿主机共享网络。
3.5 container 模式
container 模式可以让多个容器之间相互通讯,即容器之间共享网络。首先启动一个 A 容器,A 一般为 bridge 网络,接着 B 使用 –-net={id}
连接到 A 中,使用 A 的虚拟网卡,此时 A、B 共享网络,可以接着加入 B、C、D 等容器。
四、 Docker与Pod
4.1 什么是容器化应用
containerized applications 指容器化的应用,我们常常说使用镜像打包应用程序,使用 Docker 发布、部署应用程序,那么当你的应用成功在 Docker 上运行时,称这个应用是 containerized applications。
在一个产品中,好的容器化规范或方法,具有以下特点:
-
使用声明式的格式进行设置自动化,以最大限度地减少新开发人员加入项目的时间和成本;
-
与底层操作系统之间有一个干净的契约(资源隔离、统一接口),在执行环境之间提供最大的可移植性;
-
适合部署在现代云平台上,无需服务器和系统管理;
-
最大限度地减少开发和生产之间的差异,实现持续部署以实现最大敏捷性;
-
并且可以在不对工具、架构或开发实践进行重大更改的情况下进行扩展。
4.2 Pod
最简单的说法就是将多个容器打包起来一起运行,这个整体就是 Pod。
Pod通过Docker的container这网络模式,让 Pod 中的容器共享网络,也就是说,Pod 中的容器,网络是互通的,容器之间不能使用相同的端口。
Pod 是 Kubernetes 集群中最小的执行单位。在 Kubernetes 中,容器不直接在集群节点上运行,而是将一个或多个容器封装在一个 Pod 中,接着将 Pod 调度到节点上运行,这些容器会一起被运行、停止,它们是一个整体。
Pod 中的所有容器共享相同的资源和本地网络,从而简化了 Pod 中应用程序之间的通讯。在 Pod 中,所有容器中的进程共享网络,可以通过 127.0.0.1、localhost 相互进行访问。
Pod 启动时会启动一个容器,K8S 给这个容器分配虚拟 IP,接着,其他容器使用 container 网络模式,连接到这个容器中,此时有容器共享网络。
随着 Pod 负载的增加,Kubernetes 可以自动复制 Pod 以达到预期的可拓展性(部署更多的 Pod 提供相同的服务,负载均衡)。因此,设计一个尽可能精简的 Pod 是很重要的,降低因复制扩容、减少收缩过程中带来的资源损失。
前面提到,容器应当是无状态的,所以拓展 Pod 时,每个实例都提供了一模一样的服务,这些 Pod 分配到不同的节点上,可以利用更多的 CPU、内存资源。
4.3 容器与 Pod 的区别
容器包含执行特定流程或函数所需的代码(编译后的二进制可执行程序)。在 Kubernetes 之前,可以直接在物理或虚拟服务器上运行容器,但是缺乏 Kubernetes 集群所提供的可伸缩性和灵活性。
Pod 为容器提供了一种抽象,可以将一个或多个应用程序包装到一个 Pod 中,而 Pod 是 Kubernetes 集群中最小的执行单元。例如 Pod 可以包含初始化容器,这些容器为其它应用提供了准备环境,然后在应用程序开始执行前终结。Pod 是集群中复制的最小单位,Pod 中的容器作为整体被扩展或缩小。
例如对应前后端分离的项目,可能不需要把前端文件和后端程序放在一起,而是分别放在两个容器中。然后通过 Pod,将这两个容器作为一组服务打包在一起。
4.4 节点
Pod 是 Kubernetes 中最小的执行单元,而 Node 是 Kubernetes 中最小的计算硬件单元,节点可以是物理的本地服务器,也可以是虚拟机,节点即使宿主服务器,可以运行 Docker 的机器。
与容器一样,Node 提供了一个抽象层。多个 Node 一起工作形成了 Kubernetes 集群,它可以根据需求的变化自动分配工作负载,增加或减少在节点上的 Pod 数量。如果 A 节点和 B 节点的硬件资源是一致的,那么 A 、B 两个节点是等价的,如果 A 节点失败,它将自动从集群中移除,由 B 节点接管,不会出现问题。
每个节点都运行着一个名为 kubelet 的组件,它是节点的主要组件,Kubernetes 与集群控制平面组件(API Server)通信,所有对节点有影响的操作都会通过 kubectl 控制此节点。kubelet 也是 master 节点跟 worker 节点之间直接通讯的唯一组件。
kubelet 的一些功能有:
- 在节点上创建、更新、删除容器;
- 参与调度 Pod;
- 为容器创建和挂载卷;
- 使用命令查看 Pod 、容器,例如 exec、log 等时,需要通过 kubelet;
例如,集群有 A、B 两个节点,Pod 部署在哪里了,这不是用户关心的事情,用户在想看到容器的日志,可以随便找集群中的一台主机,执行命令,Kubernetes 会自动寻找容器所在的节点,然后kubectl 取得需要的内容。另外节点上还有 proxy,主要是为 Pod 提供代理服务,外界可通过此代理,使用节点的 IP 访问 Pod 中的容器。
网友评论