# 原理

# 架构

# 组件

  • k8s 包含多个组件进程,通常部署在多个主机上,组成分布式集群。

    • 每个主机称为节点(Node),分为两种类型:
      • 控制平面节点(Control Plane Node):又称为主节点(master node),负责控制整个集群、管理所有节点。
      • 工作节点(Worker Node):负责部署 Pod 。
    • 部署 k8s 组件时,可以直接运行二进制文件,也可以容器化部署。
  • 主节点一般运行以下进程:

    • kube-apiserver
    • kube-controller-manager
    • kube-scheduler
    • etcd
  • 所有节点都运行以下进程:

    • kubelet
    • kube-proxy

# 资源

  • k8s 会管理主机、容器等多种对象,又称为资源(resource)。例如:

    • Cluster
      • :集群,由 k8s 联系在一起的一组主机。
    • Node
      • :节点,k8s 集群中的一个主机。
    • Namespace
    • Pod
    • Service
  • 一些 k8s 对象之间存在上下级依赖关系,上级称为 Owner ,下级称为 Dependent 。

    • 删除一个 Owner 时,默认会级联删除它的所有 Dependent ,反之没有影响。
    • 比如一个 Deployment 是一组 Pod 的 Owner 。如果删除这些 Pod ,但保留 Deployment ,则会自动重新创建这些 Pod 。
    • 依赖关系只能位于同一个命名空间。

# Namespace

:命名空间,用于分组管理某些类型的 k8s 资源。

  • 命名空间可以管理 Pod、Service、PVC 等资源,不同命名空间下的这些资源相互隔离,互不可见。
    • 删除一个命名空间时,会删除其下的所有资源。
    • 可执行 kubectl api-resources --namespaced=false 查看不受命名空间管理的资源类型,比如 Node、IP、StorageClass、PersistentVolumes 。
  • 一个 k8s 中可以创建多个命名空间。初始有四个:
    default         # 供用户使用
    kube-system     # 供 k8s 系统内部使用,比如部署 apiserver、etcd 等系统服务
    kube-node-lease # 保存 node 的 Lease 对象
    kube-public     # 公开,未认证的用户也可访问
    

# 配置

  • 每种 k8s 对象通过一种配置文件进行管理。

    • 配置文件可以是 JSON 或 YAML 格式。
  • 配置文件的一般结构:

    apiVersion: v1              # 与 apiserver 交互时,采用的 API 版本
    kind: <sting>               # 对象的类型
    metadata:                   # 对象的元数据
      name: <sting>             # 名称,必填
      namespace: default        # 所属的命名空间
      annotations:              # 注释
        <key>: <value>
      labels:                   # 标签,用于筛选对象
        <key>: <value>
      # creationTimestamp: xx   # 创建时间,格式如 "2022-01-01T11:00:01Z"
      # ownerReferences: xx     # 指向上级对象,如果存在的话
      # resourceVersion: xx     # 配置文件的版本号,由 k8s 自动更新,是一串随机数字(不是哈希值),全局唯一
      # uid: xx                 # 每个对象会被分配一个 UID ,在整个 k8s 集群中唯一
    spec:                       # 规格,描述对象的期望状态
      <...>
    
    # status:                   # 描述对象的实际状态,这部分字段由 k8s 自动写入
    #   <...>
    
    • 大部分字段的 value 采用 string 数据类型,不能填入 number、bool 等数据类型。例如 Pod 的环境变量:
      env:
      - name: xxx
        value: "false"    # 加上双引号定界符,声明为 string 类型
        # value: false    # 如果填入该值,则 k8s 会报错:cannot unmarshal bool into Go struct field EnvVar.spec.template.spec.containers.env.value of type string
      
    • 如果给某个字段的 value 赋值为空,则相当于删除该字段。
    • 如果写入 k8s 未定义的字段,则 k8s 会自动删除。
    • k8s 对象的 name 大多需要符合 DNS 命名规范:只能包含 [0-9a-z.-] 字符,以字母、数字开头和结尾。
      • 在同一 namespace 下,同种对象的 name 不能重复。
    • annotations、labels 采用键值对格式。
      • key、value 都是 string 数据类型。
      • key 只能包含 [0-9A-Za-z._-] 字符,且以字母、数字开头和结尾。
        • 可以给 key 加上一个 <dns_domain>/ 格式的前缀。
        • 前缀 kubernetes.io/k8s.io/ 保留,供 k8s 系统内部使用。
      • 创建一个 k8s 资源之后,通常其中的 annotations 允许修改,而 labels 不允许修改。
  • 关于配置文件的版本:

    • 用户以 kubectl replace -f xx.yml 方式修改配置文件时,如果写入的 resourceVersion 值,与 k8s 当前存储的 resourceVersion 值不同,则说明用户不是在最新版本上修改,k8s 会报错并拒绝修改。
      • 这样当多个用户同时修改一个配置文件时,能保证顺序一致性。
      • 如果省略 resourceVersion 字段,或者以 kubectl patch 方式修改配置文件,则不会检查版本。
    • kubectl apply 方式修改配置文件时,会在 annotations 中增加一个 kubectl.kubernetes.io/last-applied-configuration 字段,记录上一个版本的配置文件,用于比较差异,找出当前版本修改了哪些字段。
    • Deployment 资源会增加一个 metadata.generation 字段,取值从 1 开始递增,用于记录版本序号。
  • 某些类型的 k8s 对象可选添加 metadata.finalizers 字段,定义终结器。

    • 当 k8s 删除一个对象时,如果定义了 finalizers ,则会调用相应的终结器,并添加 metadata.deletionTimestamp 字段,将对象标记为 terminating 状态。直到 finalizers 字段为空,才会实际删除对象。
    • 例如 PersistentVolume 对象默认定义了 finalizers ,当不被 Pod 使用时,才能删除。
      finalizers:
      - kubernetes.io/pv-protection
      
    • 用户可以手动修改 k8s 对象,删除其中的 finalizers ,使得它被立即删除。但这样可能引发问题,比如异常终止服务、没有清理占用的资源。

# Node

  • k8s 为每个节点创建了一个 Node 对象,配置示例:

    apiVersion: v1
    kind: Node
    metadata:
      labels:
        kubernetes.io/arch: amd64
        kubernetes.io/hostname: node-1
        kubernetes.io/os: linux
        node-role.kubernetes.io/controlplane: "true"
        node-role.kubernetes.io/etcd: "true"
        node-role.kubernetes.io/worker: "true"
      name: node-1
    spec:
      podCIDR: 10.42.1.0/24
    status:
      addresses:
      - address: 10.0.0.1
        type: InternalIP    # 内网 IP ,通常绑定到 eth0 网卡
      - address: node-1
        type: Hostname      # 主机名
      allocatable:          # 总共可分配的资源量
        cpu: "15"
        ephemeral-storage: "91546762160"
        hugepages-1Gi: "0"
        hugepages-2Mi: "0"
        memory: "60263227958"
        pods: "250"
      capacity:             # 硬件资源量
        cpu: "16"
        ephemeral-storage: 105144100Ki
        hugepages-1Gi: "0"
        hugepages-2Mi: "0"
        memory: 64155748Ki
        pods: "250"
      conditions:
        ...
    
    • 如果 node 变为非 Ready 状态超过一定时长,则 k8s 会自动驱逐该 node 上的 Pod 。然后将 node 标记为 unschedulable ,直到 node 恢复到 Ready 状态。
  • k8s 有两种监控 node 状态的方式:

    • 每个 node 的 kubelet 默认每隔 10s 发送一次请求到 apiserver ,更新 Node 对象的 status 字段。
    • 每个 node 的 kubelet 会在 kube-node-lease 命名空间创建一个 Lease 对象,并定期发送一次请求到 apiserver ,刷新 Lease 对象的 renewTime 时间戳,从而证明 node 在线。

# 关机

  • 重启一个 k8s node 时,建议用户不要直接 shutdown ,而是按以下流程操作:

    1. 执行命令 kubectl drain <node> --delete-emptydir-data --force --ignore-daemonsets ,从而驱逐该节点上已部署的 Pod 。
      • Pod 被驱逐之后,其上级 Deployment 会自动创建新 Pod 并部署到其它节点。
      • 如果某个 Deployment 仅包含一个 Ready 状态的 Pod ,且刚好部署在该节点上,则 k8s drain 会导致该 Deployement 暂时不可用。
      • 即使某个 Deployement 包含多个 Ready 状态的 Pod ,也可能因为 k8s drain 而导致 Pod 数量过少。为了避免这种情况,可创建 PodDisruptionBudget 对象,当不可用的 Pod 数量达到阈值时就阻止 k8s drain 。
    2. 执行 shutdown 等命令,从而关机。
    3. 重启节点,执行 kubectl uncordon <node> ,从而恢复调度 Pod 到该节点。
  • 如果一个节点未经过 drain 就关机,则存在以下问题:

    • 该节点上的 Pod 未经过 terminationGracePeriodSeconds 就被终止,可能丢失数据。
      • 为了解决该问题,k8s 增加了优雅关闭节点(GracefulNodeShutdown)的功能:关机时自动终止 Pod ,最多等待 shutdownGracePeriod 时长。
    • 不能清理该节点已用的资源。
      • 例如该节点部署了 StatefulSet 类型的 Pod 时,某些编号的 Pod 会一直保留在该节点,没有运行,也不能调度到其它节点。
      • 例如该节点挂载了 PVC 时,不能释放,或挂载到其它主机。
      • 为了解决该问题,k8s 增加了异常关闭节点(Non-graceful node shutdown)的功能:允许用户给节点添加污点 node.kubernetes.io/out-of-service: NoExecute ,表示该节点已停止服务,使得 kube-controller-manager 强制删除该节点的 Pod、volume 资源。然后用户再删除污点。
  • GracefulNodeShutdown 的原理:

    • kubelet 会注册到 systemd 的 inhibit lock 中,从而允许在关机之前执行操作。执行 systemd-inhibit --list 即可查看。
    • 用户通过 systemd 关机时,会触发 kubelet 来优雅关闭节点,流程如下:
      • 给当前节点添加 node.kubernetes.io/not-ready 污点,阻止新的 Pod 调度到当前节点。
      • 终止当前节点的普通 Pod ,使它们进入 Terminated 状态。
      • 终止当前节点的 critical Pod 。
        • 用户可通过 critical Pod 的形式运行 coredns、metrics-server 等重要服务,
        • 将 Pod 的 priorityClassName 配置为 system-cluster-critical 或 system-node-critical ,就会声明为 critical Pod 。
        • 例如给 kubelet 配置 shutdownGracePeriod=30s 和 shutdownGracePeriodCriticalPods=10s ,表示终止 Pod 的总耗时最多为 30s 。其中前 20s 用于终止普通 Pod ,后 10s 用于终止 critical Pod 。
    • kubelet 默认将 shutdownGracePeriod 和 shutdownGracePeriodCriticalPods 配置为 0 ,因此禁用该功能。如果用户不通过 systemd 关机,也不会触发该功能,此时属于异常关闭节点。

# Lease

  • k8s Lease 对象表示租约,常见用途:
    • 心跳检查
      • 例如每个 node 的 kubelet 会定期刷新 Lease 对象的 renewTime 时间戳。
    • 分布式锁
    • 分布式选举
      • :将 Lease 对象当作一个分布式锁使用。分布式集群中同时只能有一个实例持有 Lease 对象,意味着担任 leader 角色,而其它实例担任 candidate 角色。
      • 例:kube-scheduler 运行了多个实例,它们会抢占 kube-system 命名空间中一个名为 kube-scheduler 的 Lease 对象。配置示例:
        apiVersion: coordination.k8s.io/v1
        kind: Lease
        metadata:
          name: kube-scheduler
          namespace: kube-system
          resourceVersion: ...
        spec:
          acquireTime: "2022-08-10T06:47:48.078262Z"      # 该 Lease 对象最后一次被获取的时刻,即最后一次选举的时刻
          holderIdentity: node-1_632ad4a5-386e-4a41-97ba-7ac54ad414fc   # 该 Lease 对象目前被谁持有
          leaseDurationSeconds: 15                        # 租约的有效时长
          leaseTransitions: 12                            # 该 Lease 对象被转手了多少次。每次选举,如果新 leader 与旧 leader 不是同一个实例,则将该值加 1
          renewTime: "2022-08-12T03:17:47.583942Z"        # 该 Lease 对象最后一次被刷新的时刻
        
        • 每当有一个实例获取(acquire)到 Lease 对象,就会担任 leader 角色,完成了一次选举。
        • laeder 需要定期刷新 renewTime 时间戳,从而维持自己的 leader 身份,该操作称为续约(renew a lease)。如果 renewTime + LeaseDuration < now_time ,则租约过期,其它实例有权进行 acquire ,抢走 Lease 对象。
        • kube-scheduler 关于选举的配置参数:
          --leader-elect-lease-duration   # 配置 LeaseDuration 变量,默认值为 15s
          --leader-elect-renew-deadline   # 配置 RenewDeadline 变量,默认值为 10s
          --leader-elect-retry-period     # 配置 RetryPeriod   变量,默认值为 2s
          
        • 同理,kube-controller-manager 运行了多个实例,它们会抢占 kube-system 命名空间中一个名为 kube-controller-manager 的 Lease 对象。
  • k8s 的几个组件在使用 Lease 对象进行选举时,都是调用 leaderelection.go (opens new window) 。主要原理:每个实例会每隔 RetryPeriod 时长执行一次 tryAcquireOrRenew() 函数,检查 Lease 对象的内容,即轮询。有几种结果:
    • 发现 Lease 对象不存在,则创建它。
    • 发现 Lease 对象的 holderIdentity 不是自己,且租约过期,说明有权选举,因此进行 acquire 操作:将 Lease 对象的 holderIdentity 字段改成自己的实例名,将 acquireTime、renewTime 字段改成当前时间。
      • 如果多个实例同时修改 Lease 对象,则只有一个实例能修改成功,其它实例会因为 Lease 对象的 resourceVersion 已变化而修改失败。
    • 发现 Lease 对象的 holderIdentity 不是自己,且租约未过期,说明其它实例正在担任 leader ,因此不进行操作。
    • 发现 Lease 对象的 holderIdentity 是自己,说明自己是 leader ,因此进行 renew 操作:将 Lease 对象的 renewTime 字段改成当前时间。
      • 如果 renew 失败,则依然每隔 RetryPeriod 时长执行一次 tryAcquireOrRenew() 函数,重试 renew 。
      • 如果在 RenewDeadline 时长内重试 renew 失败,则放弃 renew ,改为 acquire 。
    • 触发选举的几种原因:
      • leader 故障,不能刷新 renewTime 。
      • apiserver 故障,导致 leader 不能刷新 renewTime 。
      • leader 与 candidate 的时钟误差超过 LeaseDuration 。

# kube-apiserver

  • 用途:提供 Restful API ,供用户、k8s 组件访问,从而管理 k8s 集群。

    • 用户可使用 kubectl 命令,作为客户端与 apiserver 交互,从而管理 k8s 。
    • k8s 组件之间一般不会直接通信,而是发送请求到 apiserver 。并且使用自签名的 ssl 证书,进行 HTTPS 加密通信。
  • 默认监听 TCP 6443 端口。

  • 客户端发送 HTTP 请求到 apiserver 时,会经过以下处理流程:

    1. apiserver 收到 HTTP 请求。
    2. apiserver 依次执行多项检查:认证、限流、鉴权、准入控制。全部通过之后,才认为该请求有效。
    3. apiserver 执行该请求包含的操作。例如创建 Pod 。
    4. apiserver 发送 HTTP 响应。 在上述流程期间,apiserver 可能记录 event、审计日志。
  • apiserver 本身提供了 proxy 功能,可以反向代理某个 k8s service 的端口,供 k8s 集群外的用户访问。

    • URL 格式为:https://${apiserver_ip}/api/v1/namespaces/${namespace}/services/${service_name}:${port_name}/proxy/
    • 执行 kubectl cluster-info 可以查看 apiserver 反向代理的所有 k8s service 。
      • 例如反向代理了 CoreDNS 服务,URL 为 https://${apiserver_ip}:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    • 缺点:
      • URL 比较长。
      • 这些流量会经过 apiserver ,可能对 apiserver 造成很大负载,因此建议不要使用该功能。
  • 性能优化:

    • 建议在 k8s 集群中部署至少 2 个 apiserver 实例。当一个 apiserver 实例故障时,客户端可以访问另一个 apiserver 实例,从而实现高可用。
    • 进一步地,建议不让客户端直接访问 apiserver 。而是用 nginx 等软件部署一个负载均衡服务器,反向代理各个 apiserver 实例,然后让客户端将 HTTP 请求发送到负载均衡服务器。

# event

  • k8s 集群运行时会产生各种事件(event),像日志,方便用户了解 k8s 的运行状态。
    • k8s event 存储在 etcd 中,默认只保存最近 1 小时的 event 。
    • 如果某个 k8s 对象重复产生一种 event ,则存储在 etcd 时会合并成一个 event ,并记录 count、creationTimestamp、lastTimestamp 。
    • 例:执行命令 kubectl get events -n default -w ,可查看一个命名空间的 event 。
      • 用户可部署一个 Pod 专门执行上述命令,将 event 转换成 stdout 日志,然后长期保存。
  • event 分为几种类型:
    Normal    # 表示正常事件,没有异常。例如 Pod Scheduled、Pulling image
    Warning   # 表示警告级别的事件。例如 FailedMount、Readiness probe failed
    Errors    # 表示错误级别的事件。例如 NodeNotReady
    

# 审计日志

  • apiserver 处理客户端发来的 HTTP 请求时,支持在以下多个阶段记录审计日志(audit log)。

    RequestReceived   # 已经收到 HTTP 请求,尚未执行业务操作
    ResponseStarted   # 已经发送 HTTP Response Headers 给客户端,尚未发送 Response body
    ResponseComplete  # 已经发送 HTTP Response body 。此时该 HTTP 请求处理完毕
    Panic             # k8s 内部出错,导致 HTTP 请求未处理完毕
    
    • 只有 watch 等耗时长的操作,才存在 ResponseStarted 阶段。
    • 同一个 HTTP 请求,可能在多个阶段分别记录一条审计日志,这些日志的 auditID 相同。
    • k8s 集群可能部署了多个 apiserver 实例。只有收到 HTTP 请求的那个 apiserver 实例,才会记录审计日志。
    • event 偏向于记录 k8s 内部状态,而审计日志偏向于记录从外部发送到 k8s 的请求。
  • apiserver 支持对不同 k8s 资源,记录不同详细程度的审计日志,分为多个级别:

    None            # 不记录审计日志
    Metadata        # 只记录请求的元数据,比如 user、requestURI、verb
    Request         # 在 Metadata 的基础上,增加记录请求的 request body
    RequestResponse # 在 Request 的基础上,增加记录请求的 response body
    
  • apiserver 默认未启用审计日志。如果想启用,则需要在启动 apiserver 时添加参数:

    --audit-policy-file=audit-policy.yml  # 记录审计日志的策略
    --audit-log-format=json
    --audit-log-path=xx                   # 将审计日志输出到哪个文件
    --audit-log-maxage=<int>              # 每天轮换一次日志文件,最多保留最近几天的日志文件
    --audit-log-maxsize=<int>             # 每个日志文件最大多少 MB ,超过则轮换一次
    --audit-log-maxbackup=<int>           # 最多保留最近几个日志文件
    
  • audit-policy.yml 的内容示例:

    apiVersion: audit.k8s.io/v1
    kind: Policy
    rules:
    - level: Metadata           # 对所有 k8s 资源记录 Metadata 级别的审计日志
    
    apiVersion: audit.k8s.io/v1
    kind: Policy
    omitStages:                 # 不要在这些阶段,记录审计日志
      - RequestReceived
    rules:
      - level: RequestResponse  # 对以下 resources ,记录 RequestResponse 级别的审计日志
        resources:
        - group: ""
          resources: ["pods"]   # 这只会匹配 pods 资源,不会匹配 pods 的子资源
      - level: Metadata
        resources:
        - group: ""
          resources: ["pods/log", "pods/status"]
    

# kube-controller-manager

  • 用途:运行一些控制器(controller),自动控制 Node、Pod、Service 等各种 k8s 资源。
  • k8s 内置的 controller 举例:
    • node controller
      • :负责管理 node 。比如在新增 node 时分配 CIDR 子网、当 node 非 Ready 状态时发起驱逐。
    • namespace controller
    • deployment controller
    • replicaset controller
    • statefulset controller
    • daemonset controller
    • job controller
      • :负责根据 Job 创建 Pod 。
    • cronjob controller
    • endpoints controller
      • :负责管理所有 endpoints 对象。比如监控 Service、Pod 的状态,自动修改 endpoints 。
    • serviceaccounts controller
      • :负责为新建的 namespace 创建 default service account 。
  • 用户也可以开发自定义的 controller 。

# kube-scheduler

  • 用途:决定将 Pod 分配到哪个 node 上部署,该过程称为调度。

# schedulingQueue

  • 下面根据 源代码 (opens new window) ,分析 kube-scheduler 的原理。
  • kube-scheduler 的 Run() 函数如下:
    func (sched *Scheduler) Run(ctx context.Context) {
        logger := klog.FromContext(ctx)
    
        // 启动 schedulingQueue 队列
        sched.SchedulingQueue.Run(logger)
    
        // 创建一个协程,通过 UntilWithContext() 循环执行 ScheduleOne() 函数,间隔 0 s
        // 每次循环,会从 schedulingQueue 队列(的 activeQ 队列)取出一个 Pod ,进行调度
        // 因此,同时只能调度一个 Pod
        // - 优点:避免了并发调度多个 Pod 时,竞争同一资源而发生冲突
        // - 缺点:只有一个协程,性能可能不足。实测每秒能调度几十个 Pod
        go wait.UntilWithContext(ctx, sched.ScheduleOne, 0)
    
        // 直到 context 为 done ,才结束主循环
        <-ctx.Done()
        sched.SchedulingQueue.Close()
        ...
    }
    
  • kube-scheduler 会连接到 kube-apiserver ,以 list/watch 的方式监听 Pod、Node 等对象的变化。
    • 例如发现新增的 Pod ,将它们加入 activeQ 。
    • 例如获取所有 Node 的最新信息,从而判断哪些 Node 有空闲资源来部署 Pod 。
  • 所有等待调度的 Pod ,会被放在 schedulingQueue 队列中,它包含三个队列:
    • activeQ
      • :包含希望立即被调度的 Pod 。
      • activeQ 是一个优先级队列。其中的 Pod 会按 priority 排序,使得 priority 最大的 Pod ,位于队列顶端,最先被调度。
    • unschedulablePods
      • :包含不可调度的 Pod 。这些 Pod 已经尝试调度,但发现不可调度,比如被插件拒绝。
    • podBackoffQ
      • :包含调度失败的 Pod 。
      • 如果一个 Pod 调度失败,则会被放入 podBackoffQ 或 unschedulablePods 队列,一段时间之后再移动到 activeQ 队列。
      • podBackoffQ 队列中的 Pod ,每隔 BackoffSeconds 时长,会重新尝试一次调度。
        • 如果一个 Pod 连续调度失败,则 BackoffSeconds 时长会倍增。
        • 默认配置了 podInitialBackoffSeconds=1s ,表示 BackoffSeconds 的初始时长。
        • 默认配置了 podMaxBackoffSeconds=10s ,表示 BackoffSeconds 的最大时长。建议加大该配置参数,从而降低 kube-scheduler 的负载。
        • 不管一个 Pod 尝试调度多少次,只要没有成功调度,就一直不会部署。
  • schedulingQueue 的 Run() 函数如下:
    func (p *PriorityQueue) Run(logger klog.Logger) {
        // 创建一个协程,通过 Until() 循环执行 flushBackoffQCompleted() ,间隔 1s
        // flushBackoffQCompleted() 会检查 podBackoffQ 队列中的每个 Pod ,
        // 如果一个 Pod 距离上一次调度的时长,超过 BackoffSeconds ,则移动到 activeQ 队列
        go wait.Until(func() {
          p.flushBackoffQCompleted(logger)
        }, 1.0*time.Second, p.stop)
    
        // 创建一个协程,通过 Until() 循环执行 flushUnschedulablePodsLeftover() ,间隔 30s
        // flushUnschedulablePodsLeftover() 会检查 unschedulablePods 队列中的每个 Pod ,
        // 如果一个 Pod 在 unschedulablePods 队列的停留时长超过 podMaxInUnschedulablePodsDuration=5m ,则移动到 podBackoffQ 或 activeQ 队列
        go wait.Until(func() {
            p.flushUnschedulablePodsLeftover(logger)
        }, 30*time.Second, p.stop)
    }
    

# ScheduleOne

  • ScheduleOne() 函数如下:

    func (sched *Scheduler) ScheduleOne(ctx context.Context) {
        // 从 activeQ 取出一个 Pod 。如果 activeQ 为空,则阻塞等待
        podInfo, err := sched.NextPod(logger)
        pod := podInfo.Pod
        logger.Info("Attempting to schedule pod", "pod", klog.KObj(pod))
        ...
    
        // schedulingCycle() 负责决定将 Pod 调度到哪个 Node
        scheduleResult, assumedPodInfo, status := sched.schedulingCycle(schedulingCycleCtx, state, fwk, podInfo, start, podsToActivate)
        if !status.IsSuccess() {
            ...
        }
        ...
    
        // 以下流程会通过协程,异步地执行。因此马上结束本次 ScheduleOne() ,开始执行下一次 ScheduleOne()
        go func() {
            // bindingCycle() 负责将 Pod 绑定到 Node
            status := sched.bindingCycle(bindingCycleCtx, state, fwk, scheduleResult, assumedPodInfo, start, podsToActivate)
            if !status.IsSuccess() {
                ...
            }
            ...
        }()
    }
    
  • schedulingCycle() 的运行流程,主要包括以下阶段:

    • filter(过滤)
      • :遍历 Node 列表,过滤出允许调度该 Pod 的所有 Node ,称为可用节点(feasibleNodes)。
      • 如果节点总数大于 100 ,则为了减少遍历的耗时,只要找到的 feasibleNodes 达到节点总数的 percentageOfNodesToScore% ,就提前结束遍历。
        • 如果用户未配置 percentageOfNodesToScore 参数,则 k8s 会根据节点总数,给该参数赋值为 50 或更低。
      • 为了避免某些 Node 一直未被遍历,每次遍历 Node 列表时,会以上一次遍历的终点,作为本次遍历的起点。
      • 过滤时,会执行多个插件,每个插件负责一种检查。
        • 例如 fit 插件会检查 Node 是否满足 Pod 的 requests 资源。
        • 例如 TaintToleration 插件会检查 Pod 是否容忍 Node 的污点。
        • 为了减少耗时,会创建 parallelism=16 个协程,并发执行这些插件。
    • postFilter
      • 如果 filter 阶段没找到 feasibleNodes ,则执行 PostFilter 类型的插件,比如 Preemption 插件会考虑能否抢占 Node 。
    • score(评分)
      • 如果 feasibleNodes 只包含一个 Node ,则直接使用该 Node ,用于部署该 Pod 。
      • 如果 feasibleNodes 包含不止一个 Node ,则给每个 Node 评分,比如考虑亲和性。然后选用评分最高的一个 Node 。
        • 如果存在多个评分最高的 Node ,则随机选用其中一个 Node 。

# kubelet

  • 用途:
    • 在某个 node 上保持运行,并将当前 node 注册到 apiserver ,并定期上报状态。
    • 调用容器运行时,来创建、管理、监控 Pod 。
  • 默认监听 TCP 10250 端口。
  • kubelet 部署 Pod 时,会调用 CRI 接口 RuntimeService.RunPodSandbox ,创建一个沙盒(Pod Sandbox),然后在 Sandbox 中启动该 Pod 的全部容器。
    • Sandbox 负责提供一个 Pod 运行环境。不同 Pod 的 Sandbox 相互隔离。
    • Sandbox 通常基于 Linux namespace、Cgroup 技术实现。也可以基于虚拟机实现,比如 kata-containers 。
      • 为每个 Pod 创建一个 ipc namespace、一个 network namespace、一个 uts namespace ,属于 pause 容器,被该 Pod 中所有容器共享。
      • 为 Pod 中所有容器分别创建一个 pid namespace、mnt namespace ,从而隔离各个容器。
      • 为 Pod 中每个容器分别创建一个 Cgroup ,从而分别限制资源开销。
  • 启动 Pod 时,kubelet 会先在每个 Pod 中运行一个 pause 容器。
    • pause 容器的内容很简单,只是运行一个简单的 pause 程序,循环执行 pause() 函数进行睡眠。
    • pause 容器的作用是便于管理 Linux namespace 。例如:
      • 启动 Pod 时,先创建 pause 容器,然后基于它创建 network 等 Linux namespace ,共享给其它容器。
      • Pod 内普通容器终止时,pause 容器依然运行,能避免 Linux namespace 被自动删除。
    • 如果用 docker stop 终止 pause 容器,则 kubelet 会在几秒后发现,会自动终止 Pod 内所有容器,然后根据 restartPolicy 重启 Pod ,创建新的 pause 容器、普通容器。
  • kubelet 中的 PLEG(Pod Lifecycle Event Generator)模块负责执行 relist 任务:获取本机的容器列表,检查一遍所有 Pod 的状态,如果状态变化则生成 Pod 的生命周期事件。
    • 缺点:属于轮询的工作模式。Pod 数量越多,执行一次 relist 的耗时越久。
    • 每执行一次 relist ,会等 1s 再执行下一次 list 。
    • 如果某次 relist 的耗时超过 3min ,则报错 PLEG is not healthy ,并将当前 Node 标记为 NotReady 状态。

# kube-proxy

  • 用途:管理 node 的网络,将访问 Service 的流量反向代理到 EndPoints 中的 Pod 。

有多种代理模式:

# userspace

  • :k8s v1.2 之前的默认模式。
  • 原理:
    • 监听所有 Service、EndPoints 的变化。
    • 配置 iptables 规则,将访问 service_ip:port 的流量转发到 kube-proxy 监听的端口。
    • kube-proxy 监听一些端口,反向代理到各个 Service 的 EndPoints 中的 pod_ip:port 。
      • EndPoints 包含多个 pod_ip 时,默认按轮询算法进行负载均衡。
  • 缺点:
    • 流量会先后被 iptables、kube-proxy 转发,总共转发两次,而且需要从内核态传递到用户态,性能较低。

# iptables

  • :k8s v1.2 之后的默认模式。

  • 原理:

    • 监听所有 Service、EndPoints 的变化。据此配置 iptables 规则,将访问 Service IP 的流量转发到 EndPoints 中的 pod_ip:port 。
      • EndPoints 包含多个 pod_ip 时,默认按随机算法进行负载均衡,还可选轮询算法。
    • 转发数据包时会进行 NAT ,实现透明代理。
      • 将数据包转发给 EndPoints 时,会将数据包的目标 IP 改为 pod_ip ,即 DNAT 。
      • 转发 EndPoints 返回的数据包时,会将数据包的源 IP 改为 pod_ip ,即 SNAT 。
  • 缺点:

    • 修改 iptables 规则时,需要先用 iptables-save 导出,然后修改,最后用 iptables-restore 导入,有一定耗时。
    • 处理每个数据包时,需要线性查找与其匹配的 iptables 规则,时间复杂度为 O(n) 。因此 Service 数量较多时,耗时较久。
  • 例:创建一个名为 nginx 的 k8s service ,查看关于它的 iptables 规则:

    [root@CentOS ~]# iptables-save | grep nginx
    # 访问该 service IP 的流量,会跳转到名为 KUBE-SVC-YWVGZR6AWC7VR6ZM 的 chain
    -A KUBE-SERVICES -d 10.43.1.1/32 -p tcp -m comment --comment "base/nginx cluster IP" -m tcp --dport 80 -j KUBE-SVC-YWVGZR6AWC7VR6ZM
    
    # KUBE-SVC-* 先执行 KUBE-MARK-MASQ :如果流量不来自 Pod IP ,则进行 SNAT ,将 src_ip 改为当前 node 的 Cluster IP
    # 然后将流量按概率交给某个 KUBE-SEP-* 处理,对应该 Service 的其中一个 EndPoints
    -A KUBE-SVC-YWVGZR6AWC7VR6ZM ! -s 10.42.0.0/16 -d 10.43.1.1/32 -p tcp -m comment --comment "base/nginx cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
    -A KUBE-SVC-YWVGZR6AWC7VR6ZM -m comment --comment "cem/nginx" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-IWNS2PXXMEEOL3FS
    -A KUBE-SVC-YWVGZR6AWC7VR6ZM -m comment --comment "cem/nginx" -j KUBE-SEP-WEMVFNE7YWEP6ZGY
    
    # KUBE-SEP-* 先执行 KUBE-MARK-MASQ ,处理来自 EndPoints 自身的流量
    # 然后将流量转发到 EndPoints 中的 pod_ip:port
    -A KUBE-SEP-IWNS2PXXMEEOL3FS -s 10.42.2.12/32 -m comment --comment "cem/nginx" -j KUBE-MARK-MASQ
    -A KUBE-SEP-IWNS2PXXMEEOL3FS -p tcp -m comment --comment "cem/nginx" -m tcp -j DNAT --to-destination 10.42.2.12:80
    -A KUBE-SEP-WEMVFNE7YWEP6ZGY -s 10.42.2.13/32 -m comment --comment "cem/nginx" -j KUBE-MARK-MASQ
    -A KUBE-SEP-WEMVFNE7YWEP6ZGY -p tcp -m comment --comment "cem/nginx" -m tcp -j DNAT --to-destination 10.42.2.13:80
    

# IPVS

  • :k8s v1.8 新增的模式,相当于在 iptables 模式的基础上增加 IPVS 负载均衡器。
  • 原理:
    • 为每个 Service IP 创建一个 IPVS 负载均衡器。
    • 通过 ipset 命令创建一些包含 Service IP 的哈希集合。然后配置 iptables 规则,如果数据包的目标 IP 匹配 ipset ,则交给对应的 IPVS 处理,并进行 NAT 。
  • 优点:
    • 通过 ipset 大幅减少了 iptables 规则的数量,并且哈希查找的速度更快。
    • 支持多种负载均衡算法。

# nftables

  • :k8s v1.29 新增的模式,但尚不成熟,不建议使用。
  • 目前主流 Linux 发行版正在将防火墙后端从 iptables 过渡到 nftables ,因此 k8s 也逐渐过渡到 nftables ,以实现更好的性能。

# etcd

  • 用途:提供分布式数据库,存储 k8s 的配置数据、状态数据。

  • 默认监听 TCP 2379、2380 端口。

    • 一般将 etcd 部署在 k8s 主节点上,仅供同一主机的 apiserver 访问。
    • 也可以将 etcd 部署在无 apiserver 的主机上,或者部署在 k8s 集群之外,只需能够被 apiserver 访问。
  • 部署 k8s 集群时,默认 etcd 未启用密码认证,可能被任何人读写数据,相当于拥有 k8s 管理员权限。因此建议采取以下安全措施:

    • 用防火墙保护 etcd 的端口,限制 IP 白名单,只允许被 apiserver、其它 etcd 实例访问。
    • 进一步地,可在启动 apiserver 时添加参数 --encryption-provider-config=encryption.yml ,传入以下配置文件,让 apiserver 将 Secret 资源写入 etcd 时,基于密钥进行加密。
      apiVersion: apiserver.config.k8s.io/v1
      kind: EncryptionConfiguration
      resources:
        - resources:
            - secrets
          providers:
            - aesgcm:
                keys:
                - name: key1
                  secret: ******    # 一个经过 base64 编码的密钥,可执行 head -c 32 /dev/urandom | base64 生成
            - identity: {}
      
      这样即使某人绕过防火墙,访问到了 etcd ,也只能看到加密存储的数据。
  • apiserver 在 etcd 中存储的所有数据,按目录层级排列,如下:

    /registry/configmaps/default/kube-root-ca.crt   # 路径格式为 /registry/<resource_type>/<namespace>/<resource_name>
    /registry/daemonsets/default/mysql
    /registry/pods/default/mysql-p5jk2
    /registry/endpointslices/default/mysql-2kcmx
    /registry/services/endpoints/default/mysql
    /registry/services/specs/default/kafka