K8s 学习笔记(二)

1,027 阅读22分钟

继续学习 K8s,学习资料为 K8s 官方文档。上次直接从 tutorial 开始看起。这次开始仔细从概念看起。

1. Kubernetes 组件

1.1 Control plane components

理论上控制平面组件可以在所有节点上运行,但是一般来说会在一个计算机上(Master)运行所有控制平面组件。

1.1.1 kube-apiserver

第一个介绍的控制平面组件是 kube-apiserver,用于提供 Kubernetes API 服务。可以运行多个 kube-apiserver 实例,用以负载均衡。

1.1.2 etcd

用于存储键值对,一般充当 K8s 集群数据的数据库。通常需要有备份计划。

1.1.3 kube-scheduler

这个组件用于监视那些没有调度到节点上的 pods,以安排它们在节点上运行。也就是通过计算决定新的 pods 要在哪个节点上运行。

1.1.4 kube-controller-manager

在 master 上运行控制器的组件。控制器监控集群,致力于让当前状态变成期望状态。理论上来说每个控制器都应该是一个进程,但是在 K8s 中为了保持简化,所有的控制器都被编译在同一个文件上,在同一个进程中运行。
控制器分为以下几类:

  1. 节点控制器:负责监控节点是否故障,进行通知和响应。
  2. 任务控制器:负责管理一次性任务 Jobs,负责安装 Pods 来运行这些任务直至完成。
  3. 端点控制器:负责安装 Pods 和 Services。
  4. 服务账户和令牌控制器:用于控制新建账户和令牌。

1.1.5 cloud-controller-manager

在云上运行控制器的组件。作用为:

  • 将 Cluster 继承到云上。
  • 将 和云交互的组件 和 只能和 Cluster 交互的组件区分开。

和 kube-controller-manager 一样,也是全部集成到一个可执行文件上。可以运行多个副本,进行负载均衡。

  1. 节点控制器:用于在节点终止后用于检查云上节点是否以删除。
  2. 路由控制器:用于在底层云基础架构中设置路由。
  3. 服务控制器:用于在云中创建,更新,删除云的负载均衡。

1.2 node components

每个 node 上都有 node components,用于维护 pods 和提供 K8s 的运行环境。

1.2.1 kubelet

每个 node 上都有一个代理叫做 kubelet,用于保证容器都运行在 pods 中。
kubelet 接受一个 podSpec(这个 spec 可以通过各种渠道得到,比如 ymal),确保这些 podSpec 里描述的容器运行状态健康。kubelet 不会管不是 K8s 集群的东西。

1.2.2 kube-proxy

kube-proxy 是在每个节点上运行的网络代理。是实现 Service 的一部分。
kube-proxy 维护节点的网络规则。这些规则允许集群内部/外部的网络会话与 pods 进行通信。
kube-proxy 可用于与外部通信和负载均衡的流量分配。当然,这些也都是 Service 的作用。

1.2.3 container run time

运行容器的环境,一般来说就是 Docker。

1.3 addons

插件使用 K8s 的资源来实现集群功能。

1.3.1 集群 DNS

几乎所有集群都需要有集群 DNS,因为很多应用需要 DNS。
集群 DNS 也是一个 DNS 服务器,它为 Kubernetes 服务提供 DNS 记录。

1.3.2 Dashboard

Dashboard 是一个基于 Web 的用户界面,可以用来管理 K8s。

1.3.3 还有资源监控,日志等等插件

2. Kubernetes API

K8s 的核心就是 Kubernetes API。负责提供 HTTP API,以便集群各组件,用户,外部组件相互通信。
也可以编写程序来访问 API,可以使用客户端库。
kubectl,kubeadm 等工具就是在使用 Kubernetes API。

3. Kubernetes 对象

Kubernetes 使用 Kubernetes 对象来表示集群的状态。当 Kubernetes 对象是一个目标性记录,一旦对象被创建,K8s 会尽力去确保这个对象存在。因此,创建一个对象,实际上是在告诉 K8s 它在集群上应该是什么样的状态,也就是一个期望状态。
操作 Kubernetes 对象需要用到 Kubernetes API,比如 kubectl。

描述 K8s 对象,一般使用 yaml 格式。类似于 json,xml,是一种表示数据的格式。语法很像 python+markdown。总之就是非常简单。

3.1 对象管理

有三种方式:

  1. 命令式命令。直接使用命令行创建,更新,删除对象/资源。
  2. 命令式对象配置。把要用到的命令写在一个 yaml 文件中,运行这个文件进行配置。
    • kubectl create/replace/delete -f xxx.yaml
  3. 声明式对象配置。类似于 2,但是只有一个 apply 命令。k8s 会自动监测 yaml 文件配置的改变。后接一个配置文件夹。会自动监测这个文件夹中的所有配置文件的改变。
    • kubectl apply -f configs/
    • kubectl apply -R -f configs/

3.2 yaml 语法

基础语法就不说了,直接说各个字段的含义:

  1. apiVersion:K8s API 的版本,到2021年5月11日,此时最新版本依旧是 v1.
  2. kind:创建的对象的类别
  3. metadata:帮助辨识对象的标记,包括 name,UID,namespace,label。UID 会由 K8s 自动生成。
  4. spec:对象的各种信息,每个对象的字段都不一样。具体请查看官方文档

4. K8s 各种基础概念

4.1 namespace

使用 namespace 进行资源隔离。不同 namespace 的对象不能相互访问。

4.2 pods

K8s 中甚至连各个组件都是以 pod 的形式运行的。在 kube-system 命名空间里可以看到。 如果一个 pod 是被控制器控制的,那么删除 pod 是没用的。会自动创建新的 pod。

4.3 label

没什么好说的,简单

4.4 deployment

deployment 是一种 pod 控制器。如果只删除 pod 而不删除控制器,那么依然会生成新的 pod 来代替。

image.png 顺便一提,老版本创建 deployment 用的是 run。新版本的 run 则只会创建 pod,并不会生成 deployment。 新版本创建 deployment 要用 create deployment。

5. pod 详解

Pod 里可以是一个或多个容器。实际上,pod 里永远都会有一个 pause 容器。 作用为:

  1. 根据 pause 容器的健康状态,来判断整个 pod 的健康状态。
  2. 可以在 pause 容器上设置 ip,其他所有容器都采用这个 ip 地址。

image.png

5.1 pod.spec.containers

这里还有个坑,就是 docker 的坑。如果没有前台程序,那么就会自动关闭。所以如果直接启动一个 ubuntu,其实是会发现报错 crashloopback 的。

spec:
    containers:
        - name: xxx # 容器的名字(不是 pod 的名字)
          image: xxx # 镜像
          imagePullPolicy: xxx # 镜像拉取策略,具体见 # 5.1.1
          command: xxx # 覆盖 Dockerfile 的 ENTRYPOINT
          args: xxx # 覆盖 Dockerfile 的参数配置。如果没写 command,写了 args,
                  # 那么就会运行 ENTRYPOINT,同时使用 args 的参数。
          env:  # 容器环境变量列表
            - name1: xxx
              value1: xxx
            - name2: xxx
              value2: xxx
          ports: # 容器要暴露的端口列表,具体见 # 5.1.2 
            - name: xxx # 端口名称,如果非要指定的话,不能重复
              containerPort: xxx # 最重要,暴露的端口号
              hostPort: xxx # 暴露在 host 上的端口,会占用主机端口
                          # 所以如果指定的话,replicas 就不能大于 1。因为会冲突。
                          # 一般不会多此一举去指定,还占用了 host 的端口空间
                          # 就用 pods 自己的 ip 多好啊
              hostIP: xxx # 绑定的主机 IP,也是没什么意义,一般不指定
              protocol: xxx # TCP,UDP,SCTP 三选一,默认是 TCP
          resources: # 资源配额,对容器资源的限制,具体见 # 5.1.3
              limits:
                  cpu: xx
                  memory: xx
              requests:
                  cpu: xx
                  memory: xx

5.1.1 镜像拉取策略

yaml 文件中 pod 属性的 spec 中有这么个属性叫做 imagePullPolicy。这个指的是当本地已经有了某个镜像的时候,还要不要从远程仓库中下载新的镜像。有三种值可以选定:

  1. Always。永远都是下载新的。
  2. IfNotPresent。本地有就用本地,没有就用远程。
  3. Never。永远只用本地的。本地没有就会报错。 这里有个坑,如果 image 后面接一个具体版本号,那么策略就是 IfNotPresent。如果是 latest,那么策略就是 Always。

5.1.2 ports

是一个列表,表示要暴露哪些端口。对于每个端口而言,实际上除了 containerPort,其他一般都不写。

5.2.3 resources,资源配额

对容器资源要有限制,以免某些容器占用过多内存,挤兑其他容器的运行资源。
resources 有两个子选项,limits 和 requests。

  • limits 表示最大不能超过多少资源,如果超过将会重启容器。
  • requests 表示启动容器需要的最少资源。如果资源不够,则不会运行容器。 每个子选项又可以分别对 cpu 和内存进行资源分配。

5.2 pod 的生命周期

pod 从创建到终止的整个过程分为如下:

  1. Pod 的创建过程。
  2. 运行 Init Container(初始化容器),名词,是一种特殊容器。
  3. 运行 Main COntainer(主容器/用户容器)。
  4. Pod 的终止过程。

5.2.1 pod 的创建过程

  1. 用户通过 kubectl 或其他 api,向 apiServer 请求创建 pod。
  2. apiServer 开始生成 pod 对象信息,并存入 etcd 中,返回确认信息。
  3. apiServer 开始反映 etcd 中 pod 对象的变化,其他组件则通过 watch 机制来监控 apiServer 反映的变化。(也就是apiServer是一个中介,其他组件通过 apiServer 上的变化,来监控 etcd 的变化)。
    • (注:这里注意,这里全都是监控机制,也就是说 apiServer 并不会主动告诉其他组件各种变化信息,而是组件来监听 apiServer 的变化)
  4. schedular 发现要创建新的 pod 了,于是计算要分配的 node,然后告诉 apiServer。
  5. kubelet 一直在 watch apiServer,突然发现有新分配的 pod 了,而且还是分配到自己的 node 上的,于是调用 Docker 生成容器,并将结果返回给 apiServer。
  6. apiServer 接受到信息后,存入 etcd。

5.2.2 pod 的终止过程

  1. 用户向 apiServer 发送删除 pod 的命令。
  2. apiServer 中的 pod 对象信息会随着时间推移而慢慢变化,在宽限期内(默认 30 s)(因为 pod 关闭需要一定时间),pod 都被视为 dead 状态。
  3. 将 pod 标记为 terminating 状态。
  4. kubelet 监控到 pod 变成 terminating 状态了,于是启动 pod 关闭过程。
  5. 端点控制器监控到 pod 的关闭行为,于是将该端点所有相关的 service 的该端点移除。
  6. 如果该 pod 具有 preStop hook,那么在标记为 terminating 的同时,便会运行该 hook。
  7. pod 中的 container 接受到停止信号,开始停止。(一般需要一段时间)
  8. 如果过了宽限时间,还没有停止完成,那么向该 pod 发送强制终止的信号。
  9. kubelet 请求 apiServer 将该 pod 宽限时间设置为 0,表示 pod 已被删除,此时 pod 对于用户已不可见。

5.2.3 pod 运行 init container 的过程

5.2.3.1 什么是 init container?

init container 就是在 pod 运行之前所要运行的容器(们),主要是做一些环境的准备工作。
init container 有两大特征。

  1. init container 必须运行成功。如果运行失败的话,就会重启,直到运行成功。
  2. init container 必须按照定义的顺序执行。只有前面的 init container 运行成功后,才能运行后面的。全部 init container 运行成功后,才会运行主容器。(主容器就是主要运行的容器,比如说一个 nginx服务器的 pod,主容器就是 nginx) init container 一般作用有如下:
  3. 提供主容器不具备的工具程序或者自定义代码。
  4. 因为 init container 必定先于主容器,且必定运行成功。可以用于延后主容器的运行,直到依赖条件被满足。 比如说一个 nginx 服务器的 pod,它要和 mysql 连接。但是连接前怎么确保能够连上 mysql 呢。那么可以先在 init container 中和 mysql 连接,如果失败就重新连接,直到可以连接了。才让 nginx 真正与 mysql 进行连接。(最简单的方式比如一个自定义代码,去 ping mysql 的服务器,ping 不通就是无限 ping,直到 ping 通为止)

5.2.3.2 init container 的 yaml 语法

yaml 语法如下,和 containers 平级:

spec:
    containers:
      - name: xxx
        image: xxx
    initContainers:
      - name: xxx
        image: busybox
        command: ["/bin/bash", "-c", "untill ping x.xx.xx.xx -c 1; do echo waiting for mysql...; sleep 2; done;"]

5.2.4 钩子

钩子可以让用户在 K8s 定义的某个时间点运行指定代码。
K8s 在主容启动前后定义了两个 hook。postStart 和 preStop.

  • postStart 顾名思义,当容器启动后运行。虽然不会阻塞容器的启动,但是如果 postStart 失败了,容器会被重启。
  • preStop 相反,是在容器终止之前运行,因此会阻塞容器的删除。 (为什么 start 是 post,而 stop 是 pre 呢?因为这保证了 hook 运行的时候容器皆为启动状态)

hook 有三种用法:

  1. exec 命令用法:可以在 hook 执行的时候运行一段命令
    lifecycle:
        postStart:
            exec:
                command: ["/bin/bash", "-c", "echo hello, this is postStart"]
    
  2. tcpSocket 用法:在当前容器尝试访问指定 socket
    lifecycle:
        postStart:
            tcpSocket:
                port: 80
    
  3. httpGet 用法:在当前容器向某 url 发起 http 请求
    lifecycle:
        postStart:
            httpGet: # 其实就是访问 schema://host:port/path
                schema: <HTTP 或者 hTTPS> # 访问协议
                host: xx.xx.xx.xx # 主机地址
                port: xx # 端口号
                path: /xx/xx # URI 地址
                
    

5.2.5 容器探测

容器探测用于监测容器的健康状态,有两种方式:

  • livenessProbe,用于检测容器是否故障。如果失败,则直接重启容器。
  • readinessProbe,用于监测容器是否能够接受流量,如果失败则暂时不转发流量给该容器。比如一个服务器刚启动时,还不能够处理业务流量。 yaml 语法和 hook 一模一样,同样的三种方式和名字和用法:
livenessProbe:
    exec:
        command: xxx # 运行命令,返回 0 则为正常,否则不正常
livenessProbe:
1. 
    tcpSocket:
        port: xx # 探测是否能够访问该 port,不能则不正常
libenessProbe:
    httpGet: # 访问 url,如果返回状态码在 200-399 则正常,否则不正常
        schema: <HTTP 或者 hTTPS> # 访问协议
        host: xx.xx.xx.xx # 主机地址,不写就是该 pod 自身的地址
        port: xx # 端口号
        path: /xx/xx # URI 地址

除了用 exec,tcpSocket,httpGet 来声明探测方式,还可以加一些属性来标识探测的各种属性,比如说探测时间啊,间隔啊,失败多少次算失败啊之类的。和 exec 这些属性。具体可以查文档,也可以直接 explain 查询

$ kubectl explain pod.spec.containers.livenessProbe

5.2.6 重启策略

pod 的重启策略有三种:

  1. Always:默认值,当容器失效的时候,自动重启。
  2. OnFailure: 容器终止运行且退出码不为 0 的时候重启。
  3. Never: 不重启。 (注意,重启策略是 pod 的级别,pod 内所有容器都适用同一个策略) 重启时间:首次进行重启的时候,容器立刻重启。但是之后的重启都会延迟一段时间,且随着次数增多越来越长,最大 300s。以免在某个错误容器上多次重启浪费资源。

如果设置成了 Never 后,livenessProbe 检测失败了,则容器会停止运行,显示为 completed 状态,并不再重启。

5.3 pod 的调度

之前提到过,scheduler 会在 pod 启动前计算这个 pod 将要被安装到哪个 node 上。但是经常有需要人工指定某个 pod 必须在某个 node 上运行的需求,因此就需要了解 pod 的四种调度方式:

  1. 自动调度:完全由 schedular 的计算来分配。
  2. 定向调度:nodeName 和 nodeSelector。通过 node 的名字或者 label 来分配。
  3. 亲和性调度:根据 nodeAffinity,podAffinity,podAntiAffinity 来调度。
  4. 污点-容忍调度:taints 和 toleration。

5.3.1 自动调度

默认,没什么好说的

5.3.2 定向调度

注意:定向调度是强制性的(两种都是),即使声明的 nodeName 不存在,依然会往上调度。只是结果当然是 pod 没法运行。

5.3.2.1 nodeName

简单粗暴,直接调度到所声明的 nodeName 的 node 上,跳过 schedular 的计算。

spec:
    containers:
      - name: xx
        image: xx
    nodeName: node1

5.3.2.2 nodeSelector

将 pod 调度到指定 label 的 node 上。语法如下:

spec:
    contianers:
      - name: xx
        image: xx
     nodeSelector: 
         label1: value1

5.3.3 亲和性调度

由于定向性调度是强制性的,为了更加灵活,就有了亲和性调度。亲和性调度会尽量去找指定的 node,如果找不到就选择不满足条件的 pod。 Affinity 分为三类:

  1. nodeAffinity:声明一个希望调度的节点,这是个偏好
  2. podAffinity:以正在运行的 pod 为目标,比如说有一个 pod1 已经在 node2 上运行了,那么指定这个 pod1,就会偏好于 node2.
  3. podAntiAffinity:和 2 相反,尽量不和声明的 pod 在同一个 node 上。

5.3.3.1 亲和性和反亲和性的应用场景

  1. 那么什么时候使用亲和性呢(也就是 1 和 2)?
    • 如果两个应用频繁交互,比如 nginx 和 mysql,那么就应该使用亲和性,避免在不同的 node 上,不然这两个应用交互的时候就会有较高的网络延迟。
  2. 那么什么时候使用反亲和性呢(也就是 3)?
    • 如果使用多个应用实例副本进行负载均衡,那么有必要给这些 pod 使用反亲和性,以让这些 pod 尽量分散。提高应用的高可用性。(因为如果都在一个 node 上,那么如果这个 node 故障了,就没有一个应用能够访问)

5.3.3.2 nodeAffinity

可以通过 kubectl explain pod.spec.affinity.nodeAffinity 查看

spec:
    containers:
      - name: xx
        image: xx
    affinity:
        nodeAffinity:
            requiredDuringSchedulingIgnoreDuringExecution: # 硬限制,必须满足
                nodeSelectorTerms: # 一个硬限制节点选择器
                  - matchFields: # 匹配节点字段,不推荐
                  - matchExpressions: # 匹配节点标签,推荐
                    - key: xx
                      values: xx
                      operator: <Exists/DoesNotExist/In/NotIn/Gt/Lt> # 看下面三个例子
                    - key: 1
                      operator: Exists # 表示存在一个叫 1 的 key 即可匹配,不能写 value
                    - key: 2
                      operator: In
                      values: ["21", "22", "23"] # 表示 key 匹配, value 在其中一个即可
                    - key: 3
                      operator: Gt
                      values: "31" # 表示 key 匹配,value 大于 "31",很少使用
                      
            preferredDuringSchedulingIgnoreDuringExecution: # 偏好,最好满足
                preference: # 一个软限制节点选择器
                  - matchFields:
                  - matchExpressions:
                    - key: xx
                      values: xx
                      operator: xx
                  weight: xx # 这个选择器的权重          

注意事项:

  1. 如果同时声明了 nodeSelector 和 nodeAffinity,那么必须两个条件都要满足才会分配 pod
  2. 如果 nodeAffinity 声明了多个 nodeSelectorTerm,那么只需要满足其中一个即可
    nodeSelectorTerms: # 只需满足一个 expression 或者 fields即可
      - matchExpressions:
        ...
      - matchExpressions:
       ...
      - matchFileds:
    
  3. 如果一个 nodeSelectorTerms 有多个 matchExpression,则必须满足所有的才行。
    nodeSelectorTrems:
      - matchExpressions: # 这两对 key,value 配对规则都要满足
        - key: 
          operator:
          values:
        - key:
          operator:
          values: 
      - matchExpressions: # 但是同为 nodeSelectorTerm 这一级的话,则只需要满足一个就行
        - key:
          operator:
          values: 
    
  4. 如果一个 pod 所在的 node 在 pod 运行过程的时候 label 发生了改变,pod 不再符合亲和性了,系统并不会把这个 pod 给换掉,而是忽略此变化。换句话说,调度规则只在一开始的时候有效。

5.3.3.3 podAffinity

具体查看 kubectl explain pod.spec.affinity.podAffinity

5.3.3.4 podAntiAffinity

同上

5.3.4 taint 和 tolerations

pod 通过声明 taint 来拒绝 pod 调度上来。pod 通过 tolerations 容忍 taint,可以调度上来。

5.3.4.1 taint

taint 是在 node 上的属性,因此 yaml 没法设置,而是通过 kubectl 设置:

$ kubectl taint nodes/node1 key:value:<PreferNoSchedule/NoSchedule/NoExecute>
# PreferNoSchedule: 尽量避免调度到该 node 上,除非没有其他 node 可以选择了。
# NoSchedule: 拒绝调度到该 node 上,但是不影响已经存在的 pods
# NoExecute: 不仅拒绝调度到该 node 上,还会把已经存在的 pods 驱逐到其他节点上。
# 去除污点
$ kubectl taint nodes/node1 key:value-

# 去除所有污点
$ kubectl taint nodes/node1 key-

5.2.4.2 tolerations

tolerations 是 pod 的属性,因此可以使用 yaml 设置

spec:
    containers:
      - name: xx
        image: xx
    tolerations:
      # 以下其实加起来就是 xx=xx:NoExecute
      - key: xx # 要容忍的 key
        operator: "Equal" # Equal 或者 Exists,Exists 的时候 value 不用写
        value: xx # 要容忍的 value
        effect: "NoExecute" # 容忍的规则,空的话就是匹配所有规则
        tolerationSeconds: xx # 前面提到过,如果是 NoExecute,那么现有的 pod 会被驱逐,这个就是等待驱逐的时间。

6. pod 控制器详解

K8s 中的 pod 有两类:

  • 一类是 K8s 直接创建出来的,一旦删除就没了。
  • 还有一类是 pod 控制器创建出来的,一旦删除就会被 pod 控制器重新创建。生产环境主要用的是 pod 控制器。 pod 控制器有很多种,最常用的是 deployment,deployment 通过控制另一个 pod 控制器 replicaSet 来控制 pods。

6.1 replicaSet 控制器

replicaSet 控制器的作用是保持固定数量的 pods 能够保持运行。会对这些 pods 保持监听,一旦有 pod 发生故障或停止,就会创建新的 pod 来代替。此外还能够进行应用扩缩容(增加或减少 pods 的固定数量)和版本镜像更新。

6.2 deployment 控制器

deployment 控制器是 v1.2 版本才引入的。deployment 并不是直接控制 pod,而是控制 replicaSet,由 replicaSet 控制 pod。功能更加强大。

  1. 支持 replicaSet 的所有功能。
  2. 支持发布的停止,继续。
  3. 支持滚动版本升级和回退。

6.2.1 yaml 语法

先给一个综观:

apiVersion: apps/v1
kind: Deployment
metadata:
    name: xx
    namespace: xx
    labels:
        label0: value0
spec:
    replicas: xx # 期望有几个 pods
    revisionHistoryLimit: xx # 保留多少个历史版本,用于版本回退。版本回退实际上是通过保留 rs 实现的。
    paused: <false/true> # 暂停部署,默认为 false,如果为 true,那么部署之后不会立刻创建 pods
    progressDeadlineSeconds: xx # 部署的超时时间,如果部署时间因为拉取镜像等原因过长,就会上报该情况。
                                # 不过并不会阻止 deployment 继续部署
    strategy: # 镜像更新的策略
        type: RollingUpdate # 使用哪种策略
        rollingUpdate: # 如果选的是滚动更新,那么这里就可以对滚动更新进行设置
            maxSurge: xx/xx% # 最大可以额外存在的副本数
            maxUnavailable: xx/xx% # 最大不可用的 pods 数
    selector:
        matchLabels:
            label1: value1
            label2: value2
        macthExpressions:
          - key: xx
            operator: In
            values: ["xx", "xx"]
    template: # 按照模板来创建 pod
        metadata:
            labels:
                label1: value1
                label2: value2
        spec:
            containers:
              - name: xx
                image: xx

如果想要改变一些配置,除了直接修改 yaml 文件后使用 apply,还可以使用 edit 命令,依然会打开一个 yaml 文件,但是可以指定修改哪个对象的 yaml 配置。

# 修改 deployment01 的 yaml 配置
$ kubectl edit deployments/deployment01 

6.2.2 strategy

strategy 可以有两种选项,Recreate 和 RollingUpdate(默认)。 Recreate: 更新的时候,先终止所有的 pods,然后再重新创建所有的 pods。 RollingUpdate:只终止部分 pods,更新这些 pods,然后再终止一部分 pods,再更新,直到全部更新完。保证一直都有 pods 在运行,保证应用不宕机。

spec:
    strategy:
        type: <Recreate/RollingUpdate>
        rollingUpdate: # 只有 type 为 RollingUpdate 时生效,Recreate 没有对应的配置项
            maxUnavailable: xx/xx% # 最大不可用的 pods,默认 25%
            maxSurge: xx/xx% # 最大可额外存在的副本胡,默认 25%

6.2.3 版本更新和回退

实际上 deployment 的版本更回退,其实靠的是回退 replicaSet。
每次版本更新的时候,实际上会创建新的 replicaSet,但同时旧的 replicaSet 并不会被删除。这点通过 kubectl get rs 可以看到。
之前在 yaml 配置中可以看到,在 spec 中有一个 revisionHistoryLimit,说是保留多少个版本,其实也就是指定保留多少个 rs 的意思。

  • 在版本更新的时候(指滚动更新),旧的 replicaSet 的 pods 数会慢慢减少,而新的 replicaSet 的 pods 数会慢慢增加。
  • 版本回退也是同样的道理。

deployment 支持多种版本升级/回退的操作,或者说对于 kuubectl rollout 后接可以有多种选项:

  • status: 查看当前升级的状态
  • history: 显示升级的历史记录(需要在之前操作的时候加 `--record=true``)
  • pause: 暂停升级
  • resume: 继续暂停的升级
  • restart: 重新进行升级
  • undo: 回滚到上一版本,可以用 --to-revision 回到指定版本 版本就是使用 history 命令后最左边的数字,1 就是最老的版本,如果升级过两次,那就有 3 个版本,1 是最新的版本号,3 是最新的版本号。

6.2.4 canary test

之前提到过 rollout 命令可以有 pause 和 resume,这乍一看有点意※义※不※明。为什么要有这样的需求呢?
比如说 canary test 就可以有这样的需求,在更新后,新版本的 pods 只出现了一部分,立刻暂停更新。这样就会出现,只有一部分新版本的 pods,大部分都是旧版本的 pods。然后删选出一小部分用户去使用新版本,确认没有问题之后,再使用 resume 把整个部署更新到新版本。
比如说如下命令,使用 && 让其立刻暂停:

$ kubectl set image deployments/deployment01 my-nginx=nginx:1.12.0 && kubectl rollout pause deployments/deployment01

(注:这只是 canary test 的其中一种方式)

6.3 HPA 控制器

之前我们都是手动改变 replicas 的数量的,但这不是很符合 K8s 的思想 - 自动控制。
因此就出现了 Horizonal Pod Autoscaler。目的是自动控制 pods 的数量。
HPA 可以监测 pods 的状态,实现 pods 数量自调整:

  1. 比如说现在有 3 个 pods,每个 pods 都正常运行。
  2. 突然,流量激增,每个 pod 都超负荷运行,HPA 监测到这种状况,经过计算,创建了几个新的 pods,pods 的压力得到缓解。
  3. 等到高峰期过了之后,HPA 发现每个 pods 占用量变得特别少,没必要这么多。于是经过计算,又删除了一些 pods。 HPA 有一个指标,指的就是定义什么时候算 pod 占用率高需要新建 pods。

6.4 daemonSet 控制器

daemonSet 控制器会在每一个 node 上都运行一个 pod,一般用于日志收集等作用。
如果某个应用需要在每个 node 上都运行,且都只运行一个,那么就适合使用 daemonSet 控制器。 daemonSet 控制器的特点:

  • 每当集群中多加一个 node,daemonSet 控制器就会在该节点上添加一个 pod
  • 如果一个 node 从集群中移除,上面的 pod 也会被垃圾回收。

6.4.1 yaml

基本和 deployment 相同,主要是不用写 replicas。其他区别看文档或者 explain。

6.5 job 控制器

job 控制器用于批量处理一次性任务。特点如下:

  1. 当 job 控制器创建的 pods 成功执行结束后,job 控制器会记录成功的数量。
  2. 当成功数量达到指定值后,job 控制器将完成执行。

6.6 cronJob 控制器

cronJob 控制器通过控制 job 控制器来控制 pods。
job 控制器在创建之后会立刻执行其控制的 pods。但是通过 cronJob 可以控制 job 控制器的运行时间,并且可以周期性运行。也就是说有如下两个特点:

  1. cronJob 可以自由控制 job 控制器运行的时间点。
  2. cronJob 可以重复运行 job 控制器。 这两点都是 job 控制器所没有的特性。

7. Service 详解

在 K8s 中,Service 是用于进行流量负载的对象。分为 service 和 ingress。service 用于四层路由负载,ingress 用于七层路由负载。

7.1 Service

由于 pod 的 ip 会一直改变,但是通常我们需要固定的 ip 来访问 pod。于是 service 应运而生:
service 会统一在同一个服务内的 pods,提供一个统一的入口地址。

7.1.1 kube-proxy

实际上 service 只是一个概念,真正起到流量负载的是 kube-proxy,kube-proxy 运行在每个 node 上。kube-proxy 的运行机制如下:

  1. 首先当 service 被用户创建后,对应的数据会被 apiServer 接受到,并写入 etcd。
  2. 此时 kube-proxy 监听到 apiServer 的变化,于是读取改变。将 service 的对应关系写入 node,变成访问规则,以便之后使用。 kube-proxy 规则会在所有节点上生成,因为可以在所有节点上访问。 kube-proxy 有多种工作模式:
  3. userspace(最早):稳定,效率低。
  4. iptables(后来):效率比 1 高,但是只能使用轮询和随机策略来访问 pods。而且如果访问 pods 失败后无法重新尝试。
  5. ipvs(最新):当前最新模式,效率更高,支持更多 LB(负载均衡) 策略。 使用 ipvs 必须安装 ipvs 模块,如果没装,就会自动降级为 iptables

7.1.2 yaml

apiVersion: v1
kind: Service
metadata:
    
spec:
    selector: # 选择器,表示选择哪些对象进入服务
        label1: value1
    type: xxx # 上一篇文章讲的 4 种方法
    clusterIP: xxx # 顾名思义,因为其他 type 都是 ClusterIP 的超集,所以都可以设置这个
                   # 不设置会自动生成一个,如果所有 service 都选同一个的话,那么下面的抽象端口就需要注意不能重复了
    sessionAffinity: xx # 取值为 "ClientIP" 或者 "None"
    sessionAffinityConfig:
        clientIP: xx # 当上面为 ClientIP 时可以设置这个,可以让所有流量全都流到指定 pod 上。
    ports:
      - protocol: TCP 
        port: xxx # service 的抽象端口,pod 通过该端口和 service 连接。
                  # service 的 IP 是完全虚拟的,不走任何网络,端口也是
                  # 基本上可以随便写就行,直接访问 ClusterIP 的时候就用这个端口
                  # 不过为了方便,一般设置为和 targetPort 相同
        targetPort: xxx # 容器接受流量的端口,占用的是 pod 上的端口
        nodePort: xxx # type 为 nodePort 以上时可以设置,用于外部访问 service。占用的是 node 的端口。

type 的四种方式这里再详细解释一下:

  1. ClusterIP: 默认的 type,使用这种方式只能在集群内部访问 service。通过集群内部虚拟的 IP 暴露 service。注意这就是上面 yaml 代码里注释提到的 service 的那个完全虚拟的 IP。ClusterIP 不是 Cluster 的 IP,而是 service 的 IP。在内部通过 <ClusterIP>:<Port>访问,port 就是虚拟的那个端口。
  2. NodePort: 通过每个节点上的 IP 和端口暴露 service。使用的是 node 的 IP 地址。是 ClusterIP 的超集,因此也有 ClusterIP(service 自身的 IP)。可以在外部通过 <NodeIp>:<NodePort> 访问。实际上 NodePort 也是通过路由到 ClusterIP 来实现访问的。
  3. LoadBalancer: 这个方式需要云服务的支持,使用云服务商提供的负载均衡器向外部暴露服务。当流量来的时候,会先路由到 NodePort,然后 NodePort 又路由到 ClusterIP。也就是说,其实这个也是 NodePort 的超集。
  4. ExternalName: 将服务映射到 externalName 字段上,通过字段访问服务。

image.png

7.1.3 ClusterIP

所有的 Service 都会有一个虚拟的 IP,叫做 ClusterIP。访问 ClusterIP 后,会被转发机制(username,iptables或者ipvs)根据 LB 策略访问 pod。 如果不设置的话,基本就是轮询。可以尝试创建一个包含 3 个 pods 的 service。然后使用 <ClusterIP>:<port>(port 就是之前 yaml 文件里的那个没有前缀的 port 属性)。可以发现每次访问都是和上次不同的 pod。

7.1.3.1 EndPoints

Endpoints 是在 etcd 中用于存储一个 service 与其所有的 pods 的对应关系的对象。当创建 selector 的时候,apiServer 就会在 etcd 中创建 EndPoints。 image.png

7.1.4 headless service(无头服务)

当在 yaml 里将 ClusterIP 设置为 None 的时候,就会形成 headless service。意思就是 K8s 不给 service 分配 IP 了。无法通过 ClusterIP:port 来访问 pods。这个时候,就可以使用其他服务发现工具来分配流量了。
不过 Endpoints 依然会创建,service 和 pods 之间依然有联系。只是不能用 clusterIP 访问罢了。

7.1.5 NodePort

为了能够在集群外部访问 pods,就有了 NodePort。工作原理就是将 service 的端口映射到 node 的端口上。
注意:这个映射会映射到所有的 NodeIP 上的同一个 NodePort,所以不用担心在不同的 node 上能不能访问。其实使用任何 node 的 IP 的同一个 NodePort 都可以访问。

也就是说,每次新增一个 service 的时候:

  1. ClusterIP 根据设置可能会多增一个(自动),可能不会(手动设置同一个 ClusterIP 的不同 port)。
  2. NodePort 会多添加一个,并且给所有的 node 都添加一个 <NodeIP>:<NodePort> 映射到对应的 ClusterIP:port 上. image.png

7.1.6 & 7.1.7 loadBlancer & external name

7.2 ingress

使用 service,想要对外暴露服务,一个是 NodePort,还有一个是 LoadBalancer。但这两种方式都有缺点:

  1. NodePort 的缺点:每个服务都会占用一个端口,而使用的又是 node 的端口,如果服务较多的话,很容易导致机器上的端口不够用。
  2. LoadBalancer 的缺点,由于是 NodePort 的超集,所以以上的缺点也有。此外,每个 service 都要创建一个 Lb,麻烦。还有个缺点之前也提到过,必须有云提供商的支持。

为了解决这个问题,K8s 提供了一个新的资源对象,叫做 ingress。ingress 带来的好处非常大,只需要一个 NodePort 或者一个 LB,就可以暴露多个 service。
ingress 的工作原理如图:
image.png ingress 对每个 service 都设置了一个域名。ingress 有着诸多映射规则,标明每个域名对应到哪个服务上。

注意,ingress 只是一个存储配置的对象,还有一个叫做 ingress controller 的对象监听 ingress 上的配置,将其转化为对应的配置。ingress controller 有很多种选择,转化后的配置也不同。也就是具体实现流量代理的程序。比如说 nginx 的 ingress controller,就会转化成 nginx 的反向代理配置,使用反向代理来转发流量。
(踩坑:查看 /etc/resolv.conf 的时候总是和网上说的不一样,搞半天结果发现是在 pod 里查看) (踩坑2:终于知道为什么 K8s 总是拉取不到本地镜像了,不是配置原因,是因为实际部署是在 node 上,而我一直都是在 master 上创建的 images,node 上没有,当然部署不到了)