前言

Kubernetes 可以理解成一个对计算、网络、存储等云计算资源的抽象后的标准 API 服务。几乎所有对 Kubernetes 的操作,不管是用 kubectl 命令行工具,还是在UI或者CD Pipeline 中,都相当于在调用其 REST API。很多人说 Kubernetes 复杂,除了其本身实现架构复杂以外,还有一个原因就是里面有二十多种原生资源的 API 学起来曲线比较陡。但不用担心,我们只要抓住本质 – 提供容器计算能力的平台,就能纲举目张,很容易快速理解。在 K8S 中,最重要也最基础的资源是 Pod,翻译一下就是“豆荚”,我们用下面这个最最基础的 Nginx 容器为例,搞清楚豆荚的一生,K8S 就懂了一半。大家也不需要研究 Kubernetes 怎么搭建,推荐用 OrbStack 在本地一键安装一套 Docker & K8S 环境出来,快速开始实验。首先,写一段这样的 Yaml 文件出来。

# nginx.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  containers:
    - name: web
      image: nginx
      ports:
        - name: web
          containerPort: 80
          protocol: TCP

然后用 kubectl 将 nginx 的 Pod 进行创建,命令后面加 -v8 是详细日志模式,可以看出来 kubectl 到底做了什么事情。

kubectl create pod -f nginx.yaml -v8

kubectl get pod -v8

在 OpenLens 或 K9S 等可视化工具中,我们可以看到一个叫 Nginx 的 Pod 就被”生“出来了,从 kubectl 的详细日志中也可以看到 POST/GET 等请求的信息。​​​​​​

I1127 14:55:06.886901   83798 round_trippers.go:463] GET http://127.0.0.1:60649/77046cfbc5f80b52d9a1501954ee0672/api/v1/namespaces/default/pods?limit=500
I1127 14:55:06.886916   83798 round_trippers.go:469] Request Headers:
I1127 14:55:06.886921   83798 round_trippers.go:473]     User-Agent: kubectl...
I1127 14:55:07.166333   83798 round_trippers.go:580]     Cache-Control: no-cache, private

可以看到,在创建完成 Pod 之后,实际的 Pod 比我们在 Yaml 中声明的字段更多,这些多出来的字段就是 Pod 从出生后的经历证明:由调度器调度到集群可用节点、交给 kubelet 管理 Pod 生命周期、分配网络IP、挂载临时存储、容器运行时拉取镜像启动容器、控制器协调校正运行状态等等。

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  namespace: default
status:
  phase: Running
  hostIP: 10.....
  podIP: 10.....
  conditions:
    - type: Initialized
      status: 'True'
      lastProbeTime: null
      lastTransitionTime: '2023-11-27T06:59:13Z'
    - type: Ready
      status: 'True'
      lastProbeTime: null
    .....
spec:
  volumes:
    - name: kube-api-access-72rkq
      ......
  containers:
    - name: web
      image: nginx
      ports:
        - name: web
          containerPort: 80
          protocol: TCP
      resources: {}
      volumeMounts:
        - name: kube-api-access-72rkq
          readOnly: true
          mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      terminationMessagePath: /dev/termination-log
      terminationMessagePolicy: File
      imagePullPolicy: Always
  restartPolicy: Always
  terminationGracePeriodSeconds: 30
  dnsPolicy: ClusterFirst
  serviceAccountName: default
  serviceAccount: default
  securityContext: {}
  schedulerName: default-scheduler
  tolerations:
    - key: node.kubernetes.io/not-ready
      operator: Exists
      effect: NoExecute
      tolerationSeconds: 300
    - key: node.kubernetes.io/unreachable
      operator: Exists
      effect: NoExecute
      tolerationSeconds: 300
  priority: 0
  enableServiceLinks: true
  preemptionPolicy: PreemptLowerPriority

展开来看,运行一个容器,必要的就是3大件:计算、网络、存储。

  1. 计算资源,就是 CPU/Mem/GPU,是在 spec 的 container 部分声明,这个案例中没有设置到底需要多少 resources,requests 和 resources.limits 为空,也就是说可能占满整个宿主机,这种情况一般叫 Best-Effort QoS 级别,调度优先级是比较低的。

  2. 实际情况下一般会设置合理的 requests/limits,达到 Burstable QoS 级别或者设置 requests、limits 一模一样达到 Guarantee 级别。这个 Pod 经过调度器调度到某个节点之后,就会交给一个叫 CRI(Container Runtime Interface)的接口,让 CRI 的实现来把容器真正建出来,通常是 containerd,cri-o,podman, docker 等等。

  3. 网络方面,可以看到在 status 里面,多出了 PodIP 字段,这个是调用底层一个叫 CNI(Container Network Interface)的接口,让 CNI 的实现层给出的IP,这个过程比较复杂,涉及到一个叫 pause 容器的东西,入门的时候可以忽略这些细节。

  4. 存储方面,可以看到自动挂载了一个 volume/volumeMounts,这是对 Pod 挂载的额外存储,可能是配置文件或密钥,也可能是挂载一些云厂商提供的持久化存储,比如 EBS、EFS 盘,则会涉及到 K8S 第三类底层接口,CSI(Container Storage Interface,我们用的云厂商的 Kubernetes 发行版本一般都已经内置了CSI的实现。

有了计算网络存储,Pod 就运行起来了,如果我们要更新 Pod,可以用 Update, Patch 接口,但是,Pod是一个 Kubernetes 的原子资源,只能更新极少数字段,比如 image 和 readinessGate。如果想结束掉这个 Pod,可以用 Delete 接口,来让 Pod 进入 Terminating 状态,最终被控制器删除,回收掉计算资源,容器镜像文件最终也会被 GC 掉。

这里只是讲了最浅显的流程,详细的 Pod 生命周期可以参考这里:https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/,尤其是一些和业务强相关的生命周期活动,比如 postStart 并行的启动 hook,preStop 串行的停止 hook,发送 SIGTERM 尝试结束进程,过了 Graceful Period 后发送 SIGKILL 信号等等,对业务很有用。

Kubernetes 集群视角的计算、网络、存储

至此,我们明白了计算网络存储资源,如何赋予到 Pod 这个载体上。那么,计算、网络、存储的资源池本身,在 Kubernetes 里叫什么?Kubernetes 集群的计算节点叫 Node,和传统云平台对硬件机器的定义不同,Node 也是抽象的资源,可以长出来,也可以死掉,不和运行哪个容器直接绑定,而是通过 label/selector,affinity 等调度相关的机制关联到 Pod。Kubernetes 集群的块存储资源叫 PersistentVolume,实际使用场景下,一般是用分布式文件系统来实现,根据业务的磁盘请求,自动创建 PersistentVolume 的东西叫 StorageClass。

  • Kubernetes 的网络是分好几层的:让整个集群变成一个大内网的 Pod 网络;让集群内服务互相访问自带 L4 负载均衡的 Service 网络,以及做精细流量治理的 L7 Ingress/GatewayAPI/ServiceMesh 网络。还有控制网络访问策略的 NetworkPolicy 资源。
  • 首先我们看 Pod 网络,虽然不同 CNI 实现的网络原理差别巨大,但目的都是一样的,给每个 Pod 分配IP,并打通和其他 Pod 之间的路由。比如 AWS 就用了一个很讨巧的方式实现,直接给 Pod 分配当前 VPC-Subnet 的二级IP,DHCP和路由表都是复用的,Pod 之间和现有 EC2 节点之间的通信方式完全一样。
  • 再来看作为内部L4负载均衡器的 Service 网络,给每个 K8S services 资源分配一个虚拟 IP(ClusterIP)。ClusterIP 分配后,kube-proxy 组件负责来实现这个虚拟IP的路由的创建和 Pod Endpoint 变化的实时校正。

还是以 AWS EKS 为例,EKS 默认使用的 iptables 模式,kube-proxy 会在每个节点上把每个 ClusterIP Service 的 IP 写入 iptables,用 iptables 命令可以看到实现细节。

  • 由于每次变更导致的 iptables 修改,大规模集群用 K8S 内置的 Service 负载均衡是存在性能问题的,切换到 ipvs 模式可以解决;

  • 还有一种没有 ClusterIP 的 Headless Service,借助 DNS 实现了端点自动发现,不是常规的 L4 负载均衡;

  • 如果需要直接把某个 Service 暴露到公网去,还有 NodePort/LoadBalancer 类型的 Service,kube-proxy 会在集群每个节点 listen NodePort 端口,iptables写入 NodePort 对应的 DNAT 规则;

  • LoadBalancer / NodePort类型的Service还有一个关键字段“externalTrafficPolicy”,简单理解是跨节点负载均衡模式还是本地节点直连模式,跨节点负载均衡还会引起外部 LB 的健康检查失效问题以及内部服务无法获取 Client IP 问题,这些都是平台方需要处理好的,不需要业务方关心,业务团队记住一个原则,永远不要用 NodePort Service 就行。

# https://zhuanlan.zhihu.com/p/196393839
iptables -L -n
# Chain OUTPUT (policy ACCEPT)
# target     prot opt source               destination         
# KUBE-PROXY-FIREWALL  all  --  anywhere             anywhere             ctstate NEW /* kubernetes load balancer firewall */
# KUBE-SERVICES  all  --  anywhere             anywhere             ctstate NEW /* kubernetes service portals */
# KUBE-FIREWALL  all  --  anywhere             anywhere  

iptables -L -t nat
#Chain KUBE-SVC-TCOU7JCQXEZGVUNU (1 references)
#target     prot opt source               destination         
#KUBE-SEP-HI2KQBDGYW5OVKWN  all  --  anywhere             anywhere             /* kube-system/kube-dns:dns -> 10.52.xx.xx:53 */ statistic mode random probability 0.50000000000

#KUBE-SEP-XQT5TF2PMBOMEGDC  all  --  anywhere             anywhere             /* kube-system/kube-dns:dns -> 10.52.xx.xx:53 */

最后了解一下L7服务网络,一般由类似 Nginx Ingress/Envoy 之类的应用流量负载均衡器提供,对于业务来说,就当把 nginx conf 拆成一个一个 yaml 片段就好。Ingress 直管南北向流量,而 Service Mesh 把东西南边向流量全托管了,也能实现一些比 Nginx conf 里面更复杂的行为,比如流量加密、鉴权、故障注入、熔断降级、重试等等。

其他原生资源要么是对 Pod 套娃、要么是打辅助的

Kubernetes 的 API 设计非常符合单一职责原则(SRP),Pod 就是一个包容器的单纯的豆荚,delete 了就没了。但是,你要跑服务怎么办?没关系,单一职责原则下,实现新功能就是一个套娃,Kubernetes 抽象了一个叫 Deployment 套住一个叫 ReplicaSet的东西,ReplicaSet 再来套住 Pod。这样,Deployment 只管变更处理轮换 RS,RS 只管保证有 n 个 pod 在跑,Pod 没了再生,死了重启,这里也体现了 Erlang 典型的 let it crash 思维。哪天你说要训练一个AI模型干掉 OpenAI,Kubernetes 给你提供了一个叫 CronJob和 Job 的抽象,CronJob 套住了 Job,Job 又套住了 Pod。Job 只管一次性 run 的东西,要重试几次,跑完多久删 Pod 这些事,Cronjob则是一个天然的分布式cron,只管定时把 job 这个东西生出来。CronJob 和 Job 广泛用在大数据处理管线、CICD管线,AI训练这些领域。OpenAI 也是一个包含 8000 多个节点的巨大Kubernetes 集群训练出来的。哪天你又想用 Kubernetes 运行一个数据库,恭喜头铁的你,学到了最复杂的一种原生资源,StatefulSet,deployment是把豆荚当牲畜,想杀就杀,StatefulSet 是把Pod 当宠物,宠物不好养的,每个 Pod 都不能随便动,更新的时候只能按序一个一个更新。除了这些对 Pod 套娃的资源,剩下的可以理解成打辅助的,比如把流量引入集群的Ingress,内部流量负载均衡的 Service,给每个Pod提供的分布式配置ConfigMap、分布式密钥存储 Secret。还有一些策略控制和资源限额的辅助类,这里不一一展开了。

重新思考 Kubernetes 是什么

到这里,我们大概搞清楚了 Kubernetes 对于使用者来说意味着什么。从Kubernetes 自身的组件视图来看包括这些东西:

  • 每个机器装一个叫 kubelet 的 Agent,控制这台机器运行什么

  • 每个机器装一个 kube proxy 的东西用来托管网络防火墙规则,并装一个 CNI 的实现,控制集群内部的 Pod IP 分配和网络路由

  • 可选的,装一个CSI的实现,接管持久化存储盘的创建和挂载

  • 这些东西都听 API Server + Controller Manager + Scheduler 组成的控制中心,这套控制中心暴露一套标准的可扩展的 REST API,数据全部存到了 ETCD 元数据集群里。让我们操作分布式集群,再也不用撸 shell 命令,一切命令都 API 化,一切资源都变成了 ETCD 的数据记录。

了解了这些,也就明白了 Kubernetes 本质上是对现有技术的封装,形成了一套云资源操作系统,真正干活的还是服务器上的进程而已,真正对资源做隔离的还是 cgroup 和 namespace 这些 linux 内核原有的东西。了解了这些,也就明白了,为什么 Kubernetes 挂了不影响正在跑的服务?为什么在Kubernetes 集群做应用性能调优,还是去看 EC2 用什么 instance type,PV存储是哪一代的 EBS、EFS,还是去看 Subnet 内核 VPC 之间怎么优化 RTT 延迟和提升带宽?Kubernetes 包含了分布式集群的一切,Kubernetes 又一无所有。

Kubernetes 的 A/B 面

  • Kubernetes 带来的最大的几个好处,分别是标准化、弹性、可扩展。REST API 带来的管理界面完全标准化,存个 ETCD 记录就创建或校正资源状态带来了极致弹性,扩缩容就在弹指之间。开发一个自定义资源就实现任意功能带来了丰富的可扩展性,演化出庞大的 CloudNative 生态系统。既要又要还要,标准,弹性,可扩展,都有了,看起来很完美,但任何事物都有两面,Kubernetes 的这些好处,也是坏处的根源。标准化的暗面是复杂化。标准要考虑到所有情况,所以这个标准不可能简单,仅仅是一个Pod的spec,就有几十个字段,可能一小半字段大部分人都没有见过。
  • Kubernetes 学透的难度和自建运维难度,从培训考证机构可见一斑。即使是大公司,也最好不要有自建 Kubernetes 的念头,Kubernetes 自己的几个组件每个都有上百个启动参数,要么是不懂坑有多大的年轻人,要么是假装整明白的人,要么是身在云厂商里面真正懂的人。弹性的暗面是易失性,连集群的 Node 能随时长出、随时消逝,带来了对应用架构的侵入,在 Kubernetes 中运行的有状态服务必须具备动态发现的能力,想在代码中配置静态IP的时代结束了。我还发现一个 Kubernetes 带来的效应,”日志丢失焦虑“,在VM上大概没有人会担心日志没采集到,Kubernetes上Pod飘来飘去,总有人问,服务挂掉前的最后一行日志怎么采的,采不到怎么办?
  • 可扩展的暗面是良莠不齐,在CNCF Landscape和开源社区的并不是都是优秀的产品,甚至有些问题很大的东西也流行起来。比如之前团队有位同事仔细读过 Clickhouse operator 代码,这个1.5K star的项目,代码质量可能在及格线以下。4年前和另一位同事尝试用 ES Operator 和 Kafka Helm Chart 来运维 ES/Kafka,当时成熟度还远没有达到生产可用的水平。另外,Helm 这个 Kubernetes 最流行的包管理工具,也是”worse is better“的典型代表,Helm 作者哲学家 Matt Butcher 提出 OAM 思想后,自己去搞”下一代云计算”WASM生态去了
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐