极客时间 《Kubernetes 入门实战课》 by 罗剑锋
github上的配套学习项目

全笔记:
《开篇词 (2讲)》
《入门篇 (8讲)》
《初级篇 (9讲)》
《中级篇 (8讲)》
《高级篇 (10讲)》
《加餐分享 (2讲)》

《24|PersistentVolume:怎么解决数据持久化的难题?》

  • 什么是 PersistentVolume
    PersistentVolume 对象(PV),它专门用来表示持久存储设备,但隐藏了存储的底层实现。
    作为存储的抽象,PV 实际上就是一些存储设备、文件系统,比如 Ceph、GlusterFS、NFS,甚至是本地磁盘,管理它们已经超出了 Kubernetes 的能力范围,所以,一般会由系统管理员单独维护,然后再在 Kubernetes 里创建对应的 PV。

PV 属于集群的系统资源,是和 Node 平级的一种对象,Pod 对它没有管理权,只有使用权。
我:在哪里执行 kubectl apply -f来生成PV,与PV实际挂载在哪个结点无关

  • 什么是 PersistentVolumeClaim/StorageClass
    PersistentVolumeClaim,简称 PVC,就是用来向 Kubernetes 申请存储资源的。PVC 是给 Pod 使用的对象,它相当于是 Pod 的代理,代表 Pod 向系统申请 PV。一旦资源申请成功,Kubernetes 就会把 PV 和 PVC 关联在一起,这个动作叫做绑定(bind)。
    StorageClass 抽象了特定类型的存储系统(比如 Ceph、NFS),在 PVC 和 PV 之间充当“协调人”的角色,帮助 PVC 找到合适的 PV。
    在这里插入图片描述

  • 如何使用 YAML 描述 PersistentVolume

apiVersion: v1
kind: PersistentVolume
metadata:
  name: host-10m-pv

spec:
  storageClassName: host-test
  accessModes:
  - ReadWriteOnce
  capacity:
    storage: 10Mi
  hostPath:
    path: /tmp/host-10m-pv/

accessModes定义了存储设备的访问模式,简单来说就是虚拟盘的读写权限,和 Linux 的文件访问模式差不多,目前 Kubernetes 里有 3 种:
ReadWriteOnce:存储卷可读可写,但只能被一个节点上的 Pod 挂载。
ReadOnlyMany:存储卷只读不可写,可以被任意节点上的 Pod 多次挂载。
ReadWriteMany:存储卷可读可写,也可以被任意节点上的 Pod 多次挂载。
注意,这 3 种访问模式限制的对象是节点而不是 Pod。
豆包:accessModes 本质就是控制 PV 跨节点并发挂载的权限:
ReadOnlyMany/ReadWriteMany:支持多节点同时挂载;
RWO:仅允许单个节点同时挂载,其余节点同一时间无法绑定该 PV

字段hostPath指定了存储卷的本地路径,也就是我们在节点上创建的目录。
我注:与本文不同的是,我没有创建该目录,相关pod挂载后它自动创建在结点了。评论区有人说这取决于hostPath.type

  • 如何使用 YAML 描述 PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: host-5m-pvc

spec:
  storageClassName: host-test
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Mi
  • 如何在 Kubernetes 里使用 PersistentVolume
    一旦 PVC 对象创建成功,Kubernetes 就会立即通过 StorageClass、resources 等条件在集群里查找符合要求的 PV,如果找到合适的存储对象就会把它俩“绑定”在一起,你会看到这两个对象的状态都是 Bound :
    在这里插入图片描述
    我问:可以看到当PV和PVC对象创建成功,因为符合条件它俩立刻绑定了。但这时PV既未挂载到哪个结点,PVC也未实际关联到任何Pod。PV的accessModes定义了其访问模式,当模式为RWO:仅允许单个节点同时挂载,其余节点同一时间无法绑定该 PV。如果随后我们声明两个Pod, 它俩都使用这个PVC,故意让这俩pod调度到两个结点上,岂不会自相矛盾?
    豆包:第一个 Pod 调度到 node1:kubelet 成功挂载 PV,PV 锁定 node1;
    第二个 Pod 调度器在筛选节点时,检测到该 PV 已被 node1 占用,只会尝试把它调度到 node1,不会分配到其他节点

  • 如何为 Pod 挂载 PersistentVolume
    为 Pod 挂载存储卷。用法和第 14 讲里差不多,先要在 spec.volumes 定义存储卷,然后在 spec.containers.volumeMounts 挂载进容器 :
    在这里插入图片描述

《25|PersistentVolume + NFS:怎么使用网络共享存储?》

上节课。。。我们使用的是 HostPath。。。而 Kubernetes 里的 Pod 经常会在集群里“漂移”,所以这种方式不是特别实用。
要改成网络存储,这样 Pod 无论在哪里运行,只要知道 IP 地址或者域名,就可以通过网络通信访问存储设备。
今天的这次课里,我选择了相对来说比较简单的 NFS 系统(Network File System),以它为例讲解如何在 Kubernetes 里使用网络存储,以及静态存储卷和动态存储卷的概念。

  • 如何安装 NFS 服务器
    NFS 采用的是 Client/Server 架构,需要选定一台主机作为 Server,安装 NFS 服务端;其他要使用存储的主机作为 Client,安装 NFS 客户端工具。
sudo apt -y install nfs-kernel-server

给 NFS 指定一个存储位置,也就是网络共享目录。这里为了简单起见,使用了临时目录 /tmp/nfs :mkdir -p /tmp/nfs

配置 NFS 访问共享目录,修改 /etc/exports文件,指定目录名、允许访问的网段,还有权限等参数。e.g. 在此文件加下面这行:/tmp/nfs 192.168.56.0/8(rw,sync,no_subtree_check,no_root_squash,insecure)

改好之后,需要用 exportfs -ra 通知 NFS,让配置生效,再用 exportfs -v 验证效果:
在这里插入图片描述

使用 systemctl 来启动 NFS 服务器:

sudo systemctl start  nfs-server
sudo systemctl enable nfs-server
sudo systemctl status nfs-server

还可以使用命令 showmount 来检查 NFS 的网络挂载情况:
在这里插入图片描述

  • 如何安装 NFS 客户端
    sudo apt -y install nfs-common。安装完成后也可以用 showmount 检查 NFS 能否正常挂载:
    在这里插入图片描述

现在让我们尝试手动挂载一下 NFS 网络存储,先创建一个目录 /tmp/test 作为挂载点,然后用命令 mount 把 NFS 服务器的共享目录挂载到刚才创建的本地目录上:sudo mount -t nfs 192.168.56.105:/tmp/nfs /tmp/test
最后测试一下,我们在 /tmp/test 里随便创建一个文件,再回到NFS服务器检查共享目录 /tmp/nfs,也会看到一个同样的文件。

  • 如何使用 NFS 存储卷

先来手工分配一个存储卷。。accessModes 可以设置成 ReadWriteMany,这是由 NFS 的特性决定的,它支持多个节点同时访问一个共享目录。因为这个存储卷是 NFS 系统,所以我们还需要在 YAML 里添加 nfs 字段,指定 NFS 服务器的 IP 地址和共享目录名。我的补充:若不创建目录,后面创建相关Pod时会失败:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-1g-pv

spec:
  storageClassName: nfs
  accessModes:
    - ReadWriteMany
  capacity:
    storage: 1Gi

  nfs:
    path: /tmp/nfs/1g-pv
    server: 192.168.0.105

创建PVC 对象,这里期望容量写成1GB,和 PV 的容量相同 :

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-static-pvc

spec:
  storageClassName: nfs
  accessModes:
    - ReadWriteMany

  resources:
    requests:
      storage: 1Gi

创建 PV和PVC 对象之后,根据它俩的描述,Kubernetes会把它们绑定在一起。
再创建一个 Pod,和上节课一样,用 persistentVolumeClaim 指定 PVC 的名字,把 PVC 挂载成它的一个 volume :
在这里插入图片描述
我们在k8s节点上安装了 NFS 客户端,Kubernetes会自动执行 NFS 挂载动作,把 NFS 的共享目录 /tmp/nfs/1g-pv 挂载到 Pod 里的 /tmp,完全不需要我们去手动管理。
因为 NFS 是一个网络服务,不会受 Pod 调度位置的影响,所以只要网络通畅,这个 PV 对象就会一直可用,数据也就实现了真正的持久化存储。

  • 如何部署 NFS Provisoner
    能不能让创建 PV 的工作实现自动化呢?或者说,让计算机来代替人类来分配存储卷呢?这个在 Kubernetes 里就是“动态存储卷”的概念,它可以用 StorageClass 绑定一个 Provisioner 对象,而这个 Provisioner 就是一个能够自动管理存储、创建 PV 的应用
    目前,Kubernetes 里每类存储设备都有相应的 Provisioner 对象,对于 NFS 来说,它的 Provisioner 就是NFS subdir external provisioner。NFS Provisioner 也是以 Pod 的形式运行在 Kubernetes 里的,在 GitHub 的 deploy 目录里是部署它所需的 YAML 文件,一共有三个,分别是 rbac.yamlclass.yamldeployment.yaml
    第一个要修改的是 rbac.yaml,它使用的是默认的 default 名字空间,应该把它改成其他的名字空间比如kube-system,避免与普通应用混在一起。
    第二个要修改的是 deployment.yaml,它要修改的地方比较多。首先要把名字空间改成和 rbac.yaml 一样,比如是 kube-system,然后重点要修改 volumes 和 env 里的 IP 地址和共享目录名,必须和集群里的 NFS 服务器配置一样:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-client-provisioner
  labels:
    app: nfs-client-provisioner
  # replace with namespace where provisioner is deployed
  namespace: kube-system
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: nfs-client-provisioner
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccountName: nfs-client-provisioner
      containers:
        - name: nfs-client-provisioner
          image: m.daocloud.io/registry.k8s.io/sig-storage/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
            - name: NFS_SERVER
              value: 192.168.56.105        #改IP地址
            - name: NFS_PATH
              value: /tmp/nfs              #改共享目录名
      volumes:
        - name: nfs-client-root
          nfs:
            server: 192.168.56.105        #改IP地址
            path: /tmp/nfs              #改共享目录名

这里改了镜像地址,在前面加m.daocloud.io/1

就可以在 Kubernetes 里创建 NFS Provisioner 了:

kubectl apply -f rbac.yaml
kubectl apply -f class.yaml
kubectl apply -f deployment.yaml
  • 如何使用 NFS 动态存储卷
    因为有了 Provisioner,我们就不再需要手工定义 PV 对象了,只需要在 PVC 里指定 StorageClass 对象,它再关联到 Provisioner。
    接下来我们定义一个 PVC,向系统申请 10MB 的存储空间:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-dyn-10m-pvc

spec:
  storageClassName: nfs-client
  accessModes:
    - ReadWriteMany

  resources:
    requests:
      storage: 10Mi

创建好 PVC后可以看到,由于有 NFS Provisioner,自动创建了一个 PV,大小刚好是在 PVC 里申请的 10MB:
在这里插入图片描述
在NFS 服务器上查看共享目录,也会发现多出了一个目录:
在这里插入图片描述
在Pod 里还是用 volumes 和 volumeMounts 挂载:
在这里插入图片描述

  • 小结

《26|StatefulSet:怎么管理有状态的应用?》

  • 什么是有状态的应用
    有一些应用,运行状态信息就很重要了,如果因为重启而丢失了状态是绝对无法接受的,这样的应用就是“有状态应用”。“有状态应用”的例子也有很多,比如 Redis、MySQL 这样的数据库
    “状态”不仅仅是数据持久化。。。只使用 Deployment,多个实例之间是无关的,启动的顺序不固定,Pod 的名字、IP 地址、域名也都是完全随机的,这正是“无状态应用”的特点。但对于“有状态应用”,多个实例之间可能存在依赖关系,比如 master/slave、active/passive,需要依次启动才能保证应用正常运行,外界的客户端也可能要使用固定的网络标识来访问实例,而且这些信息还必须要保证在 Pod 重启后不变。

  • 如何使用 YAML 描述 StatefulSet
    和 DaemonSet 类似,StatefulSet 也可以看做是 Deployment 的一个特例
    YAML 文件里除了 kind 必须是“StatefulSet”,在 spec 里还多出了一个serviceName字段,其余的部分和 Deployment 是一模一样的:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis-sts

spec:
  serviceName: redis-svc
  replicas: 2
  selector:
    matchLabels:
      app: redis-sts

  template:
    metadata:
      labels:
        app: redis-sts
    spec:
      containers:
      - image: redis:5-alpine
        name: redis
        ports:
        - containerPort: 6379
  • 如何在 Kubernetes 里使用 StatefulSet

StatefulSet 所管理的 Pod 不再是随机的名字了,而是有了顺序编号,从 0 开始分别被命名为 redis-sts-0、redis-sts-1,Kubernetes 也会按照这个顺序依次创建(0 号比 1 号的 AGE 要长一点),这就解决了“有状态应用”的第一个问题:启动顺序。

在 Pod 里查看环境变量 $HOSTNAME 或者是执行命令 hostname,都可以得到这个 Pod 的名字 redis-sts-0。
有了这个唯一的名字,应用就可以自行决定依赖关系了

第三个问题:网络标识,这就需要用到 Service 对象。
写 Service 对象时,metadata.name 必须和 StatefulSet 里的 serviceName 相同,selector 里的标签也必须和 StatefulSet 里的一致:

apiVersion: v1
kind: Service
metadata:
  name: redis-svc

spec:
  selector:
    app: redis-sts

  ports:
  - port: 6379
    protocol: TCP
    targetPort: 6379

Service 发现这些 Pod 不是一般的应用,而是有状态应用,需要有稳定的网络标识,所以就会为 Pod 再多创建出一个新的域名,格式是“Pod 名. 服务名. 名字空间.svc.cluster.local”。当然,这个域名也可以简写成“Pod 名. 服务名”。进入 Pod 内部,用 ping 命令来验证一下:
在这里插入图片描述
我们可以在 Service 里添加一个字段 clusterIP: None ,告诉 Kubernetes 不必再为这个对象分配 IP 地址。我的补充: clusterIP: None即Headless Service,对它DNS解析会返回所有 Pod IP

在这里插入图片描述

  • 如何实现 StatefulSet 的数据持久化
    为了强调持久化存储与 StatefulSet 的一对一绑定关系,Kubernetes 为 StatefulSet 专门定义了一个字段volumeClaimTemplates,直接把 PVC 定义嵌入 StatefulSet 的 YAML 文件里:
    在这里插入图片描述
    StatefulSet创建成功后,就可以看到其关联的存储卷了:
    在这里插入图片描述
    看这两个 PVC 的命名,不是随机的,是有规律的,用的是 PVC 名字加上 StatefulSet 的名字组合而成,所以即使 Pod 被销毁,因为它的名字不变,还能够找到这个 PVC,再次绑定使用之前存储的数据。

《27|滚动更新:如何做到平滑的应用升级降级?》

  • Kubernetes 如何定义应用版本
    在 Kubernetes 里应用的版本变化就是 template 里 Pod 的变化,哪怕 template 里只变动了一个字段,那也会形成一个新的版本,也算是版本变化。
    Kubernetes用摘要算法计算 template 的 Hash 值作为版本号
    Pod 名字里的那串随机数就是 Pod 模板的 Hash 值,也就是 Pod 的版本号

  • Kubernetes 如何实现应用更新
    查看应用更新的状态:kubectl rollout status deployment ngx-dep :
    在这里插入图片描述
    使用命令 kubectl describe deploy可以更清楚地看到 Pod 的变化情况:
    在这里插入图片描述
    滚动更新就是由 Deployment 控制的两个同步进行的“应用伸缩”操作,老版本缩容到 0,同时新版本扩容到指定值,是一个“此消彼长”的过程。
    在这里插入图片描述

  • Kubernetes 如何管理应用更新
    在Deployment更新的过程中,可以随时使用 kubectl rollout pause 来暂停更新,用 kubectl rollout resume 来继续更新。
    查看更新历史使用的命令是 kubectl rollout history ,可以在命令后加上参数 --revision 来查看每个版本的详细信息。
    想要回退到上一个版本,就可以使用命令 kubectl rollout undo,也可以加上参数 --to-revision 回退到任意一个历史版本:
    在这里插入图片描述
    kubectl rollout undo 的操作过程其实和 kubectl apply 是一样的,执行的仍然是“滚动更新”,只不过使用的是旧版本 Pod 模板,把新版本 Pod 数量收缩到 0,同时把老版本 Pod 扩展到指定值。

  • Kubernetes 如何添加更新描述
    Deployment 的 metadata.annotations字段的值可以任意写,Kubernetes 会自动忽略不理解的 Key-Value,但要编写更新说明就需要使用特定的字段 kubernetes.io/change-cause
    在这里插入图片描述

  • 小结
    另外,在 Deployment 里还有其他一些字段可以对滚动更新的过程做更细致的控制,它们都在 spec.strategy.rollingUpdate 里,比如 maxSurge、maxUnavailable 等字段,分别控制最多新增 Pod 数和最多不可用 Pod 数。maxSurge and maxUnavailable cannot both be zero

  • 评论区:
    其实Deployment并不是直接控制Pod,Pod的归属对象是ReplicaSet,也就是说Deployment控制的是ReplicaSet(版本这个概念其实我们可以等同于是ReplicaSet),然后ReplicaSet控制Pod的数量。我们可以通过 kubectl get rs 来看下具体内容:
    在这里插入图片描述
    我:Deployment创建、修改 Pod 模板都会新建 ReplicaSet。Deployment滚动更新的原理是,依据 maxSurge、maxUnavailable 循环运行以下步骤:
    1,上调新 RS 的 spec.replicas → 新RS创建Pod
    2,等待新 Pod 就绪 + 等待 minReadySeconds时长
    3,缩减旧 RS 的 spec.replicas → 旧RS销毁Pod
    重复直到新 RS副本数等于期待值,旧RS副本数归 0

《28|应用保障:如何让Pod运行得更健康?》

  • 容器资源配额
    Pod 的字段 spec.containers.resources,下面有两个字段:requests,limits,分别表示容器使用资源的下/上限。e.g. :
apiVersion: v1
kind: Pod
metadata:
  name: ngx-pod-resources

spec:
  containers:
  - image: nginx:alpine
    name: ngx

    resources:
      requests:
        cpu: 10m
        memory: 100Mi
      limits:
        cpu: 20m
        memory: 200Mi

Kubernetes 允许容器精细分割 CPU,其实是效仿了UNIX 时间片的用法,最小单位m表示 千分之一 的 CPU 时间,比如此Pod向系统申请的是 1% 的 CPU 时间,运行时的上限是 2%的CPU 时间。

  • 什么是容器状态探针
    Kubernetes 为检查应用状态定义了三种探针Probe,也可以叫探测器),它们分别对应容器不同的状态:
    Startup,启动探针,用来检查应用是否已经启动成功,适合那些有大量初始化工作要做,启动很慢的应用。
    Liveness,存活探针,用来检查应用是否正常运行,是否存在死锁、死循环。
    Readiness,就绪探针,用来检查应用是否可以接收流量,是否能够对外提供服务。

如果一个 Pod 里的容器配置了探针,Kubernetes 在启动容器后就会不断地调用探针来检查容器的状态:

  1. 如果 Startup 探针失败,Kubernetes 会认为容器没有正常启动,就会尝试反复重启,后面的 Liveness 探针和 Readiness 探针也不会启动。如果成功,会并行无限循环探测Liveness 和 Readiness。
  2. 如果 Liveness 探针失败,Kubernetes 就会认为容器发生了异常,也会重启容器。
  3. 如果 Readiness 探针失败,Kubernetes 会认为容器虽然在运行,但内部有错误,不能正常提供服务,就会把容器从 Service 对象的负载均衡集合中排除,不会给它分配流量。
  • 如何使用容器状态探针

startupProbe、livenessProbe、readinessProbe 这三种探针的配置方式都是一样的,关键字段有这么几个:
periodSeconds,执行探测动作的时间间隔,默认是 10 秒探测一次。
timeoutSeconds,探测动作的超时时间,如果超时就认为探测失败,默认是 1 秒。
successThreshold,连续几次探测成功才认为是正常,对于 startupProbe 和 livenessProbe 来说它只能是 1。
failureThreshold,连续探测失败几次才认为是真正发生了异常,默认是 3 次。

至于探测方式,Kubernetes 支持 3 种:Shell、TCP Socket、HTTP GET,它们也需要在探针里配置:
exec,执行一个 Linux 命令,比如 ps、cat 等等,和 container 的 command 字段很类似。
tcpSocket,使用 TCP 协议尝试连接容器的指定端口。
httpGet,连接端口并发送 HTTP GET 请求。

e.g.:

apiVersion: v1
kind: ConfigMap
metadata:
  name: ngx-conf

data:
  default.conf: |
    server {
      listen 80;
      location = /ready {
        return 200 'I am ready';
      }
    }

---

apiVersion: v1
kind: Pod
metadata:
  name: ngx-pod-probe

spec:
  volumes:
  - name: ngx-conf-vol
    configMap:
      name: ngx-conf

  containers:
  - image: nginx:alpine
    name: ngx
    ports:
    - containerPort: 80

    volumeMounts:
    - mountPath: /etc/nginx/conf.d
      name: ngx-conf-vol

    startupProbe:
      periodSeconds: 1
      exec:
        command: ["cat", "/var/run/nginx.pid"]

    livenessProbe:
      periodSeconds: 10
      tcpSocket:
        port: 80

    readinessProbe:
      periodSeconds: 5
      httpGet:
        path: /ready
        port: 80                        

创建此Pod后,用kubectl logs查看日志,里面会记录HTTP GET 探针的执行情况。可以看到Kubernetes 正是以大约 5 秒一次的频率,向 URI /ready 发送 HTTP 请求,不断地检查容器是否处于就绪状态:
在这里插入图片描述

将startupProbe的命令改为["cat", "nginx.pid"] #错误的文件后,将Pod删除后重新创建,可以看到:StartupProbe 探测失败的时候,Kubernetes 就会不停地重启容器,现象就是 RESTARTS 次数不停地增加
在这里插入图片描述
然后我将startupProbe改回正确的,将livenessProbe设错,比如错误的端口,再将Pod删除后重新创建,可以看到pod也会重启,因为 failureThreshold 的次数默认是三次,periodSeconds默认10,所以30秒会重启一次。而因为readinessProbe探测成功,pod是Ready的:
在这里插入图片描述
另外如评论区所说的,在容器重启几次之后,pod的status会变为CrashLoopBackOff。豆包说:短时间内多次反复重启,K8s 直接标记状态为 CrashLoopBackOff(崩溃回退)

  • 小结
    资源配额使用的是 cgroup 技术
    在这里插入图片描述

《29|集群管理:如何用名字空间分隔系统资源?》

  • 如何使用名字空间
    创建名字空间:kubectl create ns test-ns

想要把一个对象放入特定的名字空间,需要在它的 metadata 里添加一个 namespace 字段,比如以下Pod:

apiVersion: v1
kind: Pod
metadata:
  name: ngx
  namespace: test-ns

spec:
  containers:
  - image: nginx:alpine
    name: ngx

kubectl apply 创建这个对象之后,我们直接用 kubectl get 是看不到它的,因为默认查看的是“default”名字空间,想要操作其他名字空间的对象必须要用 -n 参数明确指定:kubectl get pod -n test-ns

因为名字空间里的对象都从属于名字空间,所以在删除名字空间的时候一定要小心,一旦名字空间被删除,它里面的所有对象也都会消失。

  • 什么是资源配额
    名字空间的资源配额需要使用一个专门的 API 对象,叫做 ResourceQuota,简称是 quota。

因为资源配额对象必须依附在某个名字空间上,所以在它的 metadata 字段里必须明确写出 namespace(否则就会应用到 default 名字空间)。
ResourceQuota 对象的使用方式比较灵活,若要限制整个名字空间的配额,需要在 spec 里使用 hard 字段,意思就是硬性全局限制。

下面我们先创建一个名字空间dev-ns,再创建一个资源配额对象dev-qt:

apiVersion: v1
kind: Namespace
metadata:
  name: dev-ns

---

apiVersion: v1
kind: ResourceQuota
metadata:
  name: dev-qt
  namespace: dev-ns

spec:
  hard:
    requests.cpu: 10
    requests.memory: 10Gi
    limits.cpu: 10
    limits.memory: 20Gi

    requests.storage: 100Gi
    persistentvolumeclaims: 100

    pods: 100
    configmaps: 100
    secrets: 100
    services: 10

    count/jobs.batch: 1
    count/cronjobs.batch: 1
    count/deployments.apps: 1

各类资源配额字段简单归类:

  1. CPU 和内存配额,使用 request.*limits.*,这是和容器资源限制是一样的。比如这里requests.cpu:10表示
    当前 ns 下,所有 Pod 容器的 requests.cpu 累加总和 ≤10 核,imits.cpu:10表示当前 ns 下,所有 Pod 容器的 limits.cpu 累加总和 ≤10 核。
  2. 存储容量配额,使 requests.storage 限制的是 PVC 的存储总量,也可以用 persistentvolumeclaims 限制 PVC 的个数。
  3. 核心对象配额,使用对象的名字(英语复数形式),比如 pods、configmaps、secrets、services。
  4. 其他 API 对象配额,使用 count/name.group 的形式,比如 count/jobs.batch、count/deployments.apps。注:这里的group可以从kubectl api-resources中APIVERSION列中看到,比如deployment的APIVERSION是“apps/v1”,job和cronjob的APIVERSION是“batch/v1”
  • 如何使用资源配额
    创建quota后用命令 kubectl describe查看,会给出一个清晰的表格:
    在这里插入图片描述
    创建对象超出资源配额时会失败:
    在这里插入图片描述

  • 默认资源配额
    在名字空间加上了资源配额限制之后,它会有一个合理但比较“烦人”的约束:要求所有在里面运行的 Pod 都必须用字段 resources 声明资源需求,否则就无法创建,比如:
    在这里插入图片描述

辅助对象LimitRange,简称limits,它能为 API 对象添加默认的资源配额限制。
spec.limits 是它的核心属性,描述了默认的资源限制。
type 是要限制的对象类型,可以是 Container、Pod、PersistentVolumeClaim。
defaultRequest 默认申请的资源,对应容器里的 resources.requests,只适用于 Container。
default 是默认的资源上限,对应容器里的 resources.limits,只适用于 Container。
max、min 是对象能使用的资源的最大最小值。
e.g. :

apiVersion: v1
kind: LimitRange
metadata:
  name: dev-limits
  namespace: dev-ns

spec:
  limits:
  - type: Container
    defaultRequest:
      cpu: 200m
      memory: 50Mi
    default:
      cpu: 500m
      memory: 100Mi
  - type: Pod
    max:
      cpu: 800m
      memory: 200Mi

创建 LimitRange 之后,用 kubectl describe 就可以看到它的状态:
在这里插入图片描述

现在我们就可以不用编写 resources 字段直接创建 Pod 了,创建成功后用kubectl describe 查看该Pod的状态,可以看到 LimitRange 为它自动加上的资源配额:
在这里插入图片描述

《30|系统监控:如何使用Metrics Server和Prometheus?》

Kubernetes 为集群提供的两种系统级别的监控项目:Metrics Server 和 Prometheus,以及基于它们的水平自动伸缩对象 HorizontalPodAutoscaler。

  • Metrics Server
    Linux 系统有一个命令 top 能够实时显示当前系统的 CPU 和内存利用率。Kubernetes 也提供了类似的命令,就是 kubectl top,不过必须要安装一个插件 Metrics Server 才可以。

Metrics Server 是一个专门用来收集 Kubernetes 核心资源指标(metrics)的工具,它定时从所有节点的 kubelet 里采集信息,但是对集群的整体性能影响极小。它调用 kubelet 的 API 拿到节点和 Pod 的指标,再把这些信息交给 apiserver,这样 kubectl、HPA 就可以利用 apiserver 来读取指标了:在这里插入图片描述

下载Metrics Server的ymal文件,对其做两处修改:
1,加上一个额外的运行参数 --kubelet-insecure-tls,为的是不必使用TLS协议从而简化部署(生产环境慎用):在这里插入图片描述
2, 像前文那样修改镜像链接,加上m.daocloud.io/,如此不必翻墙也不必像作者那样手动下载镜像再打tag了:
在这里插入图片描述

改完这两处后,使用此yaml文件部署Metrics Server ,等几分钟就可以看到它正常运行了:在这里插入图片描述

执行命令,查看集群里节点和 Pod 状态:

kubectl top node
kubectl top pod -n kube-system

在这里插入图片描述

  • HorizontalPodAutoscaler
    Metrics Server另外一个更重要的功能是辅助实现应用的水平自动伸缩
    Kubernetes 为此定义了一个新的 API 对象,叫做HorizontalPodAutoscaler,简称是hpa。顾名思义,它是专门用来自动伸缩 Pod 数量的对象,适用于 Deployment 和 StatefulSet,但不能用于 DaemonSet。
    HorizontalPodAutoscaler 的能力完全基于 Metrics Server。

下面我们就来看看该怎么使用 HorizontalPodAutoscaler,下面定义了 Nginx 应用的 Deployment,作为自动伸缩的目标对象:
在这里插入图片描述
在这个 YAML 里只部署了一个 Nginx 实例,注意在它的 spec 里一定要用 resources 字段写清楚资源配额,否则 HorizontalPodAutoscaler 会无法获取 Pod 的指标,也就无法实现自动化扩缩容。

接下来我们要用命令 kubectl autoscale 创建一个 HorizontalPodAutoscaler 的样板 YAML 文件,它有三个参数:
min,Pod 数量的最小值,也就是缩容的下限。
max,Pod 数量的最大值,也就是扩容的上限。
cpu-percent,CPU 使用率指标,当大于这个值时扩容,小于这个值时缩容。我注(参考豆包):它不是OS原生的CPU 使用率,而是容器实际CPU消耗 ÷ 容器Pod声明的spec.resources.requests.cpu × 100% 。
e.g., 为上面的 Nginx 应用创建 HorizontalPodAutoscaler,指定 Pod 数量最少 2 个,最多 10 个,CPU 使用率指标设置的小一点,5%,方便我们观察扩容现象:

export out="--dry-run=client -o yaml"              # 定义Shell变量
kubectl autoscale deploy ngx-hpa-dep --min=2 --max=10 --cpu-percent=5 $out

得到的 YAML 描述文件大致是这样:

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: ngx-hpa

spec:
  maxReplicas: 10
  minReplicas: 2
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: ngx-hpa-dep
  targetCPUUtilizationPercentage: 5

这个HorizontalPodAutoscaler创建后,它会发现 Deployment 里的实例只有 1 个,不符合 min 定义的下限的要求,就扩容到 2 个:
在这里插入图片描述

下面我们来给 Nginx 加上压力流量,运行一个测试 Pod,使用的镜像是httpd:alpine,它里面有 HTTP 性能测试工具 ab(Apache Bench)。我们用ab向 Nginx 发送一百万个请求,持续 1 分钟。在此过程中我们用 kubectl get hpa -w命令实时观察 HorizontalPodAutoscaler 的运行状况:在这里插入图片描述
可以看到,当它发现目标的 CPU 使用率超过了预定的 5% 后,就会以 2 的倍数开始扩容,一直到数量上限,然后持续监控一段时间,如果 CPU 使用率回落,就会再缩容到最小值。

  • Prometheus
    在这里插入图片描述
    Prometheus 系统的核心是它的 Server,里面有一个时序数据库 TSDB,用来存储监控数据,另一个组件 Retrieval 使用拉取(Pull)的方式从各个目标收集数据,再通过 HTTP Server 把这些数据交给外界使用。
    在 Prometheus Server 之外还有两个重要的组件:
    Push Gateway,用来适配一些特殊的监控目标,把默认的 Pull 模式转变为 Push 模式。
    Alert Manager,告警中心,预先设定规则,发现问题时就通过邮件等方式告警。
    此外,Grafana 是图形化界面,可以定制大量直观的监控仪表盘。

下载kube-prometheus 0.11的源码包:wget https://github.com/prometheus-operator/kube-prometheus/archive/refs/tags/v0.11.0.tar.gz 并解压缩。

第一步,修改 prometheus-service.yaml、grafana-service.yaml,为其添加type: NodePort,为的是直接通过节点的 IP 地址访问:

apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/component: prometheus
    app.kubernetes.io/instance: k8s
    app.kubernetes.io/name: prometheus
    app.kubernetes.io/part-of: kube-prometheus
    app.kubernetes.io/version: 2.36.1
  name: prometheus-k8s
  namespace: monitoring
spec:
  type: NodePort
  ports:
  - name: web
    port: 9090
    targetPort: web
  - name: reloader-web
    port: 8080
    targetPort: reloader-web
  selector:
    app.kubernetes.io/component: prometheus
    app.kubernetes.io/instance: k8s
    app.kubernetes.io/name: prometheus
    app.kubernetes.io/part-of: kube-prometheus
  sessionAffinity: ClientIP
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/component: grafana
    app.kubernetes.io/name: grafana
    app.kubernetes.io/part-of: kube-prometheus
    app.kubernetes.io/version: 8.5.5
  name: grafana
  namespace: monitoring
spec:
  type: NodePort
  ports:
  - name: http
    port: 3000
    targetPort: http
  selector:
    app.kubernetes.io/component: grafana
    app.kubernetes.io/name: grafana
    app.kubernetes.io/part-of: kube-prometheus

第二步,是修改 kubeStateMetrics-deployment.yaml、prometheusAdapter-deployment.yaml,它们分别有一个k8s.gcr.io的镜像,像前文那样加上m.daocloud.io/ 。

执行kubectl create 命令来部署 Prometheus(评论区提到可通过kubectl delete命令来进行逆操作,我按此尝试但没有完全成功):

kubectl create -f manifests/setup
kubectl create -f manifests

我注:我第一次部署时不成功,有个Pod一直pending:
在这里插入图片描述
我这样应对的:去除 node 的污点 node-role.kubernetes.io/master :kubectl taint nodes --all node-role.kubernetes.io/master-

小费了点周折,又等了一会才看到所有Pod正常运行了:
在这里插入图片描述
通过http://nodeIP:32019 可访问Prometheus 自带的 Web 界面,可以使用 PromQL 来查询指标(比如node_memory_Active_bytes,表示当前正在使用的内存容量):
在这里插入图片描述

通过http://nodeIP:32755 可访问grafana,默认的用户名和密码都是admin。Grafana 内部已经预置了很多强大易用的仪表盘,比如可以在左侧菜单栏的“Dashboards - Browse”里选择“Kubernetes / Compute Resources / Namespace (Pods)”这个仪表盘 :
在这里插入图片描述

《31|网络通信:CNI是怎么回事?又是怎么工作的?》

  • Kubernetes 的网络模型
    Docker 的网络方案简单有效,但问题是它只局限在单机环境里工作,跨主机通信非常困难。针对 Docker 的网络缺陷,Kubernetes 提出了一个自己的网络模型IP-per-pod,能够很好地适应集群系统的网络需求,它有下面的这 4 点基本假设:
    集群里的每个 Pod 都会有唯一的一个 IP 地址。
    Pod 里的所有容器共享这个 IP 地址。
    集群里的所有 Pod 都属于同一个网段。
    Pod 直接可以基于 IP 地址直接访问另一个 Pod,不需要做麻烦的网络地址转换(NAT)。

  • 什么是 CNI
    为了把这个网络模型落地实现,Kubernetes制定了一个标准:CNI(Container Networking Interface)。
    CNI 为网络插件定义了一系列通用接口,开发者只要遵循这个规范就可以接入 Kubernetes,为 Pod 创建虚拟网卡、分配 IP 地址、设置路由规则,最后就能够实现IP-per-pod网络模型。依据实现技术的不同,CNI 插件可以大致上分成OverlayRouteUnderlay三种。

Flannel最早是一种 Overlay 模式的网络插件,使用 UDP 和 VXLAN 技术,后来又用 Host-Gateway 技术支持了 Route 模式。Flannel 简单易用,是 Kubernetes 里最流行的 CNI 插件,但它在性能方面表现不是太好,所以一般不建议在生产环境里使用。
Calico是一种 Route 模式的网络插件,使用 BGP 协议(Border Gateway Protocol)来维护路由信息,性能要比 Flannel 好,而且支持多种网络策略,具备数据加密、安全隔离、流量整形等功能。
Cilium是一个比较新的网络插件,同时支持 Overlay 模式和 Route 模式,它的特点是深度使用了 Linux eBPF 技术,在内核层次操作网络数据,所以性能很高,可以灵活实现各种功能。

  • CNI 插件是怎么工作的
    Flannel 比较简单,我们先以它为例看看 CNI 在 Kubernetes 里的工作方式。
    用 Deployment 创建 3 个 Nginx Pod,作为研究对象。使用命令 kubectl get pod 可以看到,有1个 Pod 运行在 master 节点上,另有两个 Pod 运行在 worker 节点上:
    在这里插入图片描述
    Flannel 默认使用的是基于 VXLAN 的 Overlay 模式,整个集群的网络结构我画了一张示意图:
    在这里插入图片描述
    从单机的角度来看的话,Flannel 的网络结构和 Docker 几乎是一模一样的,只不过网桥换成了cni0,而不是docker0。

在master节点上的那个Pod里执行命令 ip addr 就可以看到它里面的虚拟网卡eth0。如下图,第1个数字2是序号,意思是第2号设备,@if18是它另一端连接的虚拟网卡,序号是18:
在这里插入图片描述登录到其宿主机master节点,还是用命令 ip addr看这个节点的网络情况。返回结果包含下图,可以看到master节点上的第18号设备,名字是vethc2d3273e@if2,veth表示它是一个虚拟网卡,而后面的@if2就是 Pod 里对应的 2 号设备,也就是eth0网卡了:在这里插入图片描述

那么cni0网桥的信息该怎么查看呢?这需要在宿主机(master)上使用命令 brctl show。如下图,可以发现cni0网桥上有 多 个虚拟网卡,其中就有vethc2d3273e,所以这个网卡就被“插”在了cni0网桥上,然后因为虚拟网卡的“结对”特性,Pod 也就连上了cni0网桥:在这里插入图片描述

弄清楚了本机网络,我们再来看跨主机的网络,它的关键是节点的路由表,用命令 route 查看。如下图,它告诉我们有这些信息:
10.10.0.0/24 网段的数据,都要走cni0网桥。
10.10.1.0/24 网段的数据,都要走 flannel.1 设备,也就是 Flannel。
192.168.56.0/24 网段的数据,都要走 enp0s3 设备,也就是我们宿主机的网卡。
在这里插入图片描述
假设我们要从 master 节点的10.10.0.64访问 worker 节点的10.10.1.177,按照以上路由表,就要让flannel来处理。如下图,flannel决定要把数据发到192.168.56.104,也就是 worker 节点。它会在原始网络包前面加上这些额外的信息,封装成 VXLAN 报文,用enp0s3网卡发出去,worker 节点收到后再拆包,执行类似的反向处理,就可以把数据交给真正的目标 Pod 了:在这里插入图片描述

  • 使用 Calico 网络插件
    下载Calico 的 YAML 文件:wget https://projectcalico.docs.tigera.io/manifests/calico.yaml
    下载后,参考评论区第3条评论,和它提到的2,类似本学习笔记第17节安装 Flannel,要设置正确的网卡:
    在这里插入图片描述
    参考本学习笔记第17节安装Flannel,因为kubeadm init时设置了pod-network-cidr=10.10.0.0/16,这里也要设置:在这里插入图片描述

另外,由于 Calico 使用的镜像较大,为了加快安装速度,可以考虑在每个节点上预先使用 docker pull 拉取镜像。我在Calico YAML 文件中读到几个镜像地址是:docker.io/calico/cni:v3.25.0,ocker.io/calico/node:v3.25.0,docker.io/calico/kube-controllers:v3.25.0,所以运行:

docker pull calico/cni:v3.25.0
docker pull calico/node:v3.25.0
docker pull calico/kube-controllers:v3.25.0

此外,为了在安装Calico之前删除Flannel,我做了以下操作:
1.
在这里插入图片描述
2. 在每个k8s节点上操作:
2.1

sudo ip link delete flannel.1  #删除 flannel 专属 VXLAN 隧道网卡
sudo ip link delete cni0    #删除cni0网桥 

2.2 删除CNI 目录下所有 flannel 配置文件:在这里插入图片描述

ip route | grep flannel   #可选:确认无flannel相关路由

2.3 验证删干净了:

# 1. 无flannel.1、cni0
ip link | grep -E "flannel.1|cni0"
# 2. /etc/cni/net.d 无flannel配置
sudo ls /etc/cni/net.d/ | grep flannel

kubectl apply -f calico.yaml安装Calico。参考评论区第3条评论,如果安装Calico某些意外导致错误,需要清除网络插件的安装信息,重启kubelet:

sudo bash -c 'rm -f /etc/cni/net.d/*'
sudo rm -rf /var/lib/cni/calico
sudo rm -rf /var/lib/cni/flannel
sudo systemctl restart kubelet

安装Calico后,查看它的运行状态,可看到它也是在kube-system名字空间:
在这里插入图片描述

我们仍然创建 3 个 Nginx Pod 来做实验:
在这里插入图片描述
在master节点上的那个Pod里执行命令 ip addr 就可以看到它里面的虚拟网卡eth0 :
在这里插入图片描述
登录到其宿主机master节点,还是用命令 ip addr看这个节点的网络情况。返回结果包含下图,可以看到master节点上的第23号设备:
在这里插入图片描述
不同于前面Flannel模式,没有cni0网桥:
在这里插入图片描述

这是因为 Calico不是 Overlay 模式,而是 Route 模式,所以它就没有用 Flannel 那一套,而是在宿主机上创建路由规则,让数据包不经过网桥直接“跳”到目标网卡去。
来看一下master节点上的路由表就能明白:在这里插入图片描述
其中红框就对应master节点Nginx Pod。蓝线对应跨主机通信如何路由的:子网掩码255.255.255.192 → 前缀 26,网段范围:10.10.171.64 ~ 10.10.171.127,所以访问另外两个nginx pod(IP 10.10.171.76、10.10.171.77), 会转发worker节点(192.168.56.104)。10.10.171.77

  • 小结
    Flannel 支持 Overlay 模式,它使用了 cni0 网桥和 flannel.1 设备,本机通信直接走 cni0,跨主机通信会把原始数据包封装成 VXLAN 包再走宿主机网卡发送,有性能损失。
    Calico 支持 Route 模式,它不使用 cni0 网桥,而是创建路由规则,把数据包直接发送到目标网卡,所以性能高。

《32|实战演练:玩转Kubernetes(3)》

  • 部署 Dashboard
wget https://raw.githubusercontent.com/kubernetes/dashboard/v2.6.0/aio/deploy/recommended.yaml
kubectl apply -f recommended.yaml

在这里插入图片描述

  • 部署 Ingress/Ingress Controller
    本节尝试失败,放弃。后续我直接把dashboard Service type改为NodePort,直接访问的。

  • 访问 Dashboard
    要创建一个用户,才能登录进 Dashboard。下载admin.yml后安装它,会创建一个 Dashboard 的管理员账号admin-user。这个账号不能用简单的“用户名 + 密码”的方式登录,需要用到一个 Token,可以用 kubectl get secret、kubectl describe secret 查到:
    在这里插入图片描述
    为了方便访问dashboard,我把dashboard Service type改为了NodePort:
    在这里插入图片描述
    用浏览器访问此网站时提示不安全,这里不同浏览器的表现还不一样,Chrome没有继续前进的选项,Edge用键盘输入thisisunsafe就可以了,firefox可以继续前进。应该是dashboard网站证书的缘故,而各浏览器的安全策略也不相同。然后在网页中输入账号admin-user的token,就登录了,如下图。Dashboard能够以图形的方式显示CPU和内存状态,是因为Metrics Server:
    在这里插入图片描述
    在这里插入图片描述


  1. DaoCloud(道客云),国内专业容器云厂商,专门做 Kubernetes、容器镜像服务,专门代理同步海外封闭镜像仓库,解决国内无法直连拉取的痛点,其原理是定时完整同步海外镜像到国内服务器,当拉取m.daocloud.io/原完整镜像地址时,流量直接走国内线路 ↩︎

  2. kubernetes集群节点多网卡,calico/flannel组件如何指定网卡 ↩︎

更多推荐