1. 从单机到集群:Docker与K8s的技术定位差异

第一次接触Docker时,我被它"一次构建,随处运行"的特性震撼了。记得2015年调试一个Python服务,光是让它在测试环境跑起来就花了三天,而用Docker容器部署只用了15分钟。但当我尝试在生产环境部署50个容器时,手动管理很快变成了灾难——这正是Kubernetes(K8s)要解决的问题。

Docker本质是单机容器引擎,它的核心价值在于:

  • 通过cgroups和namespace实现进程隔离
  • 用分层镜像机制解决环境一致性问题
  • 提供docker build/run等CLI工具链

而K8s是分布式操作系统,专注解决:

  • 跨主机容器调度(比如把Web服务容器和数据库容器分散在不同节点)
  • 自动扩缩容(电商大促时自动增加订单服务实例)
  • 服务网格(让前端Pod能自动发现后端Pod的变化)

实际案例:我们有个电商应用,用Docker打包了商品服务(SpringBoot)、支付服务(Go)、推荐服务(Python)三个组件。在开发阶段,用docker-compose能完美模拟联调环境。但上线后需要:

  1. 支付服务在双11期间自动扩容到20个实例
  2. 某个商品服务实例崩溃时自动重启
  3. 推荐服务升级时做到零停机 这些正是K8s的Pod、Deployment、Service等资源对象的用武之地。

2. 容器化工作流:开发者的实战路径

2.1 从Dockerfile到镜像仓库

一个典型的Java微服务容器化流程如下:

# 阶段1:构建
FROM maven:3.8-jdk-11 AS builder
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src/ ./src/
RUN mvn package -DskipTests

# 阶段2:运行
FROM openjdk:11-jre-slim
COPY --from=builder /target/service.jar /app/
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app/service.jar"]

这个多阶段构建的Dockerfile做了三件关键事:

  1. 用构建镜像编译出可执行jar
  2. 用更小的运行时镜像承载最终应用
  3. 明确声明容器监听端口

构建完成后,需要推送到镜像仓库:

docker build -t registry.example.com/order-service:v1.2 .
docker push registry.example.com/order-service:v1.2

2.2 K8s的部署宣言:YAML的艺术

同样的服务,在K8s中需要定义Deployment和Service:

# order-service-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
      - name: order-service
        image: registry.example.com/order-service:v1.2
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: "500m"
            memory: "512Mi"

这个声明式配置告诉K8s:

  • 保持3个Pod副本运行
  • 每个Pod分配0.5核CPU和512MB内存
  • 容器监听8080端口

3. 进阶协作模式:当K8s遇见Docker

3.1 镜像拉取策略优化

生产环境中,合理配置imagePullPolicy很重要:

spec:
  containers:
  - name: my-app
    image: my-registry/app:v1
    imagePullPolicy: IfNotPresent

三种策略的适用场景:

  • Always:持续交付环境(每次重启都拉取最新镜像)
  • IfNotPresent:测试环境(优先使用本地镜像)
  • Never:离线环境(完全依赖本地镜像)

3.2 容器运行时演进

虽然Docker仍是主流,但K8s已转向更轻量的运行时架构:

Kubelet → CRI → containerd → runc
                ↘ CRI-O

这种变化带来两个实际影响:

  1. 如果需要docker build,需单独安装Docker CLI
  2. 调试容器时命令变化:
    # 旧方式
    docker ps 
    # 新方式
    crictl ps
    

4. 排错实战:典型问题排查指南

4.1 镜像拉取失败

当Pod状态显示ImagePullBackOff时:

  1. 检查镜像标签是否存在
    docker pull registry.example.com/app:v1
    
  2. 验证密钥配置
    kubectl create secret docker-registry my-secret \
      --docker-server=registry.example.com \
      --docker-username=user \
      --docker-password=pass
    

4.2 资源限制引发的OOM

某次线上事故中,Java应用频繁被K8s杀死。最终发现是JVM堆内存超出容器限制:

resources:
  limits:
    memory: "1Gi"

解决方案:

  1. 设置JVM参数:-XX:MaxRAMPercentage=75.0
  2. 容器内存限制放宽到1.5Gi

5. 性能调优:从基础到进阶

5.1 容器密度优化

通过资源请求/限制提升节点利用率:

resources:
  requests:
    cpu: "250m"
    memory: "256Mi"
  limits:
    cpu: "1"
    memory: "1Gi"

经验值:

  • 开发环境:requests=limits(稳定性优先)
  • 生产环境:limits=2*requests(弹性优先)

5.2 镜像构建加速

多阶段构建的进阶技巧:

# 利用构建缓存
RUN --mount=type=cache,target=/root/.m2 mvn package

6. 安全加固:不可忽视的防线

6.1 最小权限原则

关键配置项:

securityContext:
  runAsNonRoot: true
  allowPrivilegeEscalation: false
  capabilities:
    drop: ["ALL"]

6.2 镜像扫描

集成到CI流水线:

docker scan my-image --severity high

7. 未来展望:云原生技术栈演进

虽然K8s和Docker的组合已成事实标准,但新兴技术正在补充这个生态:

  • 镜像构建:Buildpacks(无需编写Dockerfile)
  • 服务网格:Istio(增强版Service)
  • 无状态运行时:WasmEdge(超轻量容器)

在可预见的未来,Docker仍会是开发者最熟悉的容器工具,而K8s将继续作为集群管理的事实标准。两者的协作模式可能会变化,但解决的核心问题——高效交付可靠应用——永远不会过时。

更多推荐