Kubernetes 入门:从 Pod 到 Deployment
很多人第一次接触 Kubernetes(以下简称 K8s)时,都会被它那张密密麻麻的架构图劝退:API Server、etcd、Controller Manager、Scheduler、kubelet、kube-proxy……名词一个接一个,还没动手就先懵了。
实际上,日常写应用、跑服务的开发者,真正需要打交道的对象就那么几个:Pod、Service、Deployment、ConfigMap、Secret、Ingress。把这些吃透,剩下的细节用的时候再查文档也来得及。
这篇文章不讲架构图,从最核心的 Pod 说起,一步步讲到 Deployment,用最小的 YAML 让你跑起第一个真正”能自愈”的服务。
一、Pod:最小的调度单位
Pod 是 K8s 里最小的、可以创建和调度的单元。一个 Pod 里可以有一个或多个容器,这些容器共享网络命名空间和存储卷。
关键点在于:你几乎从不直接创建 Pod。生产环境里 Pod 通常是被 Deployment、StatefulSet 或 DaemonSet 这类控制器管起来的。但理解 Pod 是理解一切的前提,所以先看一个最小的 Pod 定义:
apiVersion: v1
kind: Pod
metadata:
name: hello-pod
labels:
app: hello
spec:
containers:
- name: hello
image: nginx:1.25
ports:
- containerPort: 80
几个容易忽略的字段说明一下:
labels是给 Pod 打的标签。K8s 的很多机制(Service 选择后端、Deployment 管理副本)都靠标签来”认领”对象,标签设计得好不好,直接决定后续维护是否顺畅。containerPort只起声明作用,不真正暴露端口。要让外部能访问,得靠 Service 或者直接kubectl port-forward。
用 kubectl apply -f pod.yaml 就能创建它。不过要是这个 Pod 挂了,它不会自动重启,因为没人”负责”它。这正是控制器登场的原因。
二、Deployment:让副本可以自愈
直接管理 Pod 有几个明显的坑:删了不会重建、更新要手动换镜像、副本多了少了要自己调。Deployment 解决的就是这些问题——你声明”我想要 3 个副本跑这个镜像”,剩下的交给它。
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-deploy
spec:
replicas: 3
selector:
matchLabels:
app: hello
template:
metadata:
labels:
app: hello
spec:
containers:
- name: hello
image: nginx:1.25
ports:
- containerPort: 80
这段 YAML 里有两个地方要特别留意,也是新手最容易翻车的:
-
selector.matchLabels和template.metadata.labels必须对得上。 Deployment 通过 selector 找到自己管理的 Pod,而 Pod 又是从 template 生成出来的。如果两边标签不一致,apply 的时候 apiserver 会直接报错拒绝。 -
template里的内容本质上就是一个 Pod 的定义。 你把第一个例子里 Pod 的spec原样搬进来即可,区别只是少了apiVersion、kind、name这几行元信息。
Deployment 底下其实是 ReplicaSet(简称 RS)在干活。每做一次滚动更新,Deployment 会新建一个 RS,老的 RS 逐步缩到零。所以你会看到 kubectl get rs 里有好几个名字带哈希后缀的对象,这是正常的。
滚动更新与回滚
更新镜像只需要改 image 字段然后重新 apply:
kubectl set image deployment/hello-deploy hello=nginx:1.26
kubectl rollout status deployment/hello-deploy
默认策略是滚动更新:逐步用新 Pod 替换旧 Pod,保证过程中始终有副本在提供服务。如果新版有问题,可以快速回滚:
kubectl rollout undo deployment/hello-deploy
rollout undo 回退到上一个版本,靠的是 Deployment 保留的历史 ReplicaSet。这也解释了为什么更新频繁时 get rs 会冒出一堆旧对象。
三、资源限制与探针:两个不能省的东西
跑起来只是第一步,能不能稳定跑,取决于你有没有配资源限制和健康检查。
资源限制
不给 Pod 设资源,等于告诉集群”这个容器想吃多少吃多少”。一旦某个容器内存泄漏,可能把整个节点拖垮,连带同节点的其他服务一起遭殃。
spec:
containers:
- name: hello
image: nginx:1.25
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
requests 是调度器做决策的依据,保证节点上有足够资源才把 Pod 放上去;limits 是硬上限。内存超过 limit 会被 OOM 杀掉;CPU 超了不会被杀,只会被限流。这两个值一天不合适,节点的资源利用率就很难看。
就绪与存活探针
只靠”容器进程在不在”判断服务是否健康,是远远不够的。进程可能没退,但应用已经起不来了。这时候需要探针:
spec:
containers:
- name: hello
image: nginx:1.25
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 10
periodSeconds: 10
两个探针的分工不一样:
- readinessProbe(就绪探针):判断容器能不能接流量。失败时 Service 会把流量从这个 Pod 摘掉,但不会重启它。
- livenessProbe(存活探针):判断容器要不要重启。失败会触发容器重建,适合用来补救卡死的进程。
新手常见错误是把 liveness 探针指向一个依赖外部资源(比如数据库)的接口,一旦数据库抖动,探针集体失败触发重启风暴,反而把服务搞得更糟。记住一个原则:liveness 只测”进程自己是不是废了”,readiness 才测”现在能不能接活”。
四、让外部访问:Service 兜底
Pod 的 IP 是会变的——Pod 一重建,IP 就换。所以不能用 IP 直连,得用 Service 提供一个稳定的入口,并负责负载均衡:
apiVersion: v1
kind: Service
metadata:
name: hello-svc
spec:
selector:
app: hello
ports:
- port: 80
targetPort: 80
type: ClusterIP
selector 会选中所有 app: hello 的 Pod,作为后端。Service 拿到的是一个固定的 ClusterIP,集群内部通过这个名字就能访问到健康的副本,Pod 再怎么重建也不受影响。
默认的 ClusterIP 只在集群内部可达。要做到从外部访问,有几种常见做法:
- NodePort:在每个节点上开一个固定端口,把流量转给 Service。简单,但端口范围有限,生产环境不太推荐直接暴露。
- LoadBalancer:依赖云厂商分配一个外部负载均衡器,云上部署最常用。
- Ingress:做七层路由,按域名、路径把请求分发给不同的 Service,一个入口管多个服务。
对于自建集群或者本机调试,最快的方式其实是 kubectl port-forward,把 Service 端口映射到本地:
kubectl port-forward service/hello-svc 8080:80
浏览器打开 localhost:8080 就能看到 Nginx 页面了。
五、小结
把这套串起来,一个”像样”的服务应该具备这几个要素:
- 用 Deployment 声明副本数和滚动更新策略,让它自愈、可回滚;
- 在 template 里配好 resources 的 requests/limits,别让单个容器拖垮节点;
- 用 readiness/liveness 探针 区分”能不能接流量”和”要不要重启”;
- 用 Service 提供稳定入口,屏蔽 Pod IP 的漂移。
Pod → Deployment → Service,这三层走通,K8s 在你手里就不再是一堆抽象名词,而是一个能真正扛住线上流量的运行平台。下一步再去啃 ConfigMap、Secret 和 Ingress,把配置和外部流量也理顺,思路会顺畅很多。
刚开始接触,别急着背架构图和那两百多个 API 对象。先跑起来,遇到问题再查,比一上来啃全书有效得多。