Pod~全

Pod~全,第1张

文章目录
  • kubernetes简介
    • kubernetes架构
    • kube-apiserver
    • kube-controller-manager
    • kube-scheduler
    • kubelet
    • kube-proxy
    • kubectl
    • 核心资源对象
      • pod
      • Label
      • Namespace
      • Deployment
      • Service
  • 搭建集群
  • 资源清单
    • YAML 文件基本语法格式
    • kubernetes API文档和kubectl explain
  • pod原理
  • pod生命周期
    • pod状态
    • 重启策略
    • 初始化容器
    • Pod Hook
  • Pod 健康检查(探针)
  • Pod 资源配置
  • pod进阶使用
    • 静态 Pod
    • 配置文件
    • Downward API
      • 环境变量
      • Volume 挂载
  • PodPreset
    • 启用 PodPreset
    • 案例

Pod是kubernetes的最小管理单元,里面可以存放多个容器

kubernetes简介

Kubernetes(简称 K8S) 的出现是容器化技术发展的必然结果,容器化是应用程序级别的虚拟化,运行单个内核上有多个独立的用户空间实例,这些实例就是容器;容器提供了将应用程序的代码、运行时、系统工具、系统库和配置打包到一个实例中的标准方法,而且容器是共享一个内核的;由于容器技术的兴起,导致大量的容器应用出现,所以就出现了一些用来支持应用程序容器化部署和组织的容器编排技术,一些流行的开源容器编排工具有 Docker Swarm、Kubernetes 等,但是在发展过程中 Kubernetes 现在已经成为了容器编排领域事实上的一个标准了。

Kubernetes 是 Google 团队发起的一个开源项目,它的目标是管理跨多个主机的容器,用于自动部署、扩展和管理容器化的应用程序,主要实现语言为 Go 语言,他的理论基础来源与 Google 内部的 Borg 项目,所以 Kubernetes 项目的理论基础就比其他开源项目要“先进”很多,因为 Borg 系统一直依赖就被称为 Google 公司内部最强大的“私密武器”。

kubernetes架构

从上面我们可以看出 Kubernetes 由 Master 和 Node 两种节点组成,这两种角色分别对应着控制节点和工作节点(可以理解为老板和员工)。

其中 Master 节点由三个独立的组件组成,它们分别是负责整个集群通信的 API 服务的 kube-apiserver、负责容器调度的 kube-scheduler 以及负责维护集群状态的 kube-controller-manager 组件。整个集群的数据都是通过 kube-apiserver 保存到 etcd 数据库中的,而其他所有组件的通信也都是通过 kube-apiserver 和 etcd 数据库进行通信的,都不会直接和 etcd 进行通信(api-server是集群 *** 作请求(http)的的唯一入口。

工作节点上最核心的组件就是 kubelet,当然还有底层的容器运行时,比如 Docker,其中 kubelet 就是主要来实现和底层的容器运行时进行通信的,这个通信的过程也被 Kubernetes 抽象成了一个 CRI(Container Runtime Interface)的远程调用接口,这个接口里面定义了容器运行时的所有标准 *** 作,比如创建容器、删除容器等等,只是目前 kubelet 里面内置了 Docker 关于这个 CRI 实现的一个 shim,所以如果我们底层是 Docker 容器的话就不需要单独安装这个 CRI 的实现的组件了。其他的容器运行时是需要提供这样的一个接口实现组件的。所以对于 Kubernetes 来说他根本不关心你部署的到底是什么容器运行时,只要你这个容器运行时可以实现 CRI 接口就可以被 Kubernetes 来管理。

kubelet 的另外一个重要功能就是调用网络插件(CNI)和存储插件(CSI)为容器配置网络和存储功能,同样的 kubelet 也是把这两个重要功能通过接口暴露给外部了,所以如果我们想要实现自己的网络插件,只需要使用 CNI 就可以很方便的对接到 Kubernetes 集群当中去。

kube-apiserver

API Server 提供了资源对象的唯一 *** 作入口,其它所有组件都必须通过它提供的 API 来 *** 作资源数据。只有 API Server 会与 etcd 进行通信,其它模块都必须通过 API Server 访问集群状态。API Server 作为 Kubernetes 系统的入口,封装了核心对象的增删改查 *** 作。API Server 以 RESTFul 接口方式提供给外部客户端和内部组件调用,API Server 再对相关的资源数据(全量查询 + 变化监听)进行 *** 作,以达到实时完成相关的业务功能。以 API Server 为 Kubernetes 入口的设计主要有以下好处:

保证了集群状态访问的安全
API Server 隔离了集群状态访问和后端存储实现,这样 API Server状态访问的方式不会因为后端存储技术 Etcd 的改变而改变,让后端存储方式选择更加灵活,方便了整个架构的扩展

kube-controller-manager

Controller Manager 用于实现 Kubernetes 集群故障检测和恢复的自动化工作。主要负责执行各种控制器:

Replication Controller:主要是定期关联 Replication Controller (RC) 和 Pod,以保证集群中一个 RC (一种资源对象) 所关联的 Pod 副本数始终保持为与预设值一致。

Node Controller:Kubelet 在启动时会通过 API Server 注册自身的节点信息,并定时向 API Server 汇报状态信息。API Server 在接收到信息后将信息更新到 Etcd 中。Node Controller 通过 API Server 实时获取 Node 的相关信息,实现管理和监控集群中的各个 Node 节点的相关控制功能。

ResourceQuota Controller:资源配额管理控制器用于确保指定的资源对象在任何时候都不会超量占用系统上物理资源。

Namespace Controller:用户通过 API Server 可以创建新的 Namespace 并保存在 Etcd 中,Namespace Controller 定时通过 API Server 读取这些 Namespace 信息来 *** 作 Namespace。比如:Namespace 被 API 标记为优雅删除,则将该 Namespace 状态设置为 Terminating 并保存到 Etcd 中。同时 Namespace Controller 删除该 Namespace 下的 ServiceAccount、Deployment、Pod 等资源对象。

Service Account Controller:服务账号控制器主要在命名空间内管理 ServiceAccount,以保证名为 default 的 ServiceAccount 在每个命名空间中存在。

Token Controller:令牌控制器作为 Controller Manager 的一部分,主要用作:监听 serviceAccount 的创建和删除动作以及监听 secret 的添加、删除动作。

Service Controller:服务控制器主要用作监听 Service 的变化。比如:创建的是一个 LoadBalancer 类型的 Service,Service Controller 则要确保外部的云平台上对该 Service 对应的 LoadBalancer 实例被创建、删除以及相应的路由转发表被更新。

Endpoint Controller:Endpoints 表示了一个 Service 对应的所有 Pod 副本的访问地址,而 Endpoints Controller 是负责生成和维护所有 Endpoints 对象的控制器。Endpoint Controller 负责监听 Service 和对应的 Pod 副本的变化。定期关联 Service 和 Pod (关联信息由 Endpoint 对象维护),以保证 Service 到 Pod 的映射总是最新的。

kube-scheduler

Scheduler 是负责整个集群的资源调度的,主要的职责如下所示:

主要用于收集和分析当前 Kubernetes 集群中所有 Node 节点的资源 (包括内存、CPU 等)负载情况,然后依据资源占用情况分发新建的 Pod 到 Kubernetes 集群中可用的节点
实时监测 Kubernetes集群中未分发和已分发的所有运行的 Pod
实时监测 Node 节点信息,由于会频繁查找 Node 节点,所以 Scheduler
同时会缓存一份最新的信息在本地
在分发 Pod 到指定的 Node 节点后,会把 Pod 相关的 Binding(固定) 信息写回 API Server,以方便其它组件使用

kubelet

kubelet 是负责容器真正运行的核心组件,主要的职责如下所示:

负责 Node 节点上 Pod 的创建、修改、监控、删除等全生命周期的管理
定时上报本地 Node 的状态信息给 API Server
kubelet 是 Master 和 Node 之间的桥梁,接收 API Server 分配给它的任务并执行
kubelet 通过 API Server 间接与 Etcd 集群交互来读取集群配置信息
kubelet 在 Node 上做的主要工作具体如下:

设置容器的环境变量、给容器绑定 Volume、给容器绑定 Port、根据指定的 Pod 运行一个单一容器、给指定的 Pod 创建Network 容器
同步 Pod 的状态
在容器中运行命令、杀死容器、删除 Pod 的所有容器

(基本上容器的 *** 作都是有kubelet来负责)
Network容器,就是docker篇中container模式下被共享网络的容器

kube-proxy

kube-proxy 是为了解决外部网络能够访问集群中容器提供的应用服务而设计的,Proxy 运行在每个Node 上。

每创建一个 Service,kube-proxy 就会从 API Server 获取 Services 和 Endpoints 的配置信息,然后根据其配置信息在 Node 上启动一个 Proxy 的进程并监听相应的服务端口。

当接收到外部请求时,kube-proxy 会根据 Load Balancer 将请求分发到后端正确的容器处理。

kube-proxy 不但解决了同一宿主机相同服务端口冲突的问题,还提供了 Service 转发服务端口对外提供服务的能力。

kube-proxy 后端使用随机、轮循等负载均衡算法进行调度。

你设置个svc,不论是集群内还是nodeport类型,它的提供服务的端口都会被kube-proxy开启的进程监控,一旦有合法的流量访问到该svc的该端口,就会有kube-proxy流量转发到实际的pod,kube-proxy通过svc和endpoint列表知道实际的pod(其实底层的实现是修改iptables规则)

kubectl

Kubectl 是 Kubernetes 的集群管理命令行客户端工具集。通过 Kubectl 命令对 API Server 进行 *** 作,API Server 响应并返回对应的命令结果,从而达到对 Kubernetes 集群的管理

核心资源对象

上面我们都是在架构层面了解 Kubernetes,但是似乎没有发现关于容器的说明,Kubernetes 作为容器编排引擎,那么他是怎么去对容器进行编排的呢?在 Kubernetes 集群中抽象了很多集群内部的资源对象,我们可以通过这些资源对象去 *** 作容器的编排工作。

pod

Pod 是一组紧密关联的容器集合,它们共享 PID、IPC、Network 和 UTS namespace,是Kubernetes 调度的基本单位。Pod 的设计理念是支持多个容器在一个 Pod 中共享网络和文件系统,可以通过进程间通信和文件共享这种简单高效的方式组合完成服务。我们知道容器本质上就是进程,那么 Pod 实际上就是进程组了,只是这一组进程是作为一个整体来进行调度的。


在 Kubernetes 中,所有资源对象都使用资源清单(yaml或json)来定义,yaml更简单清晰,后续你也可以直接用helm安装资源对象

[root@master pod]# cat > nginx-pod.yaml <
> apiVersion: v1
> kind: Pod
> metadata:
>   name: nginx
>   labels:
>     app: nginx
> spec:
>   containers:
>   - name: nginx
>     image: nginx
>     ports:
>     - containerPort: 80
> EOF
[root@master pod]# kubectl apply -f nginx-pod.yaml
pod/nginx created
[root@master pod]# kubectl get pod
NAME    READY   STATUS              RESTARTS   AGE
nginx   0/1     ContainerCreating   0          6s

用户通过 REST API 创建一个 Pod apiserver 将其写入 etcd
scheduluer 检测到未绑定 Node 的Pod,开始调度并更新 Pod 的 Node 绑定
kubelet 检测到有新的 Pod 调度过来,通过 container runtime运行该 Pod
kubelet 通过 container runtime 取到 Pod 状态,并更新到 apiserver 中

先创建pod,发现未调度,调度过去,kubelet那边负责镜像下载容器生成返回结果等 *** 作

Label

Label 标签在 Kubernetes 资源对象中使用很多,也是非常重要的一个属性,Label 是识别 Kubernetes 对象的标签,以 key/value 的方式附加到对象上(key最长不能超过63字节,value 可以为空,也可以是不超过253字节的字符串)上面我们定义的 Nginx 的 Pod 就添加了一个 app=nginx 的 Label 标签。Label 不提供唯一性,并且实际上经常是很多对象(如Pods)都使用相同的 Label 来标志具体的应用。Label 定义好后其他对象可以使用 Label Selector 来选择一组相同 Label 的对象(比如 Service 用 Label 来选择一组 Pod)。Label Selector 支持以下几种方式:

等式,如 app=nginx 和 env!=production
集合,如 env in (production, qa)
多个Label(它们之间是AND关系),如app=nginx,env=test

多个标签的情况要同时满足才算满足

Namespace

Namespace(命名空间)是对一组资源和对象的抽象集合,比如可以用来将系统内部的对象划分为不同的项目组或用户组。常见的 Pods、Services、Deployments 等都是属于某一个 Namespace 的(默认是default),比如上面我们的 Nginx Pod 没有指定 namespace,则默认就在 default 命名空间下面,而 Node, PersistentVolumes 等资源则不属于任何 Namespace,是全局的。

注意它并不是 Linux Namespace,二者没有任何关系,它只是 Kubernetes 划分不同工作空间的一个逻辑单位。

Deployment

我们说了 Pod 是 Kubernetes 集群中的最基本的调度单元,但是如果想要创建同一个容器的多份拷贝,需要一个一个分别创建出来么,那么能否将 Pods 划到一个逻辑组里面呢?Deployment 就是来管理 Pod 的资源对象。

Deployment 确保任意时间都有指定数量的 Pod“副本”在运行。如果为某个 Pod 创建了 Deployment 并且指定3个副本,它会创建3个 Pod,并且持续监控它们。如果某个 Pod 不响应,那么 Deployment 会替换它,始终保持总数为3。

如果之前不响应的 Pod 恢复了,现在就有4个 Pod 了,那么 Deployment 会将其中一个终止保持总数为3。如果在运行中将副本总数改为5,Deployment 会立刻启动2个新 Pod,保证总数为5。持回滚和滚动升级。

当创建 Deployment 时,需要指定两个东西:

Pod 模板:用来创建 Pod 副本的模板
Label 标签:Deployment 需要监控的 Pod 的标签。
现在已经创建了 Pod 的一些副本,那么这些副本上如何进行负载呢?如何把这些 Pod 暴露出去呢?这个时候我们就需要用到 Service 这种资源对象了。

Service

Service 是应用服务的抽象,通过 Labels 为应用提供负载均衡(kube-proxy来实现)和服务发现(label来实现)。匹配 Labels 的 Pod IP 和端口列表组成 Endpoints,由 kube-proxy 负责将服务 IP 负载均衡到这些 Endpoints 上。

每个 Service 都会自动分配一个 cluster IP(仅在集群内部可访问的虚拟地址)和 DNS 名,其他容器可以通过该地址或 DNS 来访问服务,而不需要了解后端容器的运行。(集群内部的dns管理是有kube-dns这类组件来管理,过去采用的dns服务pod名字叫kube-dns,现在用的coredns,在kube-system下,用k8s-app=kube-dns可以过滤出来

了解了上面的几个基本概念后,我们就完全可以把我们的容器服务迁移到 Kubernetes 集群上了。当然我们还得先搭建好我们的 Kubernetes 集群环境。

搭建集群

也是本专栏的一篇文章,关于kubernetes集群的搭建

资源清单 YAML 文件基本语法格式

前面我们得 Kubernetes 集群已经搭建成功了,现在我们就可以在集群里面来跑我们的应用了。要在集群里面运行我们自己的应用,首先我们需要知道几个概念。

第一个当然就是应用的镜像,因为我们在集群中运行的是容器,所以首先需要将我们的应用打包成镜像,前面的课程中我们已经学习过如何将应用打包成镜像,这里就不再赘述了。

镜像准备好了,Kubernetes 集群也准备好了,其实我们就可以把我们的应用部署到集群中了。但是镜像到集群中运行这个过程如何完成呢?必然有一个地方可以来描述我们的应用,然后把这份描述告诉集群,然后集群按照这个描述来部署应用。

在之前 Docker 环境下面我们是直接通过命令 docker run 来运行我们的应用的,在 Kubernetes 环境下面我们同样也可以用类似 kubectl run 这样的命令来运行我们的应用,但是在 Kubernetes 中却是不推荐使用命令行的方式,而是希望使用我们称为资源清单的东西来描述应用,资源清单可以用 YAML 或者 JSON 文件来编写,一般来说 YAML 文件更方便阅读和理解,所以我们的课程中都会使用 YAML 文件来进行描述。

通过一个资源清单文件来定义好一个应用后,我们就可以通过 kubectl 工具来直接运行它:

kubectl create -f xxxx.yaml

我们知道 kubectl 是直接 *** 作 APIServer 的,所以就相当于把我们的清单提交给了 APIServer,然后集群获取到清单描述的应用信息后存入到 etcd 数据库中,然后 kube-scheduler 组件发现这个时候有一个 Pod 还没有绑定到节点上,就会对这个 Pod 进行一系列的调度,把它调度到一个最合适的节点上,然后把这个节点和 Pod 绑定到一起(写回到 etcd),然后节点上的 kubelet 组件这个时候 watch 到有一个 Pod 被分配过来了,就去把这个 Pod 的信息拉取下来,然后根据描述通过容器运行时把容器创建出来,最后当然同样把 Pod 状态再写回到 etcd 中去,这样就完成了一整个的创建流程。

案例:

nginx-deployment.yaml

apiVersion: apps/v1  # API版本
kind: Deployment  # API对象类型
metadata:
  name: nginx-deploy
  labels:
    chapter: first-app
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2  # Pod 副本数量
  template:  # Pod 模板
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80
kubectl create -f nginx-deployment.yaml

默认default命名空间
kubectl get pod
kubectl get deploy

我们可以看到会在集群中生成两个 Pod 出来。而整个资源清单文件对应到 Kubernetes 中就是一个 API Object(API 对象),我们按照这些对象的要求填充上对应的属性后,提交给 Kubernetes 集群,就可以为我们创建出对应的资源对象,比如我们这里定义的是一个 Deployment 类型的 API 对象,我们按照这个 API 对象的要求填充了一些属性,就会为我们创建出对应的资源对象
Deployment 这个资源对象就是用来定义多副本应用的对象,而且还支持对每个副本进行滚动更新,上面我们的资源清单中的描述中有一个属性 replicas: 2,所以最后生成两个副本的 Pod。

而这个 Deployment 定义的副本 Pod 具体是什么样的,是通过下面的 Pod 模板来定义的,就是 template 下面的定义,这个模板中定义了我们的 Pod 中只有一个名为 nginx 的容器,容器使用的镜像是 nginx:1.7.9(spec.containers[0].image),并且这个容器监听的端口是 80(这里没有定义pod端口,targetport,实际上targetport和containerport默认一致,定义任意一个即可)(spec.containers[0].ports[0].containerPort),另外我们还为 Pod 添加了一个app: nginx这样的 Label 标签,这里需要非常注意的是上面的 selector.matchLabels 区域就是来表示我们的 Deployment 来管理哪些 Pod 的,所以这个地方需要和 Pod 模板中的 Label 标签保持一致,这个 Label 标签之前我们也提到过是非常重要的。

另外我们也可以发现每个 API 对象都有一个 Metadata 的字段,用来表示该对象的元数据的,比如定义 name、namespace 等,比如上面 Deployment 和 Pod 模板中都有这个字段,至于为什么 Pod 模板中没有 name 这个元信息呢,这是因为 Deployment 这个控制器会自动在他自己的 name 基础上生成 Pod 名,不过 Deployment 下面定义的 Label 标签就没有 Pod 中定义的 Label 标签那么重要了,只是起到一个对该对象标识和过滤的作用

kubectl get deployment,pod -l chapter=first-app

我们可以使用一个 kubectl describe 命令来查看资源对象的详细信息,其中的event排错时常用
可以看到很多该资源的信息

kubectl describe pod nginx-deploy-54f57cf6bf-2fdjz
对于pod,还可以通过日志查看
kubectl logs pod_name -n namespace

kubectl get api资源类型 资源名如pod名称 -n namespace -o wide|yaml

另外一个方面如果我们相对我们的应用进行升级的话应该怎么办呢?这个 *** 作在我们日常工作中还是非常常见的,而在 Kubernetes 这里也是非常简单的,我们只需要修改我们的资源清单文件即可,比如我们把镜像升级到最新版本nginx:latest:

1:
vim 资源文件,将镜像版本更改,保存退出,然后apply 该文件即可

2:
kubectl edit pod_name -n namespace
像vim 一样修改,保存退出,不用手动apply

通过kubectl apply命令来直接更新,这个命令也是推荐我们使用的,我们不必关心当前的 *** 作是创建,还是更新,执行的命令始终是 kubectl apply,Kubernetes 则会根据 YAML 文件的内容变化,自动进行具体的处理,所以无论是创建还是更新都可以直接使用这个命令(create只是用文件来创建,不会用文件来更新)

通过在命令后面加上 --watch 参数来查看 Pod 的更新过程

通过在命令后面加上 --watch 参数来查看 Pod 的更新过程

最后,如果需要把我们的应用从集群中删除掉,可以用 kubectl delete 命令来清理:

kubectl delete -f nginx-deployment.yaml
kubectl delete api资源类型 资源名 -n namespace

YAML 是专门用来写配置文件的语言,非常简洁和强大,远比 JSON 格式方便。YAML语言(发音 /ˈjæməl/)的设计目标,就是方便人类读写。它实质上是一种通用的数据串行化格式。

它的基本语法规则如下:

大小写敏感
使用缩进表示层级关系
缩进时不允许使用Tab键,只允许使用空格 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可

#表示注释,从这个字符一直到行尾,都会被解析器忽略

在 Kubernetes 中,我们只需要了解两种结构类型就行了:

Lists(列表)
Maps(字典)
也就是说,你可能会遇到 Lists 的 Maps 和 Maps 的 Lists,等等。不过不用担心,你只要掌握了这两种结构也就可以了,其他更加复杂的我们暂不讨论。

Maps
首先我们来看看 Maps,我们都知道 Map 是字典,就是一个 key:value 的键值对,Maps 可以让我们更加方便的去书写配置信息,例如:

---
apiVersion: v1
kind: Pod

第一行的—是分隔符,是可选的,在单一文件中,可用连续三个连字号—区分多个文件。这里我们可以看到,我们有两个键:kind 和 apiVersion,他们对应的值分别是:v1 和 Pod。上面的 YAML 文件转换成 JSON 格式的话,你肯定就容易明白了:

{
    "apiVersion": "v1",
    "kind": "pod"
}

我们在创建一个相对复杂一点的 YAML 文件,创建一个 KEY 对应的值不是字符串而是一个 Maps:

---
apiVersion: v1
kind: Pod
metadata:
  name: ydzs-site
  labels:
    app: web

上面的 YAML 文件,metadata 这个 KEY 对应的值就是一个 Maps 了,而且嵌套的 labels 这个 KEY 的值又是一个 Map,你可以根据你自己的情况进行多层嵌套。

上面我们也提到了 YAML 文件的语法规则,YAML 处理器是根据行缩进来知道内容之间的嗯关联性的。比如我们上面的 YAML 文件,我用了两个空格作为缩进,空格的数量并不重要,但是你得保持一致,并且至少要求一个空格(什么意思?就是你别一会缩进两个空格,一会缩进4个空格)。我们可以看到 name 和 labels 是相同级别的缩进,所以 YAML 处理器就知道了他们属于同一个 Map,而 app 是 labels 的值是因为 app 的缩进更大。

百度搜索yaml和json在线转换,有很多在线转换的网站

Lists
Lists就是列表,说白了就是数组,在 YAML 文件中我们可以这样定义:

args
  - Cat
  - Dog
  - Fish

你可以有任何数量的项在列表中,每个项的定义以破折号(-)开头的,与父元素之间可以缩进也可以不缩进。对应的 JSON 格式如下:

{
    "args": [ 'Cat', 'Dog', 'Fish' ]
}

当然,Lists 的子项也可以是 Maps,Maps 的子项也可以是 Lists 如下所示:

---
apiVersion: v1
kind: Pod
metadata:
  name: ydzs-site
  labels:
    app: web
spec:
  containers:
    - name: front-end
      image: nginx
      ports:
        - containerPort: 80
    - name: flaskapp-demo
      image: cnych/flaskapp
      ports:
        - containerPort: 5000

比如这个 YAML 文件,我们定义了一个叫 containers 的 List 对象,每个子项都由 name、image、ports 组成,每个 ports 都有一个 key 为 containerPort 的 Map 组成,同样的,我们可以转成如下 JSON 格式文件:

{
    "apiVersion": "v1",
    "kind": "Pod",
    "metadata": {
        "name": "ydzs-site",
        "labels": {
            "app": "web"
        }
    },
    "spec": {
        "containers": [{
            "name": "front-end",
            "image": "nginx",
            "ports": [{
                "containerPort": "80"
            }]
        }, {
            "name": "flaskapp-demo",
            "image": "cnych/flaskapp",
            "ports": [{
                "containerPort": "5000"
            }]
        }]
    }
}

理解yaml逻辑只需注意两点就行,-只用来表示数组,提示数组,真正的逻辑关系永远是用缩进来表示,也就是看你对齐那个

kubernetes API文档和kubectl explain

编写清单文件,不熟悉的话可以去看官方的api文档,这里建议使用kubectl explain

[root@master pod]# kubectl explain deploy.spec
KIND:     Deployment
VERSION:  apps/v1

RESOURCE: spec <Object>

DESCRIPTION:
     Specification of the desired behavior of the Deployment.

     DeploymentSpec is the specification of the desired behavior of the
     Deployment.

FIELDS:
   minReadySeconds      <integer>
     Minimum number of seconds for which a newly created pod should be ready
     without any of its container crashing, for it to be considered available.
     Defaults to 0 (pod will be considered available as soon as it is ready)

   paused       <boolean>
     Indicates that the deployment is paused.

   progressDeadlineSeconds      <integer>
     The maximum time in seconds for a deployment to make progress before it is
     considered to be failed. The deployment controller will continue to process
     failed deployments and a condition with a ProgressDeadlineExceeded reason
     will be surfaced in the deployment status. Note that progress will not be
     estimated during the time a deployment is paused. Defaults to 600s.

   replicas     <integer>
     Number of desired pods. This is a pointer to distinguish between explicit
     zero and not specified. Defaults to 1.

   revisionHistoryLimit <integer>
     The number of old ReplicaSets to retain to allow rollback. This is a
     pointer to distinguish between explicit zero and not specified. Defaults to
     10.

   selector     <Object> -required-
     Label selector for pods. Existing ReplicaSets whose pods are selected by
     this will be the ones affected by this deployment. It must match the pod
     template's labels.

   strategy     <Object>
     The deployment strategy to use to replace existing pods with new ones.

   template     <Object> -required-
     Template describes the pods that will be created.

pod原理

一个pod中有多个容器

为什么需要 Pod
假设 Kubernetes 中调度的基本单元就是容器,对于一个非常简单的应用可以直接被调度直接使用,没有什么问题,但是往往还有很多应用程序是由多个进程组成的,有的同学可能会说把这些进程都打包到一个容器中去不就可以了吗?理论上是可以实现的,但是不要忘记了 Docker 管理的进程是 pid=1 的主进程,其他进程死掉了就会成为僵尸进程,没办法进行管理了,这种方式本身也不是容器推荐的运行方式,一个容器最好只干一件事情,所以在真实的环境中不会使用这种方式。

那么我们就把这个应用的进程进行拆分,拆分成一个一个的容器总可以了吧?但是不要忘记一个问题,拆分成一个一个的容器后,是不是就有可能出现一个应用下面的某个进程容器被调度到了不同的节点上呀?往往我们应用内部的进程与进程间通信(通过 IPC 或者共享本地文件之类)都是要求在本地进行的,也就是需要在同一个节点上运行。

所以我们需要一个更高级别的结构来将这些容器绑定在一起,并将他们作为一个基本的调度单元进行管理,这样就可以保证这些容器始终在同一个节点上面,这也就是 Pod 设计的初衷。

Pod 概念
在一个 Pod 下面运行几个关系非常密切的容器进程,这样一来这些进程本身又可以收到容器的管控,又具有几乎一致的运行环境,也就完美解决了上面提到的问题。

其实 Pod 也只是一个逻辑概念,真正起作用的还是 Linux 容器的 Namespace 和 Cgroup 这两个最基本的概念,Pod 被创建出来其实是一组共享了一些资源的容器而已。首先 Pod 里面的所有容器,都是共享的同一个 Network Namespace,但是涉及到文件系统的时候,默认情况下 Pod 里面的容器之间的文件系统是完全隔离的,但是我们可以通过声明来共享同一个 Volume。

对于共享同一个 Network Namespace 这个概念是不是比较熟悉,我们之前在 Docker 网络模式的章节中讲解了网络的 Container 模式,我们可以指定新创建的容器和一个已经存在的容器共享一个 Network Namespace,在运行容器的时候只需要指定 –net=container:目标容器名 这个参数就可以了,但是这种模式有一个明显的问题那就是容器的启动有先后顺序问题,那么 Pod 是怎么来处理这个问题的呢?那就是加入一个中间容器(没有什么架构是加一个中间件解决不了的?),这个容器叫做 Infra 容器(pause容器),而且这个容器在 Pod 中永远都是第一个被创建的容器,这样是不是其他容器都加入到这个 Infra 容器就可以了,这样就完全实现了 Pod 中的所有容器都和 Infra 容器共享同一个 Network Namespace 了,如下图所示:

所以当我们部署完成 Kubernetes 集群的时候,首先需要保证在所有节点上可以拉取到默认的 Infra 镜像,默认情况下 Infra 镜像地址为 k8s.gcr.io/pause:3.1,这个容器占用的资源非常少,但是这个镜像默认是需要科学上网的,所以很多时候我们在部署应用的时候一直处于 Pending 状态,因为所有 Pod 最先启动的容器镜像都拉不下来,肯定启动不了,启动不了其他容器肯定也就不能启动了
官方仓库下载不了,可以去阿里云搜索这个镜像下载下来,可能还需要打标签
从上面图中我们可以看出普通的容器加入到了 Infra 容器的 Network Namespace 中,所以这个 Pod 下面的所有容器就是共享同一个 Network Namespace 了,普通容器不会创建自己的网卡,配置自己的 IP,而是和 Infra 容器共享 IP、端口范围等,而且容器之间的进程可以通过 lo 网卡设备进行通信:

也就是容器之间是可以直接使用 localhost 进行通信的;
看到的网络设备信息都是和 Infra 容器完全一样的; 也就意味着同一个Pod 下面的容器运行的多个进程不能绑定相同的端口;
而且 Pod 的生命周期只跟 Infra 容器一致,而与容器 A 和 B 无关。

同一个pod中容器共享了infra的ip和port,所以多个容器将设置的端口不能相同,这也是为什么我们很少直接指定containerport的原因,端口等设置交给kubelet自动配置

对于文件系统 Kubernetes 是怎么实现让一个 Pod 中的容器共享的呢?默认情况下容器的文件系统是互相隔离的,要实现共享只需要在 Pod 的顶层声明一个 Volume,然后在需要共享这个 Volume 的容器中声明挂载即可。

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  volumes: 
  - name: varlog
    hostPath: 
      path: /var/log/counter
  containers:
  - name: count
    image: busybox
    args:
    - /bin/sh
    - -c
    - >
      i=0;
      while true;
      do
        echo "$i: $(date)" >> /var/log/1.log;
        i=$((i+1));
        sleep 1;
      done
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  - name: count-log
    image: busybox
    args: [/bin/sh, -c, 'tail -n+1 -f /var/log/1.log']
    volumeMounts:
    - name: varlog
      mountPath: /var/log

示例中我们在 Pod 的顶层声明了一个名为 varlog 的 Volume,而这个 Volume 的类型是 hostPath,也就意味这个宿主机的 /var/log/counter 目录将被这个 Pod 共享,共享给谁呢?在需要用到这个数据目录的容器上声明挂载即可,也就是通过 volumeMounts 声明挂载的部分,这样我们这个 Pod 就实现了共享容器的 /var/log 目录,而且数据被持久化到了宿主机目录上。

这个方式也是 Kubernetes 中一个非常重要的设计模式:sidecar
模式的常用方式。典型的场景就是容器日志收集,比如上面我们的这个应用,其中应用的日志是被输出到容器的 /var/log
目录上的,这个时候我们可以把 Pod 声明的 Volume 挂载到容器的 /var/log 目录上,然后在这个 Pod 里面同时运行一个
sidecar 容器,他也声明挂载相同的 Volume 到自己容器的 /var/log (或其他)目录上,这样我们这个 sidecar
容器就只需要从 /var/log 目录下面不断消费日志发送到 Elasticsearch
中存储起来就完成了最基本的应用日志的基本收集工作了。

除了这个应用场景之外使用更多的还是利用 Pod 中的所有容器共享同一个 Network Namespace 这个特性,这样我们就可以把
Pod 网络相关的配置和管理也可以交给一个 sidecar 容器来完成,完全不需要去干涉用户容器,这个特性在现在非常火热的 Service
Mesh(服务网格)中应用非常广泛,典型的应用就是 Istio

如何划分 Pod

上面我们介绍了 Pod 的实现原理,了解到了应该把关系紧密的容器划分到同一个 Pod 中运行,那么怎么来区分“关系紧密”呢?举一个简单的示例,比如我们的 Wordpress 应用,是一个典型的前端服务器和后端数据服务的应用,那么你认为应该使用一个 Pod 还是两个 Pod 呢?

如果在同一个 Pod 中同时运行服务器程序和后端的数据库服务这两个容器,理论上肯定是可行的,但是不推荐这样使用,我们知道一个 Pod 中的所有容器都是同一个整体进行调度的,但是对于我们这个应用 Wordpress 和 MySQL 数据库一定需要运行在一起吗?当然不需要,我们甚至可以将 MySQL 部署到集群之外对吧?所以 Wordpress 和 MySQL 即使不运行在同一个节点上也是可行的,只要能够访问到即可。

但是如果你非要强行部署到同一个 Pod 中呢?从某个角度来说是错误的,比如现在我们的应用访问量非常大,一个 Pod 已经满足不了我们的需求了,怎么办呢?扩容对吧,但是扩容的目标也是 Pod,并不是容器,比如我们再添加一个 Pod,这个时候我们就有两个 Wordpress 的应用和两个 MySQL 数据库了,而且这两个 Pod 之间的数据是互相独立的,因为 MySQL 数据库并不是简单的增加副本就可以共享数据了,所以这个时候就得分开部署了,采用第二种方案,这个时候我们只需要单独扩容 Wordpress 的这个 Pod,后端的 MySQL 数据库并不会受到扩容的影响。

将多个容器部署到同一个 Pod 中的最主要参考就是应用可能由一个主进程和一个或多个的辅助进程组成,比如上面我们的日志收集的 Pod,需要其他的 sidecar 容器来支持日志的采集。所以当我们判断是否需要在 Pod 中使用多个容器的时候,我们可以按照如下的几个方式来判断:

这些容器是否一定需要一起运行,是否可以运行在不同的节点上
这些容器是一个整体还是独立的组件
这些容器一起进行扩缩容会影响应用吗
基本上我们能够回答上面的几个问题就能够判断是否需要在 Pod 中运行多个容器了。

Pod 的设计

其实在我们理解 Pod 的时候,有一个比较好的类比的方式就是把 Pod 看成我们之前的
“虚拟机”,而容器就是虚拟机中运行的一个用户程序,这样就可以很好的来理解 Pod 的设计。

pod生命周期

pod状态

首先先了解下 Pod 的状态值,我们可以通过 kubectl explain pod.status 命令来了解关于 Pod 状态的一些信息,Pod 的状态定义在 PodStatus 对象中,其中有一个 phase 字段,下面是 phase 的可能取值:

挂起(Pending):Pod 信息已经提交给了集群,但是还没有被调度器调度到合适的节点或者 Pod 里的镜像正在下载
运行中(Running):该 Pod 已经绑定到了一个节点上,Pod 中所有的容器都已被创建。至少有一个容器正在运行,或者正处于启动或重启状态
成功(Succeeded):Pod 中的所有容器都被成功终止,并且不会再重启
失败(Failed):Pod 中的所有容器都已终止了,并且至少有一个容器是因为失败终止。也就是说,容器以非0状态退出或者被系统终止
未知(Unknown):因为某些原因无法取得 Pod 的状态,通常是因为与 Pod 所在主机通信失败导致的

除此之外,PodStatus 对象中还包含一个 PodCondition 的数组,里面包含的属性有:

lastProbeTime:最后一次探测 Pod Condition 的时间戳。
lastTransitionTime:上次Condition 从一种状态转换到另一种状态的时间。 message:上次 Condition 状态转换的详细描述。
reason:Condition 最后一次转换的原因。 status:Condition 状态类型,可以为 “True”, “False”,and “Unknown”.
type:Condition 类型,包括以下方面: PodScheduled(Pod 已经被调度到其他node 里) Ready(Pod 能够提供服务请求,可以被添加到所有可匹配服务的负载平衡池中)
Initialized(所有的init containers已经启动成功) Unschedulable(调度程序现在无法调度 Pod,例如由于缺乏资源或其他限制)
ContainersReady(Pod 里的所有容器都是 ready 状态)

重启策略

我们可以通过配置restartPolicy字段来设置 Pod 中所有容器的重启策略,其可能值为Always,OnFailure 和 Never,默认值为 Always。restartPolicy 仅指通过 kubelet 在同一节点上重新启动容器。通过 kubelet 重新启动的退出容器将以指数增加延迟(10s,20s,40s…)重新启动,上限为 5 分钟,并在成功执行 10 分钟后重置。不同类型的的控制器可以控制 Pod 的重启策略:

Job:适用于一次性任务如批量计算,任务结束后 Pod 会被此类控制器清除。Job的重启策略只能是"OnFailure"或者"Never"。
Replication Controller, ReplicaSet, or Deployment,此类控制器希望 Pod 一直运行下去,它们的重启策略只能是"Always"。
DaemonSet:每个节点上启动一个Pod,很明显此类控制器的重启策略也应该是"Always"。

初始化容器

了解了 Pod 状态后,首先来了解下 Pod 中最新启动的 Init Container,也就是我们平时常说的初始化容器。Init Container就是用来做初始化工作的容器,可以是一个或者多个,如果有多个的话,这些容器会按定义的顺序依次执行。我们知道一个 Pod 里面的所有容器是共享数据卷和Network Namespace 的,所以Init Container里面产生的数据可以被主容器使用到。从上面的 Pod 生命周期的图中可以看出初始化容器是独立与主容器之外的,只有所有的`初始化容器执行完之后,主容器才会被启动。那么初始化容器有哪些应用场景呢:

等待其他模块 Ready:这个可以用来解决服务之间的依赖问题,比如我们有一个 Web 服务,该服务又依赖于另外一个数据库服务,但是在我们启动这个 Web 服务的时候我们并不能保证依赖的这个数据库服务就已经启动起来了,所以可能会出现一段时间内 Web 服务连接数据库异常。要解决这个问题的话我们就可以在 Web 服务的 Pod 中使用一个 InitContainer,在这个初始化容器中去检查数据库是否已经准备好了,准备好了过后初始化容器就结束退出,然后我们主容器的 Web 服务才被启动起来,这个时候去连接数据库就不会有问题了。
做初始化配置:比如集群里检测所有已经存在的成员节点,为主容器准备好集群的配置信息,这样主容器起来后就能用这个配置信息加入集群。
其它场景:如将 Pod 注册到一个中央数据库、配置中心等。

案例:

apiVersion: v1
kind: Pod
metadata:
  name: init-demo
spec:
  volumes:
  - name: workdir
    emptyDir: {}
  initContainers:
  - name: install
    image: busybox
    command:
    - wget
    - "-O"
    - "/work-dir/index.html"
    - http://www.baidu.com  # https
    volumeMounts:
    - name: workdir
      mountPath: "/work-dir"
  containers:
  - name: web
    image: nginx
    ports:
    - containerPort: 80
    volumeMounts:
    - name: workdir
      mountPath: /usr/share/nginx/html

初始化容器,完成之后就会推出不会留下自己的资源,而这里是volume,是可以共享的,比如init container改变了这个volume对应的容器中的目录的权限,同样的volume的权限也会变化,init container退出,而volume已经被改了权限,别的容器可以继续沿用这个特性的volume

上面的资源清单中我们首先在 Pod 顶层声明了一个名为 workdir 的 Volume,前面我们用了 hostPath 的模式,这里我们使用的是 emptyDir{},这个是一个临时的目录,数据会保存在 kubelet 的工作目录下面,生命周期等同于 Pod 的生命周期。

然后我们定义了一个初始化容器,该容器会下载一个 html 文件到 /work-dir 目录下面,但是由于我们又将该目录声明挂载到了全局的 Volume,同样的主容器 nginx 也将目录 /usr/share/nginx/html 声明挂载到了全局的 Volume,所以在主容器的该目录下面会同步初始化容器中创建的 index.html 文件。

执行apply即可,可以kubectl get看看pod的情况

Pod Hook

我们知道 Pod 是 Kubernetes 集群中的最小单元,而 Pod 是由容器组成的,所以在讨论 Pod 的生命周期的时候我们可以先来讨论下容器的生命周期。实际上 Kubernetes 为我们的容器提供了生命周期的钩子,就是我们说的Pod Hook,Pod Hook 是由 kubelet 发起的,当容器中的进程启动前或者容器中的进程终止之前运行,这是包含在容器的生命周期之中。我们可以同时为 Pod 中的所有容器都配置 hook。

Kubernetes 为我们提供了两种钩子函数:

PostStart:这个钩子在容器创建后立即执行。但是,并不能保证钩子将在容器 ENTRYPOINT 之前运行,因为没有参数传递给处理程序。主要用于资源部署、环境准备等。不过需要注意的是如果钩子花费太长时间以至于不能运行或者挂起,容器将不能达到 running 状态。

PreStop:这个钩子在容器终止之前立即被调用。它是阻塞的,意味着它是同步的,所以它必须在删除容器的调用发出之前完成。主要用于优雅关闭应用程序、通知其他系统等。如果钩子在执行期间挂起,Pod 阶段将停留在 running 状态并且永不会达到 failed 状态。

如果 PostStart 或者 PreStop 钩子失败, 它会杀死容器。所以我们应该让钩子函数尽可能的轻量。当然有些情况下,长时间运行命令是合理的, 比如在停止容器之前预先保存状态。

另外我们有两种方式来实现上面的钩子函数:

Exec - 用于执行一段特定的命令,不过要注意的是该命令消耗的资源会被计入容器。
HTTP - 对容器上的特定的端点执行 HTTP 请求。

定义了一个 Nginx Pod,其中设置了 PostStart 钩子函数,即在容器创建成功后,写入一句话到 /usr/share/message 文件中

apiVersion: v1
kind: Pod
metadata:
  name: hook-demo1
spec:
  containers:
  - name: hook-demo1
    image: nginx
    lifecycle:
      postStart:
        exec:
          command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]

当用户请求删除含有 Pod 的资源对象时(如 Deployment 等),K8S为了让应用程序优雅关闭(即让应用程序完成正在处理的请求后,再关闭软件),K8S 提供两种信息通知:

默认:K8S 通知 node 执行 docker stop 命令,docker 会先向容器中 PID 为 1的进程发送系统信号SIGTERM,然后等待容器中的应用程序终止执行,如果等待时间达到设定的超时时间,或者默认超时时间(30s),会继续发送SIGKILL的系统信号强行kill 掉进程
使用 Pod 生命周期(利用PreStop回调函数),它在发送终止信号之前执行

默认所有的优雅退出时间都在30秒内。kubectl delete 命令支持 --grace-period=选项,这个选项允许用户用他们自己指定的值覆盖默认值。值’0’代表强制删除 pod。 在 kubectl 1.5 及以上的版本里,执行强制删除时必须同时指定 --force --grace-period=0。(grace优雅)

kubectl delete -grace-period=0 --force 

强制删除一个 pod 是从集群状态还有 etcd 里立刻删除这个 pod,只是当 Pod 被强制删除时, APIServer 不会等待来自 Pod 所在节点上的 kubelet 的确认信息:pod 已经被终止。在 API 里 pod 会被立刻删除,在节点上, pods 被设置成立刻终止后,在强行杀掉前还会有一个很小的宽限期。

定义了一个 Nginx Pod,其中设置了PreStop钩子函数,即在容器退出之前,优雅的关闭 Nginx
一个是利用 preStop 来进行优雅删除,另外一个是利用 preStop 来做一些信息记录的事情

apiVersion: v1
kind: Pod
metadata:
  name: hook-demo2
spec:
  containers:
  - name: hook-demo2
    image: nginx
    lifecycle:
      preStop:
        exec:
          command: ["/usr/sbin/nginx","-s","quit"]  # 优雅退出

---
apiVersion: v1
kind: Pod
metadata:
  name: hook-demo3
spec:
  volumes:
  - name: message
    hostPath:
      path: /tmp
  containers:
  - name: hook-demo2
    image: nginx
    ports:
    - containerPort: 80
    volumeMounts:
    - name: message
      mountPath: /usr/share/
    lifecycle:
      preStop:
        exec:
          command: ['/bin/sh', '-c', 'echo Hello from the preStop Handler > /usr/share/message']
lifestyle:
  postStart|preStop:
    exec:
      command:

在容器删除之前会执行 preStop 里面的优雅关闭命令,这个用法在后面我们的滚动更新的时候用来保证我们的应用零宕机非常有用。

Pod 健康检查(探针)

现在在 Pod 的整个生命周期中,能影响到 Pod 的就只剩下健康检查这一部分了。在 Kubernetes 集群当中,我们可以通过配置liveness probe(存活探针)和readiness probe(可读性探针)来影响容器的生命周期:

kubelet 通过使用 liveness probe 来确定你的应用程序是否正在运行,通俗点将就是是否还活着。一般来说,如果你的程序一旦崩溃了, Kubernetes 就会立刻知道这个程序已经终止了,然后就会重启这个程序。而我们的 liveness probe 的目的就是来捕获到当前应用程序还没有终止,还没有崩溃,如果出现了这些情况,那么就重启处于该状态下的容器,使应用程序在存在 bug 的情况下依然能够继续运行下去。
kubelet 使用 readiness probe 来确定容器是否已经就绪可以接收流量过来了。这个探针通俗点讲就是说是否准备好了,现在可以开始工作了。只有当 Pod 中的容器都处于就绪状态的时候 kubelet 才会认定该 Pod 处于就绪状态,因为一个 Pod 下面可能会有多个容器。当然 Pod 如果处于非就绪状态,那么我们就会将他从 Service 的 Endpoints 列表中移除出来,这样我们的流量就不会被路由到这个 Pod 里面来了。

和前面的钩子函数一样的,我们这两个探针的支持下面几种配置方式:

exec:执行一段命令
http:检测某个 http 请求
tcpSocket:使用此配置,kubelet 将尝试在指定端口上打开容器的套接字。如果可以建立连接,容器被认为是健康的,如果不能就认为是失败的。实际上就是检查端口。

apiVersion: v1
kind: Pod
metadata:
  name: liveness-exec
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

我们这里需要用到一个新的属性:livenessProbe,下面通过 exec 执行一段命令:

periodSeconds:表示让 kubelet 每隔5秒执行一次存活探针,也就是每5秒执行一次上面的cat /tmp/healthy命令,如果命令执行成功了,将返回0,那么 kubelet 就会认为当前这个容器是存活的,如果返回的是非0值,那么 kubelet 就会把该容器杀掉然后重启它。默认是10秒,最小1秒。
initialDelaySeconds:表示在第一次执行探针的时候要等待5秒,这样能够确保我们的容器能够有足够的时间启动起来。大家可以想象下,如果你的第一次执行探针等候的时间太短,是不是很有可能容器还没正常启动起来,所以存活探针很可能始终都是失败的,这样就会无休止的重启下去了,对吧?

apiVersion: v1
kind: Pod
metadata:
  name: liveness-http
spec:
  containers:
  - name: liveness
    image: cnych/liveness
    args:
    - /server
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080
        httpHeaders:
        - name: X-Custom-Header
          value: Awesome
      initialDelaySeconds: 3
      periodSeconds: 3

同样的,根据 periodSeconds 属性我们可以知道 kubelet 需要每隔3秒执行一次 liveness Probe,该探针将向容器中的 server 的 8080 端口发送一个 HTTP GET 请求。如果 server 的 /healthz 路径的 handler 返回一个成功的返回码,kubelet 就会认定该容器是活着的并且很健康,如果返回失败的返回码,kubelet 将杀掉该容器并重启它。initialDelaySeconds 指定kubelet 在该执行第一次探测之前需要等待3秒钟。

返回码

通常来说,任何大于200小于400的状态码都会认定是成功的返回码。其他返回码都会被认为是失败的返回码。

除了上面的 exec 和 httpGet 两种检测方式之外,还可以通过 tcpSocket 方式来检测端口是否正常,大家可以按照上面的方式结合kubectl explain命令自己来验证下这种方式。

另外前面我们提到了探针里面有一个initialDelaySeconds的属性,可以来配置第一次执行探针的等待时间,对于启动非常慢的应用这个参数非常有用,比如 Jenkins、Gitlab 这类应用,但是如何设置一个合适的初始延迟时间呢?这个就和应用具体的环境有关系了,所以这个值往往不是通用的,这样的话可能就会导致一个问题,我们的资源清单在别的环境下可能就会健康检查失败了,为解决这个问题,在 Kubernetes v1.16 版本官方特地新增了一个 startupProbe(启动探针),该探针将推迟所有其他探针,直到 Pod 完成启动为止,使用方法和存活探针一样:

startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30  # 尽量设置大点
  periodSeconds: 10

比如上面这里的配置表示我们的慢速容器最多可以有5分钟(30个检查 * 10秒= 300s)来完成启动。

有的时候,应用程序可能暂时无法对外提供服务,例如,应用程序可能需要在启动期间加载大量数据或配置文件。在这种情况下,您不想杀死应用程序,也不想对外提供服务。那么这个时候我们就可以使用readiness probe来检测和减轻这些情况。 Pod 中的容器可以报告自己还没有准备,不能处理 Kubernetes 服务发送过来的流量。readiness probe的配置跟liveness probe基本上一致的。唯一的不同是使用readinessProbe而不是livenessProbe。两者如果同时使用的话就可以确保流量不会到达还未准备好的容器,准备好过后,如果应用程序出现了错误,则会重新启动容器。对于就绪探针我们会在后面 Service 的章节和大家继续介绍。

另外除了上面的initialDelaySeconds和periodSeconds属性外,探针还可以配置如下几个参数:

timeoutSeconds:探测超时时间,默认1秒,最小1秒。
successThreshold:探测失败后,最少连续探测成功多少次才被认定为成功。默认是 1,但是如果是liveness则必须是 1。最小值是 1。
failureThreshold:探测成功后,最少连续探测失败多少次才被认定为失败。默认是 3,最小值是 1。

Pod 资源配置

实际上上面几个步骤就是影响一个 Pod 生命周期的大的部分,但是还有一些细节也会在 Pod 的启动过程进行设置,比如在容器启动之前还会为当前的容器设置分配的 CPU、内存等资源,我们知道我们可以通过 CGroup 来对容器的资源进行限制,同样的,在 Pod 中我们也可以直接配置某个容器的使用的 CPU 或者内存的上限。那么 Pod 是如何来使用和控制这些资源的分配的呢?

首先对于 CPU,我们知道计算机里 CPU 的资源是按“时间片”的方式来进行分配的,系统里的每一个 *** 作都需要 CPU 的处理,所以,哪个任务要是申请的 CPU 时间片越多,那么它得到的 CPU 资源就越多,这个很容器理解。

然后还需要了解下 CGroup 里面对于 CPU 资源的单位换算:

1 CPU = 1000 millicpu(1 Core = 1000m)

0.5 CPU = 500 millicpu (0.5 Core = 500m)
1Gi=1024Mi
1G=1000M

在 Pod 里面我们可以通过下面的两个参数来限制和请求 CPU 资源:

spec.containers[].resources.limits.cpu:CPU 上限值,可以短暂超过,容器也不会被停止
spec.containers[].resources.requests.cpu:CPU请求值,Kubernetes 调度算法里的依据值,可以超过,性能会下降

这里需要明白的是,如果resources.requests.cpu设置的值大于集群里每个节点的最大 CPU 核心数,那么这个 Pod 将无法调度,因为没有节点能满足它。

到这里应该明白了,requests 是用于集群调度使用的资源,而 limits 才是真正的用于资源限制的配置,如果你需要保证的你应用优先级很高,也就是资源吃紧的情况下最后再杀掉你的 Pod,那么你就把你的 requests 和 limits 的值设置成一致,在后面应用的 Qos 中会具体讲解

apiVersion: v1
kind: Pod
metadata:
  name: resource-demo1
spec:
  containers:
  - name: resource-demo1
    image: nginx
    ports:
    - containerPort: 80
    resources:
      requests:
        memory: 50Mi
        cpu: 50m
      limits:
        memory: 100Mi
        cpu: 100m

apply之后,可以docker ps 列出容器,
然后docker inspect 主容器

Pod 上的资源配置最终也还是通过底层的容器运行时去控制 CGroup 来实现的

这里要注意的是,内存是不可压缩性资源,如果容器使用内存资源到达了上限,那么会OOM,造成内存溢出,容器就会终止和退出

pod进阶使用 静态 Pod

在 Kubernetes 集群中除了我们经常使用到的普通的 Pod 外,还有一种特殊的 Pod,叫做Static Pod,也就是我们说的静态 Pod,静态 Pod 有什么特殊的地方呢?

静态 Pod 直接由节点上的 kubelet 进程来管理,不通过 master 节点上的 apiserver。无法与我们常用的控制器 Deployment 或者 DaemonSet 进行关联,它由 kubelet 进程自己来监控,当 pod 崩溃时会重启该 pod,kubelet 也无法对他们进行健康检查。静态 pod 始终绑定在某一个 kubelet 上,并且始终运行在同一个节点上。kubelet 会自动为每一个静态 pod 在 Kubernetes 的 apiserver 上创建一个镜像 Pod,因此我们可以在 apiserver 中查询到该 pod,但是不能通过 apiserver 进行控制(例如不能删除)。

创建静态 Pod 有两种方式:配置文件和 HTTP 两种方式

配置文件

配置文件就是放在特定目录下的标准的 JSON 或 YAML 格式的 pod 定义文件。用 kubelet --pod-manifest-path=来启动 kubelet 进程,kubelet 定期的去扫描这个目录,根据这个目录下出现或消失的 YAML/JSON 文件来创建或删除静态 pod。

比如我们在 node01 这个节点上用静态 pod 的方式来启动一个 nginx 的服务。我们登录到node01节点上面,可以通过下面命令找到 kubelet 对应的启动配置文件

[root@master manifests]# cat /usr/lib/systemd/system/kubelet.service.d/10-kubeadm.conf
# Note: This dropin only works with kubeadm and kubelet v1.11+
[Service]
Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf"
Environment="KUBELET_CONFIG_ARGS=--config=/var/lib/kubelet/config.yaml"
Environment="KUBELET_SYSTEM_PODS_ARGS=--pod-manifest-path=/etc/kubernetes/manifests --allow-privileged=true"
# This is a file that "kubeadm init" and "kubeadm join" generates at runtime, populating the KUBELET_KUBEADM_ARGS variable dynamically
EnvironmentFile=-/var/lib/kubelet/kubeadm-flags.env
# This is a file that the user can use for overrides of the kubelet args as a last resort. Preferably, the user should use
# the .NodeRegistration.KubeletExtraArgs object in the configuration files instead. KUBELET_EXTRA_ARGS should be sourced from this file.
EnvironmentFile=-/etc/sysconfig/kubelet
ExecStart=
ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS

Environment="KUBELET_SYSTEM_PODS_ARGS=--pod-manifest-path=/etc/kubernetes/manifests --allow-privileged=true"

主要是这条,如果没有,就手动加入,然后systemctl restart kubelet

[root@ node1 ~] $ cat <<EOF >/etc/kubernetes/manifest/static-web.yaml
apiVersion: v1
kind: Pod
metadata:
  name: static-web
  labels:
    app: static
spec:
  containers:
    - name: web
      image: nginx
      ports:
        - name: web
          containerPort: 80
EOF

通过 HTTP 创建静态 Pods

kubelet 周期地从–manifest-url=参数指定的地址下载文件,并且把它翻译成 JSON/YAML 格式的 pod 定义。此后的 *** 作方式与–pod-manifest-path=相同,kubelet 会不时地重新下载该文件,当文件变化时对应地终止或启动静态 pod。(这需要你的kubelet的配置文件下有这个–manifest-url参数,manifest显示)

kubelet 启动时,由–pod-manifest-path= 或 --manifest-url=参数指定的目录下定义的所有 pod 都会自动创建,例如,我们示例中的 static-web。(可能要花些时间拉取nginx 镜像,耐心等待…)

静态 pod 的标签会传递给镜像 Pod,可以用来过滤或筛选。 需要注意的是,我们不能通过 API 服务器来删除静态 pod(例如,通过kubectl命令),kubelet 不会删除它。

手动终止容器,可以看到kubelet很快就会自动重启容器

其实我们用 kubeadm 安装的集群,master 节点上面的几个重要组件都是用静态 Pod 的方式运行的:

[root@master manifests]# pwd
/etc/kubernetes/manifests
[root@master manifests]#  ls
etcd.yaml  kube-apiserver.yaml  kube-controller-manager.yaml  kube-scheduler.yaml

现在明白了吧,这种方式也为我们将集群的一些组件容器化提供了可能,因为这些 Pod 都不会受到 apiserver 的控制,不然我们这里kube-apiserver怎么自己去控制自己呢?万一不小心把这个 Pod 删掉了呢?所以只能有kubelet自己来进行控制,这就是我们所说的静态 Pod。

Downward API

向容器注入pod的信息
(downward向下的)
前面我们从 Pod 的原理到生命周期介绍了 Pod 的一些使用,作为 Kubernetes 中最核心的资源对象、最基本的调度单元,我们可以发现 Pod 中的属性还是非常繁多的,前面我们使用过一个 volumes 的属性,表示声明一个数据卷,我们可以通过命令kubectl explain pod.spec.volumes去查看该对象下面的属性非常多,前面我们只是简单使用了 hostPath 和 emptyDir{}(简单的volume) 这两种模式,其中还有一种模式叫做downwardAPI,这个模式和其他模式不一样的地方在于它不是为了存放容器的数据也不是用来进行容器和宿主机的数据交换的,而是让 Pod 里的容器能够直接获取到这个 Pod 对象本身的一些信息。

目前 Downward API 提供了两种方式用于将 Pod 的信息注入到容器内部:
环境变量:用于单个变量,可以将 Pod 信息和容器信息直接注入容器内部
\Volume 挂载:将 Pod
信息生成为文件,直接挂载到容器内部中去

环境变量

我们通过 Downward API 来将 Pod 的 IP、名称以及所对应的 namespace 注入到容器的环境变量中去,然后在容器中打印全部的环境变量来进行验证

apiVersion: v1
kind: Pod
metadata:
  name: env-pod
  namespace: kube-system
spec:
  containers:
  - name: env-pod
    image: busybox
    command: ["/bin/sh", "-c", "env"]
    env:
    - name: POD_NAME
      valueFrom:
        fieldRef:
          fieldPath: metadata.name
    - name: POD_NAMESPACE
      valueFrom:
        fieldRef:
          fieldPath: metadata.namespace
    - name: POD_IP
      valueFrom:
        fieldRef:
          fieldPath: status.podIP

这里用了一条命令,标准输出,你apply之后用logs查看pod的日志可以看到变量情况

我们可以看到上面我们使用了一种新的方式来设置 env 的值:valueFrom,由于 Pod 的 name 和 namespace 属于元数据,是在 Pod 创建之前就已经定下来了的,所以我们可以使用 metata 就可以获取到了,但是对于 Pod 的 IP 则不一样,因为我们知道 Pod IP 是不固定的,Pod 重建了就变了,它属于状态数据,所以我们使用 status 这个属性去获取。另外除了使用 fieldRef获取 Pod 的基本信息外,还可以通过 resourceFieldRef 去获取容器的资源请求和资源限制信息

env:
- name:
  valueFrom:
  	  filedRef:
  	    filedPath:
Volume 挂载

Downward API除了提供环境变量的方式外,还提供了通过 Volume 挂载的方式去获取 Pod 的基本信息。接下来我们通过Downward API将 Pod 的 Label、Annotation 等信息通过 Volume 挂载到容器的某个文件中去


```bash
apiVersion: v1
kind: Pod
metadata:
  name: volume-pod
  namespace: kube-system
  labels:
    k8s-app: test-volume
    node-env: test
  annotations:
    own: youdianzhishi
    build: test
spec:
  volumes:
  - name: podinfo
    downwardAPI:
      items:
      - path: labels
        fieldRef:
          fieldPath: metadata.labels
      - path: annotations
        fieldRef:
          fieldPath: metadata.annotations
  containers:
  - name: volume-pod
    image: busybox
    args: 
    - sleep 
    - "3600"
    volumeMounts:
    - name: podinfo
      mountPath: /etc/podinfo

labels和annotations会在该目录下成为文件

目前,Downward API 支持的字段已经非常丰富了,比如:

  1. 使用 fieldRef 可以声明使用:
    spec.nodeName - 宿主机名字
    status.hostIP - 宿主机IP
    metadata.name - Pod的名字
    metadata.namespace - Pod的Namespace
    status.podIP - Pod的IP
    spec.serviceAccountName - Pod的Service Account的名字
    metadata.uid - Pod的UID
    metadata.labels[‘’] - 指定的Label值
    metadata.annotations[‘’] - 指定的Annotation值
    metadata.labels - Pod的所有Label
    metadata.annotations - Pod的所有Annotation
  2. 使用 resourceFieldRef 可以声明使用:
    容器的 CPU limit
    容器的 CPU request
    容器的 memory limit
    容器的 memory request
volumes
- name:
  downwardAPI:
  - path:
    filedRef:
      filedPath:

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

在实际应用中,如果你的应用有获取 Pod 的基本信息的需求,一般我们就可以利用Downward API来获取基本信息,然后编写一个启动脚本或者利用initContainer将 Pod 的信息注入到我们容器中去,然后在我们自己的应用中就可以正常的处理相关逻辑了。

除了通过 Downward API 可以获取到 Pod 本身的信息之外,其实我们还可以通过映射其他资源对象来获取对应的信息,比如 Secret、ConfigMap 资源对象,同样我们可以通过环境变量和挂载 Volume 的方式来获取他们的信息,但是,通过环境变量获取这些信息的方式,不具备自动更新的能力。所以,一般情况下,都建议使用 Volume 文件的方式获取这些信息,因为通过 Volume 的方式挂载的文件在 Pod 中会进行热更新。

downwardapi,重启策略,钩子函数,探针,资源配置,这些都是在containers下

PodPreset

是不是希望 Kubernetes 能够提供一个功能为 Pod 自动填充一些字段呢?这个需求还是很实际的,比如我们按照命名空间来划分不同的环境,然后我们在不同的环境上部署 Pod 后自动为我们加上环境相关的 Labels、Annotations 等等信息,这样就大大提高了编写 YAML 的效率,为此,在 Kubernetes v1.11 版本后就提供了一个叫做 PodPreset(Pod 预设值)的功能可以来解决这个问题。

Kubernetes 提供了一个 PodPreset 准入控制器,当启用后,PodPreset 会将应用创建请求传入到该控制器上。当有 Pod 创建请求发生时,系统将执行以下 *** 作:

检索所有可用的 PodPreset
检查有 PodPreset 的标签选择器上的标签与正在创建的 Pod 上的标签是否匹配 尝试将由PodPreset 定义的各种资源合并到正在创建的 Pod 中
出现错误时,在该 Pod 上引发记录合并错误的事件,PodPreset不会注入任何资源到创建的 Pod 中
注释刚生成的修改过的 Pod spec,以表明它已被 PodPreset 修改过。注释的格式为
podpreset.admission.kubernetes.io/podpreset-":
“”

每个 Pod 可以匹配零个或多个 PodPrestet;并且每个 PodPreset 可以应用于零个或多个 Pod。 PodPreset 应用于一个或多个 Pod 时,Kubernetes 会修改 Pod Spec。对于 Env、EnvFrom 和 VolumeMounts 的更改,Kubernetes 修改 Pod 中所有容器的容器 spec;对于 Volume 的更改,Kubernetes 修改 Pod Spec。

可能在某些情况下,您希望你的 Pod 不会被任何 Pod Preset 所改变。在这些情况下,您可以在 Pod 的 Pod Spec 中添加上这样一个 annotation:podpreset.admission.kubernetes.io/exclude:"true"

启用 PodPreset

要启用PodPreset功能,需要确保你使用的是 kubernetes 1.8版本以上,然后需要在准入控制中加入PodPreset,另外为了定义 PodPreset 对象,还需要其中 PodPreset 的 API 版本,在 APIServer 启动参数中添加如下配置:

- --enable-admission-plugins=NodeRestriction,PodPreset   #开启PodPreset注入控制器
- --runtime-config=settings.k8s.io/v1alpha1=true   #开启api

我们这里是使用的 kubeadm 搭建的集群,所以只需要修改静态 Pod 配置即可,路径 /etc/kubernetes/manifests/kube-apiserver.yaml,修改完成后,将 kube-apiserver.yaml 文件移除 manifests 目录,然后移动回来,相当于强制重启

可以查看
kubectl api-version | grep settings

在pod的创建的namespace提前有podpreset

案例

创建pod,pod的时间一般都是UTC,即格林尼治时间,与我们的北京时间相差8个小时,我们的宿主机一般用背景时间,如果你需要同步pod和宿主机的时间,可以通过挂载宿主机的/etc/localtime到pod的/etc/localtime下实现
如果这个是常用的配置,每次都手动配置显然不合理,这时候就可以用podpreset

可以利用 PodPreset 来预设模板

apiVersion: settings.k8s.io/v1alpha1
kind: PodPreset
metadata:
  name: time-preset
  namespace: default
spec:
  selector:
    matchLabels:
  volumeMounts:
  - name: localtime
    mountPath: /etc/localtime
  volumes:
  - name: localtime
    hostPath:
      path: /etc/localtime

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/729474.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-27
下一篇 2022-04-27

发表评论

登录后才能评论

评论列表(0条)

保存