3.1.贪婪岛:Aeon(K8S 1.25 + CRI-O)

好风凭借力,送我上青云。——曹雪芹《临江仙·柳絮》

    由于项目原因,Extension部分作为单点系统目前已运行了多个生产项目,一直想把这部分内容做个总结,但事实证明,这个工程量十分巨大,而且一直在重构和进行中,文档也许严重滞后,随着云端双项目的启动:

    我打算换一种视角来讲Zero Extension的故事,对的,新故事的主角是Aeon系统,而故事的老主角依旧是Zero Extension它的故事慢慢补),新版Zero重绘了架构图(参考:https://gitee.com/silentbalanceyh/vertx-zero),本章我们就敲开Aeon的大门。章节标题叫做《氷る世界(Ψυχρός κόσμος)》,起源于霜月遥的这首歌——又是一扇二次元的大门,而Aeon的名字起源于缇欧妹纸(零之轨迹角色)启动的永世系统,aeon本身翻译:永世。第一章叫贪婪岛是因为不贪婪的人不会上云去玩,而这个标题熟悉动漫的人也知道,这是《猎人》的第三章故事,一个虚拟的世界,类似于元宇宙的发源地。

    Aeon系统最初的设计是为了上云,而又鉴于底层云平台在华为、腾讯、阿里三大平台上的高成本,所以只能万丈高楼平地起,从操作系统开始搭建底层系统,底层选择K8并非是追逐潮流,而是Zero最初的微架构就是搭建在gRPC和Etcd基础上准备玩Istio的(了解Zero的人清楚,最开始它只是个玩具),所以这次只是拿到古老的钥匙,开了当年哈迪斯残留在人间的潘多拉魔盒——重启vertx-istio项目罢了。这个故事很长,我没有信心能讲完,但我尽可能把Zero Extension和Aeon部分的内容娓娓道来,也许从这个视角,读者才明白Zero Extension的独特之处。

本章参考文档:https://kubernetes.io/docs/home/

1. 人生如初见:Container Runtime

    Aeon来到一个充满未知的世界,这个世界的名字叫做K8S(Kubernetes,来自希腊语“舵手”、“领航员”,8代表中间缩写了8个字母),带上您的初级装备,先把这个世界走一走(Zero中的第一个新角色叫做:aeon-aurora,aurora:黎明的女神),它的责任就是将zero中前后端统一打包成一个独立镜像,等待着K8S的召唤,独立镜像不意味着大胖纸——Zero Extension中的公共服务会逐渐从原始模块中抽象和分离出来形成云原生平台的共享服务,不过目前这种低等级时代,就先胖一胖。

真枪实弹的刀剑场,我会抛弃minikube环境(实验/学习环境之一),这个环境的搭建可直接参考官方教程以及各种在线的文档,本文直接搭建生产环境(3节点、应该叫伪生产环境),并做下相关战斗记录。

    本文环境信息如下:

项目版本

CentOS Stream

8

Kubernates

1.25

Dashboard

2.6.1

    准备三台机器,CPU4核,内存16G(账号root、lyu):

IP地址主机名

192.168.0.154

k8s-master

192.168.0.123

k8s-node-01

192.168.0.208

k8s-node-02

本文中所有的命令我使用的是root账号以方便做记录,若需要提升权限执行某些命令请自行追加sudo。

1.1. 容器之争

    容器运行时(全称Container Runtime),它是一个负责运行容器的软件,使Pod可以运行在上面,K8S早期的版本仅适用于特定容器运行时:Docker Engine;之后K8S增加了其他容器运行时的支持,从1.5的版本开始创建了容器运行时标准接口(CRI,Container Runtime Interface),该标准主要是为了实现编排各种异构容器运行时并在其之间实现交互操作。

    由于Docker Engine没有实现CRI,因此K8S团队尝试创建特殊代码来帮助它实现过渡,并且使dockershim代码成为K8S的一部分,由于dockershim是一个临时解决方案(参考移除Dockershim的讨论),维护dockershim已成为了K8S运维团队的沉重负担,另外在最新的CRI运行时中实现了和dockershim不兼容的功能——所以1.20版本宣布,从1.24版本开始,dockershim正式从K8S项目中移除。

    容器运行时是一个软件,主要用来组成K8S Pod的容器,K8S负责编排和调度Pod,在每一个节点上,kubelet使用抽象的容器运行时接口,所以您可以选用兼容的容器运行时,由于Docker是在CRI规范创建之前存在的,dockershim相当于K8S和Docker之间的一个适配器组件。

    不论是Docker还是CRI,底层都调用了Containerd,它是一个工业级标准的容器运行时,强调简单性、健壮性、可移植性,它诞生于Docker——Docker作为一个完整的容器引擎,主要包含三部分,由统一的Docker Daemon进程提供(1.11开始):

  • 计算:Containerd提供

  • 存储:Docker-Volume提供

  • 网络:Docker-Network提供

    当创建容器请求到达Docker Api,它会调用Containerd执行创建操作,此时Containerd会启动一个containerd-shim进程,该进程调用runc执行容器的创建;创建完成后,runc退出、containerd-shim作为容器的父进程收集运行状态并上报给containerd,并在容器中pid1的进程退出后接管容器中的子进程执行清理以确保不会出现僵尸进程。

    为什么containerd不直接调用runc,而要启动一个containerd-shim调用runc?因为容器进程是需要一个父进程来做状态收集、维持stdinfd打开以及抽象层相关工作,若父进程是Containerd,它挂掉则整个宿主机上的所有容器都会退出,引入containerd-shim解决了这个问题,所以在早期Containrd一直是作为Docker创建容器的子组件而存在。

    何为runc?很久很久以前……

    Round 1Linux基金会 vs Docker,2013年Docker开源了容器镜像格式和容器运行时,为业界提供了一种更轻量级、灵活的“计算、存储、网络”资源虚拟化和管理的解决方案,2014年容器技术引爆,各种容器编排工具也逐步发力,此时K8S的第一个Release版本也由Google发布。Docker容器的两项核心技术:Namespace(资源隔离)和Cgroup(资源管理)其并非Docker原创,而是很早就进入了Linux内核的东西,所以Docker的容器解决方案离不开Linux内核的支持:倘若有人掌握了这两项技术,谁都可以做一套类似Docker的容器解决方案

    容器技术的引爆,使得Docker容器镜像和容器运行时在当时成为了行业标准,Docker成为了行业新星,它面对各个行业神魔(Linux基金会、Google、微软等)提出的合作邀请充耳不闻,态度强硬且傲慢,力图独自主导容器生态的发展。由于Docker的运行时向下兼容性的问题导致口碑逐渐变差,于是各个神魔都打算另起炉灶自己干。Linux基金会联合这些神魔向Docker施压,最终Docker屈服,于2015年6月在Docker大会上推出容器标准,并成立了容器标准化组织(OCI、Open Container Initiative),并发展成Linux基金会下的一个项目。OCI标准主要包含两部分:

  • 容器运行时规范(runtime-spec):定义了如何根据对应配置构造容器运行时。

  • 容器镜像规范(image-spec):定义了容器运行时使用的镜像打包规范。

    而runC就是这两个规范落地的实体,它是一个符合OCI规范的轻量级容器运行时生命周期管理工具,最初也是Docker贡献给社区,来源于Docker原始的运行时管理部分,您可以运行runC -h查看帮助文档,它提供了生命周期管理、暂停、恢复、热迁移、状态查询等具体细节操作,使用runC创建和管理容器比较简单,解决了容器最核心、最底层、最基础的问题。

    Round 2K8S vs Docker,K8S的诞生正是Docker如日中天之时,那时它没有办法和Docker正面对决,只能通过硬编码的方式在kubelet中调用Docker API创建容器。OCI的诞生给了Google一把利刃,于是从K8S 1.5开始,它创建了容器运行时接口CRI,本质上就是K8S定义了一组和容器运行时交互的接口,所以只要实现了这套接口的容器运行时都可以直接对接K8S,由于当时CRI没有如今这种统治地位,有部分容器没有实现CRI接口,于是就有了shim(垫片)项目,一个shim的职责就是作为适配器将各种容器运行时本身的接口适配到K8S的CRI接口上,而dockershim就是K8S对接Docker到CRI接口上的一个实现。

    OCI标准提出后,Red Hat的一些人开始设计和构建一个更简单的运行时,这个运行时仅为K8S使用,于是就有了skunkworks项目,最后这个项目定名为CRI-O——它实现了一个最小的CRI接口——引用原话:CRI-O被设计为比其他的方案都要小,遵从Unix只做一件事并把它做好的设计哲学,实现组件重用。根据CRI-O开发者的研究报告,这个项目最开始服务于OpenShift平台,同时得到了Intel和SUSE的支持,CRI-O和CRI规范兼容,并与OCI和Docker镜像格式也兼容,CRI-O 1.0于2017年10月正式发布。

    于是2017年,Docker公司将容器运行时Containerd贡献给了CNCF,为了将Containerd接入到CRI标准中,K8S成立了cri-containerd项目,它是一个单独的守护进程,直接实现了kubeletcontainerd之间的交互,形成了Containerd 1.0的样子,之后cri-containerd又被改造成了Containerd的CRI插件位于内部,K8S启动更加高效,就形成了如下结构:

    再加上CRI-O的催化,直接兼容CRI和OCI规范,最终结果都比dockershim的方式接入K8S简单,于是这场战役以Docker失败告终。综合提到的runC, OCI, CRI-O, Containerd, CRI,它们的完整结构如如下:

    Docker和K8S的战争中,Google最初只是一个独角戏,而云原生计算基金会(CNCF, Cloud Native Computing Foundation)带来的社区力量改变了这一切,它成立的目的是构建“云原生”计算并推动其落地,这是一种围绕微服务、容器、动态调度以基础设施架构为中心的方式,这种风格就是我们通常提到的FIFEE(Google's Infrastructure For Everyone Else):为其他所有人用的Google基础设施。

    回顾Docker这位屠龙少年,虚拟化技术从虚拟机转移到容器运行时,它以简洁优雅的方式出场即王炸直接将VMware旗下的Cloud Foundry斩于马下,Build Once, Run Anywhere这句致胜的法则就是Docker打败CF的秘诀,加上Docker对开发人员的友好性,夺取了当之无愧的C位。而它在拿到融资之后,开始大量收购(DockerCompose, Docker Swarm, DockerMachine),从此这位开源的屠龙少年开始进行了商业化进程,释放的信号是未来的云厂商需要向Docker公司支付授权费用,过早暴露的商业化意图也为自己埋下了隐患。OCI基金会的成立被少年的傲慢忽略了,当时的它并没把OCI放在眼里,凭借自身的用户优势对标准漠不关心,OCI虽然在Docker的缺席中发展缓慢,而之后CNCF的成立却给OCI打了一剂强心针,也将K8S正式推出了舞台。之后的故事就没有什么可讲的了,Docker输在了视觉上:如果说Docker Swarm编排只是站在了容器视觉处理问题,而K8S则只是将容器定位为运行时环境(容器运行时),Pod和Service才是编排建模中的重点,只要符合标准的容器运行时都可以被Pod编排;如果说Docker打败CF依靠的是简洁,那么K8S打败Docker就是真正意义上的降维打击。有了这样的优势,K8S开始去Docker化操作,1.20版本会有警告,而从1.22版本之后移除了Docker的支持,1.24版本后拿掉了dockershim项目。

1.2. CRI的选择

    dockershim虽然被K8S拿掉,但您依旧可以使用Docker在本机进行开发,docker build的镜像依旧适用于所有CRI实现,而Docker在dockershim移除之后为Docker Engine开发了一个替代品cri-dockerd,您可以直接安装cri-dockerdkubelet和Docker Engine连接。

    当然您可能正在使用Docker Engine,您也可以选择迁移到Containerd,从调用链上可以看到它比原生的Docker有更好的性能和开销,您可以访问CNCF landscape提供的选项选择适合您的CRI。

    虽然Docker和大多数CRI(包括Containerd)之间的底层容器化代码是相同的,但周边会存在一定差异,迁移时需要考虑:

  • 日志配置

  • 运行时的资源限制

  • 调用 docker 或通过其控制套接字使用 Docker Engine 的节点配置脚本

  • 需要 docker 命令或 Docker Engine 控制套接字的 kubectl 插件

  • 需要直接访问 Docker Engine 的 Kubernetes 工具(例如:已弃用的 'kube-imagepuller' 工具)

  • registry-mirrors 和不安全注册表等功能的配置

  • 保障 Docker Engine 可用、且运行在 Kubernetes 之外的脚本或守护进程(例如:监视或安全代理)

  • GPU 或特殊硬件,以及它们如何与你的运行时和 Kubernetes 集成

    还有一个关注点就是创建镜像时,系统维护、嵌入容器的任务无法工作:前者,您可以使用crictl工具作为临时解决方案;后者,您可以使用新的容器创建选项,如:

1.3. Podman一枝独秀

    Podman是RedHat旗下的一款产品,它旨在使用K8S的方法来构建、管理、运行容器,作为一款主流容器的可靠替代品;RedHat从RHEL 8起,使用CRI-O/Podman取代了Docker Daemon。探讨下为什么?

  1. Docker有单点故障问题,一旦Docker Daemon死亡所有容器都将死亡。

  2. Docker Daemon拥有运行中容器的所有子进程。

  3. 所有Docker操作都必须由具有跟root相同权限的用户执行。

  4. Docker构建容器时容易导致安全漏洞。

    Podman和Docker区别如下:

    Podman是一种开源的Linux原生工具,旨在根据OCI标准开发、管理、运行容器和Pod,它是RedHat开发的一个用户友好的容器调度器,也是RedHat 8和CentOS 8中默认的容器引擎。Podman的工具集包括:

  • Podman:Pod和容器镜像管理器

  • Buildah:容器镜像生成器

  • Skepeo:容器镜像检查器

  • Runc:容器运行时和特性构建器,传递给Podman/Buildah

  • Crun:可选运行时,为Rootless容器提供更大的灵活性、控制和安全性

    Podman 2.0的优势如:

  1. 因为Docker和Podman创建的镜像都符合OCI标准,它可以使用比较流行的容器注册中心(Docker Hub或Quary.io)。

  2. Podman不会使用额外的守护进程运行容器,它可以直接和systemd集成支持在后台运行容器。

  3. 允许您创建和管理Podami,支持一组(一个或多个容器)一起工作,这有助于之后将工作负载转移到K8S和Podman的编排任务中。

  4. 它可以使用UID分离命名空间,这样等价于在容器运行时提供了额外的隔离层,更安全。

  5. 它可以基于正在运行的容器为K8S生成YAML文件(命令:podman generate kube)。

    Podman的使用定位也是兼容Docker的:

  • 构建者角度:用Podman的默认软件和Docker的区别不大,它只是在进程模型、进程关系方面有所区别,而Podman相对于Docker而言更简单,由于不需要守护进程,它的重启机制也有所变化。

  • 使用者角度:Podman和Docker的命令基本兼容,都包括容器运行时、本地镜像、镜像仓库,它的命令行工具和Docker几乎保持一致,您可直接使用alias docker=podman进行替换。


2. 他山之石:Cgroup

    前边章节讲解了容器运行时,本章节我们一起来看看另一个重量级的东西,容器运行的基石——Cgroup,我尽可能追溯更多相关内容,翻翻这个角色的历史,让您从起源开始理解Cgroup。

2.1. 指令集

    CPU指令集是CPU实现软件指挥硬件工作的桥梁,每一条汇编语句都对应了一条CPU指令,大量CPU指令一起协同工作就组成了CPU指令集。复杂指令集(CISC、Complex Instruction Set Computing)和精简指令集(RISC、Reduced Instruction Set Computing)架构是目前CPU的两大主流架构,其中CISC以Intel、AMD的x86/x64为代表,而RISC则以ARM、IBM Power为代表,二者区别如下:

CISC早期的RISC

指令集数量多,Intel早期文档1200多页说明。

指令数量少得多,通常少于100个。

指令延迟很长。

没有较长延迟指令,早期CPU都没有乘法,而是连加实现。

编码是可变长度,可以是1 ~ 15个字节。

编码是固定长度,所有指令都是4个字节。

指令操作数的方式很多、寻址复杂,通常由偏移量、基地址、变址寄存器、伸缩因子组合而成。

寻址方式简单,只有基地址和偏移量寻址。

可对存储器和寄存器执行算术、逻辑运算。

只能对寄存器执行算术和逻辑运算,对存储器则是load/store体系结构。

对机器级程序来说实现细节是不可见的。

对机器级程序来说实现细节是可见的,有些RISC禁止某些特殊的指令序列。

有条件码,作为指令执行副产品,设置了一些特殊标志位,可用于条件分支检测。

没有条件码,对条件检测来说,要用明确的测试指令,并将测试结果放到一个寄存器中。

栈密集的过程链接,栈被用来存取过程参数和返回值。

寄存器密集的过程链接,寄存器被用来存取过程参数和返回值。

    CPU指令集存在权限分级。由于它可以直接操作硬件,操作不规范造成的错误会影响整个计算机系统,造成不可挽回的损失;而对硬件的操作十分复杂、参数众多,出错概率相当大,所以操作系统内核直接屏蔽了开发人员对某些硬件操作的可能,不让开发人员碰到这部分CPU指令集。基于此,硬件设备厂商的做法就是对CPU指令集设置权限,不同级别权限能使用的CPU指令集是有限的。如Intel的CPU分为:Ring 0、Ring 1、Ring 2、Ring 3四个权限级,Windows操作系统则使用了Ring 0Ring 3两个级别,其中Ring 0权限最高,只提供给操作系统使用,而Ring 3谁都能用;对Linux系统而言,Ring 0就叫做内核态,在操作系统内核中运行,Ring 3就称为用户态,在应用程序中运行。

    操作系统内部定义了规则,让用户态和内核态可实现切换,其本质就是CPU指令集权限的区别,如:应用程序进程要读写IO,必然会使用Ring 0级指令,此时CPU指令操作权限是Ring 3级,操作系统内部会让应用程序先从Ring 3切换到Ring 0,再执行相对应的内核代码,执行完成后返回Ring 3级别。Linux系统中每个进程都有两个指令栈:用户栈和内核栈,其分别对应用户态和内核态。

    通常以下三种情况会从用户态切换到内核态:

  1. 系统调用:用户进程通过系统调用向操作系统申请资源完成工作,系统调用核心机制使操作系统为用户特别开放的一个中断来实现,又称软中断。

  2. 异常:当CPU执行用户态进程时,发生了一些没有预知的异常,这时当前运行进程会切换到处理异常的内核相关进程,也就是切换到内核态,如缺页异常。

  3. 中断:当CPU执行用户态进程时,外围设备完成用户请求操作后,向CPU发出相应中断信号,此时CPU会暂停执行下一条即将执行的指令,转到与中断信号对应的处理程序去执行,切换到内核态。如硬盘读写操作完成。

Linux源代码中,一般会把进程称为任务(Task)或线程(Thread)。

2.2. 特殊进程

    传统Unix系统中,某些只能在内核态运行的系统进程,现代操作系统会把它们的函数委托给内核线程(kernel thread)执行,它不受不必要的用户态上下文影响;注:内核线程只能运行在内核态,普通线程可以运行在内核态,也可以运行在用户态

idle进程

    所有进程的祖先叫做进程0(PID=0),idle进程 因为历史原因又叫做swapper进程,它是在Linux的初始化阶段从无到有创建的一个内核进程(系统创建的第一个进程),运行在内核态,这个祖先进程使用静态分配的数据结构,是唯一一个不通过fork()kernel_thread()产生的进程,加载完成后转变成进程调度、交换。

    多处理器系统中,每个CPU都有一个进程0,打开电源,计算机BIOS就会启动一个CPU(称CPU 0),同时禁用其他CPU;运行在CPU 0上的swapper进程初始化内核数据结构,然后激活其他CPU,通过copy_process()函数创建其他CPU的idle进程,把0传递给新创建的swapper进程作为它们的新PID。

init进程

    由进程0创建(idle进程调用kernel_thread()创建)的内核线程会执行init()函数,该函数依次完成内核初始化,它调用execve()系统函数加载可执行程序init,最终在用户空间创建init进程(PID=1, PPID=0,PPID为父进程ID)。结果,init内核进程转换成一个普通进程,并且拥有自己的进程内核数据结构,它是系统中其他所有用户进程的祖先进程——简单说Linux系统中所有的普通进程都是init进程创建并运行的,加载完成后,init转变成守护进程监视系统其他进程。

kthreadd进程

    由进程0创建(idle进程调用kernel_thread()创建)并始终运行在内核空间,负责所有内核进程的调度和管理。它的任务是管理和调度其他内核进程,会循环执行一个kthread函数,该函数作用就是运行全局链表中维护的kthread,当系统调用kernel_thread()创建内核进程时,创建的进程会被加到此链表中,因此所有的内核进程都是直接或间接的以kthreadd为父进程。

    下图是最终结构:

简单说,idle进程是祖先进程,它创造了init和kthreadd;init进程是所有用户进程的父进程,kthreadd进程是所有内核进程的父进程。

守护进程

    Linux守护进程(Linux Daemon)是运行在Linux操作系统后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件,它不需要用户输入就能运行而且提供某些服务,这些服务要么面向整个系统、要么面向某个用户程序。Linux系统的大多数服务器就是通过守护进程实现,常见的守护进程如:

  • httpdWeb服务器。

  • sendmail邮件服务器。

  • mysqld数据库服务器。

  • syslogd系统日志进程。

    守护进程一般会在系统启动时运行,除非强制终止,否则到操作系统关机时它都一直运行着。通常守护进程以超级用户root权限运行,它们要使用特殊的端口(1 - 1024)、或访问特殊资源;它没有交互式控制终端,它的任何输出都需要特殊处理(写入日志),通常以d结尾,httpd、syslogd等。爸爸在哪儿——一般用户态守护进程的父进程就是init进程(PID=1);而内核态守护进程的父进程有可能不是init进程,它真正的父进程在fork()出子进程之后就先于子进程exit()退出了,所以它可以算作init进程直接继承的孤儿进程。

    理解守护进程需先理解几个概念:

概念含义

进程组 Process Group

进程的集合,每个进程组会有一个组长(Leader),它的进程ID就是进程组的ID。

会话 Session

进程组集合,每个会话有一个组长,其进程ID就是该会话组ID。

控制终端 Controlling Terminal

每个会话可以有一个单独的控制终端,和控制终端连接的Leader就是控制进程Controlling Process

    守护进程的创建步骤如:

  • fork:守护进程的父进程通常是init进程,它在创建时从父进程fork出来一个子进程并退出父进程,此时子进程就变成了孤儿进程,算作init的子进程。

  • setid:该函数用来创建新的会话,同时脱离原来的进程组、会话、控制终端,成为新的会话组长。此时它可能会再申请一个控制终端,所以再fork一下,并保留新的子进程,这样就不是新的会话组长,不能申请控制终端了。

  • close(fd):然后关闭从父进程继承的文件描述符——最少关闭0,1,2三个文件描述符,分别对应stdin, stdout, stderr,通常会调用sysconf(_SC_OPEN_MAX)获取系统允许的最大文件描述符个数,然后全部close掉——关闭之后还应该将0,1,2重新定向到/dev/null中防止新打开的文件描述符为0,1,2

  • umask(0):设置文件掩码为了不受父进程的umask影响,可自由创建读写文件和目录。

  • chdir("/"):守护进程一般会执行到系统关机,在它运行过程中所在的目录就不能卸载(unmounted),通过将它的工作目录转移到根目录,该目录就允许卸载了(不一定选择根目录,可以选择一个不需卸载的路径)。

2.3. 资源管理

systemd

    是Linux操作系统中的一种init软件,它的作用是提供更优秀的框架以计算系统服务之间的依赖关系,并且依次实现系统初始化服务时的并行启动,达到降级shell系统开销的效果。它和init进程的区别是什么

    Linux操作系统启动会从BIOS开始,然后Linux引导程序将内核映像加载到内存执行内核初始化,初始化完成后会创建init进程(PID=1),这个进程是系统的第一个进程,它负责产生其他所有的用户进程。单纯启动内核其实毫无用处,于是我们需要一个系统可以定义、管理、控制init进程的行为,并可组织、运行许多初始化相关的任务,从而让系统进入一个用户设定的运行模式中,这个系统就是init系统(它和System V兼容,因此又称为sysvinit)。

    由于init系统依赖Shell脚本,并且是一次一个任务串行启动,所以启动特别慢,服务器配置比较高无法体会到瓶颈,但在嵌入式设备、移动终端上,这个缺陷就显而易见了。于是开发者们开始折腾,对sysvinit进行改进,先后出现了upstart和systemd两个主要的新一代init系统,Ubuntu为代表的Linux采用的就是upstart,而RHEL 7.x/CentOS 7.x中默认采用了systemd,由于systemd出现时间更晚发展却快,未来很有可能替换掉upstart成为唯一的选择。

    sysvinit使用术语runlevel定义系统运行级别,在之前系统中通常有7种运行级别,但每个Linux发行版对运行级别定义都有所区别,唯一达成共识的是0, 1, 6三个级别:

级别含义

0-关机模式

1-单用户模式

单用户只有系统管理员可以登录。

6-重启模式

执行关闭所有运行的进程,然后重新启动系统。

2-多用户模式

「不常用」不支持文件共享。

3-完全多用户模式

支持NFS服务,最常用的多用户模式,默认登录到终端界面。

4-基本不用的用户模式

实现某些特定登录请求。

5-完全多用户模式

默认登录到X-window系统——Linux图形界面。

    systemd是新一代的init系统,其主要特点是并发处理所有服务,加速开机流程。早期的运行级别(runlevel)被新的运行目标(target)取代,target命名类似multi-user.target形式,如原来的runlevel 3 = multi-user.target,原来的runlevel 5 = graphical.target

  • init管理机制:所有的服务启动脚本都放置于/etc/init.d目录,基本上都是用bash shell编写的脚本程序,系统启动时依次执行——这就是开篇提到的为什么说systemd是降级shell系统开销(常用命令daemonchkconfig)。

  • systemd管理机制:它是运行在用户态的应用程序,包含了一个完整软件包,配置文件位于/etc/systemd目录下,所有服务脚本位于/usr/lib/systemd/system目录下,这些脚本都以.service结尾(常用命令systemctl)。

systemctl是一个更强大的命令行工具,您可以把它看作servicechkconfig的组合体,想要查看、启动、停止、重启、启用、禁用系统级服务,都可以通过systemctl来实现。

cgroups

    cgroups全称是Control Groups,它是Linux内核提供的一种机制,这种机制根据具体的资源需求把一系列系统任务和子任务按资源等级划分到不同的组进行管理,从而为系统资源管理形成一个统一的框架。简单说,cgroups可以限制、记录任务组使用的物理资源,本质上,它是内核附加在程序上的一系列钩子(Hook),通过程序运行时对资源的调度触发对应钩子以达到资源限制和追踪的目的。

    典型的子系统如:

系统名作用

cpu子系统

限制进程的CPU利用率。

cpuacct子系统

统计cgroups中的进程cpu使用报告。

cpuset子系统

为cgroups中的进程分配单独的cpu节点、内存节点。

memory子系统

限制进程的memory使用量。

blkio子系统

限制进程的块设备io。

devices子系统

控制进程能够访问的某些设备。

net_cls子系统

标记cgroups中进程的网络数据包,然后使用tc模块(Traffic Control)对数据库进行控制。

net_prio子系统

设置cgroups中进程产生的网络流量优先级。

freezer子系统

可挂起或恢复cgroups中的进程。

ns子系统

使不同cgroups下的进程使用不同的namespace。

    上述每个子系统都需要和内核中对应的模块配合来完成资源的控制和调度,它是分组管理操作系统资源的核心模块,由Google的工程师提出,之后整合到了Linux操作系统中,设计它的目的是为不同用户层面的资源管理提供一个统一化的接口。它提供的功能如下:

  • 资源限制(Resource Limiting)

  • 优先级控制(Prioritization)

  • 资源统计(Accounting)

  • 进程组隔离(Isolation)

  • 进程组控制(Control)

    如今以容器为代表的虚拟化技术大行其道,通过了解cgroups您可以管中窥豹感受到Linux系统对资源管理的各个经脉。cgroups相关概念有四:

  1. 任务(Task):在Linux系统中它就表示一个进程。

  2. 控制组(Cgroup):资源控制的单位,可能是任务组的集合,每个任务组被分配相对应的资源,包含了一个或多个子系统——一个任务可以加入到这个组中,或者迁移到另外一个组中。

  3. 子系统(Subsystem):一个资源调度器(Resource Controller),控制各种资源的分配和资源访问权限。

  4. 层级树(Hierarchy):一种操作系统的组织结构,可理解为一个cgroup树,将cgroup串成树状结构通过虚拟端口暴露给用户。

    cgroups的整体框架如:

Docker到K8S的转变以及容器技术的发展无法绕开systemd和cgroups两个角色,我不打算在这里讲解这两个角色的所有方面,只是引导您囫囵吞枣,保证您可以在遇到环境配置问题时变得不那么困惑。

cgroup v2(k8s)

    K8S中,kubelet和底层容器运行时都要对接cgroup来强制执行资源管理——包括为容器化工作负载配置CPU/内存请求/限制,cgroup v2cgroup API的下一个版本,它提供了一个具有增强资源管理能力的统一控制系统。它的改进如:

  • API中单个统一的层次结构设计。

  • 更安全的子树委派给容器。

  • 更多新功能,如:压力阻塞信息(PSI - Pressure Stall Information)。

  • 跨多个资源的增强资源分配管理和隔离:

    • 统一核算不同类型的内存分配(网络内存、内核内存)。

    • 考虑非即时资源变化,如页面缓存回写。

    使用cgroup v2最简单的方法是使用一个默认启用cgroup v2的Linux发行版,具体要求如下:

  • 操作系统发行版启用cgroup v2。

  • Linux内核为5.6或更高版本。

  • 容器运行时支持cgroup v2,如:

    • containerd v1.4或更高版本。

    • cri-o v1.20或更高版本。

  • kubelet和容器运行时配置成使用systemd cgroup驱动

    您可以执行如下命令查看发行版的cgroup版本:

stat -fc %T /sys/fs/cgroup/
# 输出为cgroup2fs,  cgroup v2
# 输出为tmpfs,      cgroup v1

cgroup驱动(k8s)

    kubelet在管理资源时,若要对接控制组,它和容器运行时需要使用一个cgroup驱动,关键点是kubelet和容器运行时需要使用相同的cgroup驱动且使用相同的配置。可选择的驱动有两种:

  • cgroupfs:它是kubelet中默认的cgroup驱动,当使用该驱动时,kubelet和容器运行时将直接对接cgroup文件系统来配置cgroup。

    当systemd初始化系统时,不推荐使用cgroupfs驱动,systemd期望系统上只有一个cgroup管理器,此外若您想要使用cgroup v2,则需使用systemd cgroup驱动代替cgroupfs

  • systemd:若Linux发行版使用systemd作为初始化系统,初始化进程会生成一个root控制组(cgroup),并充当cgroup管理器。systemd和cgroup集成紧密,它为每个systemd单元分配一个cgroup,因此若您使用systemd用作初始化系统,同时使用cgroupfs驱动,系统中就存在两个不同的cgroup管理器。同时存在两个cgroup管理器将造成系统中针对可用的资源和使用中的资源出现两个视图,某些情况下,kubelet和容器运行时配置为使用cgroupfs、但为剩余进程使用systemd的节点在资源压力增大时变得极度不稳定。

    所以若Linux发行版是选用systemd作初始化系统时,为了缓解这种不稳定就直接使用systemd作cgroup驱动。

    要将systemd设置为cgroup驱动,需编辑KubeletConfigurationcgroupDriver选项,并将其设置为systemd。如:

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
...
cgroupDriver: systemd

    若您将systemd配置为kubelet的cgroup驱动,也必须将systemd配置为容器运行时的cgroup驱动。:更改已加入集群节点的cgroup驱动是一项敏感的操作,如果kubelet已经使用某cgroup驱动的语义创建了Pod,更改运行时以使用别的cgroup驱动,当为现有Pod重新创建PodSandbox时会产生错误,重启kubelet也可能无法解决此类问题。——这个过程最好使用自动化操作


3. 千里之行:环境安装

    为区分实际步骤和讲解,所有步骤点使用如下语法:

Action:步骤和解释

命令区间

    基本环境准备:

  • 一台兼容的Linux主机,K8S项目为基于Debian和Red Hat的Linux发行版提供了通用命令。

  • 每台机器2GB或更多内存。

  • CPU 2核以及以上。

  • 集群中所有机器的网络彼此均能相互连接(公网、内网都可以)。

  • 节点中不可以有重复主机名、MAC地址、product_uuid

  • 开启机器上的某些端口(防火墙禁用或配置)。

  • 禁用交换分区(确保kubelet正常工作)。


关于product_uuid

    如果您在自己的机器上执行ll /sys/class/dmi/id/命令,可以看到该目录下的很多参数:

    其中就有product_uuidproduct_serial它们是如何生成的?实际这些值都是在内核代码中生成的,在内核源代码项目中您可以使用git grep命令:

git grep --all-match -n -e '\bdmi\b' -e product_uuid -e product_serial

    下载Linux源代码

    执行命令后可以看到明显的两行定义:

DEFINE_DMI_ATTR_WITH_SHOW(product_serial,  0400, DMI_PRODUCT_SERIAL);
DEFINE_DMI_ATTR_WITH_SHOW(product_uuid,    0400, DMI_PRODUCT_UUID);

    此部分所有代码都位于drivers/fireware/dmi-id.c,查看源代码会发现所有属性都访问了sys_dmi_field_show()函数,之中又调用了dmi_get_system_info(),其从dmi_ident队列中返回对应元素,该表在dmi_decode()中被填充:

// dmi-id.c
// 定义
#define DEFINE_DMI_ATTR_WITH_SHOW(_name, _mode, _field)		\
static struct dmi_device_attribute sys_dmi_##_name##_attr =	\
	DMI_ATTR(_name, _mode, sys_dmi_field_show, _field);
// sys_dmi_field_show函数
static ssize_t sys_dmi_field_show(struct device *dev,
				  struct device_attribute *attr,
				  char *page)
{
	int field = to_dmi_dev_attr(attr)->field;
	ssize_t len;
	len = scnprintf(page, PAGE_SIZE, "%s\n", dmi_get_system_info(field));
	page[len-1] = '\n';
	return len;
}

    最终product_uuiddmi_save_uuid()函数中生成

// dmi_scan.c
/*
 *	Process a DMI table entry. Right now all we care about are the BIOS
 *	and machine entries. For 2.5 we should pull the smbus controller info
 *	out of here.
 */
 static void __init dmi_decode(const struct dmi_header *dm, void *dummy){
    switch (dm->type) {
	case 0:		/* BIOS Information */
        // ......
		break;
	case 1:		/* System Information */
        // ......
		dmi_save_ident(dm, DMI_PRODUCT_SERIAL, 7);
		dmi_save_uuid(dm, DMI_PRODUCT_UUID, 8);
		break;
    // ......
 }
  • 对于product_uuid:查看SMBIOS specification中的System-UUID有具体描述,并且有一个表格解释每部分数字代表的含义,通过解码该UUID可从中获取部分信息,并非自由随机的UUID生成。

  • 对于product_serial:这个就是设备序列号,如您在硬件包装中可以查看到的设备具体序号信息。

Action:在三台机器上执行下边命令确定三个product_uuid都不同

# 查看 product_uuid 值
cat /sys/class/dmi/id/product_uuid

    实际上通过命令枚举出来的文件虽然可以用cat查看相关信息,但它底层数据结构并不是文件,而是内核函数的接口(参考sysfs),所以若您要更改product_uuid的值,需要编辑对应的内核文件并重建整个内核并引导它(而不是发行版提供的文件)。再者您虽然可以在DMI表中更改这些值(比较棘手),但不推荐这么干,这些值并非随机UUID,它的每一位都存在一定意义,某些情况下可能会有用,电脑需依赖它执行系统识别,所以更改是一个高风险操作。这些值全部来源于DMI(全称:Desktop Management Interface)表,DMI和BIOS一起都是硬编码到存储器中(主板BIOS的flash芯片),内核函数只是单纯读取这些值罢了。除了product_uuid,最好还是执行ip linkifconfig -a查看MAC和网口是否重复,若有重复需要调整


3.1. 预处理:OS准备

3.1.1. 启用cgroup v2

    cgroup v2早已经在Linux 4.5的时候加入到内核了,CentOS 8默认使用了4.18作为内核版本,但系统中仍然默认使用的是cgroup v1。

Action:查看cgroup版本

stat -fc %T /sys/fs/cgroup/
# v1 - tmpfs,
# v2 - cgroup2fs

    由于系统上默认开启的是cgroup v1,所以需配置一下系统切换到cgroup v2。

Action: 查看v1是什么样子

# 直接执行 mount 命令,或带参数查询
mount                     
mount -t cgroup

# 您可以多做一个步骤查看操作系统是否支持 cgroup v2,确定支持后再切换
grep cgroup2 /proc/filesystems

    mount命令中显示出来的cgroups目录就是v1的形态,接下来切换到v2,v2的切换方法其实很简单,就是在重新启动的时候加上一个内核引导参数:

systemd.unified_cgroup_hierarchy=1

    该参数的含义是打开cgroup的unified属性,注意一点就是CentOS 8没有像Ubuntu一样将cgroup v2挂载到/sys/fs/cgroup/unified中,而是直接挂载到了/sys/fs/cgroup下,所以使用之前建议使用命令mount |grep cgroup检查以确定挂载信息:

Action:切换cgroup到v2

grubby --update-kernel=ALL --args=systemd.unified_cgroup_hierarchy=1

执行完此步骤之后重启操作系统,然后执行下边命令查看是否切换成功

stat -fc %T /sys/fs/cgroup/
mount |grep cgroup
ll -p /sys/fs/cgroup                      # 查看一下v2的目录结构变化

    v2比v1的挂载信息简单很多

3.1.2. 时间同步

    CentOS 8一般使用chrony作为时间同步工具,系统默认已安装了chrony工具,若没有安装的话则执行在线安装命令,安装后默认服务是未启动的,执行相关命令启动服务。

Action:配置三台机器的时间同步服务

yum install -y chrony                     # 安装chrony
systemctl start chronyd                   # 启动chrony服务
systemctl enable chronyd                  # 设置chrony服务的开机自启动
systemctl status chronyd                  s# 查看chrony服务状态
date                                      # 查看系统时间是否同步

    三台机器时间查看如:

3.1.3. 防火墙配置

    搭建K8S过程中防火墙最方便的配置是直接关闭防火墙:

Action:关闭防火墙

systemctl stop firewalld					# 关闭防火墙
systemctl disable firewalld				# 禁用防火墙
systemctl status firewalld				# 查看防火墙状态

    这种方式在生产环境实际是不推荐的,若您想要开启防火墙,需要执行如下:

Action:启用防火墙(三台机器依次执行

  1. 确认开启防火墙服务

    systemctl restart firewalld
  2. 将集群内所有节点IP配置到防火墙可信区中

    # k8s-master: 		192.168.0.154
    # k8s-node-01: 		192.168.0.123
    # k8s-node-02:      	192.168.0.208
    firewall-cmd --permanent --zone=trusted --add-source=192.168.0.154
    firewall-cmd --permanent --zone=trusted --add-source=192.168.0.123
    firewall-cmd --permanent --zone=trusted --add-source=192.168.0.208
  3. 增加防火墙规则

    firewall-cmd --permanent --direct --add-rule ipv4 \
    		filter INPUT 1 -j ACCEPT -m comment --comment "kube-proxy redirects"
    firewall-cmd --permanent --direct --add-rule ipv4 \
    		filter FORWARD 1  -j ACCEPT -m comment --comment "docker subnet"
  4. 设置防火墙伪装ip,打开NAT(默认是关闭的)

    firewall-cmd --add-masquerade --permanent
  5. 放行所有K8S和NodePort端口

    firewall-cmd --permanent --zone=public --add-port=30000-32767/tcp
  6. 重新加载配置

    firewall-cmd --reload

3.1.4. 禁用Swap

    Linux中的交换分区(Swap分区)近似于Windows中的虚拟内存,它是Linux操作系统中的虚拟内存分区,当物理内存使用完之后,系统会将磁盘空间(Swap分区)虚拟成内存来使用。它和Windows系统中交换文件作用类似,但它是一段连续的磁盘空间,并且对用户不可见。

    虽然Swap分区能当做虚拟内存来使用,可它的速度比物理内存慢了许多,因此若需要更快的速度的话不能依赖Swap交换分区,最好的办法依旧是加大物理内存,Swap只是一种临时解决方案。K8S为什么禁用Swap?这是网上争论比较多的一个话题,从K8S 1.8开始,几乎每次部署都需要在节点系统中禁用Swap,而从资料无法找到禁用Swap的原因。

Linux内核这块的设计就是为了利用Swap,完全禁用它会产生负面的影响。

    讨论贴上的某种解释:K8S的思想是将实例紧密打包为尽可能接近100%的利用率,所有部署都应固定有CPU/内存限制,因此若调度程序将Pod发送到计算机,绝对不要使用Swap,因为它会减慢速度。——其实这种说法是站不住脚的(来源bullshit),不正确使用Swap实际是大部分开发者一种懒的表现,证明它对内存子系统理解不深入(就像我这种)以及缺乏基本的系统管理技能,若基础设施服务设计不当导致错误使用了Swap,有可能会对系统运行性能产生影响。正确处理Swap、分析内存、并确定如何在不影响Swap情况下利用内存子系统是有可能的——所以性能的观点是有问题的,除去Swap对系统性能造成的损失,如何正确使用它很重要,若把Pods放到磁盘中有可能会影响性能,但从设计上考虑有部分内容是应该放到磁盘空间的。

Action:禁用交换分区

  1. 执行命令查看swap交换分区

    free -m
    # Swap 行的值应该是0,下边结果可以看到有4G
    # 		total        used        free      shared  buff/cache   available
    # Mem:		3757        1224        1080          20        1452        2270
    # Swap:		4047           0        4047
  2. 临时关闭交换分区(主要禁用/proc/swaps中所有交换分区)

    swapoff --version
    # swapoff from util-linux 2.32.1
    swapoff -a				# 临时关闭交换分区			
    					# 临时启用:swapon -a
    swapon -v				# 无输出(输出为空)表示已关闭
  3. 永久禁用——先查看/etc/fstab记录

    more /etc/fstab
    # 找到交换分区记录
    /dev/mapper/cl-swap     swap      swap    defaults        0 0
  4. 将带有swap那一行记录注释掉或删除掉,然后重启系统

  5. 启动完成后使用free -m检查

3.1.5. 禁用SELinux

    安全增强型Linux(Security-Enhanced Linux)简称SELinux,它是Linux的一个内核模块,也是Linux的一个安全子系统。SELinux主要由美国安全局开发,2.6以及以上的Linux内核都默认集成了SELinux模块,它的结构和配置十分复杂,而且有很多概念性的东西,精通难度极高。很多Linux系统管理员觉得麻烦都直接禁用了SELinux,若熟练掌握SELinux,那么系统可能会变得无坚不摧(虽无绝对安全,但强化过也不错)。

    SELinux的主要作用是最大限度地减小系统中服务进程可访问的资源(最小权限原则):若以root身份的网络服务存在漏洞,黑客就可利用该漏洞,以root身份为所欲为了,而SELinux就是为了解决这个问题而诞生。

  • 自主访问控制DAC:在禁用SELinux的操作系统中,决定一个资源是否被访问的因素是:某个资源是否拥有用户的权限(读、写、执行),只要访问这个资源的进程符合上述条件就可被访问,而root用户不受限制,系统上任意资源都可无限制访问,这种权限管理的主体是用户,称为自主访问控制(DAC)。

  • 强制访问控制MAC:在启用SELinux的操作系统中,决定一个资源是否被访问的因素除上述之外,还需要判断每一类进程是否拥有对某一类资源的访问权限。如此,即使是root身份运行,也需要判断这个进程的类型以及允许访问的资源类型才能决定是否允许访问某个资源,进程的活动空间被无限压缩。这种权限管理机制的主体是进程,也称为强制访问控制(MAC),它分两类:

    • 类别安全(MCS)模式

    • 多级安全(MLS)模式

    SELinux结构如下:

    SELinux基本概念如:

  1. 主体(Subject):可和进程等价。

  2. 对象(Object):被主体访问的资源,可以是文件、目录、端口、设备等。

  3. 策略规则(Policy & Rule):对哪些进程执行管制、怎么管制由策略决定,一套策略中包含多个规则,系统根据规则设定启用/禁用某些功能,且规则的设置是模块化、可扩展的。CentOS中主要有三种:

    • targeted:对大部分网络服务执行管制(默认策略)。

    • minimum:以targeted为基础,仅对选定网络服务进行管制。

    • mls:多级安全保护,对所有进程进行管制,这是最严格的策略,配置难度非常大,除非对安全有极高要求,否则一般不用。

  4. 安全上下文(Security Context):SELinux的核心。

  5. 工作模式:主要有三种工作模式:

    • enforcing:强制模式,违反SELinux规则的行为将被阻止并记录到日志中。

    • permissive:宽容模式,违反SELinux规则的行为只会记录到日志中,一般调试用。

    • disabled:关闭SELinux

  6. SELinux工作流程

    以上设置都可以在/etc/selinux/config中设置,对工作模式而言:从disabled切换到启用(enforcingpermissive),需重启系统,反之亦然;enforcingpermissive相互切换可直接运行命令setenforce 1|0执行快速切换。若系统在关闭状态运行了一段时间,重新打开SELinux有可能重启会非常慢,系统会为磁盘中的文件重建安全上下文,而且SELinux日志记录需借助auditd.service服务,该服务不要禁用。

Action:禁用SELinux

  1. 先执行命令查看当前SELinux工作模式

    getenforce
    # Enforcing / Permissive 表示已开启
    /usr/sbin/sestatus -v
    # SELinux status:		  enabled
  2. 编辑系统配置文件,原始文件位于/etc/selinux/config,而/etc/sysconfig/selinux实际是一个指向/etc/selinux/config的软链接文件,修改/etc/sysconfig/selinux会破坏链接关系使其变成普通文件而不是SELinux的配置文件,所以建议修改/etc/selinux/config

    # /etc/selinux/config
    # /etc/sysconfig/selinux
    vim /etc/selinux/config
    # 修改成:SELINUX=disabled
  3. 重启系统再次调用getenforce确认SELinux已关闭。

3.1.6. 修改Host文件

    一般情况下我们为了保证主机和主机之间的连通性,会使用这种非标准的方式直接将hosts文件修改,但真实场景下,这个步骤即使不做,域名管理服务也会导致这些主机之间可直接找到,修改hosts的弊端在于网络环境改变,若手工修改了hosts文件,网络环境改变之后有可能导致节点和节点之间的连通性直接被破坏。

Action:修改Hosts

vim /etc/hosts
# 追加记录
# 192.168.0.154   k8s-master
# 192.168.0.123   k8s-node-01
# 192.168.0.208   k8s-node-02

3.1.7. 网桥

    K8S环境中,为什么一定要开启bridge-nf-call-iptables?网桥参数是Linux网络部分的内核参数,如果不开启或中途因某些操作导致参数被关闭,有可能会造成一些奇怪的网络问题,排查起来十分麻烦。K8S网桥的容器网络有很多种实现,很大一部分实现使用了Linux网桥(作者:roc)。


基于网桥的容器网络

  • K8S中每个Pod的网卡都是veth设备,veth pair的另一端连接了宿主机上的网桥。

  • 由于网桥是虚拟的二层设备,同节点的Pod之间通讯直接走二层转发,跨节点通信才会经过宿主机eth0。

Service同节点通信问题

    不管是 iptables 还是 ipvs 转发模式,Kubernetes 中访问 Service 都会进行 DNAT,将原本访问 ClusterIP:Port 的数据包 DNAT 成 Service 的某个 Endpoint (PodIP:Port),然后内核将连接信息插入 conntrack 表以记录连接,目的端回包的时候内核从 conntrack 表匹配连接并反向 NAT,这样原路返回形成一个完整的连接链路:

    但是 Linux 网桥是一个虚拟的二层转发设备,而 iptables conntrack 是在三层上,所以如果直接访问同一网桥内的地址,就会直接走二层转发,不经过 conntrack:

  1. Pod 访问 Service,目的 IP 是 Cluster IP,不是网桥内的地址,走三层转发,会被 DNAT 成 PodIP:Port。

  2. 如果 DNAT 后是转发到了同节点上的 Pod,目的 Pod 回包时发现目的 IP 在同一网桥上,就直接走二层转发了,没有调用 conntrack,导致回包时没有原路返回。

    由于没有原路返回,客户端与服务端的通信就不在一个 “频道” 上,不认为处在同一个连接,也就无法正常通信,常见问题现象就是DNS解析失败——当 coredns 所在节点上的 pod 解析 dns 时,dns 请求落到当前节点的 coredns pod 上时,就可能发生这个问题


    上图显示,netfilter实际上既可以在L2层过滤,也可以在L3层过滤,所以网桥中一般会有下边参数:要求iptables不对bridge的数据执行处理

net.bridge.bridge-nf-call-ip6tables = 0
net.bridge.bridge-nf-call-iptables = 0
net.bridge.bridge-nf-call-arptables = 0

    或者执行命令iptables -t raw -I PREROUTING -i BRIDGE -s x.x.x.x -j NOTRACK来处理,若net.bridge.bridge-nf-call-iptables=1,也就意味着二层的网桥在转发包时也会被iptables的FORWARD规则过滤,这样就会出现L3层的iptables规则去过滤L2的帧数据问题。

Action:开启网桥配置

  1. 配置overlay br_netfilter

    cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
    overlay
    br_netfilter
    EOF
  2. 加载配置

    modprobe overlay
    modprobe br_netfilter
  3. 修改配置

    cat > /etc/sysctl.d/k8s.conf << EOF
    net.bridge.bridge-nf-call-ip6tables = 1
    net.bridge.bridge-nf-call-iptables = 1
    net.ipv4.ip_forward = 1
    EOF
  4. 执行命令加载配置

    sysctl --system

    :如果配置过程中不做第1,2步,那么在最后执行sysctl --system时参数只会有net.ipv4.ip_forward = 1,不会出现图中的另外两个参数信息,证明这两个参数未生效,所以需执行完整四步才可得到截图中的效果。

3.1.8. CNI网络插件

    K8S 1.25的版本支持用于集群联网的容器网络插件接口(CNI)插件,您必须使用您的集群相兼容的并且满足您需求的CNI插件,若要实现K8S网络模型,您需要一个CNI插件。CNI,全称是Container Network Interface——容器网络的API接口,它是K8S中一个标准的调用网络实现的接口,kubelet通过这个标准的API来调用不同的网络插件以实现不同的网络配置方式,实现了这个接口就是CNI插件,常见的如:

  • Calico

  • Flannel

  • Terway

  • Weave Net

  • Contiv


封装网络

    封装网络模型提供了一个在K8S节点形成的三层网络拓扑上的逻辑二层L2网络,通过这个网络不需要路由分发就可以获得一个隔离的二层容器网络,封装网络带来了少量的数据表处理负荷以及因为 Overlay 封装的 IP 报头而增加的 IP 数据包大小。封装的信息通过 UDP 在 Kubernetes 节点中分发,并在网络控制节点间交换关于 MAC 地址之间如何相互访问的信息。封装通常采用的是 VXLAN、IPsec 和 IP-in-IP 技术。

    这种网络模型在K8S节点之间创建了一种特别的网桥以连接节点中的容器。当您需要一个扩展的二层网桥时更加倾向于采用这种网络模型,这种网络模型对K8S节点间的三层网络延迟非常敏感,如果数据中心分布在不同的地理位置,请确保不同位置间的网络足够的低延迟避免网络中断。

    Flannel / Canel / Weave 采用了这种网络模型

非封装网络

    非封装网络模型提供了一种在容器之间进行三层路由的网络。该模型不创建一个隔离的二层网络或者封装负荷。没有封装负荷,但该网络模型需要 Kubernetes 节点进行路由分发的管理。相对于采用 IP 报头来封装,这种网络模型采用一类网络协议在 Kubernetes 节点间分发路由信息来实现 Pod 连接,诸如BGP 协议。

    这种网络模型在 Kubernetes 节点之间创建了一种网络路由器,用以提供容器之间如何路由连接的信息。当您需要一个三层路由网络时将更加倾向于采用这种网络模型。这种网络模型动态地更新 Kubernetes 节点操作系统层面的路由,它对网络延迟较小敏感。

    Calico / Romana 采用了这种网络模型


    K8S 1.25对CNI的最低要求:您必须使用v0.4.0或更高版本的CNI规范相符合的CNI插件,推荐使用一个兼容v1.0.0CNI规范的插件(插件本身可兼容多个规范版本)。网络语境中,容器运行时(Container Runtime)是在节点上的守护进程,被配置用来为kubelet提供CRI服务,具体而言,容器运行时必须配置为加载所需的CNI插件,从而实现K8S网络模型。

在1.24之前,CNI插件也可以由kubelet使用命令参数cni-bin-drinetwork-plugin管理,K8S 1.24移除了这些命令参数,CNI插件的管理工作本来就不由kubelet负责。

    您需要参考如下教程编译CNI Plugins的最新版:

Action:下载编译最新Plugins

  1. 下载最新版的源代码:

    git clone https://github.com/containernetworking/plugins
    cd plugins
    git checkout release-1.1
  2. 编译CNI插件:

    ./build_linux.sh 		# Linux
    # ./build_windows.sh		# Windows

    您可以看到编译过程中的输出:

    Building plugins 
      bandwidth
      firewall
      portmap
      sbr
      tuning
      vrf
      bridge
      host-device
      ipvlan
      loopback
      macvlan
      ptp
      vlan
      dhcp
      host-local
      static
  3. 将编译结果拷贝到目录/opt/cni/bin

    mkdir -p /opt/cni/bin
    cp bin/* /opt/cni/bin/

    集群模式下需要针对网络进行相关设置,这里我选择使用calico配置,您可以按照如下步骤先安装calico插件:

Action:安装calico网络插件

  1. 下载CNI插件

    curl -L -o /opt/cni/bin/calico https://github.com/projectcalico/cni-plugin/releases/download/v3.20.6/calico-amd64
    chmod 755 /opt/cni/bin/calico
    curl -L -o /opt/cni/bin/calico-ipam https://github.com/projectcalico/cni-plugin/releases/download/v3.20.6/calico-ipam-amd64
    chmod 755 /opt/cni/bin/calico-ipam
  2. 查看配置文件并拷贝子网信息

    # cat /etc/cni/net.d/87-crio-bridge.conf

    内容如下:

    {
    	"cniVersion": "0.3.1",
    	  "name": "crio",
    	  "type": "bridge",
    	  "bridge": "cni0",
    	  "isGateway": true,
    	  "ipMasq": true,
    	  "hairpinMode": true,
    	  "ipam": {
    		"type": "host-local",
    		"routes": [
    			{ "dst": "0.0.0.0/0" }
    		],
    		"ranges": [
    			[{ "subnet": "10.85.0.0/16" }]
    		]
    	}
    }

    本章节未完结,剩余部分参考Install CNI Plugin,最开始是因为有个问题牵涉到CNI所以以为Root Cause在这边,最终定位到问题缘由后发现貌似已经不需要处理CNI部分了,所以此处仅保留最新版(v3.20.6)的安装步骤。在K8S集群搭建完成之后(节点加入之后),执行下边命令直接在集群上安装网络插件:

kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml

    最后执行如下命令查看结果:

kubectl get pods --all-namespaces

    您可能需要等待一段时间等整个容器从初始化完成到运行,下边两个图是开始到结束的对比:

运行命令后

启动完成后


3.2. 安装:K8S环境

3.2.1. 升级CentOS 8 Stream

    最早在华为云购买的CentOS版本是CentOS 8.52021年底结束支持),查看CRI-O的官方文档时才知道有CentOS 8 Stream二者有什么关系呢?追本溯源,我们来拔一拔CentOS操作系统的历史,看看CentOS Stream是个什么角色,而红帽公司又为什么关闭CentOS。

  • 2003年:Fedora项目成立,该项目统筹Fedora Linux操作系统开发社区,完全开源;当时社区由Red Hat Linux和Fedora Linux项目合并成立,所以成立之初就得到了红帽公司的赞助。

  • 2004年:红帽公司发布了自己的商业Linux发行版本RHEL,RHEL使用Fedora作为上游,但发布周期更长;虽然RHEL是商业版本,但它的源代码是开源的,任何人都可以使用这份代码,只是需要商业支持时付费。同年,CentOS项目成立,它是一个社区支持的发行版本,使用了RHEL作为上游开发的项目,和RHEL不一样的是它完全开源、更新比RHEL稍慢,但更稳定一些,所以CentOS和RHEL分别由不同开发团队研发。

  • 2006年:(CentOS 4.4)一般情况下RHEL新发布一个版本,CentOS会在2~3个月内发布相关编译版本,2006年Red Hat采用了和CentOS相同的版本约定,从此二者相安无事最少十年。

  • 2014年:2014年红帽公司收购了CentOS,包括CentOS项目商标所有权以及大量核心开发人员,收购之后红帽旗下就产生了三个主要的Linux发行版:

    • Fedora:新的Linux功能和特性,作为实验版快速迭代各种新功能,评估稳定的功能都会逐渐加入到RHEL中。

    • RHEL:面相企业收费的稳定版本。

    • CentOS:去除商标等信息构建的免费社区版(等价于RHEL的免费版本)。

  • 2019年:CentOS团队宣布和红帽合作推出一个新的滚动版Linux:CentOS Stream,它介于Fedora和RHEL之间,通过发布很多小版本,以社区的力量来帮助RHEL发布更快更稳定的版本,其实从此时开始似乎就和CentOS分道扬镳了。

  • 2020年:CentOS官方发表博客,决定将CentOS项目迁移到CentOS Stream,并且官宣未来不再维护CentOS了。

  • 2021年:CentOS不再提供技术支持,而且CentOS 7的生命也将在2024年结束,之后不会再有CentOS 9了。

    CentOS Stream究竟能否用于生产?CentOS Stream出现后,首先发布顺序有了变化:

上游版本中游版本下游版本

之前

Fedora

RHEL

CentOS

之后

Fedora

CentOS Stream

RHEL

    红帽做这个事的原因真是割韭菜?其实不是,它的主要目的是加强社区影响和贡献,从以前的流程看起来,CentOS作为最后一个阶段的复刻版本,其社区力量对RHEL本身没有影响,而Fedora和RHEL又完全属于RHEL公司行为,没有开源社区参与,而CentOS Stream作为中游版本,社区贡献就可以发挥出最大价值,也减少了RHEL的竞争。另外一个不可否认的事实就是:CentOS一直作为RHEL的可替代版本角色没有了,对于习惯RHEL/CentOS系列使用者而言,未来没有免费的CentOS可以选择了,只能选择RHEL

    红帽官方声明中,希望用户信任CentOS Stream版本,可关键问题在于:CentOS Stream并不是一个稳定的生产版本,它是一个RHEL的上游或中游开发版本。而且做出此改动时,RHEL改变了CentOS 8的支持时间,原本预定的2029年结束也直接从2021年底腰斩。CentOS创始人对此特别不满,所以决定重新开发一个新的基于RHEL的发行版,这就是Rocky Linux

    吃螃蟹?对的,我们打算试试——首先,这个系统的稳定性是可以的,完全可以用在我们正式的生产环境中,CentOS从最初的版本走到现在,相信其底子并不是单方面一句话可以否定的。而且对个人用户、中小型企业用户而言,基本上都是用来开发、测试、做网站应用搭建,从这点来讲也不会有太大的问题,除非您对操作系统风险要求极高(有强迫症就不聊了),那么这种场景需求是完全可接受CentOS Stream的。

Action:CentOS 8.5 升级到 CentOS 8 Stream (针对CentOS 8.3一级以上的版本执行升级,非8.3版本可先更新到8.3以上再执行该步骤。)

  1. 先查看当前版本:

    cat /etc/redhat-release
    # 输出:			CentOS Linux release 8.5.2111
    # 后续教程升级后:		CentOS Stream release 8
  2. 执行命令

    dnf --disablerepo '*' --enablerepo extras swap centos-linux-repos centos-stream-repos
  3. 执行命令

    dnf distro-sync
  4. 完成后重启系统,再执行

    hostnamectl

3.2.2. CRI-O安装

    由于我们搭建K8S的版本是1.25,优先不考虑Docker作为容器运行时,此处只对比ContainerdCRI-O两种。Docker Engine没有实现CRI,但这是容器运行时在K8S工作中所需的,为此,必须安装一个额外的cri-dockerd服务才可以,它是一个基于传统的内置Docker引擎项目。

    下边表格中包括支持的操作系统已知端点:

操作系统运行时Unix域套接字/Windows命名管道路径

Linux

containerd

unix:///var/run/containerd/containerd.sock

Linux

CRI-O

unix:///var/run/crio/crio.sock

Linux

Docker Engine(cri-dockerd)

unix:///var/run/cri-dockerd.sock

Windows

containerd

npipe:////./pipe/containerd-containerd

Windows

Docker Engine(cri-dockerd)

npipe:////./pipe/cri-dockerd


Containerd

    Containerd是一个来自Docker的高级容器运行时,并且实现了CRI规范,它是从Docker项目中分离出来,之后Containerd被捐赠给云原生计算基金会(CNCF)为容器社区提供创建新容器解决方案的基础。所以Docker内部使用了Containerd,当您安装Docker时也会安装Containerd。Containerd通过CRI插件实现了K8S容器运行时接口(CRI),您可以管理容器的整个生命周期,包括镜像传输、存储到容器执行、监控再到网络。

CRI-O

    CRI-O是另一个实现了容器运行时接口(CRI)的高级容器运行时,可以使用OCI(开放容器协议)兼容的运行时,它是Containerd的一个替代品。CRI-O诞生于RedHat、IBM、英特尔、SUSE、Hyper等公司,是专门从头开始创建的,作为K8S的一个容器运行时,它提供了启动、停止和重启容器的能力,就像Containerd一样。

容器运行时必须至少支持v1alpha2版本的容器运行时接口,K8S 1.25默认使用v1 版本的CRI API,如果容器运行时不支持v1版本的API,则kubelet会回头到使用(已弃用的)v1alpha2版本的API。


    CRI-O的官方安装文档您可以参考CRI-O Installation Instructions,此处记录下来我的安装过程,选择使用CRI-O初衷有二:

  1. 该容器运行时默认使用了systemd cgroup驱动,可直接对接前文步骤处理好的cgroup v2。

  2. 该容器运行时是直接从CRI规范出发重新独立开发的容器运行时,属于比Containerd的新生代。

    安装CRI-O时先阅读K8S版本偏差,CRI-O对操作系统的支持如(分类纯粹为了排版,无先后):

Cent OS系Debian系Ubuntu系其他

CentOS 8

Debian Unstable

xUbuntu 22.04

Fedora 31+

CentOS 8 Stream

Debian Testing

xUbuntu 21.10

openSUSE

CentOS 7

Debian 11

xUbuntu 20.04

Debian 10

xUbuntu 18.04

Rasbian 10

    根据操作系统不同支持程度,您可以先选择K8S的版本,K8S的版本格式一般是x.y.z表示,其中x是主要版本,y时次要版本,z时补丁版本,目前可选择的版本如(补丁详情):

  • 1.23, 1.24, 1.25:最新的三个次要版本

  • 1.19:相关更新的版本获得大概1年的补丁支持

  • 1.18:以及更早的版本获得大概9个月的补丁支持

    选择版本并设置环境变量:export VERSION=xxx,我在安装时选择1.25(最新版)。

Action:安装CRI-O

  1. 选择对应版本并设置环境变量VERSION

    # export VERSION=1.25:1.25.1	# 这种格式可直接处理补丁版本,最新补丁1.25.1
    export VERSION=1.25		# 最常用格式,一般只写主版本和次要版本
  2. 由于是CentOS系统,还需设置额外的环境变量OS

    操作系统变量OS

    CentOS 8

    CentOS_8

    CentOS 8 Stream

    CentOS_8_Stream

    CentOS 7

    CentOS_7

    ```shell

    export OS=CentOS_8_Stream

    # 虽然前边已经升级过操作系统,但由于

    ```

  3. 若您安装的版本在1.24.0之前,那么您需要额外做这一步,K8S从1.24.0开始已经不再依赖containernetworking-plugins包,而是使用自己的CNI插件。

    yum install containernetworking-plugins
  4. 执行下边命令安装库相关信息:

    # 官方步骤(有问题)
    # curl -L -o /etc/yum.repos.d/devel:kubic:libcontainers:stable.repo https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/devel:kubic:libcontainers:stable.repo
    # curl -L -o /etc/yum.repos.d/devel:kubic:libcontainers:stable:cri-o:$VERSION.repo https://download.opensuse.org/repositories/devel:kubic:libcontainers:stable:cri-o:$VERSION/$OS/devel:kubic:libcontainers:stable:cri-o:$VERSION.repo
    # 替换方案(重新设置库文件)
    cd /etc/yum.repos.d/
    wget https://download.opensuse.org/repositories/home:alvistack/CentOS_8_Stream/home:alvistack.repo
  5. 然后执行命令安装

    yum install cri-o
  6. 安装完成后您可以使用systemctl启动或查看相关状态

    systemctl start crio
    systemctl status crio
    systemctl enable crio

    上述步骤中的1,2主要是为官方命令量身打造的步骤,最终目的就是设置库信息,而官方文档中的如下命令设置库会有问题,遇到Error: Unable to find a match: cri-o,而步骤4则是解决办法(参考:Issue 6209中的回复,里面有个链接点进去查看不同版本下的解决办法)。

    安装完成后,为了防止在kubeadm init初始化集群时失败(国外不会碰到这个问题),您还需要额外的步骤。

Action:重载沙箱(Pause)镜像。

  1. 由于CRI-O默认使用了systemd cgroup驱动,所以驱动不用管,但是您需要修改一个东西:

    # 默认配置文件地址
    vim /etc/crio/crio.conf
  2. 在打开的配置文件中找到[crio.image]节点,并且编辑:

    [crio.image]
    # 追加下边这一行
    pause_image="registry.aliyuncs.com/google_containers/pause:3.8"
  3. 保存文件并退出,然后重新加载配置或重启服务

    systemctl reload crio     # 重新加载变更配置
    systemctl restart crio    # 重启crio服务

    问题详情参考《4.1.4. The kubelet is not running》。

3.2.3. Docker/Containerd安装

    由于使用CRI-O在容器运行时初始化过程一直出错,加上网上使用CRI-O的文档太少,所以曲线救国,追加一个章节安装Containerd尝试看是否可把容器运行时切换到Containerd重新来过,反正criocontainerd不冲突,所以两个服务可同时运行,由于Docker中自带该组件,所以安装个Docker有备无患。

Action:安装Docker

  1. 安装yum工具集

    yum install -y yum-utils
  2. 添加docker库

    yum-config-manager \
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo
  3. 直接安装Docker Engine

    yum install docker-ce docker-ce-cli containerd.io docker-compose-plugin

    确认匹配码060A 61C5 1B55 8A7F 742B 77AA C52F EB6B 621E 9F35(必须符合才接受)

  4. 编辑配置文件

    vim /etc/containerd/config.toml

    编辑内容如:

    # disabled_plugins = ["cri"]			# 这一行注释掉,启用CRI
    [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
    	[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
    		SystemdCgroup = true

    注意:kubeadm工具对sock的检查很严格,最好在系统重只运行一个:Podman / Docker / Crio三个工具,否则可能遇到如下警告,虽然该警告不会对执行过程产生影响,但最好是保留一个,反正在管理和运维过程中您可以直接在三者之间切换:

Found multiple CRI endpoints on the host. Please define which one do you wish to use by setting \
the 'criSocket' field in the kubeadm configuration file: unix:///var/run/containerd/containerd.sock, \
unix:///var/run/crio/crio.sock To see the stack trace of this error execute with --v=5 or higher

    还需要说明的是,《3.3.1.镜像下载》章节中提到的下载镜像的命令针对不同的容器运行时其内容会不一样,所以新安装了Docker或Containerd之后需要重新拉一次保证本地仓库中已经拥有了相关镜像信息。

3.2.4. 三剑客:kubeadm/kubelet/kubectl

    接下来您需要在每台机器上安装三个核心软件包:

  • kubeadm:用来初始化集群的指令。

  • kubelet:在集群中的每个节点上来启动Pod和容器等。

  • kubectl:用来与集群通信的命令工具。

    后续创建集群需要使用kubeadm工具,而kubeadm不能帮着你安装或管理kubeletkubectl,所以您需要确保它们三者的版本相匹配,若不这样做,则可能存在发生版本偏差的风险,可能导致一些意料之外的错误和问题。

Action:安装kubeadm/kubelet/kubectl

  1. 执行如下命令设置库信息

    cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
    [kubernetes]
    name=Kubernetes
    baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-\$basearch
    enabled=1
    gpgcheck=1
    gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
    exclude=kubelet kubeadm kubectl
    EOF

    国内只能使用阿里云镜像库:https://mirrors.aliyun.com/kubernetes/

    cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
    [kubernetes]
    name=Kubernetes
    baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-\$basearch
    enabled=1
    gpgcheck=1
    gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
    exclude=kubelet kubeadm kubectl
    EOF
  2. 由于之前已禁用过SELinux了,官方步骤第二步不用做。

  3. 执行对应安装命令

    yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
  4. 最后将kubelet服务设置成自启动:

    systemctl enable kubelet
    systemctl start kubelet

3.3. 高光时刻:kubeadm

    接下来的确是kubeadm的高光时刻,几乎全程使用它来配置K8S集群,由于步骤复杂有可能遇到很多坑,所以此处单开章节来迎接这位环境配置过程中的新主角。

3.3.1. 镜像下载

    国内由于限制了Google的访问,你可以感觉到深深的恶意,所以为了解决这种失落感,只能考虑另外的镜像地址,而kubeadm命令执行过程中也会拥有更多的参数,可用地址列表:

  • registry.cn-hangzhou.aliyuncs.com/google_containers

  • registry.aliyuncs.com/google_containers

Action:镜像下载 (选择Master机器)直接执行如下命令将所需的镜像全部下载下来(v1.25.0

kubeadm config images pull --image-repository registry.aliyuncs.com/google_containers

国内后续所有kubeadm参数都有可能追加--image-repository registry.aliyuncs.com/google_containers参数以保证镜像可正常下载。

3.3.2. 控制平面:Control Panel

    控制平面节点是运行控制平面组件的机器,它包括

  • Etcd——集群数据库。

  • API Server——命令行工具kubectl与之通信。

    初始化控制平面需注意

  1. 推荐)若计划将单个控制平面kubeadm集群升级成高可用,您应该指定--control-plane-endpoint为所有控制平面节点设置共享端点,端点可以是负载均衡器的DNS名称或IP地址。

  2. 选择一个Pod网络插件,并验证是否需要为kubeadm init传递参数,根据您选择的第三方网络插件,可能需要设置--pod-network-cidr的值。

  3. 可选kubeadm视图通过使用已知的端点列表来检测容器运行时,使用不同的容器运行时或在预配置的节点上安装多个容器运行时,请为kubeadm init指定--cri-socket参数。

  4. 可选)除非另有说明,否则kubeadm使用默认网关关联的网络端口来设置此控制平面节点API Server广播地址,若要使用其他网络端口,请为kubeadm init设置--apiserver-advertise-address=<ip-address>参数,若要部署IPv6集群则必须指定一个IPv6地址。

    :运行过kubeadm init之后若初始化失败,单独启动kubelet有可能会遇到failed to load Kubelet config file /var/lib/kubelet/config.yaml, error failed to read kub的问题,遇到该问题不要着急,这是因为这个目录下的配置是kubeadm init创建的,如果初始化成功这个问题就会得到解决。

    生产环境不同于开发环境,任意一个警告最好都不要放过,我反复使用kubeadm init遇到了各种各样的环境问题,所以最终选择使用配置文件的方式来执行kubeadm init,官方也推荐初始化时使用--config指定配置文件,并在配置文件中设置相对应的配置。最初K8S使用命令行参数的目的是为了支持动态配置kubelet(Dynamic Kubelet Configuration),但这个特性从1.22版本开始就已经被弃用,而且在1.24中被直接移除,所以正式环境中还是使用类似ansible的DevOps工具将一份自定义的配置分发到各个节点更好。

  1. 先执行下边命令打印一份默认配置:

    kubeadm config print init-defaults --component-configs KubeletConfiguration
  2. 然后将该内容保存出来放在k8s-configuration.yaml文件中,并重新编辑(参考注释):

    apiVersion: kubeadm.k8s.io/v1beta3
    bootstrapTokens:
    - groups:
      - system:bootstrappers:kubeadm:default-node-token
      token: abcdef.0123456789abcdef
      ttl: 24h0m0s
      usages:
      - signing
      - authentication
    kind: InitConfiguration
    localAPIEndpoint:
      # 命令行: apiserver-advertise-address=192.168.0.154
      # 旧:	1.2.3.4
      # 新:	192.168.0.154
      advertiseAddress: 192.168.0.154
      bindPort: 6443
    nodeRegistration:
      # 默认使用了Containerd,如果使用CRI-O就更改成新值
      # 命令行: --cri-socket=/run/crio/crio.sock
      # 旧:	unix:///var/run/containerd/containerd.sock
      # 新:	unix:///run/crio/crio.sock
      # 新:	unix:///run/containerd/containerd.sock
      criSocket: unix:///run/crio/crio.sock
      imagePullPolicy: IfNotPresent
      # 主机名更改
      # 旧:	node
      # 新:	k8s-master	
      name: k8s-master
      # 更新过后的配置
      taints: 
      
    ---
    apiServer:
      timeoutForControlPlane: 4m0s
    apiVersion: kubeadm.k8s.io/v1beta3
    certificatesDir: /etc/kubernetes/pki
    clusterName: kubernetes
    controllerManager: {}
    dns: {}
    etcd:
      local:
        dataDir: /var/lib/etcd
    # 由于是国内,将镜像地址修改
    # 命令行:--image-repository registry.aliyuncs.com/google_containers
    # 旧:	registry.k8s.io
    # 新:	registry.aliyuncs.com/google_containers
    imageRepository: registry.aliyuncs.com/google_containers
    kind: ClusterConfiguration
    kubernetesVersion: 1.25.0
    controlPlaneEndpoint: "192.168.0.154:6443"
    networking:
      dnsDomain: cluster.local
      # 追加子网IP,暂时和CNI中的值对应
      podSubnet: 10.85.0.0/16
      serviceSubnet: 10.96.0.0/12
    scheduler: {}
    ---
    apiVersion: kubelet.config.k8s.io/v1beta1
    authentication:
      anonymous:
        enabled: false
      webhook:
        cacheTTL: 0s
        enabled: true
      x509:
        clientCAFile: /etc/kubernetes/pki/ca.crt
    authorization:
      mode: Webhook
      webhook:
        cacheAuthorizedTTL: 0s
        cacheUnauthorizedTTL: 0s
    # 此处已经是 systemd,所以不用再更改
    cgroupDriver: systemd
    clusterDNS:
    - 10.96.0.10
    clusterDomain: cluster.local
    cpuManagerReconcilePeriod: 0s
    evictionPressureTransitionPeriod: 0s
    fileCheckFrequency: 0s
    healthzBindAddress: 127.0.0.1
    healthzPort: 10248
    httpCheckFrequency: 0s
    imageMinimumGCAge: 0s
    kind: KubeletConfiguration
    logging:
      flushFrequency: 0
      options:
        json:
          infoBufferSize: "0"
      verbosity: 0
    memorySwap: {}
    nodeStatusReportFrequency: 0s
    nodeStatusUpdateFrequency: 0s
    rotateCertificates: true
    runtimeRequestTimeout: 0s
    shutdownGracePeriod: 0s
    shutdownGracePeriodCriticalPods: 0s
    staticPodPath: /etc/kubernetes/manifests
    streamingConnectionIdleTimeout: 0s
    syncFrequency: 0s
    volumeStatsAggPeriod: 0s
  3. 准备好配置文件后,您就可以直接使用如下命令初始化:

    kubeadm init --config k8s-configuration.yaml

    您可以在控制台看到如下输出:

    root@k8s-master: ~/cloud # kubeadm init --config k8s-configuration.yaml                                                                                                                [23:05:35]
    [init] Using Kubernetes version: v1.25.0
    [preflight] Running pre-flight checks
            [WARNING SystemVerification]: missing optional cgroups: blkio
    [preflight] Pulling images required for setting up a Kubernetes cluster
    [preflight] This might take a minute or two, depending on the speed of your internet connection
    [preflight] You can also perform this action in beforehand using 'kubeadm config images pull'
    [certs] Using certificateDir folder "/etc/kubernetes/pki"
    [certs] Generating "ca" certificate and key
    [certs] Generating "apiserver" certificate and key
    [certs] apiserver serving cert is signed for DNS names [k8s-master kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local] and IPs [10.96.0.1 192.168.0.154]
    [certs] Generating "apiserver-kubelet-client" certificate and key
    [certs] Generating "front-proxy-ca" certificate and key
    [certs] Generating "front-proxy-client" certificate and key
    [certs] Generating "etcd/ca" certificate and key
    [certs] Generating "etcd/server" certificate and key
    [certs] etcd/server serving cert is signed for DNS names [k8s-master localhost] and IPs [192.168.0.154 127.0.0.1 ::1]
    [certs] Generating "etcd/peer" certificate and key
    [certs] etcd/peer serving cert is signed for DNS names [k8s-master localhost] and IPs [192.168.0.154 127.0.0.1 ::1]
    [certs] Generating "etcd/healthcheck-client" certificate and key
    [certs] Generating "apiserver-etcd-client" certificate and key
    [certs] Generating "sa" key and public key
    [kubeconfig] Using kubeconfig folder "/etc/kubernetes"
    [kubeconfig] Writing "admin.conf" kubeconfig file
    [kubeconfig] Writing "kubelet.conf" kubeconfig file
    [kubeconfig] Writing "controller-manager.conf" kubeconfig file
    [kubeconfig] Writing "scheduler.conf" kubeconfig file
    [kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
    [kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
    [kubelet-start] Starting the kubelet
    [control-plane] Using manifest folder "/etc/kubernetes/manifests"
    [control-plane] Creating static Pod manifest for "kube-apiserver"
    [control-plane] Creating static Pod manifest for "kube-controller-manager"
    [control-plane] Creating static Pod manifest for "kube-scheduler"
    [etcd] Creating static Pod manifest for local etcd in "/etc/kubernetes/manifests"
    [wait-control-plane] Waiting for the kubelet to boot up the control plane as static Pods from directory "/etc/kubernetes/manifests". This can take up to 4m0s
    [apiclient] All control plane components are healthy after 5.005058 seconds
    [upload-config] Storing the configuration used in ConfigMap "kubeadm-config" in the "kube-system" Namespace
    [kubelet] Creating a ConfigMap "kubelet-config" in namespace kube-system with the configuration for the kubelets in the cluster
    [upload-certs] Skipping phase. Please see --upload-certs
    [mark-control-plane] Marking the node k8s-master as control-plane by adding the labels: [node-role.kubernetes.io/control-plane node.kubernetes.io/exclude-from-external-load-balancers]
    [mark-control-plane] Marking the node k8s-master as control-plane by adding the taints [node-role.kubernetes.io/control-plane:NoSchedule]
    [bootstrap-token] Using token: abcdef.0123456789abcdef
    [bootstrap-token] Configuring bootstrap tokens, cluster-info ConfigMap, RBAC Roles
    [bootstrap-token] Configured RBAC rules to allow Node Bootstrap tokens to get nodes
    [bootstrap-token] Configured RBAC rules to allow Node Bootstrap tokens to post CSRs in order for nodes to get long term certificate credentials
    [bootstrap-token] Configured RBAC rules to allow the csrapprover controller automatically approve CSRs from a Node Bootstrap Token
    [bootstrap-token] Configured RBAC rules to allow certificate rotation for all node client certificates in the cluster
    [bootstrap-token] Creating the "cluster-info" ConfigMap in the "kube-public" namespace
    [kubelet-finalize] Updating "/etc/kubernetes/kubelet.conf" to point to a rotatable kubelet client certificate and key
    [addons] Applied essential addon: CoreDNS
    [addons] Applied essential addon: kube-proxy
    
    Your Kubernetes control-plane has initialized successfully!
    
    To start using your cluster, you need to run the following as a regular user:
    
      mkdir -p $HOME/.kube
      sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
      sudo chown $(id -u):$(id -g) $HOME/.kube/config
    
    Alternatively, if you are the root user, you can run:
    
      export KUBECONFIG=/etc/kubernetes/admin.conf
    
    You should now deploy a pod network to the cluster.
    Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
      https://kubernetes.io/docs/concepts/cluster-administration/addons/
    
    You can now join any number of control-plane nodes by copying certificate authorities
    and service account keys on each node and then running the following as root:
    
      kubeadm join 192.168.0.154:6443 --token abcdef.0123456789abcdef \
            --discovery-token-ca-cert-hash sha256:b363066ec514bbc9efce5a11d39fc568cd4c86e067fdd5c3e32fbe57239269a9 \
            --control-plane 
    
    Then you can join any number of worker nodes by running the following on each as root:
    
    kubeadm join 192.168.0.154:6443 --token abcdef.0123456789abcdef \
            --discovery-token-ca-cert-hash sha256:b363066ec514bbc9efce5a11d39fc568cd4c86e067fdd5c3e32fbe57239269a9
  4. 执行完初始化后,您可以运行crictl ps -a查看容器状态:

  5. 然后执行下边命令:

      mkdir -p $HOME/.kube
      cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
      chown $(id -u):$(id -g) $HOME/.kube/config
  6. 最后运行命令查看集群状态:

    kubectl cluster-info      # 查看集群状态
    kubectl get node          # 查看节点状态

    看到上边截图,证明您的控制平面:Control Panel就已经成功安装完成,并且该节点启动成功。

3.3.3. 集群卸载

    先讲卸载流程,因为我来回执行了几次。

Action:卸载集群

  1. 使用适当的凭证与控制平面节点通信,运行:

    kubectl drain <node name> --delete-emptydir-data --force --ignore-daemonsets
  2. 删除节点之前,重置kubeadm的安装状态

    kubeadm reset
  3. 重置过程不会重置或清除iptables规则或IPVS表,若希望重置iptables,必须手动进行:

    iptables -F && iptables -t nat -F && iptables -t mangle -F && iptables -X
  4. 如果要充值IPVS表,则必须运行以下命令:

    ipvsadm -C
  5. 卸载完之后最好重启系统保证cgroup资源管理部分已经被彻底释放。

3.3.4. 节点安装:Node

    在每个节点上按照上述步骤设置好crio容器,然后执行下边命令:

kubeadm join 192.168.0.154:6443 --token abcdef.0123456789abcdef \
    --discovery-token-ca-cert-hash sha256:28797229293e78b531462b5dc12ee23038bd7c5a037a0c45e7c0206c3ccb7c2f
    # 旧值,反复初始化引起的变化
    # sha256:b363066ec514bbc9efce5a11d39fc568cd4c86e067fdd5c3e32fbe57239269a9

    在节点机器上您可以看到如下输出:

[preflight] Running pre-flight checks
        [WARNING SystemVerification]: missing optional cgroups: blkio
[preflight] Reading configuration from the cluster...
[preflight] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Starting the kubelet
[kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap...

This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.

Run 'kubectl get nodes' on the control-plane to see this node join the cluster.

    在控制平面上执行如下命令查看:

# 执行命令
kubectl get node
# 输出
NAME          STATUS   ROLES           AGE   VERSION
k8s-master    Ready    control-plane   8h    v1.25.0
k8s-node-01   Ready    <none>          68s   v1.25.0
k8s-node-02   Ready    <none>          63s   v1.25.0

    截图中您可以看到整个状态变化的流程,这样执行之后另外两个节点就完整添加到集群中了。

3.3.5. 界面化:Dashboard

    Dashboard是基于网页的官方K8S用户界面,您可以使用Dashboard将容器应用部署到K8S集群中,也可以对容器应用排错,还能管理集群资源。你可以使用Dashboard获取运行在集群中的应用概览信息,也可以创建或修改K8S资源(Deployment, Job, DaemonSet等)。由于官方文档中Dashboard是不带RBAC的访问模式,推荐真实环境中使用Bearer令牌登录,官方教程中创建的样本用户具有管理特权用于教育目的

    参考下载的最新版配置文件内容:

# Copyright 2017 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

apiVersion: v1
kind: Namespace
metadata:
  name: kubernetes-dashboard

---

apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard

---

kind: Service
apiVersion: v1
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard
spec:
  # 在此处修改Dashboard Service的服务为nodePort方式
  type: NodePort
  ports:
    - port: 443
      targetPort: 8443
      nodePort: 31000     # 对外暴露端口30001
  selector:
    k8s-app: kubernetes-dashboard

---

apiVersion: v1
kind: Secret
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard-certs
  namespace: kubernetes-dashboard
type: Opaque

---

apiVersion: v1
kind: Secret
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard-csrf
  namespace: kubernetes-dashboard
type: Opaque
data:
  csrf: ""

---

apiVersion: v1
kind: Secret
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard-key-holder
  namespace: kubernetes-dashboard
type: Opaque

---

kind: ConfigMap
apiVersion: v1
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard-settings
  namespace: kubernetes-dashboard

---

kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard
rules:
  # Allow Dashboard to get, update and delete Dashboard exclusive secrets.
  - apiGroups: [""]
    resources: ["secrets"]
    resourceNames: ["kubernetes-dashboard-key-holder", "kubernetes-dashboard-certs", "kubernetes-dashboard-csrf"]
    verbs: ["get", "update", "delete"]
    # Allow Dashboard to get and update 'kubernetes-dashboard-settings' config map.
  - apiGroups: [""]
    resources: ["configmaps"]
    resourceNames: ["kubernetes-dashboard-settings"]
    verbs: ["get", "update"]
    # Allow Dashboard to get metrics.
  - apiGroups: [""]
    resources: ["services"]
    resourceNames: ["heapster", "dashboard-metrics-scraper"]
    verbs: ["proxy"]
  - apiGroups: [""]
    resources: ["services/proxy"]
    resourceNames: ["heapster", "http:heapster:", "https:heapster:", "dashboard-metrics-scraper", "http:dashboard-metrics-scraper"]
    verbs: ["get"]

---

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
rules:
  # Allow Metrics Scraper to get metrics from the Metrics server
  - apiGroups: ["metrics.k8s.io"]
    resources: ["pods", "nodes"]
    verbs: ["get", "list", "watch"]

---

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: kubernetes-dashboard
subjects:
  - kind: ServiceAccount
    name: kubernetes-dashboard
    namespace: kubernetes-dashboard

---

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: kubernetes-dashboard
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: kubernetes-dashboard
subjects:
  - kind: ServiceAccount
    name: kubernetes-dashboard
    namespace: kubernetes-dashboard

---

kind: Deployment
apiVersion: apps/v1
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard
spec:
  replicas: 1
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      k8s-app: kubernetes-dashboard
  template:
    metadata:
      labels:
        k8s-app: kubernetes-dashboard
    spec:
      securityContext:
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: kubernetes-dashboard
          image: kubernetesui/dashboard:v2.6.1
          imagePullPolicy: Always
          ports:
            - containerPort: 8443
              protocol: TCP
          args:
            - --auto-generate-certificates
            - --namespace=kubernetes-dashboard
            # Uncomment the following line to manually specify Kubernetes API server Host
            # If not specified, Dashboard will attempt to auto discover the API server and connect
            # to it. Uncomment only if the default does not work.
            # - --apiserver-host=http://my-address:port
          volumeMounts:
            - name: kubernetes-dashboard-certs
              mountPath: /certs
              # Create on-disk volume to store exec logs
            - mountPath: /tmp
              name: tmp-volume
          livenessProbe:
            httpGet:
              scheme: HTTPS
              path: /
              port: 8443
            initialDelaySeconds: 30
            timeoutSeconds: 30
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            runAsUser: 1001
            runAsGroup: 2001
      volumes:
        - name: kubernetes-dashboard-certs
          secret:
            secretName: kubernetes-dashboard-certs
        - name: tmp-volume
          emptyDir: {}
      serviceAccountName: kubernetes-dashboard
      nodeSelector:
        "kubernetes.io/os": linux
      # Comment the following tolerations if Dashboard must not be deployed on master
      tolerations:
        - key: node-role.kubernetes.io/master
          effect: NoSchedule

---

kind: Service
apiVersion: v1
metadata:
  labels:
    k8s-app: dashboard-metrics-scraper
  name: dashboard-metrics-scraper
  namespace: kubernetes-dashboard
spec:
  ports:
    - port: 8000
      targetPort: 8000
  selector:
    k8s-app: dashboard-metrics-scraper

---

kind: Deployment
apiVersion: apps/v1
metadata:
  labels:
    k8s-app: dashboard-metrics-scraper
  name: dashboard-metrics-scraper
  namespace: kubernetes-dashboard
spec:
  replicas: 1
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      k8s-app: dashboard-metrics-scraper
  template:
    metadata:
      labels:
        k8s-app: dashboard-metrics-scraper
    spec:
      securityContext:
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: dashboard-metrics-scraper
          image: kubernetesui/metrics-scraper:v1.0.8
          ports:
            - containerPort: 8000
              protocol: TCP
          livenessProbe:
            httpGet:
              scheme: HTTP
              path: /
              port: 8000
            initialDelaySeconds: 30
            timeoutSeconds: 30
          volumeMounts:
          - mountPath: /tmp
            name: tmp-volume
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            runAsUser: 1001
            runAsGroup: 2001
      serviceAccountName: kubernetes-dashboard
      nodeSelector:
        "kubernetes.io/os": linux
      # Comment the following tolerations if Dashboard must not be deployed on master
      tolerations:
        - key: node-role.kubernetes.io/master
          effect: NoSchedule
      volumes:
        - name: tmp-volume
          emptyDir: {}

    您可以按照下边步骤安装Dashboard UI,

Action:安装Dashboard

  1. 由于要配置RBAC,所以先下载配置文件:

    wget https://raw.githubusercontent.com/kubernetes/dashboard/v2.6.1/aio/deploy/recommended.yaml
  2. 按照配置文件中的备注修改配置文件,修改节点为:kind: Service

    kind: Service
    apiVersion: v1
    metadata:
      labels:
        k8s-app: kubernetes-dashboard
      name: kubernetes-dashboard
      namespace: kubernetes-dashboard
    spec:
    # 在此处修改Dashboard Service的服务为nodePort方式
      type: NodePort
      ports:
        - port: 443
          targetPort: 8443
          nodePort: 31000     # 对外暴露端口31000
  3. 为了防止启动失败,最好将该节点安装在Master节点而不是Node工作节点(若安装在工作节点需要配置让工作节点可直接访问apiserver,该步骤参考引用中的文章细节,此处就不赘述了),执行下边命令:

    # 如果有改动,这个命令支持重新部署,再做一次即可
    kubectl apply -f recommended.yaml
    # 执行完后执行查看命令
    kubectl get pods --all-namespaces

    上述步骤完成后,您就可以通过:https://192.168.0.154:31000/访问Dashboard了,但打开时会出现:

    这是由于默认证书浏览器无法认证,需要自定义证书,按下边步骤创建自定义证书:

Action:自定义证书

  1. 生成新证书:

    openssl genrsa -out dashboard.key 2048
    openssl req -new -sha256 -out dashboard.csr -key dashboard.key -subj '/CN=192.168.0.154'
    openssl x509 -req -sha256 -days 3650 -in dashboard.csr -signkey dashboard.key -out dashboard.crt
    # 输出信息:
    Signature ok
    subject=CN = 192.168.0.154
    Getting Private key
  2. 将目录中生成的两个证书内容拷贝成base64的文本

    cat dashboard.crt | base64
    cat dashboard.key | base64
  3. 修改recommended.yaml文件片段

    kind: Secret
    metadata:
      labels:
        k8s-app: kubernetes-dashboard
      # .... 省略
    type: Opaque
    data:
      dashboard.crt: LS0tLS1CRUdJTiBDR...
      dashboard.key: LS0tLS1CRUdJTiBSU...
  4. 修改完成后重新部署:

    kubectl apply -f recommended.yaml

    再访问您就可以看到如下界面了,直接选择Token方式登录:

    最后一个步骤是创建一个账号并获取账号对应的Token,将这个Token填写到输入框中就可以进入主界面了。

Action:创建账号获取Token

  1. 为Dashboard创建账号,将下边段保存在admin-user.yaml文件中

    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: admin-user
      namespace: kubernetes-dashboard
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: admin-user
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: cluster-admin
    subjects:
    - kind: ServiceAccount
      name: admin-user
      namespace: kubernetes-dashboard
  2. 执行命令部署该账号:

    kubectl apply -f admin-user.yaml
  3. 为该账号创建Token:

    # -n 的名空间参数必须带,否则会
    # error: failed to create token: serviceaccounts "admin-user" not found
    kubectl create token admin-user -n kubernetes-dashboard
  4. 把创建出来的Token贴到浏览器中,您就可以看到主界面了。

3.4. 移花接木:cri-dockerd

    不解释为什么要迁移到cri-dockerd,主要原因如下:

  • 目前Docker依旧是最流行的容器运行时,而且在后续很多操作中,crio还是引起了不少问题,吃了螃蟹被夹了

  • 目前使用Docker的工程师依旧是占大部分,迁移到cri-dockerd之后,可以减小团队的学习曲线,再吃一口又被夹了

  • 其他集成:很多第三方准备搭建的内容在集成时默认使用了cri-dockerd,如Harbor镜像仓库,为了方便后续集成依旧采用Docker运行时。

    当然您依旧可以采用crio作为学习过程中的运行时,在K8S学习时是无关CRI这一层的——这一点倒是比较友好,只从生产环境考虑,还是使用cri-dockerd比较稳妥,没了dockershim,有替代品也不错,下边步骤中的所有步骤都需要在所有节点(三台机器)上执行。

3.4.1. cri-dockerd安装

  1. 从三个节点删除或停止Podman:由于它和Docker使用了相同的运行时文件,不可同时启动,先停止Podman

    systemctl stop podman
    systemctl disable podman
    
    # 为了防止运行时冲突,需要删除podman
    # 删除之前可以先备份镜像,save/load 命令
    yum remove podman
  2. 安装Docker的依赖项:

    yum -y install yum-utils device-mapper-persistent-data lvm2
  3. 安装DockerCE组件

    yum install docker-ce docker-ce-cli containerd.io docker-compose-plugin
  4. 将镜像设置到阿里云(切换镜像源):

    vim /etc/docker/daemon.json

    将地址改成加速地址(并带上systemd驱动,1.24之后推荐使用systemd):

    {
      "registry-mirrors": ["xxx"],
      "exec-opts": ["native.cgroupdriver=systemd"]
    }
  5. 启动Docker

    systemctl start docker
    systemctl enable docker
  6. 下载最新版cri-dockerd

    # 版本信息可直接查看 https://github.com/Mirantis/cri-dockerd
    # 写文档时它的最新版本是 v0.2.6
    wget https://github.com/Mirantis/cri-dockerd/releases/download/v0.2.6/cri-dockerd-0.2.6.amd64.tgz
  7. 拷贝二进制文件到固定位置:

    # 解压
    tar -xf cri-dockerd-0.2.6.amd64.tgz
    # 执行文件拷贝到bin中
    cp cri-dockerd/cri-dockerd /usr/bin/
    # 设置可执行权限
    chmod +x /usr/bin/cri-dockerd
  8. 配置启动文件(最好使用下载的方式):

    cd cri-dockerd
    # 下载启动文件(下载完成后可编辑,注意查看内容,有可能需要认证)
    wget https://github.com/Mirantis/cri-dockerd/tree/master/packaging/systemd/cri-docker.service
    wget https://github.com/Mirantis/cri-dockerd/tree/master/packaging/systemd/cri-docker.socket
  9. 编辑启动文件:

    vim cri-docker.service
    # 还有一个不用更改

    注意,和前文提到的配置一样,此处需要加上启动参数,否则在国内拉取镜像会失败。

    # ....
    [Service]
    Type=notify
    # 此处追加参数信息
    ExecStart=/usr/bin/cri-dockerd --network-plugin=cni --pod-infra-container-image=registry.aliyuncs.com/google_containers/pause:3.8
    # ....
  10. 将两个文件拷贝到/usr/lib/systemd/system/目录中:

    cp -rf cri-docker.s* /usr/lib/systemd/system/
    ll /usr/lib/systemd/system/docker*
  11. 启动环境并设置开机自启动:

    # 启动cri-docker
    systemctl start cri-docker
    # 开机自启动设置
    systemctl enable cri-docker
    # 查看启动状态
    systemctl status cri-docker
    
    # 问题:
    # The binary conntrack is not installed, \
    #     this can cause failures in network connection cleanup.
    # 解决:yum install conntrack-tools

3.4.2. cri-dockerd镜像迁移

  1. 镜像拉取:迁移到cri-dockerd之后,需要做的第一件事就是镜像转移,有两种方式:

    • 如果是直接备份,这个动作需要在删除podman之前完成,执行下边命令:

      podman save REPOSITORY:TAG -o xxxx.tar

      您也可以参考网上的教程将所有镜像打包到一起,但为了区分镜像内容,所以我最终是一个一个备份的,这样处理也更方便。

    • 或者您直接根据镜像列表拉取所有镜像(比较麻烦)。

  2. 镜像导入:将目录中的所有镜像导入到Docker中:

    # 导入镜像信息
    docker load -i xxx.tar
  3. 查看Docker中的镜像:

    docker images

注意查看SIZE,网上很多打包命令在SIZE上没有做处理,导致最终每个镜像SIZE一样,这种方式导出导入的镜像是有问题的。

3.4.3. K8S环境迁移

  1. 先执行下边命令查看当前kubelet的运行时环境:

    kubectl get nodes -o wide
  2. 查看kubelet目前的配置信息:

    cat /var/lib/kubelet/kubeadm-flags.env
    
    # 注意参数:--container-runtime-endpoint=unix:///run/crio/crio.sock
  3. 修改命令中kubelet的参数--container-runtime-endpoint

    --container-runtime-endpoint=unix:///run/cri-dockerd.sock
  4. 重启kubelet服务:

    systemctl restart kubelet
  5. 查看节点运行时是否切换完成:

    # 查看是否切换完成
    kubectl get nodes -o wide
  6. 最后还要查看K8S集群的状态是否正常

    # 一切正常后,删除 cri-o
    yum remove cri-o

3.4.4. K8S环境检查

  1. 先执行命令检查当前环境是否已经全部切换到cri-dockerd上(保证所有节点都切换):

    kubectl get nodes -o wide
  2. 再检查所有的Pod状态是否正常:

    kubectl get pods --all-namespaces
  3. 最后登录Dashboard检查状态:


4. 小结

4.1. 问题清单

    本章记录下来所有正常教程执行过程中遇到的问题列表,更具配置和安装的参考性。

4.1.1. /usr/local/bin/crio No such file or directory

描述

# 执行命令
systemctl start crio
# 日志记录查看 systemctl status crio
Sep 13 22:28:49 k8s-master systemd[10765]: crio.service: Failed to execute command: No such file or directory
Sep 13 22:28:49 k8s-master systemd[10765]: crio.service: Failed at step EXEC spawning /usr/local/bin/crio: No such file or directory

解决办法

  1. 方法一:直接创建软链

    which crio					# 找到运行的crio文件位置
    ln -s /usr/bin/crio /usr/local/bin/crio
  2. 方法二:编辑crio.service文件,修改执行地址(不推荐)

4.1.2. 无法找到crio

描述

# 问题1
Writing clean shutdown supported file: open /var/lib/crio/clean.shutdown.supported: no such file or directory
# 问题2
Failed to sync parent directory of clean shutdown file: open /var/lib/crio: no such file or director

解决办法

# 查找crio目录
find / -name "crio" -type d
# 按照教程安装可能会找到三个
/run/crio			# 运行目录
/var/log/crio			# 日志目录
/etc/crio			# 配置目录
# 针对运行目录创建软链接
ln -s /run/crio /var/lib/crio

4.1.3. [WARNING FileExisting-tc] / [WARNING SystemVerification]

描述

# 执行命令
kubeadm init
# 日志记录
[init] Using Kubernetes version: v1.25.0
[preflight] Running pre-flight checks
	[WARNING FileExisting-tc]: tc not found in system path
	[WARNING SystemVerification]: missing optional cgroups: blkio

解决办法

    执行下边命令解决第一个问题:

yum install iproute-tc

    第二个问题主要原因是使用了cgroup v2,在v2中,blkio已经被io代替了,而搜了全网目前没有一个正式的解决方案,据说K8S v1.17版本中已经有PR解决了该问题,只能等着后续的版本升级看此问题是否存在,该问题只会影响IO的资源管理,不会对整个集群产生大的影响,暂时忽略。

4.1.4. The kubelet is not running

描述

# 执行命令
kubeadm init
# 错误提示
Unfortunately, an error has occurred:
        timed out waiting for the condition

This error is likely caused by:
        - The kubelet is not running
        - The kubelet is unhealthy due to a misconfiguration of the node in some way (required cgroups disabled)

If you are on a systemd-powered system, you can try to troubleshoot the error with the following commands:
        - 'systemctl status kubelet'
        - 'journalctl -xeu kubelet'
# 查看状态
Sep 14 11:58:17 k8s-master kubelet[31172]: E0914 11:58:17.591441   31172 kubelet.go:2448] "Error getting node" err="node \"k8s-master\" not found"
Sep 14 11:58:17 k8s-master kubelet[31172]: E0914 11:58:17.691997   31172 kubelet.go:2448] "Error getting node" err="node \"k8s-master\" not found"
Sep 14 11:58:17 k8s-master kubelet[31172]: E0914 11:58:17.792188   31172 kubelet.go:2448] "Error getting node" err="node \"k8s-master\" not found"

解决办法

# 简单说是我这边的解决办法
vim /etc/crio/crio.conf
# 在文件中找到 [crio.image] 追加如下
[crio.image]
pause_image="registry.aliyuncs.com/google_containers/pause:3.8"

    这个问题不得不提,因为在这个问题上耗费了太多时间(差不多2 ~ 3小时左右),提到这个问题不得不说可能性很多,但我尽可能把原理讲清楚,如此根据原理大家更容易找到自己的情况是哪一种,每种我都尝试过。因为在线搜索的大部分人使用的都是1.231.24版本,而且容器运行时使用的是Docker/Containerd,而这两个容器运行时在使用时会因为拉取镜像的过程中自然而然设置,所以大部分人不会碰到这个问题。

    网上普遍的声音有几种观点:

  1. 没有禁用swap内存(教程中不存在,3.1.4已经做了)。

  2. hostname设置或hosts设置有问题(教程中不存在,3.1.6也做了)。

  3. 容器和K8S的版本不兼容(教程中不存在,全程奔着1.25去的,看截图就知道)。

  4. pause镜像没有下载成功——根源在这里

    也是我大意了,K8S的官方文档有明确的说明(版本别搞错,1.25使用的版本是3.8):

    但是由于在执行镜像下载命令kubeadm config images pull --image-repository registry.aliyuncs.com/google_containers时,系统已经显示环境中所有镜像都下载成功,所以最初遇到这个问题时候没有反应到容器运行时这一层需独立设置,也没有定位到是第4点。最初大量的日志都显示"Error getting node" err="node \"k8s-master\" not found",我简单分析一下这个过程中系统做了什么。

    K8S在执行kubeadm init做初始化时,它会直接访问底层容器运行时,把启动每一个Pod的任务代理给它,而容器运行时启动Container时,会先检查基础镜像(Pause镜像),如果基础镜像本地不存在则从配置中的地址去拉取镜像。也就是说kubeadm config images pull做的事情是下载镜像到本地,不会消费它,真正消费镜像是kubeadm init命令来做,这个命令在执行时有两个核心镜像参数:

# 启动可设置
--image-repository=registry.aliyuncs.com/google_containers
# 1.25中已经把这个参数拿掉了
--pod-infra-container-image=xxx

    第2个参数在1.25版中被拿掉了,代码中只剩下了,噩梦

if cleanFlagSet.Changed("pod-infra-container-image") {
	klog.InfoS("--pod-infra-container-image will not be pruned by the image garbage collector in kubelet and should also be set in the remote runtime")
}

    如果这个镜像无法拉取,那么基础容器无法启动,导致最终kubelet中的API Server没办法启动,这就是第一个错误描述的情况:The kubelet is not runningkubelet如果启动失败会一直Pending在那里,默认设置了重试,所以您直接通过命令journalctl -xeu kubelet查询到的日志有可能会被冲掉而导致看不见注册失败的日志,而且你会想到错误是因为kubelet无法启动,当您定位到kubelet无法启动几乎方向就错了,因为受到kubeadm的影响,再调用systemctl start kubelet启动时候会启动失败。

kubelet.service - kubelet: The Kubernetes Node Agent
   Loaded: loaded (/usr/lib/systemd/system/kubelet.service; enabled; vendor preset: disabled)
  Drop-In: /usr/lib/systemd/system/kubelet.service.d
           └─10-kubeadm.conf
   Active: activating (auto-restart) (Result: exit-code) since Wed 2022-09-14 23:48:49 CST; 9s>
     Docs: https://kubernetes.io/docs/
  Process: 46706 ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUB>
 Main PID: 46706 (code=exited, status=1/FAILURE)

    上述错误在kubeadm init成功过后会自然解锁,最终引起问题的根源就是运行时容器这一层无法从国内拉取到默认的镜像registry.k8s.io/pause:3.6,只有更改了容器这一层的参数后才会真正解决掉这个问题。这个问题在Google上搜索基本上都是前三种居多,最后一种老实说老外应该也不会被墙。

    最后谈谈pause容器,这个容器的全称叫Infrastructure Container(又叫Infra)基础容器,K8S在node节点上会启很多pause容器,它和Pod一一对应,每个Pod里运行着一个特殊的被称为pause容器,其他容器则为业务容器,所有的业务容器共享pause容器网络栈和挂载卷,因此各自的通信会变得更加高效。详情参考The Almighty Pause Container

4.1.5. couldn't validate the identity of the API Server

描述

# 运行命令
kubeadm join 192.168.0.154:6443 --token abcdef.0123456789abcdef \
  --discovery-token-ca-cert-hash sha256:b363066ec514bbc9efce5a11d39fc568cd4c86e067fdd5c3e32fbe57239269a9
[preflight] Running pre-flight checks
        [WARNING SystemVerification]: missing optional cgroups: blkio
error execution phase preflight: couldn't validate the identity of the API Server: cluster CA found in cluster-info ConfigMap is invalid: none of the public keys "sha256:28797229293e78b531462b5dc12ee23038bd7c5a037a0c45e7c0206c3ccb7c2f" are pinned
To see the stack trace of this error execute with --v=5 or higher

    这个错误的主要原因是因为kubeadm init反复执行引起的token的hash值不对,所以需重新获取ca证书sha256编码的HASH值。

解决办法

# 在控制平面查看检查token是否有效
kubeadm token list
# 输出如下
TOKEN                     TTL         EXPIRES                USAGES       ....
abcdef.0123456789abcdef   15h         2022-09-15T16:13:42Z   authentication,signing  ...

# 运行下边命令:
openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | 
openssl dgst -sha256 -hex | sed 's/^.* //'
28797229293e78b531462b5dc12ee23038bd7c5a037a0c45e7c0206c3ccb7c2f

    从输出可以看到二者的对比,所以最直接的解决办法就是——将错误提示的hash值代替原先获得的hash值即可。:

# 命令提供的Hash值:--discovery-token-ca-cert-hash
b363066ec514bbc9efce5a11d39fc568cd4c86e067fdd5c3e32fbe57239269a9
# 而证书的正确Hash值:
28797229293e78b531462b5dc12ee23038bd7c5a037a0c45e7c0206c3ccb7c2f

4.1.6. mixing sysregistry v1/v2 is not supported

    这个问题出现在配置镜像加速的过程,由于国内镜像拉取有问题,所以此处不得不补充一个章节来处理镜像加速。本文尚未配置containerd,所以不讲它的相关配置,而且在搭建环境过程中也没有使用Docker作为CRI,加上Docker配置十分常见,您可以直接网上搜搜资料参考相关内容。配置文件表:

CRI配置文件路径

Docker

/etc/docker/daemon.json

Podman

/etc/containers/registries.conf

CRI-O

/etc/containers/registries.conf

    如果这个配置信息是正确的,直接执行:

podman pull nginx
Error: loading registries configuration "/etc/containers/registries.conf": \
mixing sysregistry v1/v2 is not supported
# 如果 /etc/containers/registried.conf 配置文件有问题,crio会启动失败,并出现下边错误日志:
level=fatal msg="validating runtime config: invalid registries: \
loading registries configuration \"/etc/containers/regi...
  1. 打开配置文件/etc/containers/registries.conf,您可以看到如下部分:

    # ....
    # 版本1配置
    [registries.search]
    registries = ['registry.access.redhat.com','registry.redhat.io','docker.io']
    # 版本1配置
    [registries.insecure]
    registries = []
    # 版本1配置
    [registries.block]
    registries = []
    
    # 版本1 + 版本2 共享配置
    # 由于 RedHat 的镜像仓库需要认证登录,所以此处将所有相关的行全部注释掉
    unqualified-search-registries = [
       # "registry.fedoraproject.org",
       # "registry.access.redhat.com",
       # "registry.centos.org",
       "docker.io"
    ]
    
    # 版本2 配置
    [[registry]]
    prefix = "docker.io"
    location = "xxx.mirror.aliyuncs.com"  # 阿里云镜像仓库
  2. 如果使用V1(默认),则直接不添加[[registry]]部分即可,若要切换到V2,除了原始配置保留unqualified-search-registries=部分,其他的V1相关配置全部需要注释掉,且追加[[registry]]即可

  3. 然后分别运行如下命令可检查配置,且不会出现此处相关问题

    podman pull nginx     # 不报错可正常拉取
    systemct restart crio # 可正常启动,不报错
  4. 进入/etc/containers/registries.conf.d/目录,将所有.conf文件全部重命名为.conf.bak

  5. 这样就配置好镜像加速器了(推荐使用Podman管理CIR-O镜像信息),必须确认镜像内容是对的,执行podman imagescrictl images应该得到相同结果,而docker images则结构会不一致。参考执行截图:

这样镜像加速器就配置完成了,这套配置需要在每个节点都配,否则在创建Deployment或启动Pod时会出现ErrImagePull的错误。

4.1.7. BIRD is not ready

calico/node is not ready: BIRD is not ready: BGP not established with 172.18.0.1

    使用下边命令查看详细信息:

kubectl describe -n kube-system pods calico-node-m8pjd

    这个问题是K8S中常见的网络无法连通的问题,主要原因可能是机器中网卡太多导致无法查找到正确的网卡连通。

  1. 重新下载calico.yaml文件:

    wget https://docs.projectcalico.org/manifests/calico.yaml
  2. 查看自己的正确网卡,记下名字:

    ifconfig
  3. 编辑下载的calico.yaml,找到下边这行,追IP_AUTODETECTION_METHOD

            - name: CLUSTER_TYPE
              value: "k8s,bgp"
            # 追加IP_AUTODETECTION_METHOD
            - name: IP_AUTODETECTION_METHOD
              value: "interface=eth*"
  4. 然后重新执行一次网络组件的apply:

    kubectl apply -f calico.yaml

4.2. 最后的话

    教程走到这里,整个K8S 1.25 + CRI-O的基础环境就告一段落,本文参考了大量文档以及前人留下来的宝贵资料,并对开发人员在搭建环境过程可能遇到的各种问题以及概念进行了作者的解读和整理,主要是踩了不少坑,也在整个过程中解决了大量的问题,希望最终对您有所帮助。

Last updated