PinkHello
做一个快乐的程序猿
回望K8S 白话容器

进程开启

容器, 到底是什么?

前面提出: 容器是一种沙盒技术. 就是一个集装箱, 把应用装起来的技术. 这样, 应用与应用之间有了边界不至于互相干扰; 有了这些集装箱, 也方便搬来搬去.

码农都知道可执行的二进制文件是代码的可执行镜像(executable image). 一旦程序执行起来, 内存数据、寄存器的值、堆栈的指令、打开的文件等这些集合汇集成一个程序的计算机执行环境总和: 进程.

进程: 静态表现是程序, 动态表现计算机的数据和状态的总和。

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

  • Cgroups 技术 制造约束的主要手段
  • Namespace 技术 修改进程视图的主要方法

docker run , -it 告诉 Docker 启动容器后, 需要分配一个文本输入/输出环境, 也就是 TTY, 跟容器的标准输入相关联, 这样我们就可以和这个Docker容器进行交互了。而 /bin/sh 就是我们在 Docker 容器里运行的程序.

> docker run -it busybox /bin/sh
/ #

帮我启动一个容器, 在容器里执行 /bin/sh, 并且给我分配一个命令行终端跟这个容器进行交互, 在这个执行环境下可以完全执行LINUX命令,且与宿主机完全隔离在不同的世界中.

Docker对被隔离应用的进程空间做了手脚, 使得这些进程只能看到重新计算的进程编号, 可是实际上, 他们在宿主机的操作系统里, 还是原来的第N号进程. 这种技术就是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);

这时,新创建的这个进程就会看到一个全新的进程空间,在这个进程空间里,他的PID1,之所以说看到,是因为是一个障眼法,在宿主机真实的进程空间里,这个PID还是真实的数值.当多次执行clone()调用, 会创建多个 PID Namespace, 每个 Namespace 里的应用进程,都会认为自己是当前容器里的第1号进程,看不仅宿主机的也看不到其他的Namespace.

备注: Linux提供了不同的Namespace,去应对不同的进程上下文

  • PID Namespace
  • Mount Namespace
  • IPC Namespace
  • UTS Namespace
  • Network Namespace
  • User Namespace

Docker容器,就是在创建容器进程时候,指定了这个进程所需要启用的一组 Namespace 参数, 这样, 容器就只能 看到 当前 Namespace 所限定的 资源、文件、设备、状态 或者 配置。所以说, 容器,其实是一种特殊的进程。

容器与虚拟机工作原理

可以看出图中 Hypervisor 是虚拟机主要部分。它通过硬件虚拟化功能,模拟出了运行一个操作系统需要的各种硬件,比如 CPU、内存、I/O设备等。 这样,用户的进程可以在这个虚拟的机器中,只能看到虚拟环境的文件和目录以及设备,起到隔离的作用。 而右边的图,Docker Engine替换了Hypervisor,但是有个核心一点Docker Engine并不少轻量级虚拟化技术。

LinuxNamespace工作方式后, 在使用Docker的时候,Docker并没有一个真正的Docker容器运行在宿主机里面,而是Docker启动还是原来的应用进程,只不过在创建这些进程时候,加上了各种Namespace参数,使得这些进程觉得自己是在各自的PID Namespace是第一号进程,并且只能看到各自Mount Namespace里挂在的目录和文件、只能访问各自Network Namespace里的网络设备.

隔离和限制

前面提到实现 隔离 的手段: Namespace. Namespace 技术实际修改了应用进程看待整个计算机的"视图",即它的"视线"被操作系统做了限制,只能"看到"某些知道的内容.

为什么需要隔离

  • 首先,既然容器只是运行在宿主机上的一种特殊的进程,那么多个容器之间使用的就还是同一个宿主机的操作系统内核。
  • 在Linux内核中,还有许多资源和对象是不能被 Namespace 化的,最典型的例子是:时间

    容器中使用 settimeofday(2) 系统调用修改了时间,整个宿主机的时间都会被随之修改。这样肯定与预期不符

上述是为什么要隔离,下面说为什么要限制这个问题。

在宿主机上,启动多个容器都是在宿主机上的特殊进程,但是在不同的进程之间, 资源(CPU、内存)还是可能被其他进程(或者容器)占用的。

Linux Cgroups全称Linux Control Group 就是 Linux 内核中用来为进程设置资源限制的一个重要功能, 限制一个进程组能够使用的资源上限, 包括 CPU、内存、磁盘、网络带宽 等等。此外 Cgroups 还能够对进程进行优先级设置、审计,以及将进程挂起和恢复操作。

Linux中,Cgroups给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径下

# Ubuntu 下 mount 指令展示出来
> mount -t cgroup 
cpuset on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cpu on /sys/fs/cgroup/cpu type cgroup (rw,nosuid,nodev,noexec,relatime,cpu)
cpuacct on /sys/fs/cgroup/cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct)
blkio on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
memory on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
...

可以看到在 /sys/fs/cgroup 下面又很多诸如cpusetcpumemory这样的子目,也叫子系统.这些都是可以被Cgroups进行限制的资源种类,而在子系统对应的资源种类下, 你就可以看到该类资源具体可以被限制的方法。比如, 对CPU子系统来说,我们就可以看到几个配置文件,这个指令是:

> ls /sys/fs/cgroup/cpu
cgroup.clone_children cpu.cfs_period_us cpu.rt_period_us  cpu.shares notify_on_release
cgroup.procs      cpu.cfs_quota_us  cpu.rt_runtime_us cpu.stat  tasks

输出中cfs_periodcfs_quota这样的关键词。组合使用,限制进程在长度为cfs_period的一段时间内,只能被分配到总量为cfs_quotaCPU时间

如何使用cgroups呢?

在对应的子系统的下面创建一个目录,比如限制CPU进入 /sys/fs/cgroups/cpu 目录下

> cd /sys/fs/cgroups/cpu
> mkdir container
> ls container/
cgroup.clone_children cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_releasecgroup.procs cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks

这个目录就称为一个控制组,操作系统自动在新创建的 container 目录下,自动生成该子系统的对应的资源限制文件.

# 查看 container 控制组的 CPU quota 还没有任何限制:-1,CPU period 则是默认的 100 ms (100000 us)
> cat /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us 
-1
> cat /sys/fs/cgroup/cpu/container/cpu.cfs_period_us
100000

container 组里的 cfs_quota 文件写入 20 ms(20000 us)

#意味着在每 100 ms 的时间里,被该控制组限制的进程只能使用 20 ms 的 CPU 时间,也就是说这个进程只能使用到 20% 的 CPU 带宽。
> echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us
# 现在把需要被限制的进程的 PID 写入 container 组里的 tasks 文件,上面的设置就会对该进程生效了
> echo ${需要限制的进程PID} > /sys/fs/cgroup/cpu/container/tasks
# top 指令查看, 计算机CPU使用率立刻降低到20%
> top
%Cpu0 : 20.3 us, 0.0 sy, 0.0 ni, 79.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st

这样, Cgroups 的每一个子系统都有其独有的资源限制能力

  • cpu, 为进程设定cpu使用的限制;
  • blkio, 为块设备设定 I/O 限制, 一般用户磁盘等设备;
  • cpuset, 为进程分配单独的 CPU核和对应的内存节点;
  • memory, 为进程设定内存使用的限制

Linux Cgroups的设计,它就是一个子系统的目录加上一组资源限制文件的组合。而对于DockerLinux容器项目来说,它们只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录), 然后在启动进程之后,把这个进程的PID写到对应的控制组的tasks文件中.

那么在Docker容器中,如何启动的时候知道控制组下面的资源如何使用呢?

# docker run 时的参数指定
> docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash

在启动这个容器后,我们可以通过 Cgroups 文件系统下,CPU子系统中, docker这个控制组里的资源限制文件内容来确认:

> cat /sys/fs/cgroup/cpu/docker/5d5c9f67d/cpu.cfs_period_us 
100000
> cat /sys/fs/cgroup/cpu/docker/5d5c9f67d/cpu.cfs_quota_us 
20000

这就意味着这个Docker容器,只能使用到 20%CPU带宽

核心概念:

容器就是一个单进程模型. 一个正在运行的Docker容器,其实就是启用了多个Linux Namespace的应用进程,而这个进程能够使用的资源量,则受Cgroups配置的限制

一个容器的本质是一个进程, 用户的应用进程实际上就是容器的PID=1的进程, 也是其他后续创建所有进程的父进程。这就意味着,在一个容器中,你没有办法同时运行两个不同的应用,除非你能事先找到公共的PID=1的程序充当两个不同应用的父进程,这就是为什么很多会使用systemd或者supervisord代理应用本身作为容器的启动进程。

容器的本身设计,希望容器和应用能够同生命周期,这个对后续的容器编排非常重要。

Linux下的/proc目录存储的是纪录当前内核运行状态的一些列特殊文件,用户可以通过访问这些文件,查看系统以及当前正在运行的进程的信息, 比如 CPU使用、内存占用,top指令查看系统信息的主要数据来源. 在容器中执行 top 指令, 发现宿主机的CPU和内存的数据,不是当前容器的数据。

造成这个问题的原因就是,/proc 文件系统并不知道用户通过 Cgroups 给这个容器做了什么样的资源限制,即:/proc 文件系统不了解 Cgroups 限制的存在。 当然可以借助其他 lxcfs 可解决此问题

容器镜像

容器中的进程看到的文件系统又是什么样子的呢?

嘿嘿, Mount Namespace 开启后,容器进行看到的文件系统也跟宿主机完全一样。Mount Namespace 修改的,是容器进程对文件系统"挂载点"的认知。Mount Namespace 跟其他的 Namespace 的使用略有不同的地方:它对容器进程视图的改变,一定是伴随着挂载操作(mount)才能生效。

Linux操作系统中,有一个 chroot 的命令:change root file system, 改变进程的根目录到你指定的位置。这个 Mount Namespace 正是基于对 chroot 的不断改良的,也是 Linux 操作系统里第一个 Namespace。 而挂载在容器根目录上,用来为容器进程提供隔离后执行环境的文件系统,就是所谓的容器镜像。它还有一个更专业的名字叫做 rootfs(根文件系统)

一个常见的 rootfs,包含一些目录和文件:

> ls /
bin dev etc home lib lib64 mnt opt proc root run sbin sys tmp usr var

而进入容器之后执行的 /bin/bash, 就是 /bin 目录下的可执行文件, 与宿主机的 /bin/bash 完全不同。

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

  • 启用 Linux Namespace 配置;
  • 设置指定的 Cgroups 参数;
  • 切换进程的根目录(Change Root).

这样,一个完整的容器就诞生了。不过,在Docker项目在最后一步的切换上优先使用pivot_root系统调用,如果系统不支持,才会使用chroot

rootfs只是操作系统所包含的文件、配置和目录,并不包含操作系统内核。在Linux操作系统中,这两部分分开存放的。操作系统只在开机启动的时候才会加载指定版本的内核镜像。 所以说rootfs只是操作系统的"躯壳",并没有操作系统的"灵魂",同一台机器的所有容器,都共享宿主机操作系统的内核。因为共享的宿主机内核,应用程序需要配置的内核参数、加载额外的内核模块,以及跟内核进行的直接交互。内核相对于主机上所有容器的是一个全局变量,牵一发而动全身。

由于rootfs的存在,容器有了最重要的特性: 一致性

什么是容器的一致性呢?

在开发过程中、本地环境、云环境、打包是一个十分痛苦的过程(对于PAAS环境来说),有了容器镜像(rootfs)之后,优雅的解决了这个问题。

由于 rootfs 里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着应用以及它运行的所需要的所有依赖,都被封装在一起。

对于一个应用来说,操作系统本身才是它运行所需要的完整的"依赖库",有了容器镜像打包操作系统的能力,这个最基础的依赖环境也终于变成了应用沙盒的一部分。这就赋予了容器的一致性:无论在本地、云端,还是在任何地方的机器上,用户只需要解压打包好的容器镜像,这样这个应用所需要的完整的执行环境就被重现出来了。

如何解决每次升级,如何解决重复制作 rootfs 的问题呢?

Docker公司实现Docker镜像的时候没有使用重制作rootfs流程,而是在Docker在镜像的设计中,引入了层(layer)的概念。也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量的rootfs.

联合文件系统(Union File System) UnionFS, 最主要的功能是将不同位置的目录联合挂载(union mount)到同一个目录下。

> tree
.
|- A
| |- a
| |- x
|--- B
  |-- b
  |-- x

# 联合挂载,两个目录挂载到公共目录C上
> mkdir C
> mount -t aufs -o dirs=./A:./B none ./C

# 展示C文件
> tree ./C
./C
|-- a
|-- b
|-- x
# 此时对 C 里的文件进行修改,在目录 A 和 B 中都会生效

docker layer概念

关键目录:

/var/lib/docker/aufs/diff/<layer_id>
# 拉取ubuntu镜像
> docker pull ubuntu:latest
# 展示image的层
> docker image inspect ubuntu:latest
...
     "RootFS": {
      "Type": "layers",
      "Layers": [
        "sha256:f49017d4d5ce9c0f544c...",
        "sha256:8f2b771487e9d6354080...",
        "sha256:ccd4d61916aaa2159429...",
        "sha256:c01d74f99de40e097c73...",
        "sha256:268a067217b5fe78e000..."
      ]
    }

ubuntu镜像的是五层组成,这五层就是5个增量 rootfs,每一层都是 ubuntu 操作系统文件与目录的一部分; 而在使用镜像时, Docker 会把这些增量的联合挂载在一个统一的挂载点上。挂载点就是 /var/lib/docker/aufs/mnt/,比如:

/var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fcfa2a2f5c89dc21ee30e166be823ceaeba15dce645b3e
# 这个目录下是一个完整的 ubuntu 操作系统
> ls /var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fcfa2a2f5c89dc21ee30e166be823ceaeba15dce645b3e
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

前面的五个镜像层,如何被挂载到这样一个完整的Ubuntu的文件系统的呢?这个信息纪录在 AuFS 的系统目录 /sys/fs/aufs 下面。查看 AuFS 的挂载信息, 我们可以找到这个目录对应的 AuFS 的内部ID(也叫si),

# si=972c6d361e6b32ba
> cat /proc/mounts| grep aufs
none /var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fc... aufs rw,relatime,si=972c6d361e6b32ba,dio,dirperm1 0 0

# 查看被联合挂载在一起的各个层的信息
> cat /sys/fs/aufs/si_972c6d361e6b32ba/br[0-9]*
/var/lib/docker/aufs/diff/6e3be5d2ecccae7cc...=rw
/var/lib/docker/aufs/diff/6e3be5d2ecccae7cc...-init=ro+wh
/var/lib/docker/aufs/diff/32e8e20064858c0f2...=ro+wh
/var/lib/docker/aufs/diff/2b8858809bce62e62...=ro+wh
/var/lib/docker/aufs/diff/20707dce8efc0d267...=ro+wh
/var/lib/docker/aufs/diff/72b0744e06247c7d0...=ro+wh
/var/lib/docker/aufs/diff/a524a729adadedb90...=ro+wh

# 镜像的层都放置在 `/var/lib/docker/aufs/diff` 目录下,然后被联合挂载在 `/var/lib/docker/aufs/mnt` 里面  

容器的rootfs的展示

  • 第一部分: 只读层

    它是这个容器 rootfs 最下面的 5 层, 对应的正是 ubuntu 镜像的五层.他们的挂载方式都是只读的(ro+wh readonly+whiteout)

  • 第二部分: 可读写层

    它是这个容器的 rootfs 最上面的一层(6e3be5d2ecccae7cc),它的挂载方式为:rw (read write), 在这个容器中进行修改产生的内容就会以增量的方式出现在这一层。 假如删除只读层的一个文件呢?这时候 AuFS 在可读写层创建了一个 whiteout 文件,把只读层里的文件 遮挡 起来了。对上层来说,这个文件就是不可见的。 这边可读写层的作用就是存放我们自己修改后的 rootfs 后产生的增量,无论增删改都在此处处理,增量的 rootfs

  • 第三部分: Init

    它是以"-init"结尾的层,夹在只读层和读写层之间, Init 层是Docker项目单独生成的内部层,专门用来存放 /etc/hosts/etc/resolv.conf 等信息,这些文件本来属于只读的Ubuntu镜像一部分,但用户往往需要在启动的时候写入一定指定的值 hostname,用户可以在可读写层对他们进行修改。

    可是我们修改往往只对当前容器生效,我们并不希望执行 docker commit 时,把这些信息连同可读写层一起提交掉。所以Docker的做法,时修改的这些文件以后,以一个单独的层挂载出来,而用户执行的 docker commit 只会提交可读写层,所以不会包含这些内容。

    最终这 7 层都被联合挂载到 /var/lib/docker/aufs/mnt 目录下

Docker容器

docker 如何实现容器的

  • Linux Namespace 隔离能力
  • Linux Cgroups 限制能力
  • 基于 rootfs 文件系统的增量实现

开发的应用的如何容器化的步骤

1、Dockerfile 制作容器镜像

制作rootfs过程,Docker提供了一个便捷的方式: Dockerfile

举例个写个 app.py, 使用 Flask 启动一个Web服务器。

from flask import Flask
import socket
import os

app = Flask(__name__)

@app.route('/')
def hello():
    html = "<h3>Hello {name}!</h3>" \
           "<b>Hostname:</b> {hostname}<br/>"           
    return html.format(name=os.getenv("NAME", "world"), hostname=socket.gethostname())
    
if __name__ == "__main__":
    app.run(host='0.0.0.0', port=80)
# 展示 Python 依赖的关系
> cat requirements.txt
Flask
# 使用官方 python镜像
FROM python
# 切换工作目录
WORKDIR /app
# 将当前目录下内容复制到 /app
ADD . /app
# 按照应用依赖
RUN pip install -r requirements.txt
# 允许外界访问容器80端口
EXPOSE 80
# 设置环境变量
ENV NAME helloworld
# 启动python应用
CMD ["python","app.py"]

有了 Dockerfile 可以进行 Docker 镜像的制作

# 使用dockerfile打一个 名为 helloworld 的镜像
> docker build -t helloword .
# 展示docker
> docker image ls
REPOSITORY            TAG                 IMAGE ID
helloworld         latest              654286cdf963
# 启动一个容器 8080 映射 80
> docker run -p 8080:80 helloword
# docker push
# docker tag
> docker inspect --format '{{ .State.Pid }}' 4ddf4638572d
25686
# 可以看到,一个进程的每种 Linux Namespace,都在它对应的 /proc/[进程号]/ns 下有一个对应的虚拟文件,并且链接到一个真实的 Namespace 文件上。
> ls -l /proc/25686/ns
total 0
lrwxrwxrwx 1 root root 0 Aug 13 14:05 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 ipc -> ipc:[4026532278]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 mnt -> mnt:[4026532276]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 net -> net:[4026532281]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 pid -> pid:[4026532279]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 pid_for_children -> pid:[4026532279]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 uts -> uts:[4026532277]

这也就意味着:一个进程,可以选择加入到某个进程已有的 Namespace 当中,从而达到“进入”这个进程所在容器的目的,这正是 docker exec 的实现原理。

2、Volume 机制,允许将宿主机上的指定的目录或者文件挂载到容器里面进行读取和修改

> docker run -v /test ...
> docker run -v /home:/test ...

只不过,在第一种情况下,由于你并没有显示声明宿主机目录,那么 Docker 就会默认在宿主机上创建一个临时目录 /var/lib/docker/volumes/[VOLUME_ID]/_data,然后把它挂载到容器的 /test 目录上。而在第二种情况下,Docker 就直接把宿主机的 /home 目录挂载到容器的 /test 目录上。

当容器进程被创建之后,尽管开启了 Mount Namespace,但是在它执行 chroot(或者 pivot_root)之前,容器进程一直可以看到宿主机上的整个文件系统。

而宿主机上的文件系统,也自然包括了我们要使用的容器镜像。这个镜像的各个层,保存在 /var/lib/docker/aufs/diff 目录下,在容器进程启动后,它们会被联合挂载在 /var/lib/docker/aufs/mnt/ 目录中,这样容器所需的 rootfs 就准备好了。

所以,我们只需要在 rootfs 准备好之后,在执行 chroot 之前,把 Volume 指定的宿主机目录(比如 /home 目录),挂载到指定的容器目录(比如 /test 目录)在宿主机上对应的目录(即 /var/lib/docker/aufs/mnt/[可读写层 ID]/test)上,这个 Volume 的挂载工作就完成了。

更重要的是,由于执行这个挂载操作时,“容器进程”已经创建了,也就意味着此时 Mount Namespace 已经开启了。所以,这个挂载事件只在这个容器里可见。你在宿主机上,是看不见容器内部的这个挂载点的。这就保证了容器的隔离性不会被 Volume 打破。

而这里要使用到的挂载技术,就是 Linux 的绑定挂载(bind mount)机制。它的主要作用就是,允许你将一个目录或者文件,而不是整个设备,挂载到一个指定的目录上。并且,这时你在该挂载点上进行的任何操作,只是发生在被挂载的目录或者文件上,而原挂载点的内容则会被隐藏起来且不受影响。

应用的镜像

Kubernetes 本质

回顾

一个容器:Linux NamespaceLinux Cgroupsrootfs 三种技术构建的进程隔离环境。

一个正在允许的 Linux 的容器:

  • 一组联合挂载在 /var/lib/docker/aufs/mnt 上的 rootfs, 容器的静态视图(容器镜像)
  • 一个有 Namespace + Cgroups 构成的隔离环境,容器的动态视图(容器运行时)

总结

在整个开发流程中 “开发 - 测试 - 发布”,真正承载容器信息传递的是容器镜像!然而云计算商想要与全部用户关联起来,那么只有通过容器镜像。 容器只是开发者手里的小工具,但是想从容器进入容器云的方式,就需要 容器编排 技术.

容器编排技术

大战之后,Kubernetes 应运而生

Kubernetes 顶层设计

  • 编排、调度、容器云、集群管理
  • 路由网关、水平扩展、监控、备份、灾难恢复

kubernetes架构

Kubernetes 是由 Master 和 Node 两种节点,控制节点 与 计算节点

  • 控制节点(三个组件)
    • kube-apiserver 负责API服务
    • kube-scheduler 负责调度
    • kube-controller-manager 负责容器编排
  • 计算节点
    • 核心 kubelet 负责和容器运行时(比如docker)交互
      • 交互的时候的依赖接口 CRI (Container Runtime Interface)的远程调用接口
      • 通过 gRPC 协议 与 Device Plugin 进行交互
      • 调用网络插件为容器配置网络 CNI (Container Networking Interface)
      • 调用存储插件为容器配置持久化存储 CSI (Container Storage Interface)
  • etcd 整个集群的持久化数据,由 kube-apiserver 处理后保存在 Etcd 中

从一开始,Kubernetes 就没有衣服到 Docker 项目上,没有将它作为架构的核心,只是将它作为了最底层的容器运行时的实现

Kubernetes 项目最主要的设计思想:从宏观的角度、以统一的方式定义任务之间的各种关系,为将来支持更多种类的关系留有余地

例如: Kubernetes 在访问关系上的操作

Pod 是 Kubernetes 的最基础的对象。 Service 是 Kubernetes 提供的访问关系的服务对象

我现在两个应用各自为POD,现在要做到A应用访问B应用,在使用时候,对于容器需要 IP 地址信息不变等等。 Kubernetes的做法是 Pod 绑定一个 Service 服务,而 Service 服务声明的 IP 地址等信息是不变的,这个Service服务主要作用就是作为 Pod 的代理入口,从而替代Pod对外暴露一个固定的网络地址。 这样对于调用方只需要关系 Service 声明信息,而Service后端真正代理的Pod 的IP地址、端口等信息的自动更新、维护是 Kubernetes的职责。

kubernetes全景图

  • Pod
  • Service 描述访问关系
  • Secret 密钥
  • Job 描述一次性运行的POD
  • DaemonSet 描述每个宿主机必须且只能运行一个副本的守护进程服务
  • CronJob 描述定时任务

如何编排一个K8S项目

  • 通过编排对象, 比如 Pod、Job、CronJob 等,来描述试图管理的应用;
  • 定义服务对象, 比如 Service、Secret、Horizontal Pod Autoscaler等,会负责具体的平台级功能

声明式 API 对应的 编排对象服务对象,都是 Kubernetes 项目中的 API 对象(API Object)


最后修改于 2021-05-15