# 原理
# 架构
# 组件
k8s 包含多个组件进程,通常部署在多个主机上,组成分布式集群。
- 每个主机称为节点(Node),分为两种类型:
- 控制平面节点(Control Plane Node):又称为主节点(master node),负责控制整个集群、管理所有节点。
- 工作节点(Worker Node):负责部署 Pod 。
- 部署 k8s 组件时,可以直接运行二进制文件,也可以容器化部署。
- 每个主机称为节点(Node),分为两种类型:
主节点一般运行以下进程:
- kube-apiserver
- kube-controller-manager
- kube-scheduler
- etcd
所有节点都运行以下进程:
- kubelet
- kube-proxy
# 资源
k8s 会管理主机、容器等多种对象,又称为资源(resource)。例如:
- Cluster
- :集群,由 k8s 联系在一起的一组主机。
- Node
- :节点,k8s 集群中的一个主机。
- Namespace
- Pod
- Service
- Cluster
一些 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 # 保存 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 系统内部使用。
- 可以给 key 加上一个
- 大部分字段的 value 采用 string 数据类型,不能填入 number、bool 等数据类型。例如 Pod 的环境变量:
某些类型的 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 关机时,应该先执行命令
kubectl drain <node> --delete-emptydir-data --force --ignore-daemonsets
,从而驱逐该节点已部署的 Pod 。然后再用 shutdown 等命令关机。- 执行
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 资源。然后用户再删除污点。
- 该节点上的 Pod 未经过 terminationGracePeriodSeconds 就被终止,可能丢失数据。
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 关机,也不会触发该功能,此时属于异常关闭节点。
- kubelet 会注册到 systemd 的 inhibit lock 中,从而允许在关机之前执行操作。执行
# 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 ,并默认采用 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
- :负责管理 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 。
- node controller
- 用户也可以开发自定义的 controller 。
# kube-scheduler
- 功能:决定将 Pod 分配到哪个 node 上部署,该过程称为调度。
- 调度分为两个步骤:
- 过滤(filtering)
- :遍历所有 Node ,筛选出允许调度该 Pod 的所有 Node ,称为可调度节点。比如 Node 必须满足 Pod 的 requests 资源、Pod 必须容忍 Node 的污点。
- 如果没有可调度节点,则 Pod 一直不会部署。
- 对于节点总数低于 100 的 k8s ,默认设置了 percentageOfNodesToScore=50 ,即当可调度节点数量达到总数的 50% 时就停止遍历,从而减少耗时。
- 为了避免某些节点一直未被遍历,每次遍历 Node 列表时,会以上一次遍历的终点作为本次遍历的起点。
- 打分(scoring)
- :给每个可调度节点打分,选出一个最适合部署该 Pod 的 Node 。比如考虑亲和性。
- 如果存在多个打分最高的 Node ,则随机选取一个。
- 过滤(filtering)
# 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 的生命周期事件。
- 每执行一次 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 。
- 监听所有 Service、EndPoints 的变化。据此配置 iptables 规则,将访问 Service IP 的流量转发到 EndPoints 中的 pod_ip:port 。
- 缺点:
- 修改 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 的端口,只允许被其它 etcd、apiserver 访问。
- 通过 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: {}