# 原理
# 架构
# 组件
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
:命名空间,用于分组管理某些类型的 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 系统内部使用。
- 可以给 key 加上一个
- 创建一个 k8s 资源之后,通常其中的 annotations 允许修改,而 labels 不允许修改。
- 大部分字段的 value 采用 string 数据类型,不能填入 number、bool 等数据类型。例如 Pod 的环境变量:
关于配置文件的版本:
- 用户以
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 ,使得它被立即删除。但这样可能引发问题,比如异常终止服务、没有清理占用的资源。
- 当 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 ,而是按以下流程操作:
- 执行命令
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 。
- 执行 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 。并且使用自签名的 ssl 证书,进行 HTTPS 加密通信。
默认监听 TCP 6443 端口。
客户端发送 HTTP 请求到 apiserver 时,会经过以下处理流程:
- apiserver 收到 HTTP 请求。
- apiserver 依次执行多项检查:认证、限流、鉴权、准入控制。全部通过之后,才认为该请求有效。
- apiserver 执行该请求包含的操作。例如创建 Pod 。
- 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
。
- 例如反向代理了 CoreDNS 服务,URL 为
- 缺点:
- URL 比较长。
- 这些流量会经过 apiserver ,可能对 apiserver 造成很大负载,因此建议不要使用该功能。
- URL 格式为:
性能优化:
- 建议在 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 。
- node controller
- 用户也可以开发自定义的 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 尝试调度多少次,只要没有成功调度,就一直不会部署。
- activeQ
- 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 。
- filter(过滤)
# 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 。
- 监听所有 Service、EndPoints 的变化。据此配置 iptables 规则,将访问 Service IP 的流量转发到 EndPoints 中的 pod_ip:port 。
缺点:
- 修改 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 时,基于密钥进行加密。这样即使某人绕过防火墙,访问到了 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: {}
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