CTO Plus技术服务栈

Docker 与 Kubernetes 生产级实践:从容器化到集群管理

Docker容器最佳实践(多阶段构建、镜像优化、安全扫描)和Kubernetes集群管理(Pod设计模式、HPA/VPA自动伸缩、网络策略、存储管理、Helm Chart模板化部署),覆盖CICD流水线中的容器化全流程。

SteveRocket
北京,中国
2 min read

容器化不只是”写个 Dockerfile”

很多团队把应用容器化理解为”写个 Dockerfile,能跑就行”。但生产环境的容器化远不止于此——镜像大小、构建速度、安全性、资源管理,每个环节都可能成为线上事故的根源。

我们团队在过去两年容器化了 30+ 个微服务,从最开始 2GB 的臃肿镜像到现在 50MB 的精简镜像,踩过的坑值得分享。

Docker 最佳实践

多阶段构建

多阶段构建是 Docker 最被低估的特性之一。它的核心思想是:构建环境和运行环境分离

# ============ 构建阶段 ============
FROM golang:1.22-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server .

# ============ 运行阶段 ============
FROM alpine:3.19

RUN apk --no-cache add ca-certificates tzdata
ENV TZ=Asia/Shanghai

COPY --from=builder /app/server /server

USER 1000:1000
EXPOSE 8080

ENTRYPOINT ["/server"]

效果对比:

  • 单阶段构建镜像:~800MB(包含 Go 编译器、源码、依赖)
  • 多阶段构建镜像:~15MB(只有二进制 + ca-certificates)

选择正确的基础镜像

基础镜像大小安全性适用场景
scratch0MB最高静态编译的 Go/Rust 程序
alpine~5MB需要 shell 和基础工具
distroless~20MB最高不需要 shell 的 Java/Node/Python
ubuntu~77MB需要完整系统库
python:3.12~1GB避免!用 slim 或 alpine 版本

推荐使用 distroless:

# 用 distroless 替代 alpine
FROM gcr.io/distroless/static-debian12:nonroot

COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]

distroless 没有包管理器、没有 shell、甚至没有 ls 命令。攻击者即使入侵了容器,能做的事情也非常有限。

层级优化

Docker 的每个指令创建一个层,层的顺序直接影响构建速度和镜像大小:

# ❌ 不好:频繁变化的放在前面
COPY . .                      # 每次代码改动都要重建
RUN pip install -r requirements.txt  # 依赖不改也要重新下载

# ✅ 好:稳定的放前面,频繁变化的放后面
COPY requirements.txt .
RUN pip install -r requirements.txt  # 只在依赖变化时重建
COPY . .                              # 代码变化只影响这一层

.dockerignore 的重要性

# 不传到 Docker context 中
node_modules/
.git/
.gitignore
*.md
.env
.env.local
dist/
build/
coverage/
*.log
Dockerfile
docker-compose.yml

没有 .dockerignoreCOPY . . 会把所有文件(包括 node_modules.git 目录)都复制进镜像。

安全扫描

# Trivy 镜像扫描
trivy image myapp:latest

# Docker Scout
docker scout quickview myapp:latest
docker scout recommendations myapp:latest

# 在 CI 中集成
docker build -t myapp:$CI_COMMIT_SHA .
trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:$CI_COMMIT_SHA

Kubernetes 部署实战

Pod 设计模式

Sidecar 模式:主容器 + 辅助容器

apiVersion: v1
kind: Pod
metadata:
  name: app-with-sidecar
spec:
  containers:
  # 主容器:应用
  - name: app
    image: myapp:latest
    ports:
    - containerPort: 8080
    
  # Sidecar:日志采集
  - name: log-collector
    image: fluentd:latest
    volumeMounts:
    - name: logs
      mountPath: /var/log/app
  
  volumes:
  - name: logs
    emptyDir: {}

Init Container:在主容器启动前执行初始化任务

spec:
  initContainers:
  - name: init-db
    image: postgres:16
    command: ['sh', '-c', 'until pg_isready -h db-service; do sleep 2; done']
  
  - name: init-migration
    image: myapp:latest
    command: ['./migrate']
  
  containers:
  - name: app
    image: myapp:latest

Init Container 按顺序执行,全部成功后才启动主容器。适合数据库等待、数据迁移、配置初始化等场景。

资源管理

Requests vs Limits:

resources:
  requests:   # 调度器用来分配节点的保证资源
    cpu: "500m"
    memory: "512Mi"
  limits:     # 容器能使用的最大资源
    cpu: "2000m"
    memory: "1Gi"

关键原则:

  • requests 设置合理:太低会导致节点过载,太高会浪费资源
  • limits 必须设置:没有 limits 的 Pod 可能 OOM 整个节点
  • 不要设 CPU limits(争议):CPU 是可压缩资源,设 limits 可能导致不必要的 throttling

健康检查

livenessProbe:   # 容器是否活着(失败 → 重启)
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  failureThreshold: 3

readinessProbe:  # 容器是否准备好接收流量(失败 → 从 Service 移除)
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

startupProbe:    # 容器是否启动完成(1.16+)
  httpGet:
    path: /startup
    port: 8080
  failureThreshold: 30
  periodSeconds: 10

三个探针的区别:

  • startupProbe:只用于启动阶段,保护慢启动的应用
  • livenessProbe:运行时检查,失败就重启
  • readinessProbe:流量开关,失败就切断流量但不会重启

livenessProbe 的常见陷阱:

  • 不要检查外部依赖(数据库、Redis)——依赖挂了重启应用没用
  • 用独立的轻量端点(不要和业务逻辑耦合)
  • initialDelaySeconds 设足够长

自动伸缩

HPA(水平自动伸缩):

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: myapp-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300  # 缩容前等待 5 分钟
      policies:
      - type: Percent
        value: 50
        periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0   # 扩容立即生效
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15

网络策略

默认情况下,K8s 集群中所有 Pod 之间可以互相通信。生产环境必须配置 NetworkPolicy:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-network-policy
spec:
  podSelector:
    matchLabels:
      app: api-server
  policyTypes:
  - Ingress
  - Egress
  ingress:
  # 只允许来自 frontend 和 monitoring 的流量
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    - podSelector:
        matchLabels:
          app: monitoring
    ports:
    - protocol: TCP
      port: 8080
  egress:
  # 允许访问数据库
  - to:
    - podSelector:
        matchLabels:
          app: postgres
    ports:
    - protocol: TCP
      port: 5432
  # 允许 DNS 查询
  - to:
    - namespaceSelector: {}
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53

存储管理

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-pvc
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: ssd  # 高性能存储
  resources:
    requests:
      storage: 100Gi
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres
  replicas: 3
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: ssd
      resources:
        requests:
          storage: 100Gi

StatefulSet 的 volumeClaimTemplates 会为每个 Pod 自动创建独立的 PVC,确保数据隔离。

Helm Chart 模板化

# values.yaml
replicaCount: 3
image:
  repository: myapp
  tag: latest
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 8080

resources:
  requests:
    cpu: 500m
    memory: 512Mi
  limits:
    memory: 1Gi

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "myapp.fullname" . }}
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "myapp.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "myapp.selectorLabels" . | nindent 8 }}
    spec:
      containers:
      - name: {{ .Chart.Name }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        ports:
        - containerPort: {{ .Values.service.port }}
        resources:
          {{- toYaml .Values.resources | nindent 10 }}

滚动更新策略

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1         # 最多多出几个 Pod
      maxUnavailable: 0   # 最多几个不可用(0 = 一个都不能少)
  minReadySeconds: 10     # Pod Ready 后等 10 秒再认为可用

生产环境推荐 maxUnavailable: 0,确保更新过程中服务不中断。

CICD 集成

GitLab CI 示例

stages:
  - build
  - test
  - scan
  - deploy

variables:
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

build:
  stage: build
  script:
    - docker build -t $IMAGE_TAG .
    - docker push $IMAGE_TAG

test:
  stage: test
  script:
    - docker run --rm $IMAGE_TAG npm test

security_scan:
  stage: scan
  script:
    - trivy image --severity HIGH,CRITICAL --exit-code 1 $IMAGE_TAG
    - trivy image --severity MEDIUM --exit-code 0 $IMAGE_TAG

deploy_staging:
  stage: deploy
  script:
    - helm upgrade --install myapp ./helm/myapp
      --set image.tag=$CI_COMMIT_SHORT_SHA
      --namespace staging
      --wait
      --timeout 5m
  environment:
    name: staging

deploy_production:
  stage: deploy
  script:
    - helm upgrade --install myapp ./helm/myapp
      --set image.tag=$CI_COMMIT_SHORT_SHA
      --namespace production
      --wait
      --timeout 5m
  environment:
    name: production
  when: manual  # 生产部署需要手动触发

我们的实践经验

经验一:镜像标签策略

不要用 latest 标签!推荐方案:

环境 + Commit SHA + 时间戳
staging-a1b2c3d-20260529
production-a1b2c3d-20260529

这样:

  • 知道每个环境跑的是哪个版本
  • 可以快速回滚到任意版本
  • 审计时有据可查

经验二:Graceful Shutdown

// Go 应用优雅关闭
func main() {
    srv := &http.Server{Addr: ":8080"}
    
    go func() {
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()
    
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatal("强制关闭:", err)
    }
}

对应的 K8s 配置:

spec:
  terminationGracePeriodSeconds: 60  # 等待应用优雅关闭的时间

经验三:PodDisruptionBudget

防止维护操作导致服务不可用:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: myapp-pdb
spec:
  minAvailable: 2  # 至少保持 2 个 Pod 可用
  selector:
    matchLabels:
      app: myapp

经验四:日志管理

不要在容器里写日志文件!输出到 stdout/stderr:

// 好的做法
log.Println("server started on :8080")

// 不要这样做
f, _ := os.Create("/var/log/app.log")
log.SetOutput(f)

stdout/stderr 会被 K8s 自动采集,可以用 kubectl logs 查看。如果需要持久化,用 Loki 或 ELK 采集。

总结

容器化和 Kubernetes 的核心不是”会用”,而是”用好”。关键要点:

  1. 多阶段构建缩小镜像,distroless 提高安全性
  2. 资源 requests/limits 必须设置,这是集群稳定的基础
  3. 三种探针各司其职,livenessProbe 不要检查外部依赖
  4. NetworkPolicy 默认拒绝,最小权限原则
  5. Helm 模板化管理多环境配置
  6. 安全扫描集成到 CI 流水线中
  7. 优雅关闭配合 terminationGracePeriodSeconds

Comments

Link copied to clipboard!