从 Docker 到 Kubernetes
从 Docker 到 Kubernetes
写在前面
本系列从 Docker 核心概念一路讲到监控日志,基本覆盖了容器化的方方面面。但当服务数量超过 10 个、机器数量超过 5 台,docker-compose 已经捉襟见肘——滚动更新、自动扩缩、跨主机调度、配置管理,这些 compose 都做不了。
本篇是 Docker 系列的最后一篇,讲怎么从 docker-compose 平滑迁移到 Kubernetes,用 Helm 管应用,用 Argo CD 做 GitOps 部署。
一、Kubernetes 是什么
1.1 一句话定义
Kubernetes(K8s)是一个跨主机的容器编排平台——自动调度、自动扩缩、自愈、滚动更新。
它解决了 compose 解决不了的:
| 痛点 | Compose | K8s |
|---|---|---|
| 多机调度 | 只能单机 | 跨主机调度 |
| 自动扩缩 | 不支持 | HPA / VPA / KEDA |
| 自愈 | 不支持 | Pod 失败自动重启 |
| 滚动更新 | --no-deps |
零中断滚动更新 |
| 配置管理 | env_file | ConfigMap / Secret |
| 服务发现 | DNS(手动网络) | Service + 自动 DNS |
| 负载均衡 | 手动配置 | Service 自动 LB |
| 灰度发布 | 复杂 | Ingress + Argo Rollouts |
1.2 核心概念速览
最小调度单位不是容器,是 Pod:
Pod (调度单位)
├── Container 1 (主容器)
├── Container 2 (sidecar,如日志收集)
└── 共享网络、存储
常用资源:
| 资源 | 作用 |
|---|---|
| Pod | 一组容器 |
| Deployment | 管理 Pod 的副本数和版本 |
| Service | 给 Pod 提供稳定的访问入口 |
| ConfigMap | 配置注入 |
| Secret | 密钥注入 |
| Volume | 持久化存储 |
| Namespace | 资源隔离 |
| Ingress | HTTP 路由 |
1.3 K8s 适合谁
适合:
- 服务数量 ≥ 10
- 多机房 / 多云
- 需要自动扩缩
- 团队 ≥ 5 人
不适合:
- 单体应用
- 单机部署
- 小团队(学习成本高)
如果你的项目还在单机,继续用 compose,别硬上 K8s。
二、本地搭建 K8s 环境
2.1 minikube(学习用)
brew install minikube
minikube start --cpus=4 --memory=8g --driver=docker
# 装 kubectl
brew install kubectl
# 看集群
kubectl get nodes
# NAME STATUS ROLES AGE
# minikube Ready control-plane 1m
# 装 dashboard
minikube dashboard
2.2 kind(K8s in Docker)
brew install kind
kind create cluster --name dev
适合 CI 跑测试。
2.3 k3d(轻量级 K8s)
brew install k3d
k3d cluster create dev --agents 3
我们开发环境用 k3d,启动 30 秒,资源占用低。
三、Dockerfile 几乎不用改
K8s 跑容器,Dockerfile 是通用的——前面 Dockerfile 篇写的所有东西都适用。
四、把 docker-compose 转成 K8s manifests
4.1 转换工具
kompose 自动转换:
brew install kompose
kompose convert -f docker-compose.yml -o k8s/
但自动转换的结果一般不能直接用,要手动调整。下面我们手写。
4.2 一个完整的 K8s 应用
假设我们要把这个 compose 服务搬到 K8s:
# docker-compose.yml
services:
api:
image: harbor.company.com/myproject/api:v1.0
ports:
- "3000:3000"
environment:
NODE_ENV: production
DB_HOST: mysql
DB_PASSWORD: secret
depends_on:
mysql:
condition: service_healthy
Deployment(替代 services.api)
# k8s/api-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: myproject
labels:
app: api
spec:
replicas: 3
selector:
matchLabels:
app: api
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 多于 replicas 的最大数
maxUnavailable: 0 # 不可用的最大数(零停机)
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: harbor.company.com/myproject/api:v1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3000
name: http
env:
- name: NODE_ENV
value: production
- name: DB_HOST
value: mysql
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: api-secrets
key: db-password
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
securityContext:
runAsNonRoot: true
runAsUser: 1001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
关键点解释:
replicas: 3:起 3 个副本(高可用)maxUnavailable: 0:零停机滚动更新(新 Pod 起来后才删旧的)resources:K8s 调度器根据 requests 决定调度,limits 防止资源滥用livenessProbe:失败 → 重启容器readinessProbe:失败 → 从 Service Endpoints 摘掉securityContext:跑非 root,只读根文件系统,丢弃所有 capabilities——生产必须
Service(替代 ports)
# k8s/api-service.yaml
apiVersion: v1
kind: Service
metadata:
name: api
namespace: myproject
spec:
selector:
app: api
ports:
- name: http
port: 80
targetPort: http
type: ClusterIP # 仅集群内部访问
Ingress(对外暴露)
# k8s/api-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api
namespace: myproject
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-body-size: 10m
spec:
ingressClassName: nginx
tls:
- hosts:
- api.company.com
secretName: api-tls
rules:
- host: api.company.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 80
ConfigMap + Secret(配置 + 密钥)
# k8s/api-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
namespace: myproject
data:
LOG_LEVEL: info
CACHE_TTL: "300"
---
apiVersion: v1
kind: Secret
metadata:
name: api-secrets
namespace: myproject
type: Opaque
stringData:
db-password: your-secret-password # 实际应该用 Sealed Secrets 或 Vault
Namespace(资源隔离)
# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: myproject
labels:
name: myproject
部署
kubectl apply -f k8s/
五、Helm:把 manifests 模板化
5.1 为什么需要 Helm
上面写死的 replicas: 3、image: ...:v1.0 在生产、staging、dev 三个环境都得改。Helm 让我们用模板 + values 管理。
5.2 Helm chart 结构
helm create myapp
生成:
myapp/
├── Chart.yaml
├── values.yaml
├── templates/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── configmap.yaml
│ ├── secret.yaml
│ ├── _helpers.tpl
│ └── NOTES.txt
└── charts/
5.3 Chart.yaml
apiVersion: v2
name: myapp
description: A Helm chart for myapp
type: application
version: 1.0.0
appVersion: "1.0.0"
5.4 values.yaml
# 默认值
replicaCount: 3
image:
repository: harbor.company.com/myproject/myapp
pullPolicy: IfNotPresent
tag: "1.0.0"
imagePullSecrets:
- name: harbor-pull-secret
service:
type: ClusterIP
port: 80
targetPort: 3000
ingress:
enabled: true
className: nginx
annotations: {}
hosts:
- host: api.company.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- api.company.com
secretName: api-tls
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
env:
- name: NODE_ENV
value: production
- name: LOG_LEVEL
value: info
secrets:
db-password: "your-password"
5.5 模板:deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "myapp.selectorLabels" . | nindent 8 }}
spec:
imagePullSecrets:
{{- toYaml .Values.imagePullSecrets | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
env:
{{- toYaml .Values.env | nindent 12 }}
envFrom:
- secretRef:
name: {{ include "myapp.fullname" . }}-secrets
livenessProbe:
httpGet:
path: /health
port: http
readinessProbe:
httpGet:
path: /ready
port: http
resources:
{{- toYaml .Values.resources | nindent 12 }}
5.6 部署到不同环境
# dev 环境
helm install myapp ./myapp \
--namespace myproject-dev \
--create-namespace \
--values values-dev.yaml \
--set replicaCount=1 \
--set ingress.hosts[0].host=dev-api.company.com
# staging
helm install myapp ./myapp \
--namespace myproject-staging \
--create-namespace \
--values values-staging.yaml
# production
helm install myapp ./myapp \
--namespace myproject-prod \
--create-namespace \
--values values-prod.yaml
values-prod.yaml:
replicaCount: 5
ingress:
hosts:
- host: api.company.com
paths:
- path: /
pathType: Prefix
autoscaling:
minReplicas: 5
maxReplicas: 20
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 2000m
memory: 2Gi
六、自动扩缩容(HPA)
6.1 基于 CPU/内存
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
6.2 基于自定义指标(Prometheus)
用 Prometheus Adapter 暴露 Prometheus 指标给 K8s:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-queue
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 3
maxReplicas: 20
metrics:
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "100" # 每个 Pod 100 RPS
七、GitOps:Argo CD
7.1 什么是 GitOps
所有变更通过 Git 提交——Helm values、K8s manifests 都存在 Git 里,Argo CD 监听变化,自动同步到集群。
7.2 Argo CD 部署
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
Web UI:
kubectl port-forward svc/argocd-server -n argocd 8080:443
# 访问 https://localhost:8080
# 用户名:admin,密码从 secret 拿
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
7.3 Application 定义
# argocd/myapp-prod.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp-prod
namespace: argocd
spec:
project: default
source:
repoURL: git@github.com:myorg/myapp-config.git
targetRevision: HEAD
path: helm/myapp
helm:
valueFiles:
- values-prod.yaml
destination:
server: https://kubernetes.default.svc
namespace: myproject-prod
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
7.4 完整工作流
1. 开发者提 PR(改 values.yaml 或 chart 模板)
2. Code Review + 合并
3. Argo CD 检测到 Git 变化
4. Argo CD 计算差异,执行 sync
5. 应用更新到 K8s
6. 健康检查失败 → 自动回滚
我们 2024 年开始用 Argo CD,部署出错率从 5% 降到 0.5%——GitOps 的最大价值是审计和回滚。
八、真实迁移:公司测试平台从 compose 到 K8s
8.1 迁移前的状态
- 5 台物理机
- 每个服务用 docker-compose 起
- 滚动更新要 SSH 到对应机器手动操作
- 服务发现靠互相写 IP(运维维护一份 Excel)
- 平均故障恢复时间(MTTR):40 分钟
8.2 迁移策略
阶段 1:基础设施(2 周)
- 部署 K8s 集群(3 master + 5 worker)
- 部署 Argo CD
- 部署 Ingress Controller(nginx)
- 部署 cert-manager(自动 HTTPS)
- 部署 Prometheus + Grafana + Loki
阶段 2:无状态服务迁移(2 周)
先把无状态服务搬上 K8s:API、Web、Worker。
每个服务:
- 写 Helm chart(基于模板生成,改 values)
- 在 dev 环境跑通
- 在 staging 环境跑 1 周,观察稳定性
- 切生产流量(灰度:10% → 50% → 100%)
- 下线旧 compose
阶段 3:有状态服务迁移(4 周)
MySQL、Redis、MinIO。有状态服务上 K8s 风险大,我们用了 StatefulSet:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
volumeMounts:
- name: data
mountPath: /var/lib/mysql
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: nfs-storage
resources:
requests:
storage: 500Gi
⚠️ 生产 MySQL 不要直接跑 StatefulSet——运维成本高(备份、监控、扩缩容都不简单)。我们后来用 云数据库(RDS/Cloud SQL),K8s 里只跑无状态服务。
阶段 4:下线旧架构(2 周)
- 旧机器改成 K8s worker
- 删除 compose 文件
- 文档更新
8.3 迁移效果
| 指标 | 迁移前 | 迁移后 |
|---|---|---|
| 平均部署时间 | 15 分钟 | 90 秒 |
| 平均故障恢复 | 40 分钟 | 5 分钟 |
| 机器利用率 | 30% | 70% |
| 故障率 | 5% | 0.5% |
| 运维人力 | 2 人 | 1 人(兼) |
节省了一台机器 + 一个人力,覆盖了 K8s 的学习成本。
九、K8s 的几个常见坑
9.1 坑 1:imagePullPolicy 默认值
imagePullPolicy: Always # 默认
开发环境无所谓,生产环境应该 IfNotPresent,避免每次重启都拉镜像。
9.2 坑 2:Pod 时区
K8s Pod 默认 UTC 时间,容器里看到的日志全是 UTC。两种解法:
env:
- name: TZ
value: Asia/Shanghai
volumeMounts:
- name: tz-config
mountPath: /etc/localtime
volumes:
- name: tz-config
hostPath:
path: /usr/share/zoneinfo/Asia/Shanghai
9.3 坑 3:Pod OOM 被杀
应用内存占用有尖峰,被 OOMKilled 但没有日志。
解决:
resources:
limits:
memory: 1Gi
设置 limit 让 K8s 提前 OOM,比内存争抢导致的整机卡死好得多。
9.4 坑 4:readinessProbe 失败导致流量分发异常
如果 readinessProbe 路径返回 503,但应用本身是好的——可能是因为下游 DB 还没起。
解法:
- 用 startupProbe 给应用足够的启动时间
- readinessProbe 只检查应用自身状态(不依赖下游)
startupProbe:
httpGet:
path: /startup
port: http
failureThreshold: 30
periodSeconds: 10
十、Docker 系列完结
到这里,Docker 系列 10 篇全部写完,覆盖了从入门到 K8s 的完整链路。回顾一下:
- ✅ 核心概念与生命周期
- ✅ Dockerfile 编写规范
- ✅ 镜像分层与体积瘦身
- ✅ docker-compose 编排
- ✅ Docker 网络
- ✅ 数据持久化
- ✅ 私有镜像仓库
- ✅ CI/CD 集成
- ✅ 监控与日志
- ✅ Kubernetes 迁移
接下来是 Nginx 进阶全攻略 系列,延续 nginx.md,讲清楚生产环境的每一个细节。