K8s要点简记
22年的时候需要使用CICD的时候,看了一遍k8s相关的知识点。买了一本《深入剖析Kubernetes》,大致熟悉了常用的概念。
最近因为工作调动需要做云原生相关的开发,有必要重新复习一遍相关概念,并深入部分章节的细节。
容器基础
容器原理
容器是云原生时代的进程。
之前大家都是在ECS这种虚拟机里面部署应用,操作系统一般是CentOS/Debian之类的Linux系统,服务通过Supervisor/Systemd等进程管理工具进行管理,CPU/内存等容量规划是以虚拟机为单位的,环境一般使用bash/python脚本进行初始化,监控、服务集群扩容和缩容都是运维手动配置。
容器最开始是为了解决环境问题(为应用创建隔离的运行环境),有点类似Python的virtualEnv,它的底层原理是基于Linux的资源隔离机制,也就是:
- Namespace. 通过
clone
创建进程时,可以传入CLONE_NEWPID
参数,此时这个进程会“看到”一个新的进程空间,在这里他的进程pid是1。在这个进程空间里,它是被隔离的,只能看到操作系统配置的东西;当然在宿主机看来这就是一个普通的进程; - CGroups. 用来为进程设置资源限制,如内存、CPU、磁盘、带宽等,同时还具有优先级设置、审计以及进程挂起、恢复等功能;cgroups通过文件系统实现,可以在
/sys/fs/cgroup
下看到各种资源目录,在里面建立文件夹,系统会自动创建对应的资源限制文件;将进程pid写入tasks
即可; - rootfs. 用来与宿主机隔离文件系统,基于Linux的
pivot_root
/chroot
指令;
显然,宿主机上所有容器共享同一个内核。
Docker on Windows/Mac实际是基于kvm实现的,和linux的docker完全不同。
镜像原理
docker所谓的镜像,其实就是一种rootfs的压缩包,它的创新点主要是引入了层(layer
)的概念,后者就是增量的rootfs。
docker通过联合挂载(union mount)技术将这些层挂载到统一的挂载点上,这通过文件系统的支持(如AuFS)来实现。
镜像从下到上分别是只读层、Init层和读写层:
- 一般只读层就是操作系统镜像,如
ubuntu:latest
; - Init层是启动时写入容器的配置,如hostname、hosts等;
- 可读写层则是普通用户创建镜像时修改的部分;
由于只读层的文件不能真正被修改或者删除,所以可读写层如果想要修改只读层的东西,aufs实际上是通过创建whiteout
文件,把只读层的文件遮挡起来。比如foo
文件对应的位置创建一个.wh.foo
文件,这种屏蔽一般称为白障。只读层的挂载方式也称为ro+wh
.
Docker用来制作rootfs的工具称为Dockerfile
,例如:
From python:2.7-slim
WORKDIR /app
ADD . /app
RUN pip install --trusted-host pypi.python.org -r requirements.txt
EXPOSE 80
ENV NAME world
CMD ["python", "app.py"]
这是一个python服务的打包过程,同目录应当有app.py
和requirements.txt
两个文件,进入目录,通过docker build -t <tag> .
命令即可生成docker镜像。
常用命令
使用docker run -p 4000:80 helloworld
来运行镜像,并将镜像的80端口映射到宿主机的4000端口。
使用docker ps
命令即可看到在运行的进程。如果在镜像里做了某些操作,想要把正在运行的镜像提交成一个新的镜像,可以使用docker commit
命令。
可以通过docker inspect --format '{{.State.Pid}}' <container-id>
命令,获取宿主机上容器的pid.在/proc/<pid>
下面可以看到宿主机上该容器相关的进程文件。
运行docker exec -it <container-id> /bin/sh
可以进行镜像的bash,进行某些操作,该操作的原理是利用了setns
的系统调用。
通过--net container:<container-id>
参数可以将一个容器加入另一个容器的网络空间,如果是--net host
则直接共享宿主机的网络栈。
docker镜像仓库可以使用公有的docker hub,也可以使用自建的harbor(vmvare做的).
文件系统打通
有时候宿主机和容器需要交换持久化数据,即文件访问。
Docker Volume用于将宿主机中文件挂载到容器内访问,一般通过docker run
命令启动容器时,使用-v <host-path>:<container-path>
传入挂载映射关系。
这个挂载实际上是在rootfs准备好之后,chroot
执行之前,由dockerinit
进程完成的。之后才是通过execv
系统调用,让应用进程取代自己成为pid=1的容器进程。
k8s设计概要

k8s由master和node两类节点构成,分别称为控制节点和计算节点。
master由3个组件构成,control-manager负责编排,apiserver提供接口,scheduler负责容器调度;
计算节点核心是kubelet组件,它与下面的容器运行时(如docker)通过CRI的接口来交互;和网络层以及存储层通过CNI和CSI的接口来交互。通过这些接口的抽象,可以更换底层实际的实现,所以在k8s中docker可以随时替换成别的运行时。
device plugin是k8s用来调度硬件(如GPU)的插件,kubelet通过gRPC与之交互。
k8s源自Google内部的borg系统,但是实际上后者并没有容器镜像这种东西,而是直接用Namespace+CGroup限制应用程序。borg对于k8s的指导作用主要体现在master节点上。
习惯上,k8s的使用方法是:
- 通过一个任务编排对象(如pod、cronJob等)描述你的任务对象;
- 为1中的人物定义一些运维能力对象,如Service/Ingress/HPA等;
这就是所谓的声明式API,1和2中的对象又称为API对象。
k8s集群搭建
k8s的官方推荐部署工具是kubeadm
(还有kOPs和Kubespray),建立集群的方式非常简单:
# init master
kubeadm init [--config kubeadm.yaml]
# join node to master
kubeadm join <master ip:port>
当然需要自己提前安装运行时(如containerd,如果使用DockerEngine则需要额外安装cri-dockered
适配器),此外需要打开一些端口。master节点包括:
协议 | 方向 | 端口范围 | 目的 | 使用者 |
---|---|---|---|---|
TCP | 入站 | 6443 | Kubernetes API server | 所有 |
TCP | 入站 | 2379-2380 | etcd server client API | kube-apiserver, etcd |
TCP | 入站 | 10250 | Kubelet API | 自身, 控制面 |
TCP | 入站 | 10259 | kube-scheduler | 自身 |
TCP | 入站 | 10257 | kube-controller-manager | 自身 |
node节点包括:
协议 | 方向 | 端口范围 | 目的 | 使用者 |
---|---|---|---|---|
TCP | 入站 | 10250 | Kubelet API | 自身, 控制面 |
TCP | 入站 | 30000-32767 | NodePort Services | 所有 |
NodePort默认使用30000~32767的端口,这里可以根据需要开放。截止v1.26,k8s单个集群的限制如下:
- 每个节点的 Pod 数量不超过 110
- 节点数不超过 5,000,节点的主机名不能相同
- Pod 总数不超过 150,000
- 容器总数不超过 300,000
kubeadm
在宿主机运行kubelet,然后使用容器化部署其他组件。
默认情况下kube-apiserver仅支持HTTPS通信,因此kubeadm会自动生成对应的证书,路径在/etc/kubernetes/pki
;master节点其他组件的配置文件也都在/etc/kubernetes
下。
kubeadm会通过pod的方式启动其他master组件,由于此时k8s集群还不存在,这里运行pod的方式是所谓的static pod
,其yaml路径在/etc/kubernetes/manifest
下。
之后,需要安装CNI和CSI的插件,使用kubectl apply -f
命令即可。
习惯上,k8s通过使用kubectl命令,与api对象(即yaml文件)进行交互。
API对象有Metadata
字段,即对象的元数据,其中Labels
用来做标记该API对象筛选依据;而Annotations
则一般是k8s在运行过程中自动添加在API对象上的。
pod是k8s最小运行单元,1一个pod可以包含多个容器(使用同一个namespace)。
常用命令
kubectl get
用来获取指定的API对象,如kubectl get pods -l app=nginx
,用来通过label过滤pods;kubectl describe
用来查看对象的详细信息,如kubectl describe pod <pod-name>
;kubectl create -f <name>
生成配置文件,name可以标明对象的种类,如nginx-deployment.yaml
;kubectl apply -f <name>
,应用配置。k8s会自动探测这种应用是更新还是创建;kubectl delete -f <name>
,删除API对象;kubectl exec -it <pod-name> -- /bin/bash
,类似docker exec
,进入pod中(容器的namespace中);kubectl scale deployment xxx --replicas=N
,水平伸缩(直接apply一个deployment也可以);kubectl rollout status
,滚动查看API对象的状态;kubectl edit
直接编辑etcd中的API对象定义;kubectl set
直接修改某个字段,如image
;kubectl patch
直接给API对象打补丁,补丁有具体的语法,详情可以查询文档;kubectl logs [-f] [-p] POD [-c CONTAINER]
,查看pod日志;
k8s编排原理
Pod原理
Pod是一个逻辑概念,并没有对应的实体,实际操作的仍然是容器,或者说Linux Namespace和CGroup.一组共享网络Namespace的Pod,并且可以共享Volume的容器,被称为Pod.
所有pod,k8s都需要先创建一个Infra container,其他容器共享该容器的网络Namespace,这个容器由汇编语言写成,永远处于暂停状态,所以消耗的资源极少。pod的生命周期与infra容器一致,用户容器的进出流量也可以视为通过infra容器完成。
正是因为有了infra容器这个隐藏的container,Volume才可以定义在pod层级。
Pod的定义,一方面是为了调度方便(多个容器),另一方面是为了所谓的容器设计模式。也就是容器=进程,pod=进程组/虚拟机的设计。
通过initContainers
,可以在pod中预定义多个容器,用来初始化环境,需要注意initContainers
每次都会启动,如果是一次性的初始化需要检测是否需要跳过。
initContainers一定会先于pod启动,并按着定义的顺序严格执行;但是containers里面规定的多个容器则没有明确的启动顺序,所以如果有依赖问题,需要检测指定容器是否已经就绪。
多个container协作的设计模式,又称为sidecar模式。
Pod字段
按着Pod=虚拟机这种模拟来理解,很容易明白哪些配置需要定义在pod级别,哪些是container级别。一些pod级别的常用字段(spec下面):
nodeSelector
,调度条件,如diskType:ssd
,标明该pod只能在打有这个标签的节点上被调度。已废弃,使用affinity.nodeAffinity
代替;nodeName
,这个字段是k8s赋予的,有值的时候就会认为已调度;hostAliases
,host定义;containers
,容器级别的定义;volumes
,卷声明;terminationGracePeriodSeconds
,优雅关闭等待时长;backoffLimit
,最大重启次数;activeDeadlineSeconds
,最大运行市场(一般Job里面使用);
volumes声明为emptyDir:{}
,则会在宿主机上创建一个临时目录绑定到该volume的name上;volumes也可以声明为hostPath
,通过path: /xxx
来直接映射宿主机的目录。这两种volume是最常用的的,但是数据卷有时候需要分布式文件系统,此时就要使用其他方式(如PVC)来声明了。
container级别的常用字段:
imagePullPolicy
,默认是always,可以设置为never
或者ifNotPresent
;lifecycle
,生命周期,如:postStart
、preStop
,用于定义容器生命周期各个阶段回调的命令;需要注意的是postStart
启动的时候,容器的CMD可能还未结束;livenessProbe
,存活探针。支持http探测(200或者其他)或者bash命令(通过返回0或者非0)探测,甚至tcp探测(端口存活);readinessProbe
,restartPolicy
,重新创建pod的策略,默认是Always
,可以设置为OnFailure
或者Never
。注意onFailure
的条件是所有容器都异常了,Always
则是任意一个容器异常;如果需要保留案发现场,则可以设置为Never;volumeMounts
,卷挂载。将pod级别的卷声明根据需要挂载到容器里,通过mountPath
指定挂载路径;resources
,容器的资源声明,requests
和limits
来声明最小和最大限制;
由于字段太多,开发人员编写API对象难度较大,此时运维人员可以预定义一些参数,这就是所谓的PodPreset
。这也是一类API对象,他可以通过spec.selector.matchLabels
匹配开发人员创建的API对象。注意preset需要先于pod创建。
Pod的状态
即pod.status.phase
- Pending. API对象已创建成功,但是尚未调度成功;
- Running. API对象已调度成功;
- Succeed. 已成功执行,一般是Job节点;
- Failed. 所有容器进程以非0状态码退出;
- Unknown. 未知状态,apiserver无法获取pod状态,一般是主从通信出了问题;
此外,还有一组conditions字段,包括:PodScheduled
, Ready
, Initialized
和Unschedulable
.两个字段需要结合来看。
Pod配置
pod中用户容器的配置,一般通过project volume
的方式注入进去,包括:
Secret
: 将密码保存在etcd里,可以打开加密;ConfigMap
: 一般的配置;Downward API
: 将pod的信息暴露到容器里,必须是容器启动前就能确定下来的信息,不支持运行时更改;
以上都是通过kubectl create xxx
创建(或apply),然后通过volume挂载到pod中。当然也可以通过环境变量注入,但是后者不具备自动更新的能力。
ServiceAccoutToken是一种特殊的Secret,k8s默认会隐式注入到所有容器里,这样容器中的应用可以通过kubectl控制k8s.
Deployment
用于部署的API对象,pod水平伸缩滚动更新依赖于该类API对象。
Deployment并不是直接控制Pod对象,而是通过Replica对象来控制。
对于用户而言,就是通过replicas
定义副本数量,通过spec.selector.matchLabels
匹配pod定义。
然后apply
就行,k8s会自动完成滚动更新,相关命令:
kubectl rollout undo
,版本回滚;kubectl rollout history
,查看版本历史;kubectl rollout undo xxx --to-revision=N
,回滚到某个具体的历史版本;kubectl rollout pause
和kubectl rollout resume
,暂停部署和恢复部署;
通过查看deployment对象的状态可以确定部署是否完成。
这里k8s的工作原理类似一个死循环,不断检查API对象的状态,如果不满足yaml中的声明,就动态调整直到满足为止。
StatefulSet
与ReplicaSet对应的有状态Pod,主要指代两种状态:
- 拓扑状态,即pod有严格的启动顺序,重建需要保持顺序的问题。这个statefulSet默认就能解决。解决方式是对pod赋予唯一的名称(hostname:
<name>-N
,N是编号),访问pod通过headless service对应的唯一域名来访问,创建pod时保持名称不变即可; - 存储依赖,即应用有需要落地的数据,当pod重建后能恢复这些数据;这个需要其他技术来辅助;
由于Volume的编写过于复杂,所以这里又抽象出两个新概念:
- PVC: PersistentVolumeClaim,即对存储的需求描述,大小、挂载方式等;在volume中只要指定这个pvc就行;
- PV:真正的volume细节,一般是运维编写;
PVC例子:
spec:
volumes:
- name: pv-storage
persistentVolumeClaim:
claimName: pv-claim
实际使用中一般只需要说明需要的PVC:
spec:
volumeClaimTemplate:
- metadata:
name: xxx
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
这里就指定了权限和大小,k8s会自动寻找合适的PV绑定到PVC上,PVC的名字是xxx-<podname>
.
所以对于第2个问题,直接指定PVC即可。由于绑定到Pod的PVC的命名也有与Pod相同的编号,重建pod时找到符合该规律的PVC挂载上去即可。
特别注意的是,StatefulSet
要求使用同一个容器镜像,如果容器镜像不同,需要使用Operator
.既然是同一个镜像,那么按着pod名称中的序号0,1,2…就可知道启动的顺序,这样就可以把需要先启动的放前面。常见的需求是主从集群,0为主,1,2为从这种设计。
StatefulSet的滚动更新与ReplicaSet不同,有严格的销毁和启动顺序,销毁按着序号倒序进行。可以在rollingUpdate
里按序号指定部分更新(灰度)。
DaemonSet
宿主机守护进程,在k8s上每个节点上都会运行且只运行一个pod,当然“每个节点”不是很准确,应该是拥有满足spec.selector.matchLabels
筛选pod的每个结点。
DaemonSet启动的很早,因此可以通过spec.tolerations
指定污点来忽略某些限制。
类似Deployment,支持版本管理。不同的是,Deployment的一个版本对应一个ReplicaSet,DaemonSet直接控制Pod,所以实现方式不一样。
DaemonSet版本管理使用的对象是ControllerRevision
, 可以通过get
/describe
该对象查看具体的版本信息。StateFulSet也是使用该对象实现版本管理的。
Job和CronJob
一次性或者周期性调度的任务。Job直接控制Pod,CronJob则控制Job. 一些特殊字段:
-
spec.parallelism
定义job可以启动多少个pod进行并发计算; -
spec.completions
定义job至少完成的pod数目; -
spec.concurrencypolicy
,cronjob对job重合的处理,默认为Allow
,可以设置为Forbid
或者Replace
;
CronJob的最小检查周期是10s,定义最小周期则是1分钟。
如果从上次运行时间到现在,CronJob创建失败次数超过了100,这个CronJob将不会再被调度;如果设置了spec.startingDeadlineSeconds: n
,则检查的时间点就会固定到n秒之前,次数则固定是100,不可设置。这个参数还表示cronJob容许的延迟启动时间。
声明式API
前文已经说过,尽量是用kubectl apply
来实现CURD,而不是直接用create/replace/patch
等命令,这是因为有并发处理同一个API对象的情况。k8s会在apply时自动处理各种冲突,最终达到需求的状态。
自定义API资源
即CRD(custom resource definition),用户可以自定义API对象,格式如下:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: network.samplecrd.k8s.io
spec:
group: samplecrd.k8s.io
version: v1
names:
kind: Network
plural: networks #复数
scope: Namespaced
这里需要做一些云原生开发,即golang写插件。由于涉及到编程,此处细节会单独开blog来写。
RBAC
k8s的权限控制也是基于RBAC制定的,主要概念:
- Role:角色,对应了具体的权限集;
- Subject: 主题,被赋予角色的对象;
- RoleBinding:上面两个的绑定关系;
Role的定义格式:
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: mynamespace # 必须指定namespace,默认是default
name: example-role
rules:
- apiGroups: [""]
resources: ["pods"] # 资源组
resourceName: ["mysql"] # 资源的名称
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] # 权限动词
RoleBinding的定义:
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: example-rolebinding
namespace: mynamespace # 也必须指定namespace
subjects:
- kind: User
name: example-user
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: example-role
apiGroup: rbac.authorization.k8s.io
如果想在所有namespace里都生效,需要使用clusterRole
和clusterRoleBindings
.
与User相比,习惯上更多使用ServiceAccount
来分配权限,用户在pod里通过spec.serviceAccountName
来引用该对象。
准入控制器
Admission Controllers,在RBAC校验通过之后,对象被持久化之前调用,主要用来过滤非只读请求。
可以通过webhook自定义准入控制器。
Operator
由于有状态服务过于复杂,经常需要在yaml里面写逻辑。所以另外一种更编程友好的方法是使用Operator来完成。这其实就是一种CRD配合自定义控制器的完全自定义方式。
涉及到编程,这里不再赘述。一般Stateful搞不定的,就要使用Operator来定义了。
k8s存储原理
PV和PVC的匹配机制:
- PV必须满足PVC的大小限制;
- 二者的
storageClassName
必须一致(如果没声明,则为空字符串);
k8s中专门处理持久化存储的控制器叫Volume Controller
,其维护多个控制循环,其中一个循环会持续尝试绑定PV和PVC,即PersistentVolumeController
.所谓绑定,就是将PV对象的名字填入PVC的spec.volumeName
字段。
Dynamic Provisioning
人工创建PV的方式被称为Static Provisioning
,只能对小规模集群使用人工管理。
动态分配的核心在于名为StorageClass
的API对象,其定义主要包括两个部分:
- PV的属性,如存储类型、Volume的大小等;
- 所需存储插件,如Ceph等,对应字段是
provisioner
;
k8s官方支持存储插件很多,如果实在是不支持,也可以自己开发。
Local PV
直接使用宿主机磁盘的PV,理论上IO性能最好,但是存在数据丢失风险。使用时需要注意:
- 一个PV一块盘,不应当使用宿主机的主硬盘;
- 只能使用Static Provisioning,即预先创建PV才能绑定到PVC;
- 需要延迟绑定,通过
StorageClass.volumeBindMode: WaitForFirstConsumer
延迟PV和PVC的绑定;
PV示例:
apiVersion: v1
kind: PersistentVolume
metadata:
name: example-pv
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Delete
storageClassName: local-storage # 存储类
local:
path: /mnt/disks/vol1
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions: # pod仅能在当前节点运行
- key: kubernetes.io/hostname
operator: In
values:
- node-1
StorageClass示例:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer # 延迟绑定声明
最后删除PV的顺序:
- 删除使用PV的Pod;
- 移除磁盘(unmount);
- 删除PVC;
- 删除PV;
可以通过k8s的StaticProvisioner来自动化LocalPV的相关操作。
CSI插件编写
涉及到编程,先跳过
k8s网络原理
容器的network namespace默认是隔离的,通信需要经过交换网络。
docker的实现方式是在宿主机创建一个名为docker0的网桥,网桥工作在二层网络,根据mac地址转发数据包。
然后docker为每个容器创建一对Veth Pair,这种虚拟设备成对出现,都插在docker0网桥上,一端在宿主机里,另一端在容器里(eth0)。发往任一端的数据包,在另一端都能收到,且无视namespace隔离。
对于跨主机网络,这个docker0就力不能及了,这时候就需要Flannel项目出马。
Flannel
该项目是一个框架,有多种后端实现,用于解决跨主机容器通信问题。
UDP后端
flannel进程会在每个宿主机上监听一个UDP端口,并创建一个名为flannel0
的虚拟设备,这是一个3层TUN设备,主要作用是在OS kernel层和user层之间传递ip数据包,具体来说就是将ip包在内核和flannel进程之间传递。
每个宿主机上的容器都属于该宿主机被分配的子网,flannel进程可以根据目标ip地址对应的子网找到其宿主机,然后将ip包转发给对应宿主机的flannel进程即可。后者会将数据包传入内核,根据路由表将数据转给docker0网桥。
由于数据需要多次在内核态和用户态之间切换,导致性能太低,所以已被逐渐废弃。
VXLAN后端
非常类似UDP后端,VXLAN会在宿主机上设置一个特殊的网络设备VTEP
,不过它是工作在二层。
k8s对上述方案的兼容
k8s肯定不会直接用docker0,而是通过CNI接口来进行网络通信,默认创建的网桥是cni0
.
为宿主分配子网可以使用kubeadm init --pod-network-cidr=<ipv4/n>
来指定,也可以创建完成之后通过kube-controller-manager
来指定。
CNI具体的配置须放在宿主机的/etc/cni/net.d/
中,格式类似:
{
"name": "cbr0",
"plugins":[
{
"type": "flannel",
"delegate":{ //托管给内置的插件
"hairpinMode": true, //打开发夹模式,允许自己访问自己
"isDefaultGateway": true
}
},
{
"type": "portmap",
"capabilities":{
"portMappings": true,
}
}
]
}
容器网络相关的逻辑并不在kubelet
中,而是在具体的CRI中,对于docker就是dockershim
,目前已经弃用。
目前k8s网络不支持多个CNI复用。
Infra容器创建之后会执行SetUpPod
,该函数用于为CNI插件准备参数。对于flannel,他需要的参数分为两部分:
- 各种环境变量,其中最重要的是CNI_COMMAND,可选的值是DEL或者ADD,表示把容器添加到CNI网络,或者从中移除;
- 上面配置文件中的配置信息;
参数加载完毕之后,CRI就会调用CNI插件,对于flannel而言,由于这个插件是内置在k8s网络里,所以实际上只是对配置参数做了一些补充。
之后,CNI插件就会调用bridge插件,后者会检查CNI网桥是否存在,没有则创建一个cni0. 之后,创建Veth Pair,将其中一端放在宿主机上,另一端放在infra容器里。
特别注意在infra容器的那段需要设置hairpinMode: true
,即允许容器通过Service自己访问自己。
之后bridge插件调用ipam插件,为容器分配子网中某个ip地址,并绑定到容器的eth0上。
最后,CNI插件将ip地址这些信息返回给CRI,kubelet再将这些信息添加到Pod的相关字段上。
host-gw后端
将宿主机ip作为flannel子网的下一条地址,即将宿主机当做路由器来用。这个思路其实类似物理组网。
优势是性能损失更低(10%左右),缺点是要求宿主机集群之间二层互通,而VXLan仅要求三层互通。一般公有云环境更推荐这种模式。
对于复杂项目,可以考虑使用calico项目,该项目使用BGP协议进行大规模路由表维护,避免人工维护成本。
由于BGP网络极其复杂,这里不再详述。
网络隔离
k8s通过API对象NetPolicy
来控制网络隔离,该对象通过spec.podSelector
来匹配pod,并进行白名单管控。
使用起来其实很类似IaaS中的“安全组”。
Service原理
service是由kube-proxy和iptables共同实现的。
推荐打开kube-proxy的IPVS模式以提高性能,纯iptables在大量pod时性能较差。
Service的spec.type
支持以下几种模式:
- ClusterIP,最常用的的模式。如果设为None,则为headless模式,否则通过随机负载均衡访问;
- NodePort,强行暴露pod的端口到宿主机;每个节点只能部署一个pod示例;
- LoadBalancer,对接公有云时使用;
- ExternalName,类似在DNS中直接加CNAME;
- ExternalIp,直接在
spec.externalIps
里面声明可以访问的ip地址,该地址会路由到对应的service;
对于4层协议,如果想获取客户端的真实ip,需要将spec.externalTrafficPolicy
设置为local.
Ingress对象
即k8s对反向代理的抽象,可以视为Service的Service.
其实没啥好说的,配置很类似Nginx.
k8s资源调度
资源的spec.resources.requests
表示调度需要的最小资源,spec.resource.limits
则是CGroup设置的限制值。
Pod资源QoS的三个级别:
- Guaranteed. 同时设置上述两个指标,且二者相同;
- Burstable. 至少有一个容器设置了requests;
- BestEffort. 两个参数都没设置;
当资源不足时,上述QoS的Pod回收的优先级是从低到高的。因此像DaemonSet这种资源,应当设置为Guaranteed
保证尽量不被回收。
如果Guaranteed
级别的Pod指定cpu数量(整数),则称为cpuset
,指将pod绑定到具体的核上,避免频繁的上下文切换,生产环境比较常用。
调度流程
默认调度器的调度流程分为预选和优选两步。
预选是并发遍历所有节点寻找满足API对象需求的Node并筛选出来,在单个节点上的筛选流程是固定的4步(顺序执行)。
优选则是对预选的结果分别打分,最后得分最高的被选出来。
不过最后创建Pod时,还是kubelet会二次检查确认满足条件。主要是因为前面的检查是无锁乐观的,最后一步还是要二次确认才能保证没问题。
调度优先级
默认都是0,可以创建PriorityClass
对象来指定优先级,然后在pod的spec.priorityClassName
里指定具体的优先级对象名字。
Device Plugin
cpu/内存以外的资源,都是通过Extend Resource
来实现的。Device Plugin
是一种插件,通过与Kubelet进行gRPC通信定时上报节点拥有的其他硬件资源。
CRI与容器运行时
最开始k8s是基于docker实现的,后面抽象成了CRI,并逐步从核心代码中移除了dockershim.
容器运行时除了docker之外,现在还有containerd等项目。
甚至,gVistor等项目还支持硬模拟,从而创造隔离性更好的pod(如内核级别的隔离)
k8s监控与日志
开启apiserver的aggregator模式之后,会启动一个代理。代理之后还包括metric server,这里就是k8s本身promethus格式的metric api.
HPA就是基于应用的metric server完成的.
k8s鼓励应用直接把日志直接输出到stdout和stderr,它会将日志重定向到宿主机的文件中。
明星项目和工具
- kubevela: 由于k8s配置过于复杂,对应用开发人员不太友好,该项目是为了减轻开发者负担而创建;
- kubeclipper: kubeadm的图形化打包,方便安装集群;
- helm: 类似apt的包管理工具,直接在k8s里面安装容器;
- k3s: 简化的k8s,边缘机器使用;配合docker的叫做k3d;
- kind: 利用docker快速部署集群,可以在本机开发时使用;完全兼容k8s;
- krew: kubectl的插件管理工具;
- Lens:多集群管理;
- Capsule/vCluster:单集群多租户管理工具;后者更成熟;
- SchemaHero:云原生的数据库迁移工具;
k8s国内安装
可以参考这个repo,主要使用阿里云的镜像来安装,当然完整的流程还是要参考k8s官方流程。
update: 建议使用DaoCloud的工具来安装,更加无侵入和傻瓜化。
由于装的时候踩了不少坑,这里还是记录一下详细流程。
准备工作
linux配置:
- 关闭swap,注意是永久关闭,不要临时关闭,否则重启之后kubelet运行不了:
sed -ri 's/.*swap.*/#&/' /etc/fstab
swapoff -a
-
配置静态ip,如果是云端服务,需要购买弹性ip;
-
配置内核参数,先启用对应的内核模块(ubuntu可能需要先用apt install bridge-utils):
modprobe overlay
modprobe br_netfilter
上面是临时修改。如果要永久修改,不同发行版不太一样,ubuntu只要修改/etc/modules
,在里面写入上面两个模块的名字即可。
然后编辑/etc/sysctl.conf
,加入:
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 0
net.bridge.bridge-nf-call-ip6tables = 0
vm.swappiness = 0
这个配置里面禁用了ipv6,如果想要启用双栈,则应参考这里,一般应该是不用启用的。修改之后运行sysctl -p
生效。
-
安装容器运行时,为了适配v1.24之后的k8s,这里不再安装docker,仅安装containerd。参考官方文档安装即可,如果使用apt/dnf安装,需要将CNI插件手动下载并解压到指定位置;
-
配置containerd,需要配置地方比较多,主要是:
[plugins."io.containerd.grpc.v1.cri"]
sandbox_image = "registry.aliyuncs.com/google_containers/pause:3.6"
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
endpoint = ["https://mirror.baiduce.com","https://dockerproxy.com"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."k8s.gcr.io"]
endpoint = ["registry.aliyuncs.com/google_containers"]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
...
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true
- 安装kubelet/kubectl和kubeadm,这里必须用阿里云镜像了,参考这里,修改apt/yum的配置,然后安装指定版本并冻结版本即可。
- 使用
systemctl enable containerd && systemctl start containerd
启动运行时;kubelet可能也需要类似操作; - 集群中的多台主机的主机名不能重复;
- 如果是搭建多master的HA模式,需要配置nginx/haproxy做LB,或者使用云主机商提供的LB;
安装master
以上准备工作完成后,就可以用kubeadm进行安装了,配置文件参考(v1.23):
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
localAPIEndpoint:
#这里改成你的实际ip地址
advertiseAddress: 172.16.20.14
bindPort: 6443
nodeRegistration:
criSocket: unix:///var/run/containerd/containerd.sock
imagePullPolicy: Always
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
# 这里同样需要改成实际ip,如果有LB,则设为LB的入口地址(可以是域名)
controlPlaneEndpoint: 172.16.20.14:6443
imageRepository: registry.cn-hangzhou.aliyuncs.com/google_containers
# 改为你需要的版本
kubernetesVersion: v1.23.16
networking:
# 这里必须和flannel的配置一致,用其他CNI实现则需要参考插件的描述
podSubnet: "10.244.0.0/16"
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
# 节点pod限制可以参考自己的机器配置
maxPods: 200
---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"
ipvs:
strictARP: true
根据需求修改上面的配置,之后跑kubeadm init --config xxx.yaml
用上面的文件初始化控制面.
根据提示操作,root/非root用户都需要配置一下。
运行kubectl get nodes
,确认一切正常了。
如果配置有误,可以kubeadm reset
重置,然后重新init.
配置插件
配置CNI:
kubectl apply -f https://ghproxy.com/https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
直接跑flannel的yml即可.
完成之后要看flannel能否正常work:
kubectl get pods -n kube-flannel
配置CSI,不过这一步是可选,如果有集群可以考虑使用Rook+Ceph,否则使用host-path也够了。
master节点默认禁止调度用户pod,可以通过移除taint取消限制(for 1.24之前,之后的可以移除掉NoSchdule):
kubectl taint nodes --all node-role.kubernetes.io/master-
helm的安装需要翻墙,建议直接下载二进制文件上传过去。chart可以用微软的仓库,或者华为的:
helm repo add microsoft http://mirror.azure.cn/kubernetes/charts/
安装ingress
如果自己测试的话用NodePort模式就够了,稍微大一点规模的微服务,一般都是用ingress的.
ingress的安装其实也是kubectl apply -f
,问题是这个镜像要从Google拉,所以不能直接用。将yaml下载到本地,替换registry.k8s.io
为k8s.m.daocloud.io
,然后再apply即可。
后面需要配置Nginx,跟传统的其实差不多,只是upstream是动态的cluster domain.