从 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 适合谁

适合:

不适合:

如果你的项目还在单机,继续用 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

关键点解释:

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 迁移前的状态

8.2 迁移策略

阶段 1:基础设施(2 周)

阶段 2:无状态服务迁移(2 周)

先把无状态服务搬上 K8s:API、Web、Worker。

每个服务:

  1. 写 Helm chart(基于模板生成,改 values)
  2. 在 dev 环境跑通
  3. 在 staging 环境跑 1 周,观察稳定性
  4. 切生产流量(灰度:10% → 50% → 100%)
  5. 下线旧 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 周)

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:
  httpGet:
    path: /startup
    port: http
  failureThreshold: 30
  periodSeconds: 10

十、Docker 系列完结

到这里,Docker 系列 10 篇全部写完,覆盖了从入门到 K8s 的完整链路。回顾一下:

  1. ✅ 核心概念与生命周期
  2. ✅ Dockerfile 编写规范
  3. ✅ 镜像分层与体积瘦身
  4. ✅ docker-compose 编排
  5. ✅ Docker 网络
  6. ✅ 数据持久化
  7. ✅ 私有镜像仓库
  8. ✅ CI/CD 集成
  9. ✅ 监控与日志
  10. ✅ Kubernetes 迁移

接下来是 Nginx 进阶全攻略 系列,延续 nginx.md,讲清楚生产环境的每一个细节。

参考资料