Kubernetes官方文档个人笔记

245 阅读13分钟

# kubectl cluster-info 命令主要用于显示当前集群的控制平面(Control Plane)服务信息,尤其是 API 服务器的地址,以及其他核心组件(如 DNS、Dashboard 等)的地址。

# 在集群里启动一个临时 Pod。
kubectl run -i --tty --image busybox:latest dns-test --restart=Never --rm /bin/sh


# kubectl proxy 用于在本地机器上创建一个代理,从而能够访问 Kubernetes 集群中的服务和资源,默认监听 8001 端口:

curl -X POST http://localhost:8001/api/v1/namespaces/work/services/my-service:8077/proxy/api/v1/mgm/interface --header 'Content-Type: application/json' -d '{"type": "some_type"}'


Services & Pods & DNS

  1. Services 和 Pods 都会获得对应 DNS 记录。

  2. 不指定命名空间的 DNS 查询会被限制在 Pod 所在的命名空间内。 要访问其他命名空间中的 Service,需要在 DNS 查询中指定命名空间。

  3. DNS 查询使用 Pod 中的 /etc/resolv.conf 文件, Kubelet 为每个 Pod 配置此文件。

  4. “普通” Service 会被赋予一个形如 my-svc.my-namespace.svc.cluster.local 的 DNS A 和/或 AAAA 记录。

  5. 而没有 IP 的无头 Service 也会被赋予一个形如 my-svc.my-namespace.svc.cluster.local 的 DNS A 和/或 AAAA 记录。 与普通 Service 不同,这一记录会被解析成对应 Service 所选择的 Pod IP 的集合。

  6. Kubernetes 根据普通 Service 或无头 Service 中的命名端口创建 SRV 记录。对于每个命名端口, SRV 记录格式为 _port-name._port-protocol.my-svc.my-namespace.svc.cluster.local。对于普通 Service,该记录会被解析成端口号和域名(my-svc.my-namespace.svc.cluster.local)。比如:


80 ingress-nginx-controller.ingress-nginx.svc.cluster.local.

对于无头 Service,该记录会被解析成多个结果,及该服务的每个后端 Pod 各一个 SRV 记录, 其中包含 Pod 端口号和格式为 hostname.my-svc.my-namespace.svc.cluster.local 的域名。比如:


9200 es-cluster-1.elasticsearch.default.svc.cluster.local.

9200 es-cluster-2.elasticsearch.default.svc.cluster.local.

  1. Pod 也会有 DNS 记录,例如,对于一个位于 default 命名空间,IP 地址为 172.17.0.3 的 Pod, 如果集群的域名为 cluster.local,则 Pod 对应的 DNS 名称为:172-17-0-3.default.pod.cluster.local。 通过普通 Service 暴露出来的所有 Pod 都会有如下 DNS 解析名称可用:172-17-0-3.service-name.my-namespace.svc.cluster.local。

  2. 创建 Pod 时其主机名(从 Pod 内部观察)默认取自 Pod 的 metadata.name 值。Pod 规约中包含一个可选的 hostname 字段,可以用来指定一个不同的主机名。 当这个字段被设置时,它将优先于 Pod 的名字成为该 Pod 的主机名。 例如,给定一个 spec.hostname 设置为 “my-host” 的 Pod, 该 Pod 的主机名将被设置为 “my-host”。

  3. Pod 规约还有一个可选的 subdomain 字段,可以用来表明该 Pod 属于命名空间的一个子组。 例如,某 Pod 的 spec.hostname 设置为 “foo”,spec.subdomain 设置为 “bar”, 在命名空间 “my-namespace” 中, 主机名称被设置成 “foo” 并且对应的完全限定域名(FQDN)为 “foo.bar.my-namespace.svc.cluster.local”(从 Pod 内部观察)。

  4. 如果 Pod 所在的命名空间中存在一个无头 Service,其名称与子域相同, 则集群的 DNS 服务器还会为 Pod 的完全限定主机名返回 A 和/或 AAAA 记录。也就是说,如果想通过 Pod 的完全限定域名直接访问 Pod 可以使用这种方法。

  5. 由于 A 和 AAAA 记录不是基于 Pod 名称创建,因此需要设置了 hostname 才会生成 Pod 的 A 或 AAAA 记录。 没有设置 hostname 但设置了 subdomain 的 Pod 只会为 无头 Service 创建 A 或 AAAA 记录指向 Pod 的 IP 地址。 另外,除非在服务上设置了 publishNotReadyAddresses=True,否则只有 Pod 准备就绪 才会有与之对应的记录。

  6. 如果一个具有完全限定域名 busybox-1.busybox-subdomain.my-namespace.svc.cluster.local 的 Pod, 则默认情况下,该 Pod 内的 hostname 命令返回 busybox-1,而 hostname --fqdn 命令返回 FQDN。 而当在 Pod 规约中设置了 setHostnameAsFQDN: true 时,kubelet 会将 Pod 的全限定域名(FQDN)作为该 Pod 的主机名记录到 Pod 所在命名空间。 在这种情况下,hostname 和 hostname --fqdn 都会返回 Pod 的全限定域名。

  7. 在 kubelet 的配置文件里(/var/lib/kubelet/config.yaml), clusterDNS 配置将 DNS 解析器的 IP 信息传递给每个容器; clusterDomain 配置决定了集群的本地域名默认后缀; resolvConf 配置决定了当 Pod 规约中的 dnsPolicy 字段设置为 Default 时, 从运行所在的节点继承域名解析配置的文件。

  8. 在有 systemd 服务的 Linux 系统里,DNS 解析是由 systemd-resolved 进程负责的,它管理着三个文件:/run/systemd/resolve/resolv.conf 里存储着本地客户端能够直接连接到所有已知的上游 DNS 服务器; /run/systemd/resolve/stub-resolv.conf 告诉用户如何连接到本机的 systemd-resolved 服务,而 /etc/resolv.conf 是指向 /run/systemd/resolve/stub-resolv.conf 的软链接。

  9. Pod 规约中的 dnsPolicy 字段设置为 ClusterFirst 时,dns 查询会优先走集群的 DNS 服务器(CoreDNS),而与配置的集群域后缀不匹配的任何 DNS 查询(例如 "www.kubernetes.io") 都会由 DNS 服务器转发到上游域名服务器。 而设置为 None 时,允许 Pod 忽略 Kubernetes 环境中的 DNS 设置,Pod 会使用其 dnsConfig 字段所提供的 DNS 设置。当 Pod 的 dnsPolicy 设置为 "None" 时,必须指定 dnsConfig 字段。 而对于以 hostNetwork 方式运行的 Pod,应将其 dnsPolicy 显式设置为 "ClusterFirstWithHostNet"(如果未明确指定 dnsPolicy,则使用 "ClusterFirst")。

  10. Pod 规约中的 dnsConfig 字段用于给 Pod 进行额外的 DNS 配置。这些配置会与 dnsPolicy 设置的配置进行合并。

metrics-server

  1. metrics-server 是一个轻量级的、短期、内存存储的用于收集和聚合集群中各个节点和 Pod 的资源使用情况的服务(如 CPU 和内存)。

  2. metrics-server 收集资源使用数据的流程是:

    1> 每个节点上的 kubelet 收集节点和 Pod 的资源使用数据,主要通过 cAdvisor;

    2> metrics-server 通过 Kubernetes API 与各节点的 kubelet 通信,请求资源使用数据;

    3> metrics-server 收集和汇总来自不同节点的资源数据;

    4> metrics-server 通过 Kubernetes Metrics API 提供资源使用数据给 HPA、kubectl 等用户和系统组件;

kube-state-metrics

  1. kube-state-metrics 是 Kubernetes 中的一个组件,主要用于从 Kubernetes 集群的状态中收集指标数据。这些指标数据反映了集群内部的各种资源对象(如 Pod、Deployment、Node、Service 等)的状态, 但它只报告 Kubernetes 资源的状态,比如 Pod 的生命周期状态(Pending、Running、Failed 等),而不会收集资源的运行时性能指标,比如 CPU 使用率、内存使用量等。

  2. 工作原理:

    • kube-state-metrics 定期从 Kubernetes API Server 拉取集群中资源的状态数据;

    • 然后将资源状态数据转化为 Prometheus 兼容的指标格式,这些指标可以被 Prometheus 抓取;

    • 这些指标暴露在一个 /metrics 的 HTTP 端点,Prometheus 等监控系统会定期拉取这些数据,用于后续的监控、告警和可视化;

日志机制

  1. 容器运行时对写入到容器化应用程序的 stdout 和 stderr 流的所有输出进行处理和转发。不同的容器运行时以不同的方式实现这一点;不过它们与 kubelet 的集成都被标准化为 CRI 日志格式。

  2. CRI 日志格式的基本结构:<Timestamp> <Stream> <Log Message>

  3. kubelet 负责轮换容器日志并管理日志目录结构。 kubelet(使用 CRI)将此信息发送到容器运行时,而运行时则将容器日志写到给定位置。

  4. 有两个 kubelet 配置项: containerLogMaxSize (默认 10Mi)和 containerLogMaxFiles(默认 5)。 这些设置分别允许你分别配置每个日志文件大小的最大值和每个容器允许的最大文件数。

  5. 如果 Pod 有多个容器,通过将容器名称追加到该命令并使用 -c 标志来指定要访问哪个容器的日志:kubectl logs -c count。

  6. 运行 kubectl logs 时, 节点上的 kubelet 会处理请求并直接从日志文件读取,然后 kubelet 将返回该日志文件的内容。

  7. 默认情况下,kubelet 指示容器运行时将日志写入 /var/log/pods 中的目录。

  8. kubelet 允许将 Pod 日志目录从默认的 /var/log/pods 更改为自定义路径。 可以通过在 kubelet 的配置文件中配置 podLogsDir 参数来进行此调整。

Kubernetes 网络模型

  1. Pod 有自己的私有网络命名空间,Pod 内的所有容器共享这个命名空间。 运行在同一个 Pod 中的不同容器的进程彼此之间可以通过 localhost 进行通信。

  2. 集群中的每个 Pod 都会获得自己的、独一无二集群层面的 IP 地址。

  3. Pod 网络(也称为集群网络)处理 Pod 之间的通信,它确保(除非故意进行网络分段):

    • 所有 Pod 可以直接与所有其他 Pod 进行通信, 无论它们是在同一个节点还是在不同的节点上。

    • 节点上的代理(例如系统守护进程或 kubelet)可以与该节点上的所有 Pod 进行通信。

  4. Service API 可以为由一个或多个后端 Pod 实现的服务提供一个稳定(长效)的 IP 地址或域名, 其背后组成服务的各个 Pod 可以随时变化。

  5. Ingress API (后继者:Gateway API 待研究)使得集群外部的客户端能够访问 Service。

  6. NetworkPolicy 是一个内置的 Kubernetes API,允许你控制 Pod 之间的流量或 Pod 与外部世界之间的流量。(待研究)

  7. CNI(Container Network Interface)规范是一个用于配置容器网络的标准接口,主要定义了网络插件如何与容器运行时(如 containerd、CRI-O 等)进行交互。

  8. 集群网络实际上由各节点上的容器运行时来实现。常见的容器运行时使用 Container Network Interface (CNI) 插件来管理其网络和安全能力。

  9. 网络插件配置为向 Pod 分配 IP 地址;kube-apiserver 配置为向 Service 分配 IP 地址;kubelet 或 cloud-controller-manager 配置为向 Node 分配 IP 地址。

Kubernetes 节点

  1. 向 API 服务器添加节点的方式主要有两种:

    • 当 kubelet 标志 --register-node 为 true(默认)时,它会尝试向 API 服务注册自己。 这是首选模式,被绝大多数发行版选用;

    • 设置 kubelet 标志 --register-node=false,然后使用使用 kubectl 来创建和修改 Node 对象;

  2. 当 Ready 状况的 status 保持 Unknown 或 False 的时间长于 kube-controller-manager 的 NodeMonitorGracePeriod(默认为 40 秒)时, 会造成 Unknown 状态下为节点添加 node.kubernetes.io/unreachable 污点或在 False 状态下为节点添加 node.kubernetes.io/not-ready 污点。 这些污点会影响悬决的 Pod,因为调度器在将 Pod 分配到节点时会考虑节点的污点。 已调度到节点的当前 Pod 可能会由于施加的 NoExecute 污点被驱逐。 但是 Pod 还可以设置容忍度, 使得这些 Pod 仍然能够调度到且继续运行在设置了特定污点的节点上。

  3. 要标记一个节点不可调度,执行:kubectl cordon $NODENAME

将 Pod 分配到指定节点

  1. Pod 规约中的 nodeSelector 字段可以将 pod 分配到具有指定标签的 node 上:
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    env: test
spec:
  containers:
  - name: nginx
    image: nginx
    imagePullPolicy: IfNotPresent
  nodeSelector:
    disktype: ssd

  1. 也可以使用 nodeName 字段将 pod 调度到指定节点,使用 nodeName 规则的优先级会高于使用 nodeSelector 或亲和性与非亲和性或污点的规则:
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  nodeName: foo-node # 调度 Pod 到特定的节点
  containers:
  - name: nginx
    image: nginx
    imagePullPolicy: IfNotPresent
  1. 节点亲和性(nodeAffinity)。

    • requiredDuringSchedulingIgnoredDuringExecution: 调度器只有在规则被满足的时候才能执行调度。此功能类似于 nodeSelector, 但其语法表达能力更强。

    • preferredDuringSchedulingIgnoredDuringExecution: 调度器会尝试寻找满足对应规则的节点。如果找不到匹配的节点,调度器仍然会调度该 Pod。

在上述类型中,IgnoredDuringExecution 意味着如果节点标签在 Kubernetes 调度 Pod 后发生了变更,Pod 仍将继续运行。

如果同时指定了 nodeSelector 和 nodeAffinity,两者必须都要满足, 才能将 Pod 调度到候选节点上。

apiVersion: v1
kind: Pod
metadata:
name: with-node-affinity
spec:
affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
    nodeSelectorTerms:
    - matchExpressions:
       - key: topology.kubernetes.io/zone
          operator: In
          values:
          - antarctica-east1
          - antarctica-west1
    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 1
    preference:
      matchExpressions:
      - key: another-node-label-key
         operator: In
         values:
         - another-node-label-value
containers:
- name: with-node-affinity
   image: registry.k8s.io/pause:2.0

如果在与 nodeAffinity 类型关联的 nodeSelectorTerms 中指定多个条件, 只要其中一个 nodeSelectorTerms 满足(各个条件按逻辑或操作组合)的话,Pod 就可以被调度到节点上。

如果在与 nodeSelectorTerms 中的条件相关联的单个 matchExpressions 字段中指定多个表达式, 则只有当所有表达式都满足(各表达式按逻辑与操作组合)时,Pod 才能被调度到节点上。

可以为 preferredDuringSchedulingIgnoredDuringExecution 亲和性类型的每个实例设置 weight 字段,其取值范围是 1 到 100。如果存在多个满足的节点,则权重高的节点优先分配。

  1. Pod 间亲和性与反亲和性使你可以基于已经在节点上运行的 Pod 的标签来约束 Pod 可以调度到的节点,而不是基于节点上的标签。

污点和容忍度

  1. 污点能够使节点接受一类特定的 Pod 而排斥其它 Pod ,每个节点上都可以应用一个或多个污点,这表示对于那些不能容忍这些污点的 Pod, 是不会被该节点接受的;而容忍度是应用于 Pod 上的,允许调度 Pod 到有对应污点的节点,但不保证调度成功;

  2. 给节点添加污点:kubectl taint nodes node1 key1=value1:NoSchedule。 给节点 node1 增加一个污点,它的键名是 key1,键值是 value1,效果是 NoSchedule。 这表示只有拥有和这个污点相匹配的容忍度的 Pod 才能够被分配到 node1 这个节点。 若要移除上述污点,可以执行:kubectl taint nodes node1 key1=value1:NoSchedule-

apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
   env: test
spec:
containers:
- name: nginx
   image: nginx
   imagePullPolicy: IfNotPresent
tolerations:
- key: "example-key"
   operator: "Exists"
   effect: "NoSchedule"
  1. effect 有3种值:

NoExecute 这会影响已在节点上运行的 Pod,具体影响如下:

  • 如果 Pod 不能容忍这类污点,会马上被驱逐。

  • 如果 Pod 能够容忍这类污点,但是在容忍度定义中没有指定 tolerationSeconds, 则 Pod 还会一直在这个节点上运行。

  • 如果 Pod 能够容忍这类污点,而且指定了 tolerationSeconds, 则 Pod 还能在这个节点上继续运行这个指定的时间长度。 这段时间过去后,节点生命周期控制器从节点驱除这些 Pod。

NoSchedule:

  • 除非具有匹配的容忍度规约,否则新的 Pod 不会被调度到带有污点的节点上。 当前正在节点上运行的 Pod 不会被驱逐。

PreferNoSchedule:

  • PreferNoSchedule 是“偏好”或“软性”的 NoSchedule。 控制平面将尝试避免将不能容忍污点的 Pod 调度到的节点上,但不能保证完全避免。

Qos 类

  1. Kubernetes 对运行的 Pod 进行分类,并将每个 Pod 分配到特定的 QoS 类中。 Kubernetes 基于每个 Pod 中容器的资源请求和限制为 Pod 设置 QoS 类。

  2. Kubernetes 使用 QoS 类来决定从遇到节点压力(仅限内存压力)的 Node 中驱逐哪些 Pod。可选的 QoS 类有 Guaranteed、Burstable 和 BestEffort。 当一个 Node 耗尽资源时,Kubernetes 将首先驱逐在该 Node 上运行的 BestEffort Pod, 然后是 Burstable Pod,最后是 Guaranteed Pod。

  3. 当这种驱逐是由于资源压力(仅限内存压力)时, 只有超出资源请求的 Pod 才是被驱逐的候选对象。

  4. 当满足以下条件是,Pod 被分为 Guaranteed :

    • Pod 中的每个容器必须有内存 limit 和内存 request。

    • 对于 Pod 中的每个容器,内存 limit 必须等于内存 request。

    • Pod 中的每个容器必须有 CPU limit 和 CPU request。

    • 对于 Pod 中的每个容器,CPU limit 必须等于 CPU request。

  5. Burstable Pod 有一些基于 request 的资源下限保证,但不需要特定的 limit。 如果未指定 limit,则默认为其 limit 等于 Node 容量,这允许 Pod 在资源可用时灵活地增加其资源。 在由于 Node 资源压力导致 Pod 被驱逐的情况下,只有在所有 BestEffort Pod 被驱逐后 这些 Pod 才会被驱逐。

  6. Burstable Pod 判断依据:

    • Pod 不满足针对 QoS 类 Guaranteed 的判据。

    • Pod 中至少一个容器有内存或 CPU 的 request 或 limit。

  7. 如果节点遇到资源压力,kubelet 将优先驱逐 BestEffort Pod。

  8. 当 Pod 中的所有容器没有内存 limit 或内存 request,也没有 CPU limit 或 CPU request 时,Pod 属于是 BestEffort。

节点压力驱逐

  1. 节点压力驱逐是 kubelet 主动终止 Pod 以回收节点上资源的过程。

  2. kubelet 监控集群节点的内存、磁盘空间和文件系统的 inode 等资源。 当这些资源中的一个或者多个达到特定的消耗水平, kubelet 可以主动地使节点上一个或者多个 Pod 失效,以回收资源防止饥饿。

  3. kubelet 在终止用户 Pod 之前会尝试回收节点级资源。例如,它会在磁盘资源不足时删除未使用的容器镜像,如果 kubelet 回收节点级资源的尝试没有使驱逐信号低于条件,则 kubelet 开始驱逐最终用户 Pod。

如果节点只有一个 nodefs 文件系统且该文件系统达到驱逐阈值, kubelet 将按以下顺序释放磁盘空间:

  • 对已死亡的 Pod 和容器执行垃圾收集操作。

  • 删除未使用的镜像。

如果节点有一个专用的 imagefs 文件系统供容器运行时使用,kubelet 会执行以下操作:

  • 如果 nodefs 文件系统满足驱逐条件,kubelet 垃圾收集死亡 Pod 和容器。

  • 如果 imagefs 文件系统满足驱逐条件,kubelet 将删除所有未使用的镜像。

  1. kubelet 允许3种文件模式选择:

    • 所有内容都位于单个 nodefs 上,也称为 “rootfs” 或简称为 “root”, 并且没有专用镜像文件系统。

    • nodefs 位于专用磁盘上, 而 imagefs(可写和只读层)与它分开。 这通常称为“分割磁盘”(或“单独磁盘”)文件系统。

    • 容器文件系统 containerfs(与 nodefs 加上可写层相同)位于根文件系统上, 容器镜像(只读层)存储在单独的 imagefs 上。 这通常称为“分割镜像”文件系统。

默认情况下,nodefs 实际上指的是 /var/lib/kubelet 所在磁盘,而 imagefs 指的是 /var/lib/containerd 所在磁盘(containerd作为运行时)。

  1. 软驱逐:将驱逐条件与管理员所必须指定的宽限期配对。 在超过宽限期之前,kubelet 不会驱逐 Pod,可以使用以下标志来配置软驱逐条件:

    • eviction-soft: 一组驱逐条件,如果驱逐条件持续时长超过指定的宽限期,可以触发 Pod 驱逐。

    • eviction-soft-grace-period:一组驱逐宽限期, 如 memory.available=1m30s,定义软驱逐条件在触发 Pod 驱逐之前必须保持多长时间。

    • eviction-max-pod-grace-period:在满足软驱逐条件而终止 Pod 时使用的最大允许宽限期(以秒为单位)。

  2. 硬驱逐:当达到硬驱逐条件时, kubelet 会立即杀死 pod,而不会正常终止以回收紧缺的资源。可以使用 eviction-hard 标志来配置一组硬驱逐条件。

  3. kubelet 具有以下默认硬驱逐条件:

    • memory.available 小于 100Mi(Linux 节点)

    • nodefs.available 小于 10%(Windows 节点)

    • imagefs.available 小于 15%

    • nodefs.inodesFree 小于 5%(Linux 节点)

    • imagefs.inodesFree 小于 5% (Linux 节点)

注意:只有在没有更改任何参数的情况下,硬驱逐阈值才会被设置成这些默认值。 如果你更改了任何参数的值,则其他参数的取值不会继承其默认值设置,而将被设置为零。为了提供自定义值,应该分别设置所有阈值。

  1. 当 kubelet 因 inode 不足而驱逐 Pod 时, 它会根据 Pod 的磁盘使用量确定驱逐顺序。而若是因为内存压力而进行驱逐时,则是根据 Pod 的 Qos 和 Pod 的优先级来确定驱逐顺序。

  2. kubelet 根据其配置的 housekeeping-interval(默认为 10s)评估驱逐条件。

  3. 节点驱逐状态有 MemoryPressure、 DiskPressure、 PIDPressure ,当节点满足硬性或软性驱逐条件时,节点会上报这些状态,并且控制平面还会将这些节点状况映射为对应污点。 kubelet 根据配置的 --node-status-update-frequency 更新节点这些状态,默认为 10s。

  4. 可以给 kubelet 设置 evictionMinimumReclaim 项,使 kubelet 注意到某个资源耗尽时,它会继续回收该资源,直到回收到你所指定的数量为止。避免触发多次驱逐。

Pod 优先级和抢占

  1. 新增一个 PriorityClass ,并设置一个 value 值,该值的范围在 -2,147,483,648 到 1,000,000,000(含);创建 Pod 并将其 priorityClassName 设置为对应 PriorityClass 。

  2. PriorityClass 还有两个可选字段:globalDefault 和 description。 globalDefault 字段表示这个 PriorityClass 的值应该用于没有 priorityClassName 的 Pod。 系统中只能存在一个 globalDefault 设置为 true 的 PriorityClass。 如果不存在设置了 globalDefault 的 PriorityClass, 则没有 priorityClassName 的 Pod 的优先级为零。 description 字段是一个任意字符串。 它用来告诉集群用户何时应该使用此 PriorityClass。

  3. 使用了配置 preemptionPolicy: Never 的 PriorityClass 的 Pod 将被放置在调度队列中较低优先级 Pod 之前, 但它们不能抢占其他 Pod。等待调度的非抢占式 Pod 将留在调度队列中, 直到有足够的可用资源, 它才可以被调度。

  4. preemptionPolicy 默认为 PreemptLowerPriority, 这将允许该 PriorityClass 的 Pod 抢占较低优先级的 Pod 。

  5. 当启用 Pod 优先级时,调度程序会按优先级对悬决 Pod 进行排序, 并且每个悬决的 Pod 会被放置在调度队列中其他优先级较低的悬决 Pod 之前。 因此,如果满足调度要求,较高优先级的 Pod 可能会比具有较低优先级的 Pod 更早调度。 如果无法调度此类 Pod,调度程序将继续并尝试调度其他较低优先级的 Pod。

  6. Pod 被创建后会进入队列等待调度。 调度器从队列中挑选一个 Pod 并尝试将它调度到某个节点上。 如果没有找到满足 Pod 的所指定的所有要求的节点,则触发抢占逻辑。 删除一个或多个优先级低于悬决 Pod 的 Pod 以释放资源,然后再调度悬决 Pod 到对应节点上。

  7. Kubernetes 已经提供了 2 个 PriorityClass: system-cluster-critical 和 system-node-critical,这是两个优先级非常高的 PriorityClass。一般用于 k8s 的关键组件。

  8. 调度器的抢占逻辑在选择抢占目标时不考虑 QoS。 抢占仅会考虑 Pod 优先级。

  9. 静态 Pod 不支持 priorityClassName 字段,可以设置 priority。

本地临时存储

  1. 如果没有为 emptyDir 卷指定 sizeLimit,该卷就会消耗 Pod 的内存, 卷的用量上限为 Pod 的内存限制(Pod.spec.containers[].resources.limits.memory)。 如果没有设置内存限制,Pod 的内存消耗将没有上限,并且可能会用掉节点上的所有可用内存。
apiVersion: v1
kind: Pod
metadata:
name: frontend
spec:
containers:
- name: app
   image: images.my-company.example/app:v4
   resources:
      requests:
      ephemeral-storage: "2Gi"
      limits:
      ephemeral-storage: "4Gi"
   volumeMounts:
   - name: ephemeral
     mountPath: "/tmp"
- name: log-aggregator
   image: images.my-company.example/log-aggregator:v6
   resources:
      requests:
      ephemeral-storage: "2Gi"
      limits:
      ephemeral-storage: "4Gi"
   volumeMounts:
   - name: ephemeral
     mountPath: "/tmp"
volumes:
   - name: ephemeral
      emptyDir:
      sizeLimit: 500Mi

预留资源

  1. Kubernetes 节点上的 'Allocatable' 被定义为 Pod 可用计算资源量。 调度器不会超额申请 'Allocatable'。 目前支持 'CPU'、'memory' 和 'ephemeral-storage' 这几个参数。 一个节点上的 Allocatable 资源等于节点上的资源总量减去 kube-reserved 预留的、system-reserved 预留的以及防驱逐预留的。

  2. kubeReserved 参数用来给诸如 kubelet、容器运行时等 Kubernetes 系统守护进程记述其资源预留值。 该配置并非用来给以 Pod 形式运行的系统守护进程预留资源。

  3. kubeReservedCgroup 参数用来指定要进行 kubeReserved 保护的 cgroup 组,需要将 kubelet、容器运行时等进程放进这个 cgroup 里。如果设置了这个参数,还需要 将 kube-reserved 添加到 enforceNodeAllocatable 。如果没有设置 kubeReservedCgroup ,则 kubeReserved 设置将只会在逻辑上限制分配给 pod 的 Allocatable 资源。而不会 对 kubelet、容器运行时等进程进行实际性的保护与限制。

  4. systemReserved 参数是用来给系统守护进程预留资源的。但不建议进行该项配置。

  5. systemReservedCgroup 和上 3 一个意思。

  6. 一个节点上 Allocatable 资源计算示例:

    • 节点拥有 32Gi memory、16 CPU 和 100Gi Storage 资源;

    • kubeReserved 被设置为 {cpu: 1000m, memory: 2Gi, ephemeral-storage: 1Gi};

    • systemReserved 被设置为 {cpu: 500m, memory: 1Gi, ephemeral-storage: 1Gi};

    • evictionHard 被设置为 {memory.available: < "500Mi", nodefs.available: < "10%"}

在这个场景下,'Allocatable' 将会是 14.5 CPUs、28.5Gi 内存以及 88Gi 本地存储。 调度器保证这个节点上的所有 Pod 的内存 requests 总量不超过 28.5Gi,存储不超过 '88Gi'。

当 Pod 的内存使用总量超过 28.5Gi 或者磁盘使用总量超过 88Gi 时,kubelet 将会驱逐它们。 如果节点上的所有进程都尽可能多地使用 CPU,则 Pod 加起来不能使用超过 14.5 CPUs 的资源。

当没有配置 kubeReserved 和 systemReserved 策略,如果节点内存用量高于 31.5Gi 或 storage 大于 90Gi,kubelet 将会驱逐 Pod。

节点与控制面通信

  1. 所有从节点(或运行于其上的 Pod)发出的 API 调用都终止于 API 服务器。 其它控制面组件也都没有被设计为可暴露远程服务。 API 服务器被配置为在一个安全的 HTTPS 端口(通常为 443)上监听远程连接请求, 并启用一种或多种形式的客户端身份认证机制。

  2. Kubernetes 集群有两类用户:由 Kubernetes 管理的服务账号和普通用户。

  3. 服务账号是 Kubernetes API 所管理的用户。它们被绑定到特定的名字空间, 或者由 API 服务器自动创建,或者通过 API 调用创建。服务账号与一组以 Secret 保存的凭据相关,这些凭据会被挂载到 Pod 中,从而允许集群内的进程访问 Kubernetes API。

  4. 用户使用 kubectl、客户端库或构造 REST 请求来访问 Kubernetes API 时,它会经历多个阶段:身份认证、鉴权、准入控制,最后更改对象存储。

  5. API 服务器使用 --tls-cert-file 和 --tls-private-key-file 标志来配置使用的证书和私钥。

  6. API 服务器使用 --authorization-mode 配置来设置鉴权模式,如:--authorization-mode=Node,RBAC

  7. 给 API 服务器配置 --client-ca-file 项,就可以启动客户端证书身份认证,该配置是用来验证向 API 服务器提供的客户端证书。如果提供的客户端证书被验证通过,则 subject 中的公共名称(Common Name) 就被作为请求的用户名

  8. 给用户签发证书(创建普通用户):

  • 创建私钥和 csr:

openssl genrsa -out myuser.key 2048

openssl req -new -key myuser.key -out myuser.csr -subj "/CN=myuser"

  • 提交到 k8s 让其签发证书:
cat <<EOF | kubectl apply -f -
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: myuser
spec:
request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ1ZqQ0NBVDRDQVFBd0VURVBNQTBHQTFVRUF3d0dZVzVuWld4aE1JSUJJakFOQmdrcWhraUc5dzBCQVFFRgpBQU9DQVE4QU1JSUJDZ0tDQVFFQTByczhJTHRHdTYxakx2dHhWTTJSVlRWMDNHWlJTWWw0dWluVWo4RElaWjBOCnR2MUZtRVFSd3VoaUZsOFEzcWl0Qm0wMUFSMkNJVXBGd2ZzSjZ4MXF3ckJzVkhZbGlBNVhwRVpZM3ExcGswSDQKM3Z3aGJlK1o2MVNrVHF5SVBYUUwrTWM5T1Nsbm0xb0R2N0NtSkZNMUlMRVI3QTVGZnZKOEdFRjJ6dHBoaUlFMwpub1dtdHNZb3JuT2wzc2lHQ2ZGZzR4Zmd4eW8ybmlneFNVekl1bXNnVm9PM2ttT0x1RVF6cXpkakJ3TFJXbWlECklmMXBMWnoyalVnald4UkhCM1gyWnVVV1d1T09PZnpXM01LaE8ybHEvZi9DdS8wYk83c0x0MCt3U2ZMSU91TFcKcW90blZtRmxMMytqTy82WDNDKzBERHk5aUtwbXJjVDBnWGZLemE1dHJRSURBUUFCb0FBd0RRWUpLb1pJaHZjTgpBUUVMQlFBRGdnRUJBR05WdmVIOGR4ZzNvK21VeVRkbmFjVmQ1N24zSkExdnZEU1JWREkyQTZ1eXN3ZFp1L1BVCkkwZXpZWFV0RVNnSk1IRmQycVVNMjNuNVJsSXJ3R0xuUXFISUh5VStWWHhsdnZsRnpNOVpEWllSTmU3QlJvYXgKQVlEdUI5STZXT3FYbkFvczFqRmxNUG5NbFpqdU5kSGxpT1BjTU1oNndLaTZzZFhpVStHYTJ2RUVLY01jSVUyRgpvU2djUWdMYTk0aEpacGk3ZnNMdm1OQUxoT045UHdNMGM1dVJVejV4T0dGMUtCbWRSeEgvbUNOS2JKYjFRQm1HCkkwYitEUEdaTktXTU0xMzhIQXdoV0tkNjVoVHdYOWl4V3ZHMkh4TG1WQzg0L1BHT0tWQW9FNkpsYWFHdTlQVmkKdjlOSjVaZlZrcXdCd0hKbzZXdk9xVlA3SVFjZmg3d0drWm89Ci0tLS0tRU5EIENFUlRJRklDQVRFIFJFUVVFU1QtLS0tLQo=
signerName: kubernetes.io/kube-apiserver-client
expirationSeconds: 86400 # one day
usages:
- client auth
EOF

request 字段是 CSR 文件内容的 base64 编码值,要得到该值,可以执行cat myuser.csr | base64 | tr -d "\n"

  • 批准 CertificateSigningRequest:

kubectl certificate approve myuser

  • 导出证书:

kubectl get csr myuser -o jsonpath='{.status.certificate}'| base64 -d > myuser.crt

  • 创建了证书之后,为了让这个用户能访问 Kubernetes 集群资源,现在就要创建 Role 和 RoleBinding 了:

kubectl create role developer --verb=create --verb=get --verb=list --verb=update --verb=delete --resource=pods

  


kubectl create rolebinding developer-binding-myuser --role=developer --user=myuser

  • 添加到 kubeconfig :

kubectl config set-credentials myuser --client-key=myuser.key --client-certificate=myuser.crt --embed-certs=true

  


# 设置上下文

kubectl config set-context myuser --cluster=kubernetes --user=myuser

  


# 切换上下文

kubectl config use-context myuser

  1. 在 pod 内部访问 API 服务器:

方法一:


# 指向内部 API 服务器的主机名

APISERVER=https://kubernetes.default.svc

  


# 服务账号令牌的路径

SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount

  


# 读取 Pod 的名字空间

NAMESPACE=$(cat ${SERVICEACCOUNT}/namespace)

  


# 读取服务账号的持有者令牌

TOKEN=$(cat ${SERVICEACCOUNT}/token)

  


# 引用内部证书机构(CA)

CACERT=${SERVICEACCOUNT}/ca.crt

  


# 使用令牌访问 API

curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${APISERVER}/api

方法二:

/*
Copyright 2016 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Note: the example only works with the code within the same release/branch.
package main

import (
	"context"
	"fmt"
	"time"

	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	//
	// Uncomment to load all auth plugins
	// _ "k8s.io/client-go/plugin/pkg/client/auth"
	//
	// Or uncomment to load specific auth plugins
	// _ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
)

func main() {
	// creates the in-cluster config
	config, err := rest.InClusterConfig()
	if err != nil {
		panic(err.Error())
	}
	// creates the clientset
	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		panic(err.Error())
	}
	for {
		// get pods in all the namespaces by omitting namespace
		// Or specify namespace to get pods in particular namespace
		pods, err := clientset.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{})
		if err != nil {
			panic(err.Error())
		}
		fmt.Printf("There are %d pods in the cluster\n", len(pods.Items))

		// Examples for error handling:
		// - Use helper functions e.g. errors.IsNotFound()
		// - And/or cast to StatusError and use its properties like e.g. ErrStatus.Message
		_, err = clientset.CoreV1().Pods("default").Get(context.TODO(), "example-xxxxx", metav1.GetOptions{})
		if errors.IsNotFound(err) {
			fmt.Printf("Pod example-xxxxx not found in default namespace\n")
		} else if statusError, isStatus := err.(*errors.StatusError); isStatus {
			fmt.Printf("Error getting pod %v\n", statusError.ErrStatus.Message)
		} else if err != nil {
			panic(err.Error())
		} else {
			fmt.Printf("Found example-xxxxx pod in default namespace\n")
		}

		time.Sleep(10 * time.Second)
	}
}

  1. 获取服务账户 token:

这个 secret 对象里就会包含该命名空间下默认账户的token。

apiVersion: v1
kind: Secret
metadata:
name: default-token
annotations:
   kubernetes.io/service-account.name: default
type: kubernetes.io/service-account-token
  1. 每个 Kubernetes 名字空间至少包含一个 ServiceAccount:也就是该名字空间的默认服务账号, 名为 default。如果你在创建 Pod 时没有指定 ServiceAccount,Kubernetes 会自动将该名字空间中名为 default 的 ServiceAccount 分配给该 Pod。通过在 ServiceAccount 对象上设置 automountServiceAccountToken: false,可以放弃在 /var/run/secrets/kubernetes.io/serviceaccount/token 处自动挂载该服务账号的 API 凭据。

  2. 要使用非默认的服务账号,将 Pod 的 spec.serviceAccountName 字段设置为你想用的服务账号名称。只能在创建 Pod 时或者为新 Pod 指定模板时,你才可以设置 serviceAccountName。 你不能更新已经存在的 Pod 的 .spec.serviceAccountName 字段。

  3. 手动为 ServiceAccount 创建 API 令牌: 假设为名为 build-robot 的 ServiceAccount 创建一个长期有效的 API 令牌,首先创建如下对象:

    apiVersion: v1
    kind: Secret
    metadata:
      name: build-robot-secret
      annotations:
        kubernetes.io/service-account.name: build-robot
    type: kubernetes.io/service-account-token
    

    通过上面这个对象,控制面会自动为该 ServiceAccount 生成一个令牌,并将其保存到相关的 Secret 中,控制面也会为已删除的 ServiceAccount 执行令牌清理操作。

    也可以通过 kubectl create token build-robot直接创建 token 。

  4. 在 Pod 上指定 ImagePullSecrets :

    kubectl create secret docker-registry <name> \
      --docker-server=DOCKER_REGISTRY_SERVER \
      --docker-username=DOCKER_USER \
      --docker-password=DOCKER_PASSWORD \
      --docker-email=DOCKER_EMAIL
    

    Pod 只能引用位于自身所在名字空间中的 Secret 。

  5. 为服务账号添加 ImagePullSecrets :

    • 先生成一个镜像拉取的 Secret :
        kubectl create secret docker-registry <name> \
      --docker-server=DOCKER_REGISTRY_SERVER \
      --docker-username=DOCKER_USER \
      --docker-password=DOCKER_PASSWORD \
      --docker-email=DOCKER_EMAIL
      
    • 然后将 Secret 添加到服务账号:
      kubectl patch serviceaccount build-robot -p '{"imagePullSecrets": [{"name": "myregistrykey"}]}'
      
    • 现在,在当前名字空间中创建新 Pod 并使用 build-robot 账号时, 新 Pod 的 spec.imagePullSecrets 会被自动设置。
  6. 使用 go 客户端读取 kubeconfig 从外部访问 API Server

   /*
Copyright 2016 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Note: the example only works with the code within the same release/branch.
package main

import (
   "context"
   "flag"
   "fmt"
   "path/filepath"
   "time"

   "k8s.io/apimachinery/pkg/api/errors"
   metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
   "k8s.io/client-go/kubernetes"
   "k8s.io/client-go/tools/clientcmd"
   "k8s.io/client-go/util/homedir"
   //
   // Uncomment to load all auth plugins
   // _ "k8s.io/client-go/plugin/pkg/client/auth"
   //
   // Or uncomment to load specific auth plugins
   // _ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
)

func main() {
   var kubeconfig *string
   if home := homedir.HomeDir(); home != "" {
   	kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
   } else {
   	kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
   }
   flag.Parse()

   // use the current context in kubeconfig
   config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
   if err != nil {
   	panic(err.Error())
   }

   // create the clientset
   clientset, err := kubernetes.NewForConfig(config)
   if err != nil {
   	panic(err.Error())
   }
   for {
   	pods, err := clientset.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{})
   	if err != nil {
   		panic(err.Error())
   	}
   	fmt.Printf("There are %d pods in the cluster\n", len(pods.Items))

   	// Examples for error handling:
   	// - Use helper functions like e.g. errors.IsNotFound()
   	// - And/or cast to StatusError and use its properties like e.g. ErrStatus.Message
   	namespace := "default"
   	pod := "example-xxxxx"
   	_, err = clientset.CoreV1().Pods(namespace).Get(context.TODO(), pod, metav1.GetOptions{})
   	if errors.IsNotFound(err) {
   		fmt.Printf("Pod %s in namespace %s not found\n", pod, namespace)
   	} else if statusError, isStatus := err.(*errors.StatusError); isStatus {
   		fmt.Printf("Error getting pod %s in namespace %s: %v\n",
   			pod, namespace, statusError.ErrStatus.Message)
   	} else if err != nil {
   		panic(err.Error())
   	} else {
   		fmt.Printf("Found pod %s in namespace %s\n", pod, namespace)
   	}

   	time.Sleep(10 * time.Second)
   }
}