深入浅出k8s

部署k8s

ubuntu22.04-k8s-deploy

概念

Service

什么是Service

所谓 Service,其实就是 Kubernetes 为 Pod 分配的、固定的、基于 iptables(或者 IPVS)的访问入口。而这些访问入口代理的 Pod 信息,则来自于 Etcd,由 kube-proxy 通过控制循环来维护。

Service 原理

DNS

在 Kubernetes 中,Service 和 Pod 都会被分配对应的 DNS A 记录(从域名解析 IP 的记录)。

Pod

调度

常用定义

spec:
  hostAliases:
  - ip: "10.1.2.3"
    hostnames:
    - "foo.remote"
    - "bar.remote"

这样,这个 Pod 启动后,/etc/hosts 文件的内容将如下所示:

cat /etc/hosts
# Kubernetes-managed hosts file.
127.0.0.1 localhost
...
10.244.135.10 hostaliases-pod
10.1.2.3 foo.remote
10.1.2.3 bar.remote
spec:
  # 共享PID Namespace
  shareProcessNamespace: true
  # 共享宿主机的 Network、IPC 和 PID Namespace
  hostNetwork: true
  hostIPC: true
  hostPID: true
  containers:
  - name: nginx
    image: nginx

生命周期

Pod 遵循一个预定义的生命周期,起始于 Pending 阶段,如果至少其中有一个主要容器正常启动,则进入 Running,之后取决于 Pod 中是否有容器以失败状态结束而进入 Succeeded 或者 Failed阶段。
在 Pod 运行期间,kubelet 能够重启容器以处理一些失效场景。在 Pod 内部,Kubernetes 跟踪不同容器的状态并确定使 Pod 重新变得健康所需要采取的动作。
在 Kubernetes API 中,Pod 包含规约部分和实际状态部分。 Pod 对象的状态包含了一组 Pod 状况(Conditions)。 如果应用需要的话,你也可以向其中注入自定义的就绪性信息。
Pod 在其生命周期中只会被调度一次。 一旦 Pod 被调度(分派)到某个节点,Pod 会一直在该节点运行,直到 Pod 停止或者 被终止

阶段

Pod 生命周期的变化,主要体现在 Pod API 对象的 Status 部分,这是它除了 Metadata 和 Spec 之外的第三个重要字段。其中,pod.status.phase,就是 Pod 的当前状态,它有如下几种可能的情况:

  1. Pending。这个状态意味着,Pod 的 YAML 文件已经提交给了 Kubernetes,API 对象已经被创建并保存在 Etcd 当中。但是,这个 Pod 里有些容器因为某种原因而不能被顺利创建。比如,调度不成功。
  2. Running。这个状态下,Pod 已经调度成功,跟一个具体的节点绑定。它包含的容器都已经创建成功,并且至少有一个正在运行中。
  3. Succeeded。这个状态意味着,Pod 里的所有容器都正常运行完毕,并且已经退出了。这种情况在运行一次性任务时最为常见。
  4. Failed。这个状态下,Pod 里至少有一个容器以不正常的状态(非 0 的返回码)退出。这个状态的出现,意味着你得想办法 Debug 这个容器的应用,比如查看 Pod 的 Events 和日志。
  5. Unknown。这是一个异常状态,意味着 Pod 的状态不能持续地被 kubelet 汇报给 kube-apiserver,这很有可能是主从节点(Master 和 Kubelet)间的通信出现了问题。
    更进一步地,Pod 对象的 Status 字段,还可以再细分出一组 Conditions。这些细分状态的值包括:PodScheduledReadyInitialized,以及 Unschedulable。它们主要用于描述造成当前 Status 的具体原因是什么。
    比如,Pod 当前的 Status 是 Pending,对应的 Condition 是 Unschedulable,这就意味着它的调度出现了问题。

容器Containers

常用定义

spec:
  containers:
  - name: lifecycle-demo-container
    image: nginx
    lifecycle:
      postStart:
        exec:
          command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
      preStop:
        exec:
          command: ["/usr/sbin/nginx","-s","quit"]

postStart 容器启动后通过 echo 命令写入一段欢迎信息。

执行在 Docker 容器 ENTRYPOINT 执行之后,但不保证严格顺序,也就是说,在 postStart 启动时,ENTRYPOINT 有可能还没有结束。

preStop 容器被杀死之前(比如,收到了 SIGKILL 信号)调用 nginx 退出命令,优雅停止。

而需要明确的是,preStop 操作的执行,是同步的。所以,它会阻塞当前的容器杀死流程,直到这个 Hook 定义操作完成之后,才允许容器被杀死,这跟 postStart 不一样。
节点上的 kubelet 将等待最多宽限期(在 Pod 上指定,或从命令行传递;默认为 30 秒)以关闭容器,然后强行终止进程(使用 SIGKILL)。请注意,此宽限期包括执行 preStop 勾子的时间。

容器探针

健康检查

在 Kubernetes 中,你可以为 Pod 里的容器定义一个健康检查“探针”(Probe)。这样,kubelet 就会根据这个 Probe 的返回值决定这个容器的状态,而不是直接以容器镜像是否运行(来自 Docker 返回的信息)作为依据。
这种机制,是生产环境中保证应用健康存活的重要手段
这是一个livenessProbe探针的使用示例

spec:
  containers:
  - name: liveness
    image: busybox
    args:
    - /bin/sh
    - -c
    - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
    livenessProbe:
      exec:
        command:
        - cat
        - /tmp/healthy
      initialDelaySeconds: 5
      periodSeconds: 5

initialDelaySeconds:从启动后5s执行检查
periodSeconds:每5s执行一次检查
成功返回0,Pod就认为此容器启动成功
除了在容器中执行命令外,livenessProbe 也可以定义为发起 HTTP 或者 TCP 请求的方式

livenessProbe:
     httpGet:
       path: /healthz
       port: 8080
       httpHeaders:
       - name: X-Custom-Header
         value: Awesome
       initialDelaySeconds: 3
       periodSeconds: 3
---
    livenessProbe:
      tcpSocket:
        port: 8080
      initialDelaySeconds: 15
      periodSeconds: 20

所以,你的 Pod 其实可以暴露一个健康检查 URL(比如 /healthz),或者直接让健康检查去检测应用的监听端口。这两种配置方法,在 Web 服务类的应用中非常常用。

恢复机制

Kubernetes 里的 Pod 恢复机制,也叫 restartPolicy。它是 Pod 的 Spec 部分的一个标准字段(pod.spec.restartPolicy),默认值是 Always,即:任何时候这个容器发生了异常,它一定会被重新创建。
但一定要强调的是,Pod 的恢复过程,永远都是发生在当前节点上,而不会跑到别的节点上去。

事实上,一旦一个 Pod 与一个节点(Node)绑定,除非这个绑定发生了变化(pod.spec.node 字段被修改),否则它永远都不会离开这个节点。

这也就意味着,如果这个宿主机宕机了,这个 Pod 也不会主动迁移到其他节点上去。

而如果你想让 Pod 出现在其他的可用节点上,就必须使用 Deployment 这样的“控制器”来管理 Pod,哪怕你只需要一个 Pod 副本。

除了 Always,它还有 OnFailure 和 Never 两种情况:

而如果你要关心这个容器退出后的上下文环境,比如容器退出后的日志、文件和目录,就需要将 restartPolicy 设置为 Never。

PodPrese

PodPreset 是一种 K8s API 资源,用于在创建 Pod 时注入其他运行时需要的信息,这些信息包括 secrets、volume mounts、environment variables 等。

可以看做是 Pod 模板。

首先定义一个 PodPreset 对象,把想要的字段都加进去:

apiVersion: settings.k8s.io/v1alpha1
kind: PodPreset
metadata:
  name: allow-database
spec:
  selector:
    matchLabels:
      role: frontend
  env:
    - name: DB_PORT
      value: "6379"
  volumeMounts:
    - mountPath: /cache
      name: cache-volume
  volumes:
    - name: cache-volume
      emptyDir: {}

通过matchLabels:role: frontend匹配到对应的Pod,然后k8s会自动把PodPreset对象里的预定义的字段添加进去,这里就是envvolumeMountsvolumes3个字段。
然后我们写一个简单的Pod

apiVersion: v1
kind: Pod
metadata:
  name: website
  labels:
    app: website
    role: frontend
spec:
  containers:
    - name: website
      image: nginx
      ports:
        - containerPort: 80

其中的 Label role: frontend和PodPreset allow-database 匹配,所以会在创建Pod之前自动把预定义字段添加进去。
需要说明的是,PodPreset 里定义的内容,只会在 Pod API 对象被创建之前追加在这个对象本身上,而不会影响任何 Pod 的控制器的定义。

Pod 的终止

由于 Pod 所代表的是在集群中节点上运行的进程,当不再需要这些进程时允许其体面地 终止是很重要的。一般不应武断地使用 KILL 信号终止它们,导致这些进程没有机会 完成清理操作。
具体停止过程大致如下:

所以在程序中监听该信号可以实现优雅关闭。

Volume

概念

k8s 中的 Volume 属于 Pod 内部共享资源存储,生命周期和 Pod 相同,与 Container 无关,即使 Pod 上的容器停止或者重启,Volume 不会受到影响,但是如果 Pod 终止,那么这个 Volume 的生命周期也将结束。
这样的存储无法满足有状态服务的需求,于是推出了 Persistent Volume,故名思义,持久卷是能将数据进行持久化存储的一种资源对象。它是独立于Pod的一种资源,是一种网络存储,它的生命周期和 Pod 无关。云原生的时代,持久卷的种类也包括很多,iSCSI,RBD,NFS,以及CSI, CephFS, OpenSDS, Glusterfs, Cinder 等网络存储。可以看出,在kubernetes 中支持的持久卷基本上都是网络存储,只是支持的类型不同
k8s 中支持多种类型的卷(持久卷),比如:

挂载流程

Kubernetes 的 Volume 挂载流程分为两大部分:

  1. Volume 准备阶段:包括将远程存储设备挂载到宿主机。
  2. Volume 挂载到 Pod 容器阶段:将宿主机上的目录绑定到容器中。
    这两部分由 kube-controller-managerkubelet 和 Container Runtime Interface (CRI) 共同完成。

Volume 挂载至宿主机(远程存储 Volume)

只有远程存储(使用 PV)才需要通过此阶段,将存储设备挂载到宿主机。过程包括两个主要步骤:

Attach 阶段

负责将远程磁盘通过网络挂载到宿主机,以下是详细流程:

Mount 阶段

负责将挂载到宿主机的磁盘设备格式化并挂载到 Volume 对应的宿主机目录。

Volume 挂载到 Pod 容器

这一步由 CRI 负责,将宿主机目录通过 Bind Mount 挂载到 Pod 中。

PV (持久化存储数据卷)

有什么用

我提供什么,不关心谁在用我

PV 是集群中一块已经配置好的存储,是集群级别的资源。它如同节点(Node)一样,是构成集群基础设施的一部分。

PVC (PV 使用请求/持久卷声明)

有什么用

我需要什么,不关心存储的实现

PVC 是用户对存储资源的“申请”。它将应用对存储的需求与底层存储的实现细节解耦。

StorageClass(存储类/PV 的创建模板)

Volume Binding Mode(卷绑定模式)

关于k8s中StorageClass的VOLUMEBINDINGMODE属性的两种模式的差别

  1. Immediate
  1. WaitForFirstConsumer

创建 StorageClass & provisione

首先由运维人员创建对应 StorageClass 并在 K8S 集群中运行配套的 provisioner 组件。

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-sc
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
parameters:
  archiveOnDelete: "false"
reclaimPolicy: "Delete"
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner-nfs-sc

StorageClass 中的 provisioner 指明了要使用的 provisioner,然后以 deployment 方式部署对应的 provisioner:

当然这里还需要 RBAC 权限等配置,比较多先省略了,完整配置见 nfs-subdir-external-provisioner

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-client-provisioner-nfs-sc
  labels:
    app: nfs-client-provisioner-nfs-sc
  namespace: kube-system
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: nfs-client-provisioner-nfs-sc
  template:
    metadata:
      labels:
        app: nfs-client-provisioner-nfs-sc
    spec:
      serviceAccountName: nfs-client-provisioner
      tolerations:
      - key: "node-role.kubernetes.io/master"
        operator: "Exists"
        effect: "NoSchedule"
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: node-role.kubernetes.io/master
                operator: In
                values:
                - ""
      containers:
        - name: nfs-client-provisioner
          image: caas4/nfs-subdir-external-provisioner:v4.0.2
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: k8s-sigs.io/nfs-subdir-external-provisioner-nfs-sc
            - name: NFS_SERVER
              value: 172.20.151.105
            - name: NFS_PATH
              value: /tmp/nfs/data
      volumes:
        - name: nfs-client-root
          nfs:
            server: 172.20.151.105
            path: /tmp/nfs/data

其中以环境变量的方式指明了 PROVISIONER_NAME 为 k8s-sigs.io/nfs-subdir-external-provisioner-nfs-sc,以及 NFS 的相关参数。

StorageClass 中的 provisioner: k8s-sigs.io/nfs-subdir-external-provisioner-nfs-sc 和这里的 PROVISIONER_NAME 是对应的,因此可以找到对应的 provisioner。

创建 PVC

然后开发人员创建需要的 PVC,比如

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: test-claim
spec:
  storageClassName: nfs-sc
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Mi

需要注意的一点是 storageClassName 必须和前面创建的 StorageClass 对应。

PVC 创建之后就轮到 PersistentVolumeController 登场了。

PersistentVolumeController 会根据 PVC 寻找对应的 PV 来进行绑定,没有的话自然无法绑定。

这时候我们前面创建的 provisioner 就起作用了,他会 watch pvc 对象,比如这里我们创建了 PVC,provisioner 就会收到相应事件,然后根据 PVC 中的 storageClassName 找到对应 StorageClass,然后根据 StorageClass中的 provisioner 字段找到对应 provisioner,发现是自己处理的,就 调用 CSI Plugin 的接口 CreateVolume 创建出 volume,然后在 k8s 里创建对应的 PV 来指代这个 volume。

CSI Plugin CreateVolume 接口则由具体厂商实现,比如 阿里云实现的 CreateVolume 可能就是在阿里云上创建了一块云盘。

最后 PV 创建之后,PersistentVolumeController 就将二者进行绑定。

创建 Pod

PVC 和 PV 绑定之后就可以使用了,创建一个 Pod 来使用这个 PVC:

kind: Pod
apiVersion: v1
metadata:
  name: test-pod
spec:
  containers:
  - name: test-pod
    image: busybox:stable
    command:
      - "/bin/sh"
    args:
      - "-c"
      - "touch /mnt/SUCCESS && exit 0 | exit 1"
    volumeMounts:
      - name: nfs-pvc
        mountPath: "/mnt"
  restartPolicy: "Never"
  volumes:
    - name: nfs-pvc
      persistentVolumeClaim:
        claimName: test-claim

这里就是通过 claimName 来指定要使用的 PVC。

Pod 创建之后 k8s 就可以根据 claimName 找到对应 PVC,然后 PVC 绑定的时候会把 PV 的名字填到 spec.volumeName 字段上,因此这里又可以找到对应的 PV,然后就进入到第二节中的挂载流程了。

小结

至此,k8s 的这套持久化存储体系运作流程就算是完成了。流程如下图所示:

dynamic-provisioning

Projected Volume

为什么叫Projected Volume?

它们存在的意义不是为了存放容器里的数据,也不是用来进行容器和宿主机之间的数据交换。
这些特殊 Volume 的作用,是为容器提供预先定义好的数据。
所以,从容器的角度来看,这些 Volume 里的信息就是仿佛是被 Kubernetes“投射”(Project)进入容器当中的。这正是 Projected Volume 的含义。

到目前为止,Kubernetes 支持的 Projected Volume 一共有四种:

ConfigMap

创建

ConfigMap主要提供配置文件的键值对,主要用法如下

# 创建
$ kubectl create configmap 
# 删除
$ kubectl delete configmap ConfigMap名称
# 编辑
$ kubectl edit configmap ConfigMap名称
# 查看-列表
$ kubectl get configmap
# 查看-详情
$ kubectl describe configmap ConfigMap名称

可以使用 kubectl create configmap 从以下多种方式创建 ConfigMap。

使用

可以通过环境变量或者文件的形式挂载到Pod中(每一个键对应一个文件)
环境变量

envFrom:
    - configMapRef:
        name: env-cm

文件

volumes:
- name: foo
  configMap:
    name: cm1

Secret

Secret主要用于为Pod提供密码token等敏感数据,内部数据都是加密之后的
ServiceAccountToken 一种特殊的 Secret,是 Kubernetes 系统内置的一种“服务账户”,它是 Kubernetes 进行权限分配的对象。

平时定义Pod也没特意挂载Service Account?

为了方便使用,Kubernetes 已经为你提供了一个默认“服务账户”(default Service Account)。并且,任何一个运行在 Kubernetes 里的 Pod,都可以直接使用这个默认的 Service Account,而无需显示地声明挂载它(k8s 默认会为每一个Pod 都挂载该Volume)。

Secret有三种类型

创建

Secret 同样有多种创建方式

$ echo -n 'world'|base64
d29ybGQ=

然后填入这样的yaml中

apiVersion: v1
kind: Secret
metadata: 
  name: mysecret
type: Opaque
data:  
  hello: d29ybGQ=

使用

用环境变量加载Secret

env:
	- name: CUSTOM_HELLO
	  valueFrom:
        secretKeyRef:
          name: s2
          key: hello

通过卷挂载方式,同样一个键对应一个文件

    volumeMounts:
    - name: config
      mountPath: "/etc/foo"
      readOnly: true
  volumes:
  - name: config
    projected:
      sources:
      - secret:
          name: mysecret

Downward API

没有内容?

我也不清楚怎么用,以后补完

Controller (控制器)

Kubernetes 中的 Controller(控制器)是其自动化和自愈能力的核心。所有控制器都遵循一个通用的编排模式,即:控制循环(control loop)
这个循环的核心思想是不断地将系统的 “实际状态” (Actual State)与用户定义的 “期望状态” (Desired State)进行比较,并通过一系列操作来消除它们之间的差异。
这个过程可以用以下伪代码来描述:

for {
  期望状态 := 获取对象 X 的期望状态 (Desired State)
  实际状态 := 获取集群中对象 X 的实际状态 (Actual State)

  if 实际状态 == 期望状态 {
    // 状态一致,什么都不做
  } else {
    // 状态不一致,执行编排动作,将实际状态调整为期望状态
    执行调整(期望状态, 实际状态)
  }
}

Kubernetes 内置了多种控制器(如 Deployment, ReplicaSet, StatefulSet, DaemonSet 等),每种控制器负责管理特定类型的资源,共同构成了 Kubernetes 强大的集群管理能力。

Deployment (部署)

Deployment 是 Kubernetes 中最常用、也是最具代表性的控制器之一。它为 Pod 和 ReplicaSet 提供了一种声明式的管理方式。实际上,它是一个两层控制器,其设计精妙地将 “版本管理”“副本数量保证” 这两个职责分离开来。

注:Deployment 控制 ReplicaSet(负责版本),ReplicaSet 控制 Pod(负责副本数)。这个两层控制关系是理解 Deployment 工作原理的关键。

通过这种方式,Deployment 不仅能保证 Pod 稳定地维持在指定数量,还能轻松实现滚动更新(Rolling Update)和版本回滚(Rollback)等高级功能,是 Kubernetes 无状态应用编排的最佳实践。

ReplicaSet (副本集)

ReplicaSet 是一个相对简单的控制器,它的核心职责只有一个:保证在任何时候都有指定数量的 Pod 副本在稳定运行。它是实现应用高可用的基础。
ReplicaSet 的工作机制同样遵循控制循环模式。它通过一个标签选择器(Label Selector)来识别它应该管理的 Pod 集合。

注:虽然 ReplicaSet 可以独立使用,但在现代 Kubernetes 实践中,用户几乎不应直接操作 ReplicaSet。我们应该始终通过 Deployment 来管理应用。Deployment 会自动创建和管理其背后的 ReplicaSet,从而为我们屏蔽了版本管理的复杂细节。

网络

Kubernetes教程(二)---集群网络之 Flannel 核心原理 - 指月小筑(探索云原生)
Flannel的VXLAN模式性能远好于UDP模式,原因是UDP模式需要频繁切换用户态和内核态