Docker 已经会用,一上集群就懵?这一页负责把两边接上
在单台机器上,docker run 加一个 Compose 文件,能解决绝大多数问题。真正逼你上集群的只有四件事,而且全都发生在「机器不止一台」这个前提下。
--restart=always 只能把进程在同一台机器上拉起来。机器本身挂了、或者容器被 OOM 杀掉需要换台机器重启,它就无能为力了。
三台机器,哪台 CPU 有空、哪台内存够、哪台已经跑满?靠人记、靠脚本判,规模一上来必然失控。
Compose 换镜像要「停旧的、起新的」,中间那几秒服务是空的。要做到不停机滚动发布,得自己写编排逻辑。
docker run 是「做一件事」,做完就结束。你真正想要的是「这个服务始终保持 3 个副本在跑」——需要有人持续把现实拉回这个目标。
| 单机 Docker 的痛点 | K8s 的对应机制 |
|---|---|
| 容器挂了不自愈 | 控制器持续 reconcile + 探针;写了 replicas: 3 就永远朝 3 收敛,节点失联也能在别的节点重建 |
| 不知道该放哪台机器 | Scheduler 按容器声明的资源请求量与亲和/污点约束自动选节点 |
| 更新中断 | Deployment 滚动更新(maxSurge / maxUnavailable)+ 就绪探针,新旧实例交接不停机 |
| 命令式、无收敛 | 声明式 API:kubectl apply 提交「期望状态」,控制器负责把它变成现实 |
真正让人卡住的往往是这一层:你在 Docker 里熟悉的东西,在 K8s 里不仅换了名字,有时还被拆成了两个对象。
| 你在 Docker 里用的 | K8s 里的对应 | 差别要点 |
|---|---|---|
docker run -d nginx | Pod(由 Deployment 等控制器创建) | 你不直接跑容器,而是把 Pod 模板交给控制器,由它来生成 |
-p 8080:80 | Service(ClusterIP / NodePort / LoadBalancer) | 「暴露端口」这件事从容器身上挪到了 Service 上 |
-v / volumes: | PV + PVC(+ StorageClass) | 存储资源(PV)与使用申请(PVC)被拆开,便于统一管理 |
--env / env_file: | ConfigMap(普通配置)/ Secret(敏感) | 配置成为独立对象,可单独更新、被多个工作负载共用 |
depends_on | initContainer / readinessProbe | 不去「等依赖起来」,而是等自己真的能服务了再接流量 |
--restart=always | 控制器的 replicas + 探针 | 自愈发生在集群层面,能跨节点重建 |
docker network create | CNI 插件 + Service + NetworkPolicy | 网络是集群基础设施,不是 compose 文件里的一行声明 |
docker ps / logs / exec | kubectl get / logs / exec | 命令换了名,心智模型基本一致,上手最快 |
docker build 没有对应物。K8s 不管构建镜像——镜像是你在外面构建好、推到镜像仓库,集群只负责「拉下来跑」。这条链路属于 CI/CD,见 Docker CI/CD 流水线。
docker compose up -d 也不是一个对象。一个 Compose 文件里的多个服务,在 K8s 里会变成多个 Deployment + 多个 Service,靠标签(label)互相发现。
「容器名」不存在了。Pod 名由控制器生成(形如 web-7d9f8c6b4-x2k9p),每次重建都会变。所以 K8s 里的服务发现必须走 Service 名 + DNS,不能靠记 IP 或容器名。
没有 --rm 这种「用完消失」的语义。Pod 由控制器托管,删掉会被重建;想真的一次性任务,用 Job。
K8s 里没有「容器」这个最小调度单位。取而代之的是 Pod:一个或多个共享网络命名空间与存储卷的容器集合。它是调度、扩缩、重建的最小颗粒。
因为有些进程天生就该待在一起、共享同一个 localhost:主应用和它的日志采集 sidecar、或者必须读写同一份本地文件的辅助进程。同一个 Pod 内的容器共享 IP 和端口空间,直接 localhost 互访——这在 Docker 里得靠同一网络加 --network container: 才勉强做到。
但请记住:单容器 Pod 才是常态。绝大多数业务就是一个 Pod 一个容器,不必为了「用上这个特性」硬塞 sidecar。
# 快速试一个 Pod(学习 / 验证用;生产不要这么写)
kubectl run demo --image=nginx:1.27-alpine
kubectl get pods
kubectl get pod demo -o wide # 额外看到所在节点与 Pod IP
kubectl describe pod demo
kubectl logs demo
kubectl exec -it demo -- sh
# 用完删掉
kubectl delete pod demo
对应的声明式写法是一个 kind: Pod 的 YAML。注意这里只是演示 Pod 本身长什么样,生产里几乎不会这么写——你会写 Deployment(见第 3 节):
apiVersion: v1
kind: Pod
metadata:
name: demo
spec:
containers:
- name: web
image: nginx:1.27-alpine
ports:
- containerPort: 80
kubectl apply -f pod.yaml
K8s 上的排障,九成时间花在这四个命令上。顺序也很重要——从外往内一层层看。
| 命令 | 看什么 | 什么时候用 |
|---|---|---|
| kubectl get pods | STATUS 列:Running / Pending / CrashLoopBackOff / ImagePullBackOff;RESTARTS 次数 |
第一步永远是它。先确认「是不是真没起来」,以及卡在哪个阶段——STATUS 已经把方向指出来了 |
| kubectl describe pod <name> | 底部的 Events 区:调度失败、拉镜像失败、探针失败、被 OOMKilled | 状态不对劲时的第二站,80% 的答案在这里。Events 是 kubelet 和调度器留下的第一手说明 |
| kubectl logs <name> | 容器 stdout / stderr。加 -f 跟随,--previous 看上一轮崩溃的日志,-c 指定容器 |
容器起来了但行为不对。如果一直在重启,务必用 --previous 看它上一次为什么死 |
| kubectl exec -it <name> -- sh | 进容器内部:看文件、测网络连通、跑诊断命令 | 前三步都看不出问题时。相当于 Docker 里的 docker exec |
kubectl edit pod 改那些不可变字段会被直接拒绝;就算改成功了,下一次重建也全都丢掉。真正该改的是控制器(Deployment / StatefulSet),由它来管理 Pod。请养成习惯:碰 Deployment,不碰 Pod。你不会直接管 Pod,而是选一个控制器来管。Deployment / StatefulSet / DaemonSet 这三者覆盖了绝大多数场景——选错类型是新手最常见的结构性错误,因为它后期很难改。
| 控制器 | 管什么样的负载 | 关键特征 | 典型例子 |
|---|---|---|---|
Deployment |
无状态、副本可随意替换 | Pod 名随机、可互换;天然支持滚动更新与一键回滚 | Web 前端、API 服务、无状态 worker |
StatefulSet |
有状态、身份不能乱 | Pod 名固定且有序(db-0 / db-1)、每个 Pod 有自己的稳定存储、按顺序启停 |
MySQL / PostgreSQL 主从、Redis 集群、Kafka、etcd |
DaemonSet |
每个节点都要跑一个 | 新节点加入自动补一个、节点移除自动回收 | 日志采集(Fluent Bit)、监控 agent(node-exporter)、CNI 插件、kube-proxy |
Job / CronJob |
跑完就该结束 / 定时跑 | 完成即退出,不会被重建;CronJob 按 cron 表达式调度 | 数据迁移、批处理、定期备份、清理任务 |
# 换镜像即触发滚动更新(无需手写新 YAML)
kubectl set image deployment/web web=nginx:1.27.1-alpine
kubectl rollout status deployment/web # 盯着滚动过程,卡住会告诉你卡在哪
kubectl rollout history deployment/web # 看历史版本
# 出问题了,一条命令回滚
kubectl rollout undo deployment/web
kubectl rollout undo deployment/web --to-revision=2 # 回滚到指定版本
注意「更新」和「回滚」在 K8s 里是同一套机制:都是改期望状态、然后由控制器滚动过去。所以回滚不需要备份、不需要手工切流量,速度和新发布一样。
YAML 里的 replicas 是你写死的固定副本数。它之上还有一层 HPA(Horizontal Pod Autoscaler),能按 CPU / 内存使用率自动增减副本。三个容易踩的点:
| 要点 | 说明 |
|---|---|
| HPA 不是装好就有的 | 需要集群里先有 metrics-server,否则 HPA 拿不到 CPU / 内存数据,只会一直「unknown」 |
别和 HPA 抢 replicas | 同时写死 replicas 又挂 HPA,两边会互相覆盖。交给 HPA 管的话,就把 replicas 从 YAML 里去掉 |
| HPA 只能「加副本」 | 按副本数横向扩。如果单个 Pod 就吃满一张 GPU 的显存,加副本也救不了——那不是 HPA 的职责范围 |
先回答一个必问的问题:为什么不能直接访问 Pod?因为 Pod 是「一次性」的——它重建一次就换一个 IP。你没法把变化的 IP 写进配置里,也没法做负载均衡。
所以 K8s 引入了 Service:一组 Pod 的稳定虚拟 IP + 固定 DNS 名。后端 Pod 怎么换、换多少,都跟调用方无关——这一点和 Docker 里「容器 IP 会变」是同一类问题的同一类解法。
| 方式 | 谁访问得到 | 典型用途 | 注意 |
|---|---|---|---|
ClusterIP(默认) |
只有集群内部 | 服务之间互访 | 本机调试用 kubectl port-forward 临时打通,不要为了「能在外面访问」就改成 NodePort |
NodePort |
集群外,通过「任意节点 IP : 30000-32767」 | 简单暴露、测试环境 | 端口段是固定的;入口落在某个具体节点上,那个节点挂了入口就断 |
LoadBalancer |
集群外,云厂商提供的公网 / 内网负载均衡 | 生产对外暴露 | 依赖云环境(ACK / TKE 的 SLB / CLB);每暴露一个服务就要一个 LB,成本不低 |
Ingress |
集群外,HTTP / HTTPS 七层路由 | 多域名 / 多路径共用一个入口 | 需要先装 Ingress Controller;它本身不是负载均衡器,而是「路由规则」 |
如果你已经理解「Caddy / nginx 在前面收 HTTPS,再按域名或路径转发到后面不同的服务」,那你已经理解 Ingress 了。K8s 只是把这套配置变成了集群里的对象:Ingress 写路由规则,Ingress Controller(ingress-nginx / Traefik / Caddy 等)负责真正执行。
# Ingress:把 api.example.com 转发到后端 Service api
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
spec:
rules:
- host: dockerlab.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
Compose 里「写几个环境变量、挂两个目录」的事,在 K8s 里对应的是一套对象体系。先看对照,再看具体写法。
| Compose 里 | K8s 里 | 说明 |
|---|---|---|
environment: 明文变量 | ConfigMap + envFrom / valueFrom | 普通配置,可以单独更新、被多个工作负载共用 |
env_file: .env | ConfigMap(整文件挂载,或作为键值注入) | 也可以直接 kubectl create configmap --from-file 从文件生成 |
| 敏感变量(数据库密码等) | Secret | 见下方警示框:base64 不等于加密 |
./data:/app/data | emptyDir / hostPath(本地)/ PVC(网络存储) | 生产用 PVC。用 hostPath 会把 Pod 绑死在某台节点上,失去调度自由度 |
具名卷 volumes: dbdata: | PVC + StorageClass(动态供给) | StorageClass 决定底层用哪家存储:云盘 / NAS / Ceph / 本地盘 |
apiVersion: v1
kind: ConfigMap
metadata:
name: web-config
data:
APP_ENV: production
nginx.conf: |
server {
listen 80;
location / { root /usr/share/nginx/html; }
}
---
apiVersion: v1
kind: Secret
metadata:
name: db-credential
type: Opaque
stringData: # stringData 会自动 base64,省去手工编码
MYSQL_ROOT_PASSWORD: change-me
spec:
containers:
- name: web
image: nginx:1.27-alpine
envFrom:
- configMapRef: # 整个 ConfigMap 变成环境变量
name: web-config
env:
- name: MYSQL_ROOT_PASSWORD # 单个键注入,更可控
valueFrom:
secretKeyRef:
name: db-credential
key: MYSQL_ROOT_PASSWORD
volumeMounts:
- name: conf
mountPath: /etc/nginx/conf.d
volumes:
- name: conf
configMap:
name: web-config
kubectl get secret db-credential -o yaml,一行 | base64 -d 就能解出明文。它挡住的是「不小心把密码写进 YAML 提交到 Git」,不是「防住有权限的人」。前面都是概念,这一节把它们串成一条能真跑起来的链路。假设你本地已经有一个构建好的镜像,并且推到了镜像仓库(下面用 nginx:1.27-alpine 代表示例,换成你自己的镜像即可)。
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
labels:
app: web
spec:
replicas: 3
selector:
matchLabels:
app: web
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 最多多起 1 个
maxUnavailable: 0 # 一个都不许先停 → 真正零中断
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.27-alpine
ports:
- containerPort: 80
resources:
requests: # 调度依据:Scheduler 按这个挑节点
cpu: 100m
memory: 64Mi
limits: # 上限:超了会被 OOMKilled / 限流
cpu: 500m
memory: 256Mi
readinessProbe: # 就绪才接流量
httpGet:
path: /
port: 80
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe: # 不活了就重启
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web # 靠标签找到上面那组 Pod
ports:
- port: 80 # Service 自己的端口
targetPort: 80 # 容器里的端口
type: ClusterIP
readinessProbe 失败 → 从 Service 里摘掉、不接流量,但不重启(适合「临时忙不过来」);livenessProbe 失败 → 重启容器(适合「已经卡死」)。配反了会导致服务被反复重启。kubectl apply -f web.yaml
kubectl get pods -l app=web -o wide
kubectl get svc web
kubectl describe deployment web
kubectl rollout status deployment/web
# 本地打开 http://localhost:8080 看效果
kubectl port-forward svc/web 8080:80
# 换镜像版本 → 自动滚动更新(maxUnavailable: 0,全程不停机)
kubectl set image deployment/web web=nginx:1.27.1-alpine
kubectl rollout status deployment/web
# 假设新版本有问题,一条命令回到上一版
kubectl rollout undo deployment/web
kubectl rollout status deployment/web
# 用完清理
kubectl delete -f web.yaml
nginx:1.27-alpine 请换成你自己的镜像,并且钉明确的版本号。用 latest 会让「当前跑的是哪个版本」变成一个无法回答的问题——回滚时你不知道该回到哪,出问题也复现不了。镜像标签策略详见 Docker CI/CD 流水线。K8s 不必先买三台服务器。本地就能起一个真集群,把上面那套 YAML 跑通。三种方案各有明确适用面。
| 方案 | 本质 | 起停速度 | 最适合 |
|---|---|---|---|
kind |
Kubernetes IN Docker:把 K8s 节点跑在 Docker 容器里 | 快(数十秒) | 本地验证 YAML、CI 里跑集成测试、需要多节点 |
minikube |
单节点集群 + 丰富的 driver 与 addons | 中 | 新手学习、需要可视化面板和 ingress / metrics-server 等插件 |
k3s |
CNCF 认证的轻量发行版,可直接用于生产 | 快 | 小服务器、边缘 / IoT、资源受限环境、单机生产 |
前提是本机已经有 Docker(kind 自己没有运行时)。起得快、销毁彻底,是本地反复试 YAML 的首选。
kind create cluster --name demo
kubectl cluster-info --context kind-demo
kind delete cluster --name demo
driver 支持面最广(docker / podman / hyperkit / virtualbox / none),自带 dashboard 和一堆 addon。代价是默认资源占用比 kind 略高。
minikube start --cpus=2 --memory=4096
minikube dashboard # 打开可视化面板
minikube addons enable ingress
minikube delete
单个二进制,把 containerd、Traefik、ServiceLB、本地存储全打了进去。它不只是练手工具——小服务器、边缘、IoT 生产环境用它的很多。
# 先下载看清楚再执行,不要直接 curl | sh
curl -sfL https://get.k3s.io -o install.sh
less install.sh
sudo sh install.sh
kubectl get nodes # k3s 会自动写好 kubeconfig
sudo k3s-uninstall.sh
k3s 二进制放到目标目录,设置 INSTALL_K3S_SKIP_DOWNLOAD=true,再准备一个离线镜像目录即可,完全不依赖 get.k3s.io。内网环境(很多企业的 GPU 服务器就是这种)靠这套流程完全可行。这一节可能是全页最该读的一节。网上讲 K8s 的文章几乎都在教「怎么用」,很少有人讲「什么时候别用」——但选型的失误,代价比用错一个命令大得多。
| 你的实际情况 | 建议 |
|---|---|
| 一台服务器、几个服务 | 用 Compose。够用、够简单、够省心 |
| 两三台机器、服务不多、能容忍分钟级中断 | Compose + 反向代理就够;或者考虑托管容器服务(免去自己编排) |
| 需要多机调度、自动扩缩、滚动发布,且有人维护 | 上 K8s,优先上托管版本 |
| 团队没有专职运维,也没人愿意学 | 先别上。K8s 的隐性成本会吃掉它带来的收益 |
上了 K8s 之后,下面这些「配套工程」一件都不会少:
etcd 备份与恢复、集群证书轮换、版本升级——这些动作都有中断风险,必须排期演练。
CNI 怎么选、NetworkPolicy 要不要上;StorageClass 对接云盘还是 NFS,容量与快照怎么管。
Ingress Controller 选型 + cert-manager 自动签发续期,否则换证书还是靠手工。
metrics-server、Prometheus、日志聚合、告警。K8s 把「服务在哪台机器」抽象掉了,日志和监控也得跟着换思路。
RBAC、ServiceAccount、命名空间划分。默认配置下权限是偏松的,需要主动收紧。
构建、推送、拉取凭据(imagePullSecret)这条链路必须打通,否则集群拉不到你的镜像。
| 托管版本(ACK / TKE 等) | 自建(kubeadm / k3s) | |
|---|---|---|
| 控制面 | 云厂商负责:多副本高可用、自动升级、免运维 etcd | 你自己维护 etcd、证书、升级 |
| 节点 | 仍需你管理节点池与运维 | 全是你的活 |
| 工作负载 / 网络 / 存储 / 可观测性 | 仍然是你的活 | 也是你的活 |
| 起步门槛 | 低(半小时能有个集群) | 高 |
| 成本 | 控制面按规格收费 | 省下控制面费用,但拿人的时间换 |
注意第三行:托管只是把控制面这一块接了过去,工作负载怎么设计、网络怎么规划、存储怎么选、日志怎么收——一件都不会少。把「上了托管版」当成「从此不用运维」,是很多团队踩过的坑。