# 原理

# 架构

# 组件

  • 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

:命名空间,用于分组管理某些类型的资源,又称为项目(project)。

  • 命名空间可以管理 Pod、Service、PVC 等资源,不同命名空间下的这些资源相互隔离,互不可见。
    • 删除一个命名空间时,会删除其下的所有资源。
    • 可执行 kubectl api-resources --namespaced=false 查看不受命名空间管理的资源类型,比如 Node、IP、StorageClass、PersistentVolumes 。
  • 一个 k8s 中可以创建多个命名空间。初始有四个:
    default         # 供用户使用
    kube-system     # 供 k8s 系统内部使用,比如部署 apiserver、etcd 等系统服务
    kube-node-lease # 保存节点的 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 自动写入
    #   <...>
    
    • 修改 k8s 中的配置文件时,如果不符合语法,则会报错不能修改。
      • 如果加入了未定义的字段,则会自动删除。
    • 对象的 name 大多需要符合 DNS 命名规范:只能包含 [a-z0-9.-] 字符,以字母、数字开头和结尾。
      • 在同一 namespace 下,同种对象的 name 不能重复。
    • annotations、labels 采用键值对格式。
      • key、value 都是 String 类型,不能为 bool 等类型。
      • key 只能包含 [a-zA-Z0-9._-] 字符,以字母、数字开头和结尾。
        • 可以给 key 加上一个 <dns_domain>/ 格式的前缀。
        • 前缀 kubernetes.io/k8s.io/ 保留,供 k8s 系统内部使用。
  • 可选添加 metadata.finalizers 字段,定义终结器。
    • 当 k8s 删除一个对象时,如果定义了 finalizers ,则会调用相应的终结器,并添加 metadata.deletionTimestamp 字段,将对象标记为 terminating 状态。直到 finalizers 字段为空,才会实际删除对象。
    • 例如 PersistentVolume 对象默认定义了 finalizers ,当不被 Pod 使用时,才能删除。
      finalizers:
      - kubernetes.io/pv-protection
      

# 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:
        ...
    
  • k8s 有两种监控节点状态的方式:

    • kubelet 默认每隔 10s 上报一次节点状态到 apiserver 。
    • 为每个节点创建一个 Lease 对象。kubelet 默认每隔 10s 发送一次请求到 apiserver ,更新一次 Lease 对象,从而证明节点在线。
  • 当节点变为非 Ready 状态一段时间之后,k8s 会自动驱逐该节点上的 Pod 。然后将节点标记为 unschedulable ,直到节点恢复到 Ready 状态。

# kube-apiserver

  • 功能:提供 Restful API ,供用户、k8s 组件访问,从而管理 k8s 集群。
    • 用户可使用 kubectl 命令,作为客户端与 apiserver 交互,从而管理 k8s 。
    • k8s 组件之间一般不会直接通信,而是发送请求到 apiserver ,并默认采用 HTTPS 加密通信。
  • 默认监听 TCP 6443 端口。

# event

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

# kube-controller-manager

  • 功能:包含一些控制器(controller),用于自动控制 Node、Pod、Service 等各种 k8s 资源。
  • k8s 内置的 controller 举例:
    • node controller
      • :负责管理节点。比如在新增节点时分配 CIDR 子网、当节点非 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 ,筛选出允许调度该 Pod 的所有 Node ,称为可调度节点。比如 Node 必须满足 Pod 的 requests 资源、Pod 必须容忍 Node 的污点。
      • 如果没有可调度节点,则 Pod 一直不会部署。
      • 对于节点总数低于 100 的 k8s ,默认设置了 percentageOfNodesToScore=50 ,即当可调度节点数量达到总数的 50% 时就停止遍历,从而减少耗时。
      • 为了避免某些节点一直未被遍历,每次遍历 Node 列表时,会以上一次遍历的终点作为本次遍历的起点。
    • 打分
      • :给每个可调度节点打分,选出一个最适合部署该 Pod 的 Node 。比如考虑亲和性。
      • 如果存在多个打分最高的 Node ,则随机选取一个。

# kubelet

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

# kube-proxy

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

# etcd

  • 功能:提供分布式数据库,存储 k8s 的配置、状态数据。
  • 默认监听 TCP 2379、2380 端口。
    • 一般将 etcd 部署在主节点上,仅供本机的 apiserver 访问。
    • 也可以将 etcd 部署在无 apiserver 的主机上,或者部署在 k8s 集群之外。
  • 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
    
  • 按默认方式部署 k8s 时,etcd 未启用密码认证,可能被任何人读写数据。因此建议用防火墙规则保护 etcd 的端口,实际上所有 k8s 组件的端口都不应该对外暴露。
    • 通过 k8s RBAC 可限制用户通过 apiserver 访问 Secret 的权限,但不能阻止用户直接访问 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: {}