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 信号等等,对业务很有用。
至此,我们明白了计算网络存储资源,如何赋予到 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 里面更复杂的行为,比如流量加密、鉴权、故障注入、熔断降级、重试等等。
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 自身的组件视图来看包括这些东西:
每个机器装一个叫 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 带来的最大的几个好处,分别是标准化、弹性、可扩展。
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生态去了。
复杂,是成长的代价。
来源:https://code2life.top/2023/11/24/0074-k8s-in-30-min/