前言
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大件:计算、网络、存储。
- 计算资源,就是 CPU/Mem/GPU,是在 spec 的 container 部分声明,这个案例中没有设置到底需要多少 resources,requests 和 resources.limits 为空,也就是说可能占满整个宿主机,这种情况一般叫 Best-Effort QoS 级别,调度优先级是比较低的。
- 实际情况下一般会设置合理的 requests/limits,达到 Burstable QoS 级别或者设置 requests、limits 一模一样达到 Guarantee 级别。这个 Pod 经过调度器调度到某个节点之后,就会交给一个叫 CRI(Container Runtime Interface)的接口,让 CRI 的实现来把容器真正建出来,通常是 containerd,cri-o,podman, docker 等等。
- 网络方面,可以看到在 status 里面,多出了 PodIP 字段,这个是调用底层一个叫 CNI(Container Network Interface)的接口,让 CNI 的实现层给出的IP,这个过程比较复杂,涉及到一个叫 pause 容器的东西,入门的时候可以忽略这些细节。
- 存储方面,可以看到自动挂载了一个 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生态去了