回望K8S 容器编排与Kubernetes作业管理

Pod

podKubernetes 项目的最小的 API 对象,原子调度单位.

假设 “容器的本质是进程”,容器镜像就是 exe 安装包, kubernetes 是操作系统

Pod 最重要的一个事实是一个逻辑概念。它对于 Kubernetes 最核心的意义是 容器设计模式Kubernetes 真正处理的还是宿主机上操作系统上的 Linux 容器的 NamespaceCgroups,而不是一个所谓的 Pod 边界和隔离环境。

Pod 其实是一组共享了某些资源的容器。Pod 里面所有的容器,共享的同一个 Network Namespace,并且可以声明共享同一个 Volume.

Kubernetes 项目内部,Pod 实现需要使用一个中间容器,这个容器叫做 Infra 容器,在 Pod 中,Infra 容器永远都是第一个被创建的容器,而其他用户定义的容器,则通过 Join Network Namespace 的方式,与 Infra 容器关联在一起。

sidecar

典型的例子:WAR 包和 Web 服务器

POD后,将 WAR 包和 Tomcat 分别做成镜像,可以把他们容器结合在一起

---
apiVersion: v1
kind: Pod
metadata:
  name: javaweb-2
spec:
#  启动后做了一件事 把应用的WAR包拷贝到 /app目录中,后退出
  initContainers:
  - image: sample-war:v2
    name: war
    command: ["cp", "/sample.war", "/app"]
    volumeMounts:
    - mountPath: /app
      name: app-volume
  containers:
  - image: tomcat:7.0
    name: tomcat
    command: ["sh","-c","/root/apache-tomcat-7.0.42-v2/bin/start.sh"]
    volumeMounts:
    - mountPath: /root/apache-tomcat-7.0.42-v2/webapps
      name: app-volume
    ports:
    - containerPort: 8080
      hostPort: 8001 
  volumes:
  - name: app-volume
    emptyDir: {}
...

这个POD中,定义了两个容器,第一个容器镜像 sample-war:v2,第二个容器镜像是 Tomcat 镜像,War包容器的类型不是一个普通容器,是一个 Init Container 类型的容器。

在Pod中,所有 Init Container 定义的容器,都比 spec.containers 定义的用户容器先启动。并且, Init Container 容器会按顺序准一启动,而直到他们都启动并且退出了,用户容器才会启动。

这种组合的方式,正是容器设计模式里面最常用的一种模式:sidecar

容器的日志收集

应用把日志文件输出到容器的 /var/log 目录中,PodVolume 挂载到应用容器的 /var/log 目录上,然后在这个 Pod 里的运行一个 sidecar 容器,也声明挂载同一个 Volume 到自己的 /var/log 目录上, 这样这个 sidecar 容器只需要做一件事,把自己的 /var/log 目录中读取日志文件,转发就可以了,就是一个基本的日志收集

Pod 对象的基本概念

Pod 是容器环境的 Kubernetes 的基本单元,调度、网络、存储、以及安全相关的熟悉,都是属于 Pod 级别的。

Pod 下重要的字段和含义

  • NodeSelector : 用户将 PodNode 绑定的字段
  • NodeName : 一旦 Pod 的这个阻断被赋值,K8S 会认为这个 Pod 已经经过调度。
  • HostAliases:定义了 Podhosts 文件(比如 /etc/hosts)里的内容

凡是跟容器的 Linux Namespace 相关的属性,也一定是 Pod 级别的。 shareProcessNamespace=true

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

  • Pending。这个状态意味着,PodYAML 文件已经提交给了 KubernetesAPI 对象已经被创建并保存在 Etcd 当中。但是,这个 Pod 里有些容器因为某种原因而不能被顺利创建。比如,调度不成功。
  • Running。这个状态下,Pod 已经调度成功,跟一个具体的节点绑定。它包含的容器都已经创建成功,并且至少有一个正在运行中。
  • Succeeded。这个状态意味着,Pod 里的所有容器都正常运行完毕,并且已经退出了。这种情况在运行一次性任务时最为常见。
  • Failed。这个状态下,Pod 里至少有一个容器以不正常的状态(非 0 的返回码)退出。这个状态的出现,意味着你得想办法 Debug 这个容器的应用,比如查看 PodEvents 和日志。
  • Unknown。这是一个异常状态,意味着 Pod 的状态不能持续地被 kubelet 汇报给 kube-apiserver,这很有可能是主从节点(MasterKubelet)间的通信出现了问题。

Kubernetes 其他对象 Volume

Kubernetes 支持的 Project Volume 一共有四种:

  • Secret : 把 Pod 想要访问的加密数据,存放到 Etcd 中,然后通过在 Pod 的容器里挂载 Volume 的方式。
  • ConfigMap : 保存的是不需要加密的、应用所需的配置信息。而 ConfigMap 的用法几乎与 Secret 完全相同:你可以使用 kubectl create configmap 从文件或者目录创建 ConfigMap,也可以直接编写 ConfigMap 对象的 YAML 文件。
  • Downward API : 让 Pod 里的容器能够直接获取到这个 Pod API 对象本身的信息。

    一定是 Pod 里的容器进程启动之前就能够确定下来的信息。而如果你想要获取 Pod 容器运行后才会出现的信息,比如,容器进程的 PID,那就肯定不能使用 Downward API 了,而应该考虑在 Pod 里定义一个 sidecar 容器。

  • ServiceAccountToken : 这种把 Kubernetes 客户端以容器的方式运行在集群里,然后使用 default Service Account 自动授权的方式,被称作“InClusterConfig”.
---
apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque
data:
  user: YWRtaW4=
  pass: MWYyZDFlMmU2N2Rm
...
> kubectl create secret

容器的健康检查和恢复机制

Pod 容器定义了一个健康检查的"探针"(Probe), 这样kubelet就会根据这个 probe 返回的值决定这个容器的状态,而不是直接以容器镜像十分运行来作为依据。生产环境保证应用健康存活的重要手段。

Pod 恢复机制 restartPolicy, 它是 Pod 的 Spec 部分的一个标准字段(pod.spec.restartPolicy),默认值是 Always,即:任何时候这个容器发生了异常,它一定会被重新创建。

pod 恢复,永远只发生在当前节点,而不会跑到别的节点上去。(不会发生故障转移)需要转移的需要切换到 Deployment 这样的控制器来管理POD

你还可以通过设置 restartPolicy,改变 Pod 的恢复策略。除了 Always,它还有 OnFailureNever 两种情况:

  • Always:在任何情况下,只要容器不在运行状态,就自动重启容器;
  • OnFailure: 只在容器 异常时才自动重启容器;
  • Never: 从来不重启容器。

编排其实很简单-“控制器"模型

前面已经知道 POD 是一个复杂的API对象,实际也是对容器的进一步抽象和封装;也就是说Pod对象是容器的升级版,它对容器的组合,添加了很多的属性和字段。

Kubernetes操作POD是依赖控制器(Controller)完成的。就是 kube-controller-manager 组件

通过查看 https://github.com/kubernetes/kubernetes/tree/master/pkg/controller 源代码下能看见这些目录

deployment/ job/ podautoscaler/ cloud/ disruption/ namespace/ 
replicaset/ serviceaccount/ volume/cronjob/ garbagecollector/ nodelifecycle/ 
replication/ statefulset/ daemon/...

这些每个目录都是一种类型的 controller,各自负责某种编排功能。

控制循环(control loop)


for {
  实际状态 := 获取集群中对象X的实际状态Actual State
  期望状态 := 获取集群中对象X的期望状态Desired State
  if 实际状态 == 期望状态{
    什么都不做
  } else {
    执行编排动作将实际状态调整为期望状态
  }
}

具体实现时候,实际状态来自于Kubernetes 集群本身,期望状态来自于用户提交的 YAML 文件。

比如

  • Deployment控制器Etcd中获取到目标标签的 POD,然后统计他们的数量,这是实际状态;
  • Deployment对象Replicas 字段的值是期望状态;
  • Deployment控制器将两个状态做比较,然后根据比较结果,确定创建POD还是删除已经存在的POD

这个操作叫做协调(Reconcile)即 控制循环

为什么是循环,因为事件往往是一次性的,如果操作失败比较难处理,但是控制器循环一直尝试,更符合 Kubernetes 声明式API,最终达成一致。

deployment结构

上半部分的控制器定义(包含期望状态),下面的部分被控制对象的模板组成的。

作业副本和水平扩展

Pod 的“水平扩展 / 收缩”(horizontal scaling out/in)

如果我们更新了 DeploymentPod 模板(假如更新了容器镜像),那么 Deployment 需要遵循一种 “滚动更新”(rolling update)的方式来升级现有的容器。这也是 kubernetes 重要的概念(API对象):ReplicaSet


apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx-set
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9

一个 ReplicaSet对象,由 副本数目的定义和一个Pod模板组成的。它其实是 Deployment 的子集。而且,Deployment控制器操作的正是 ReplicaSet 对象,而不是Pod对象

kubectl scale

ReplicaSet结构

滚动更新

将一个集群中正在运行的多个 Pod 版本,交替地逐一升级的过程,就是“滚动更新”。

  • 依赖 health check 机制
  • 保证服务的连续性

DeploymentReplicaSet

Deployment 实际上是一个两层控制器。首先,它通过 ReplicaSet 的个数来描述应用的版本;然后,它再通过 ReplicaSet 的属性(比如 replicas 的值),来保证 Pod 的副本数量。

深入理解StatefulSet

  • 拓扑状态:应用间不完全对等,需要谁先启动,谁后启动,必须按照某些顺序来启动。(在Pod删除和再创建中保持稳定)
  • 存储状态:多个实例绑定了不同的存储数据,一个数据库应用的多个存储实例

拓扑状态

StatefulSet 核心功能:通过某种方式纪录这些状态,等POD被重新创建时候,能够为新的POD恢复状态。

Headless Service

Service 是 Kubernetes 项目中用来将一组 Pod 暴露给外界访问的一种机制。比如,一个 Deployment3Pod,那么我就可以定义一个 Service。然后,用户只要能访问到这个 Service,它就能访问到某个具体的 Pod

  • 第一种方式,是以 ServiceVIPVirtual IP,即:虚拟 IP)方式。
  • 第二种方式,就是以 ServiceDNS 方式, 比如 只要我访问“my-svc.my-namespace.svc.cluster.local”这条 DNS 记录,就可以访问到名叫 my-svcService 所代理的某一个 Pod

Service DNS 下两种处理方法

Normal Service。这种情况下,你访问“my-svc.my-namespace.svc.cluster.local”解析到的,正是 my-svc 这个 Service 的 VIP,后面的流程就跟 VIP 方式一致了。 Headless Service。这种情况下,你访问“my-svc.my-namespace.svc.cluster.local”解析到的,直接就是 my-svc 代理的某一个 Pod 的 IP 地址。可以看到,这里的区别在于,Headless Service 不需要分配一个 VIP,而是可以直接以 DNS 记录的方式解析出被代理 Pod 的 IP 地址。

Headless Service Yaml


apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx

StatefulSet Yaml


apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  # 多了这个 ServiceName 就是告诉 StatefulSet 控制器,在执行控制循环(Control Loop)的时候,请使用 nginx 这个 Headless Service 来保证 Pod 的“可解析身份”。
  serviceName: "nginx"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.9.1
        ports:
        - containerPort: 80
          name: web

,对于“有状态应用”实例的访问,你必须使用 DNS 记录或者 hostname 的方式,而绝不应该直接访问这些 Pod 的 IP 地址。

存储状态

StatefulSet 存储状态的管理机制,主要使用的是一个叫做 Persistent Volume Claim 功能。

要在一个 Pod 里面声明 Volume,只要在 Pod 里加上 spec.volumes 字段,然后就可以在这个字段里面定义一个具体的类型的 Volume。比如 hostPath

Kubernetes 项目引入了一组叫作 Persistent Volume Claim(PVC)和 Persistent Volume(PV)的 API 对象,大大降低了用户声明和使用持久化 Volume 的门槛。

使用PVC的两步:

  • 定义一个 PVC,声明想要的 Volume 属性

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pv-claim
spec:
  accessModes:
    - ReadWriteOnce # 挂载方式:可读写,并且只能被挂载在一个节点上,非多个节点共享
  resources:
    requests:
      storage: 1Gi  #存储大小
  • 在应用 POD 中,使用这个 PVC
apiVersion: v1
kind: Pod
metadata:
  name: pv-pod
spec:
  containers:
    - name: pv-container
      image: nginx
      ports:
        - containerPort: 80
          name: "http-server"
      volumeMounts:
        - mountPath: "/usr/share/nginx/html"
          name: pv-storage
  volumes:
    - name: pv-storage
      persistentVolumeClaim:
        claimName: pv-claim # 指定上面的PVC的名字

从上面来看,这个 Volume 又从何而来呢?(运维人员维护的 PV

# 这个 PV 对象的 spec.rbd 字段,正是我们前面介绍过的 Ceph RBD Volume 的详细定义。而且,它还声明了这个 PV 的容量是 10 GiB。这样,Kubernetes 就会为我们刚刚创建的 PVC 对象绑定这个 PV
kind: PersistentVolume
apiVersion: v1
metadata:
  name: pv-volume
  labels:
    type: local
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  rbd:
    monitors:
    # 使用 kubectl get pods -n rook-ceph 查看 rook-ceph-mon- 开头的 POD IP 即可得下面的列表
    - '10.16.154.78:6789'
    - '10.16.154.82:6789'
    - '10.16.154.83:6789'
    pool: kube
    image: foo
    fsType: ext4
    readOnly: true
    user: admin
    keyring: /etc/ceph/keyring

所以 Kubernetes 中的 PVCPV 的设计,类似于 “接口” 和 “实现” 的思想,这种解耦,避免了暴露系统更多的细节,也是职责的分离,更容易定位问题

StatefulSet 的工作原理

  • StatefulSet 控制器直接管理的是 POD
  • Kubernetes 通过 Headless Service 为这些有编号的 POD。在 DNS 服务器众生成同样带有编号的 DNS纪录。只要 StatefulSet 能够保证这些 POD 的名字编号不变,类似 “web-0.default.svc.cluster.local” 这样的 DNS纪录 就不会变,而这条纪录解析出来的PODIP地址,会随着后端的POD删除和再创建而更新。
  • StatefulSet 还为每个 POD 分配并创建一个同样编号的 PVC,这样 Kubernetes 可以通过 Persistent Volume 机制为这个 PVC 绑定对应的 PV,保证一个POD都有一个独立的 Volume(即使POD被删除,但是对应的 PVCPV 保留下来,重新创建POD的时候,还会找回来,数据还存在)

容器化守护进程的意义 DaemonSet

DaemonSet 主要的作用: 在Kubernetes集群里运行一个 Daemon Pod,这个Pod三个特征

  • 每个Kubernetes节点都会运行一个这样的POD
  • 每个节点上只有一个这样的 Pod 实例
  • 当有新的节点加入 Kuberntes 集群后,该 Pod 会自动的在新的节点上被创建出来,而当旧节点被删除后, 它上面的 Pod 也会相应地被回收掉。

eg:

  • 网络插件的 Agent 组件,必须运行在每个节点上,用来处理容器的网络
  • 存储插件 Agent 组件,必须运行在每个节点上,用来挂载远程存储目录,操作容器的 Volume 目录
  • 监控组件 以及 日志组件,也是一样,负责节点的监控信息和日志收集

跟其他编排不一样,DaemonSet 开始运行的时机,很多时候比整个 Kubernetes 集群出现的还要早。比如容器网络组件,在所有的 Worker节点状态都是 NotReady。

# fluentd-elasticsearch 镜像POD, 通过 Fluentd 将 Docker 容器日志转发到 ES 内。
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd-elasticsearch
  namespace: kube-system
  labels:
    k8s-app: fluentd-logging
spec:
  selector:
    matchLabels:
      name: fluentd-elasticsearch
  template:
    metadata:
      labels:
        name: fluentd-elasticsearch
    spec:
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      containers:
      - name: fluentd-elasticsearch
        image: k8s.gcr.io/fluentd-elasticsearch:1.20
        resources:
          limits:
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 200Mi
        # 挂载了两个hostPath类型 的 Volume    
        volumeMounts:
        - name: varlog
          mountPath: /var/log
        # Docker 容器里应用的日志,默认会保存在宿主机的 /var/lib/docker/containers/{{. 容器 ID}}/{{. 容器 ID}}-json.log 文件里,所以这个目录正是 fluentd 的搜集目标  
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
      terminationGracePeriodSeconds: 30
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers

DaemonSet Controller,首先从 Etcd 里获取所有的 Node 列表,然后遍历所有的 Node。这时就会去检查这个Node上是不是已经存在携带了 name=fluentd-elasticsearchPOD 在运行了

  • 没有这个POD,则创建一个新的 POD
  • 有这种POD,数量 > 1, 删除多余的
  • 正好一个,节点正常

DaemonSet 会自动加上 tolerations字段


apiVersion: v1
kind: Pod
metadata:
  name: with-toleration
spec:
  # “容忍”所有被标记为 unschedulable“污点”的 Node;“容忍”的效果是允许调度。
  tolerations:
  - key: node.kubernetes.io/unschedulable
    operator: Exists
    effect: NoSchedule

在正常情况下,被标记了 unschedulable“污点”的 Node,是不会有任何 Pod 被调度上去的(effect: NoSchedule)。可是,DaemonSet 自动地给被管理的 Pod 加上了这个特殊的 Toleration,就使得这些 Pod 可以忽略这个限制,继而保证每个节点上都会被调度一个 Pod。当然,如果这个节点有故障的话,这个 Pod 可能会启动失败,而 DaemonSet 则会始终尝试下去,直到 Pod 启动成功。

离线业务 Job 与 CronJob

像在线业务诸如应用一类的,抽离了描述离线业务的API对象:Job


apiVersion: batch/v1
kind: Job
metadata:
  name: pi
spec:
  template:
    spec:
      containers:
      - name: pi
        image: resouer/ubuntu-bc 
        command: ["sh", "-c", "echo 'scale=10000; 4*a(1)' | bc -l "]
      restartPolicy: Never
  backoffLimit: 4

$ kubectl describe jobs/pi
Name:             pi
Namespace:        default
Selector:         controller-uid=c2db599a-2c9d-11e6-b324-0209dc45a495
Labels:           controller-uid=c2db599a-2c9d-11e6-b324-0209dc45a495
                  job-name=pi
Annotations:      <none>
Parallelism:      1
Completions:      1
..
Pods Statuses:    0 Running / 1 Succeeded / 0 Failed
Pod Template:
  Labels:       controller-uid=c2db599a-2c9d-11e6-b324-0209dc45a495
                job-name=pi
  Containers:
   ...
  Volumes:              <none>
Events:
  FirstSeen    LastSeen    Count    From            SubobjectPath    Type        Reason            Message
  ---------    --------    -----    ----            -------------    --------    ------            -------
  1m           1m          1        {job-controller }                Normal      SuccessfulCreate  Created pod: pi-rq5rl

这个 Job 对象在创建后,它的 Pod 模板,被自动加上了一个 controller-uid=< 一个随机字符串 > 这样的 Label。而这个 Job 对象本身,则被自动加上了这个 Label 对应的 Selector,从而 保证了 Job 与它所管理的 Pod 之间的匹配关系。而 Job Controller 之所以要使用这种携带了 UID 的 Label,就是为了避免不同 Job 对象所管理的 Pod 发生重合。

如果作业失败了怎么办?

定义了 restartPolicy=Never,那么离线作业失败后 Job Controller 就会不断地尝试创建一个新 Pod 我们就在 Job 对象的 spec.backoffLimit 字段里定义了重试次数为 4(即,backoffLimit=4),而这个字段的默认值是 6。 定义的 restartPolicy=OnFailure,那么离线作业失败后,Job Controller 就不会去尝试创建新的 Pod。但是,它会不断地尝试重启 Pod 里的容器

Job 对象中,并行作业的控制方法

  • 1、spec.parallelism,它定义的是一个 Job 在任意时间最多可以启动多少个 Pod 同时运行;
  • 2、spec.completions,它定义的是 Job 至少要完成的 Pod 数目,即 Job 的最小完成数。

常用的使用 Job对象的方法

  • 外部管理器 + Job模板 (sed)
apiVersion: batch/v1
kind: Job
metadata:
  # 带遍历替换
  name: process-item-$ITEM
  labels:
    jobgroup: jobexample
spec:
  template:
    metadata:
      name: jobexample
      labels:
        jobgroup: jobexample
    spec:
      containers:
      - name: c
        image: busybox
        command: ["sh", "-c", "echo Processing item $ITEM && sleep 5"]
      restartPolicy: Never
  • 拥有固定任务数目的并行Job

  • 指定并行度(parallelism),但不设置固定的 completions 的值。

CronJob 对象


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

CronJob 是专门管理 Job 对象的控制其。只不过它的创建和删除Job依据是根据 schedule 字段来定义的。

在 定时任务的时候,可能有任务未执行完毕,就下面的Pod启动

  • concurrencyPolicy=Allow,这也是默认情况,这意味着这些 Job 可以同时存在;
  • concurrencyPolicy=Forbid,这意味着不会创建新的 Pod,该创建周期被跳过;
  • concurrencyPolicy=Replace,这意味着新产生的 Job 会替换旧的、没有执行完的 Job。
# startingDeadlineSeconds=200,意味着在过去 200 s 里,如果 miss 的数目达到了 100 次,那么这个 Job 就不会被创建执行了
spec.startingDeadlineSeconds 时间窗口
comments powered by Disqus