kubernetes基础使用(二)

Kubernetes Job

Using Job

Kubernetes Job 通过创建 Pod 来批量执行一次性任务;不同于单独跑一个 Bare Pod,由 Job 运行起来的 Pod 在机器故障等问题下会重新调度 Pod,因此更加健壮。

下面,我们通过创建一个 Pod 来感受一下 Job 的使用:

$ kubectl create -f resources/job.yaml
job "pi" created

$ kubectl get job
NAME   COMPLETIONS   DURATION   AGE
pi     1/5           29s        29s

$ kubectl get pods -l job-name=pi
NAME       READY   STATUS      RESTARTS   AGE
pi-76h5p   1/1     Running     0          12s
pi-fhww6   0/1     Completed   0          36s

这是job.yaml

apiVersion: batch/v1
kind: Job
metadata:
  name: pi
spec:
  completions: 5
  template:
    metadata:
      name: pi
    spec:
      containers:
      - name: pi
        image: cargo.caicloud.io/caicloud/perl:latest
        command: ["perl",  "-Mbignum=bpi", "-wle", "print bpi(2000)"]
      restartPolicy: Never

一段时间之后,Pod 全部运行结束,我们可以通过 kubectl get pods 查看:

$ kubectl get pods -l job-name=pi
NAME       READY   STATUS      RESTARTS   AGE
pi-6lgqw   0/1     Completed   0          45s
pi-76h5p   0/1     Completed   0          68s
pi-fhww6   0/1     Completed   0          92s
pi-mf96j   0/1     Completed   0          2m17s
pi-w9v4l   0/1     Completed   0          115s

$ kubectl get job
NAME   COMPLETIONS   DURATION   AGE
pi     5/5           117s       3m8s

观察上述 Pod 的 AGE 列,可以发现 Job 内的 Pod 都是依次运行的(总共 5 个 Pod)。Job 支持并发运行等多种控制,我们在后续任务中实现。

Job 运行完之后,删除 Job 会将所有运行结束的 Pods 也同时删掉。

$ kubectl delete job pi
job "pi" deleted

$ kubectl get pods -l job-name=pi
No resources found.

Readings

Kubernetes CronJob

Using CronJob

Kubernetes CronJob 即定时任务,就类似于 Linux 的 crontab,在指定的时间周期运行指定的作业。

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: hello
spec:
  schedule: "*/1 * * * *" # At every minute.
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: cargo.caicloud.io/caicloud/busybox:latest
            args:
            - /bin/sh
            - -c
            - date; echo Hello from the Kubernetes cluster
          restartPolicy: OnFailure

这里我们通过 kubectl create 创建一个 CronJob:

$ kubectl create -f resources/cronjob.yaml
cronjob.batch/hello created

查看 CronJob:

$ kubectl get cronjob
NAME    SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
hello   */1 * * * *   False     0        57s             69m
$ kubectl get jobs
NAME               COMPLETIONS   DURATION   AGE
hello-1566286260   1/1           11s        46s

删除 CronJob:

# 删除 CronJob 会删除它创建的所有 job 和 pod,并停止正在创建的 job
$ kubectl delete cronjob hello
cronjob.batch "hello" deleted

Readings

Kubernetes Daemonset

Using Daemonset

DaemonSet 默认在每台主机上运行 Pod,典型的场景包括日志收集、存储进程等。这里,我们通过 kubectl create 来创建一个 DaemonSet:

$ kubectl create -f resources/daemonset.yaml
daemonset "ds-nginx" created

$ kubectl get ds
NAME       DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
ds-nginx   1         1         1       1            1           <none>          4s

$ kubectl get pods
NAME             READY     STATUS    RESTARTS   AGE
ds-nginx-hnndc   1/1       Running   0          8s

可以看到,minikube 节点都运行了一个 DaemonSet。由于目前只有一个节点,很难观测到效果,但当我们向集群中增加新的节点, DaemonSet 会自动在新的节点中启动 Pod。

我们可以通过 kubectl delete 删除 DaemonSet:

$ kubectl delete daemonset ds-nginx
daemonset.extensions "ds-nginx" deleted

DaemonSet 相对于其他控制器(Deployment, Job 等)有几个特点:

由 DaemonSet controller 调度 (1.12 以前)

  • DaemonSet controller 创建 pod 时会将指定 nodeName(.spec.nodeName),所以 scheduler 会忽视它。因此:
    • 即使调度器没有启动,DaemonSet 的 pod 也会运行
    • 即时一个节点被标记为不可调度,由 DaemonSet 启动的容器也会运行在某个节点上
  • DaemonSet 启动的 Pod 会无视优先级

由 default scheduler 调度(从 1.12 开始默认开启)

  • DaemonSet controller 通过给 pod 添加 NodeAffinity,然后 default scheduler 将其绑定到目标节点,即指定 nodeName(.spec.nodeName)

Readings

Resource quota

默认情况下,namespace 只提供名字的隔离,并没有提供资源的限制(Quota)。也就是说一个 namespace 的用户可以创建任意多的资源,直到将集群的所有资源用光。为了解决这个问题,我们可以通过创建 ResourceQuota 资源来限制某个 namespace 可以使用的资源量,即:

  • 计算资源,包括 cpu 和 memory
    • cpu, limits.cpu, requests.cpu
    • memory, limits.memory, requests.memory
  • 存储资源,包括存储资源的总量以及指定 storage class 的总量
    • requests.storage:存储资源总量,如 500Gi
    • persistentvolumeclaims:pvc 的个数
    • .storageclass.storage.k8s.io/requests.storage
    • .storageclass.storage.k8s.io/persistentvolumeclaims
    • requests.ephemeral-storage 和 limits.ephemeral-storage (需要 v1.8+)
  • 对象数量,即可创建的对象的个数
    • pods, replicationcontrollers, configmaps, secrets
    • resourcequotas, persistentvolumeclaims
    • services, services.loadbalancers, services.nodeports
    • custom resources (需要 v1.15+)

Using Resource quota

我们可以通过 kubectl create -f 创建 ResourceQuota,该 quota 限制了 tutorial namespace 只能请求 1 个 CPU 和 1Gi 内存,最多只能使用 2 个 CPU 和 2Gi 内存。这里,请求指的是 kubernetes 在调度时容器时会寻找至少有 1 个 CPU 和 1Gi 内存的节点。最多使用指的是当容器可以使用超过其请求的 1 个 CPU 数量,但是一定不可能超过 2 个 CPU。kubernetes 的这个设计目的是提供更加灵活的资源模型,支持超卖。

# 这里用到了前面 lab 创建的 namespace tutorial,若已删除,需重新创建
$ kubectl create namespace tutorial
namespace/tutorial created

$ kubectl create -f resources/quota.yaml
resourcequota/tutorial-quota created
apiVersion: v1
kind: ResourceQuota
metadata:
  name: tutorial-quota
  namespace: tutorial
spec:
  hard:
    # Across all pods in a non-terminal state, the sum of CPU requests cannot exceed this value.
    requests.cpu: "1"
    # Across all pods in a non-terminal state, the sum of memory requests cannot exceed this value.
    requests.memory: 1Gi
    # Across all pods in a non-terminal state, the sum of CPU limits cannot exceed this value.
    limits.cpu: "2"
    # Across all pods in a non-terminal state, the sum of memory limits cannot exceed this value.
    limits.memory: 2Gi

查看 ResourceQuota 细节:

$ kubectl describe quota -n tutorial
Name:            tutorial-quota
Namespace:       tutorial
Resource         Used    Hard
--------         ----    ----
limits.cpu       500m    2
limits.memory    1124Mi  2Gi
requests.cpu     300m    1
requests.memory  612Mi   1Gi

pod_exceeds_quota.yaml

apiVersion: v1
kind: Pod
metadata:
  name: pod
  namespace: tutorial
spec:
  containers:
  - name: nginx
    image: nginx:1.13
    resources:
      requests:
        cpu: "2"
        memory: "1Gi"
      limits:
        cpu: "2"
        memory: "1Gi"

如果我们尝试在 tutorial namespace 下创建超过配额的资源,kubernetes 会直接返回错误:

$ kubectl create -f resources/pod_exceeds_quota.yaml -n tutorial
Error from server (Forbidden): error when creating "resources/pod_exceeds_quota.yaml": pods "pod" is forbidden: exceeded quota: default-quota, requested: limits.cpu=2,requests.cpu=2,requests.memory=1Gi, used: limits.cpu=200m,requests.cpu=100m,requests.memory=256Mi, limited: limits.cpu=2,requests.cpu=1,requests.memory=1Gi

Readings

Kubernetes Volume

Using volume

用户可以通过 Volume 来创建数据卷,使用数据卷之后,用户创建的 Pod 可以外部存储空间。Volume 是一个广义的概念,在 kubernetes 中至少包含三种类型:

  • 临时存储,比如 EmptyDir。当 Pod 删除后,EmptyDir Volume 也会被随之删除。
  • 持久化存储,比如 Ceph。kubernetes 通过 Volume 的方式挂载外部持久化存储。
  • 映射类存储,比如 ConfigMap。此类存储基于其他存储类型,为 Pod 提供元数据,配置等信息。
apiVersion: v1
kind: Pod
metadata:
  name: emptydir-pod
spec:
  containers:
  - image: cargo.caicloud.io/caicloud/nginx:1.9.3
    name: emptydir-pod
    volumeMounts:
    - mountPath: /cache
      name: cache-volume
  volumes:
  - name: cache-volume
    emptyDir: {}

无论是那种类型,都可以在 Pod 中直接指定 Volume 的信息。接下来我们创建一个最简单的 Pod,使用 EmptyDir 作为 Volume。可以看到,kubernetes 根据 yaml 文件的内容,为该 Pod 挂载了一个 cache 目录。对于 EmptyDir 而言,该目录的本质就是宿主机上的一个目录。

$ kubectl create -f resources/emptydir_pod.yaml
pod "emptydir-pod" created

$ kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
emptydir-pod             1/1     Running   0          5s

# 进入 emptydir-pod 并创建一个文件
$ kubectl exec -it emptydir-pod bash
root@emptydir-pod:/# ls cache/
root@emptydir-pod:/# touch cache/abc

# 按 'Ctrl + D' 退出 pod
$ kubectl get pods emptydir-pod -o yaml | grep uid
  uid: 15ccac29-773b-41d2-bd54-8d746a009396

# 我们将会在宿主机上看见之前创建的文件
$ minikube ssh
$ sudo ls /var/lib/kubelet/pods/15ccac29-773b-41d2-bd54-8d746a009396/volumes/kubernetes.io~empty-dir/cache-volume/abc

可以验证,当 Pod 被删除后,数据也就丢失了。若我们想使存储与 Pod 的生命周期解耦,需要使用网络存储。如果我们查看 emptydir_pod.yaml 文件,可以看到使用方法和 ConfigMap 和 Secret 完全类似。

Readings

Kubernetes PV & PVC

Using PV & PVC

对于持久化存储,Kubernetes 抽象出了 PersistentVolume (PV) 和 PersistentVolumeClaim (PVC) 两类资源。类似于 Node 与 Pod 在计算资源上的关系,PV/PVC 提供了存储的抽象。管理员创建可用的存储资源(PV),用户通过 PVC 请求需要的资源再与 Pod 进行绑定。

Volume 的生命周期包括 5 个阶段:

  • Provisioning,即 PV 的创建,可以直接创建 PV(静态方式),也可以使用 StorageClass 动态创建
  • Binding,将 PV 分配给 PVC
  • Using,Pod 通过 PVC 使用该 Volume,并可以通过 Admission Controller StorageObjectInUseProtection(1.9 及以前版本为 PVCProtection)阻止删除正在使用的 PVC
  • Releasing,Pod 释放 Volume 并删除 PVC
  • Reclaiming,回收 PV,可以保留 PV 以便下次使用,也可以直接从存储中删除
  • Deleting,删除 PV 并从存储中删除后端存储

根据这 5 个阶段,Volume 的状态有以下 4 种:

  • Available:可用
  • Bound:已经分配给 PVC
  • Released:PVC 解绑但还未执行回收策略
  • Failed:发生错误

接下来,我们感受一下如何使用 PV 和 PVC。

首先,我们需要创建新的 PV。类比于节点,可以理解为是向集群中添加节点,这里我们创建一个基于 hostPath 的 PV。基于 hostPath 的 PV 主要用来做测试,生产环境中一般采用其他存储方案如 NFS, GlusterFS, Ceph 等。

pv_hostpath.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-hostpath
spec:
  storageClassName: standard
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Recycle
  hostPath:
    path: /tmp/data1
$ kubectl create -f resources/pv_hostpath.yaml
persistentvolume "pv-hostpath" created

$ kubectl get pv
NAME          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   REASON   AGE
pv-hostpath   10Gi       RWX            Recycle          Available           standard                7s

创建 PV 之后,我们需要申请使用 PV,因此需要在 kubernetes 中创建 PVC。

pvc.yaml

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: myclaim
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 8Gi
$ kubectl create -f resources/pvc.yaml
persistentvolumeclaim "myclaim" created

kubernetes 根据 PVC 所需的容量(resources.requests.storage)和访问方式(accessMode)来调度存储资源。如下所示,我们创建的 PV 和 PVC 被 kubernetes 绑定在了一起。

$ kubectl get pv
NAME          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM             STORAGECLASS   REASON   AGE
pv-hostpath   10Gi       RWX            Recycle          Bound    default/myclaim   standard                45s

$ kubectl get pvc
NAME      STATUS   VOLUME        CAPACITY   ACCESS MODES   STORAGECLASS   AGE
myclaim   Bound    pv-hostpath   10Gi       RWX            standard       26s

PV 和 PVC 的封装主要作用是达到了平台可移植性。应用不再需要关系底层存储的细节,只需要使用 PVC 即可。接下来我们使用上面的 PVC 来创建应用。

pod_with_pvc.yaml

kind: Pod
apiVersion: v1
metadata:
  name: pod-with-pvc
spec:
  containers:
    - name: nginx
      image: cargo.caicloud.io/caicloud/nginx:1.9.3
      volumeMounts:
      - mountPath: "/var/www/html"
        name: mypd
  volumes:
    - name: mypd
      persistentVolumeClaim:
        claimName: myclaim
$ kubectl create -f resources/pod_with_pvc.yaml
pod "pod-with-pvc" created

$ kubectl get pods
NAME           READY   STATUS    RESTARTS   AGE
pod-with-pvc   1/1     Running   0          5s

$ kubectl exec -it pod-with-pvc bash
root@pod-with-pvc:/# touch /var/www/html/index.html

$ minikube ssh
$ ls /tmp/data1/
index.html

当我们删除 Pod 时,PV 和 PVC 的绑定不受任何影响,意味着我们可以重新创建 Pod 使用 PVC,数据仍然存在。

$ kubectl delete pod pod-with-pvc
pod "pod-with-pvc" deleted

$ kubectl get pvc
NAME      STATUS   VOLUME        CAPACITY   ACCESS MODES   STORAGECLASS   AGE
myclaim   Bound    pv-hostpath   10Gi       RWX            standard       2m26s

$ kubectl get pv
NAME          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM             STORAGECLASS   REASON   AGE
pv-hostpath   10Gi       RWX            Recycle          Bound    default/myclaim   standard                3m7s

如果进一步删除 PVC,可以看到 PV 进入了 Released 状态,这意味着 PV 已经被释放,但是还无法被重新使用,需要管理员手动清理 PV 数据后,将 PV 状态修改为 Available。PV 进入 Release 状态的原因是 PV 的回收策略被指定为 Recycle(spec.persistentVolumeReclaimPolicy: Recycle)。如果将回收策略改为 Delete,那么 PV 会被 kubernetes 直接删除。

$ kubectl delete pvc myclaim
persistentvolumeclaim "myclaim" deleted

$ kubectl get pv
NAME          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS     CLAIM             STORAGECLASS   REASON   AGE
pv-hostpath   10Gi       RWX            Recycle          Released   default/myclaim   standard                3m55s

PV 可以被手动删除,kubernetes 不会自动清理底层的数据。

$ kubectl delete pv pv-hostpath
persistentvolume "pv-hostpath" deleted

Readings

Kubernetes StorageClass

Using StorageClass

从上述操作的步骤可以看出,管理员需要首先创建 PV 才能让用户使用底层存储。其次,用户在使用 PVC 申请存储的时候,只能指定存储空间大小和访问模式。假如底层同时提供 GlusterFS 和 NFS,或者是不同规格的 GlusterFS,用户并没有能力指定特定的存储系统。从 1.2 版本开始,Kubernetes 逐渐引入了 StorageClass 的概念来解决上述两个问题。

首先,我们需要创建 StorageClass。minikube 自带一个基于 hostPath 类型的 StorageClass,如下所示。

$ kubectl get storageclass standard -o yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
  creationTimestamp: "2019-06-27T03:03:57Z"
  labels:
    addonmanager.kubernetes.io/mode: EnsureExists
  name: standard
  resourceVersion: "356"
  selfLink: /apis/storage.k8s.io/v1/storageclasses/standard
  uid: c9e7cc0f-2829-4adc-a313-63ea3a4950b7
provisioner: k8s.io/minikube-hostpath
reclaimPolicy: Delete
volumeBindingMode: Immediate

minikube 自带的 StorageClass 的名字为 standard,provisioner 名字为 k8s.io/minikube-hostpath。当我们创建一个 PVC 时,需要指定所需要的 StorageClass 名字,例如:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: claim-standard
spec:
  storageClassName: standard
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 8Gi

上述 PVC 将会使用 standard StorageClass 创建一个 PV,如下所示:

$ kubectl get pvc
NAME      STATUS    VOLUME                                     CAPACITY   ACCESSMODES   STORAGECLASS   AGE
myclaim   Bound     pvc-207c1e0b-a341-11e7-bf8e-0800277a7b6e   8Gi        RWX           standard       6s

$ kubectl get pv
NAME                                       CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS    CLAIM             STORAGECLASS   REASON    AGE
pvc-207c1e0b-a341-11e7-bf8e-0800277a7b6e   8Gi        RWX           Delete          Bound     default/myclaim   standard                 8s

可以看到 Kubernetes 自动生成了一个 PV。注意,动态生成的 PV 其回收属性是 Delete,即删除 PVC 会将 PV 也同时删除:

$ kubectl delete pvc myclaim
persistentvolumeclaim "myclaim" deleted

$ kubectl get pvc
No resources found.

$ kubectl get pv
No resources found.

Default StorageClass

kubernetes 集群中可以创建多个 StorageClass,其中有且仅有一个默认 StorageClass。当创建的 PVC 没有指定需要什么类型的存储时(即 pvc.spec.storageClassName 为空),Kubernetes 使用默认 StorageClass。

在 minikube 环境中,standard 即为默认的 StorageClass。我们在 PV & PVC 章节中创建的 PVC 没有指定 storageClassName,因此默认使用 standard

$ kubectl get storageclass
NAME                 PROVISIONER                AGE
standard (default)   k8s.io/minikube-hostpath   9d

StorageClass Provisioner

除了名字之外,每一个 StorageClass 都必须指明 provisioner。 provisioner 是真正创建底层存储的组件。provisioner 负责监听一个特定的事件 - “用户创建了一个 PVC,该 PVC 使用某个 StorageClass,该 StorageClass 的 provisioner 是我自己”。当发生该事件,provisioner 将会创建底层存储。例如:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: gluster-class
  annotations:
    storageclass.kubernetes.io/is-default-class: "false"
provisioner: example.com/gluster

当用户创建一个 PVC 使用 gluster-class,名为 example.com/gluster 的 provisioner 需要创建基于 GlusterFS 的 PV 供 PVC 使用。一般情况下,provisioner 以 Pod 形式运行在 kubernetes 集群中,长运行并监听上述事件。 provisioner 的稳定性和健壮性是极为重要的,否则动态创建会失效。

Readings

参考