把Docker Swarm换到Kubernetes时踩过的坑

Swarm 跑起来没太大问题,但很快就暴露了一些问题:

  • 服务发现比较基础,想做灰度发布得自己搞一些 trick
  • 健康检查支持有限,有时候容器挂了 Swarm 发现不了
  • 监控和日志收集得自己想办法,不像 K8s 那样有完整的生态
  • 团队里其他项目都在用 K8s,技能没法复用

这些单个问题都能解决,但解决成本越来越高,最后还是决定迁移到 Kubernetes。

最开始是因为项目里 Docker 越来越多,十几个容器手动启动确实费劲。

起因:为什么折腾

最开始是因为项目里 Docker 越来越多,十几个容器手动启动确实费劲。那时候觉得 Swarm 多合适,几条命令就能搞定集群,docker service create 一跑,服务就起来了。

2024 年初的时候,生产环境有三台阿里云 ECS,每台 4C8G,上面跑了大概二十几个容器。Swarm 跑起来没太大问题,但很快就暴露了一些问题:

  • 服务发现比较基础,想做灰度发布得自己搞一些 trick
  • 健康检查支持有限,有时候容器挂了 Swarm 发现不了
  • 监控和日志收集得自己想办法,不像 K8s 那样有完整的生态
  • 团队里其他项目都在用 K8s,技能没法复用

这些单个问题都能解决,但解决成本越来越高,最后还是决定迁移到 Kubernetes。

Docker Swarm 时期的实践

先说说 Swarm 时期是怎么玩的,毕竟是过渡阶段,也有一些可取的地方。

集群初始化

Swarm 的集群初始化很简单,基本上就是几条命令:

# 在第一台机器上初始化集群
docker swarm init --advertise-addr 192.168.1.10

# 在其他节点上加入集群
docker swarm join --token SWMTKN-1-xxx 192.168.1.10:2377

节点加进来之后,docker node ls 就能看到集群状态了。

服务部署

部署一个简单的 Web 服务:

# docker-compose.yml
version: '3.8'

services:
  web:
    image: nginx:1.21
    ports:
      - "80:80"
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
    networks:
      - webnet

networks:
  webnet:
    driver: overlay

部署命令也很直接:

docker stack deploy -c docker-compose.yml mystack

踩过的坑

第一个坑是 overlay 网络。刚开始用的是默认的 overlay 网络,但发现跨节点通信延迟比较高,有时候还会丢包。

后来改成指定子网:

networks:
  webnet:
    driver: overlay
    ipam:
      config:
        - subnet: 10.0.9.0/24

这样就稳定多了,但说实话,这种问题在 Swarm 里面排查起来比较费劲,没有 K8s 那样的网络策略和调试工具。

第二个坑是存储卷。Swarm 的 volume 管理比较简单,要做多节点共享存储就得自己想办法。

我们试过 NFS:

volumes:
  data:
    driver: local
    driver_opts:
      type: nfs
      o: addr=nfs-server,rw
      device: ":/path/to/share"

但 NFS 的性能确实不太行,高并发的时候延迟明显。

后来换成了阿里云的 NAS,性能好了不少,但这就意味着被云厂商绑定了。

迁移到 Kubernetes

迁移的决定是 2024 年 5 月做的,花了一个多月时间才迁移完。

为什么选 Kubernetes

说实话,Kubernetes 的复杂度确实比 Swarm 高很多,但它的优势也很明显:

  • 功能完整:自愈、自动扩缩容、滚动更新、配置管理等
  • 生态成熟:各种监控、日志、存储插件都很齐全
  • 行业标准:团队技能、招聘、社区支持都有优势
  • 可扩展性强:CRD + Operator 能玩出很多花样

这些都是 Swarm 比较欠缺的。

集群搭建

我们用的是 kubeadm 搭建的集群,因为想要多一些控制权,不想用云厂商的托管服务。

# 在 master 节点
kubeadm init --apiserver-advertise-address=192.168.1.10 --pod-network-cidr=10.244.0.0/16

# 在 worker 节点
kubeadm join 192.168.1.10:6443 --token <token> --discovery-token-ca-cert-hash sha256:<hash>

网络插件选的是 Flannel,因为比较轻量,部署也简单:

kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

应用迁移

Swarm 的 docker-compose.yml 需要转换成 Kubernetes 的 Deployment 和 Service:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: nginx
        image: nginx:1.21
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 250m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi
        livenessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 5

---
# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  type: LoadBalancer
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml

迁移中的坑

第一个坑是镜像仓库。Swarm 可以直接拉 Docker Hub 的镜像,但生产环境不能这么干。

我们建了私有镜像仓库,用 Harbor 管的:

# 先打标签
docker tag nginx:1.21 harbor.example.com/library/nginx:1.21

# 推送
docker push harbor.example.com/library/nginx:1.21

# 创建 secret
kubectl create secret docker-registry harbor-registry \
  --docker-server=harbor.example.com \
  --docker-username=<username> \
  --docker-password=<password>

# 在 pod 里引用
spec:
  containers:
  - name: nginx
    image: harbor.example.com/library/nginx:1.21
  imagePullSecrets:
  - name: harbor-registry

第二个坑是健康检查。Swarm 的健康检查比较简单,Kubernetes 的就复杂很多,但功能也更强。

livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3

readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5
  timeoutSeconds: 3
  failureThreshold: 1

这里有几个参数需要好好调:

  • initialDelaySeconds:容器启动后等多久开始检查,太早会误杀,太晚会影响可用性
  • periodSeconds:检查间隔,太频繁浪费资源,太疏漏掉问题
  • failureThreshold:失败多少次才算真的失败,容忍度要按业务来定

第三个坑是资源限制。Swarm 的资源限制比较粗糙,Kubernetes 的就精细很多。

刚开始我们给得太保守,结果在高并发的时候性能上不去。后来经过压测才慢慢调到一个合理的值:

resources:
  requests:
    cpu: 500m
    memory: 512Mi
  limits:
    cpu: 2000m
    memory: 2Gi

这里的 requestslimits 要理解清楚:requests 是调度用的,保证容器至少能拿到这么多资源;limits 是运行时的上限,防止容器占用太多。

核心功能的对比

服务发现

Swarm 的服务发现比较基础,服务名直接就能解析成 IP:

# 在一个容器里访问另一个服务
curl http://backend:8080/api

Kubernetes 的服务发现就比较复杂了,有 ClusterIP、NodePort、LoadBalancer 几种类型,每种都有使用场景。

# ClusterIP(默认,集群内访问)
kind: Service
spec:
  type: ClusterIP
  # ...

# NodePort(通过节点端口访问)
kind: Service
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 80
    nodePort: 30080

# LoadBalancer(云厂商提供的负载均衡器)
kind: Service
spec:
  type: LoadBalancer
  # ...

Kubernetes 还有 Ingress,可以做基于域名的路由:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host: web.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: web
            port:
              number: 80

负载均衡

Swarm 的负载均衡是基于 VIP 的,比较简单,但也比较有限。

Kubernetes 的负载均衡就强大多了,可以支持多种算法和会话保持:

apiVersion: v1
kind: Service
metadata:
  annotations:
    service.beta.kubernetes.io/alibaba-cloud-loadbalancer-spec: slb.s1.small
spec:
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800
  # ...

自愈能力

Swarm 的自愈能力比较基础,主要是容器重启。

Kubernetes 的自愈能力就比较全面了:

  • Pod 重启:容器挂了会自动重启
  • 节点自愈:节点挂了会自动迁移 Pod
  • 自动扩缩容:根据负载自动调整副本数
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web
  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

滚动更新

Swarm 的滚动更新比较简单,主要是配置 update_config

deploy:
  update_config:
    parallelism: 1
    delay: 10s
    failure_action: rollback

Kubernetes 的滚动更新就精细多了:

spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0

这里 maxSurge 是更新过程中最多可以多出多少个 Pod,maxUnavailable 是最多可以有多少个 Pod 不可用。

还有 Deployment 的 revision,可以回滚到之前的版本:

# 查看历史版本
kubectl rollout history deployment web

# 回滚到上一个版本
kubectl rollout undo deployment web

# 回滚到指定版本
kubectl rollout undo deployment web --to-revision=2

真实的踩坑记录

网络问题的排查

有一次服务突然不通了,Pod 状态是 Running,但就是访问不到。

先检查 Service:

kubectl get svc web
kubectl describe svc web

Service 没问题,再检查 Pod:

kubectl get pods -l app=web
kubectl describe pod <pod-name>

Pod 也没问题,进入 Pod 内部检查:

kubectl exec -it <pod-name> -- /bin/bash
curl localhost

本地访问没问题,那问题可能在网络层。

最后发现是 iptables 规则被误删了,恢复之后就正常了。

这次踩坑让我学到一点:Kubernetes 的网络比较复杂,排查问题要有方法,不能瞎猜。

存储问题

存储的问题也挺多。刚开始用 emptyDir,结果 Pod 重启数据就丢了。

后来改用 hostPath,但又遇到了权限问题。

apiVersion: v1
kind: Pod
metadata:
  name: storage-test
spec:
  containers:
  - name: app
    image: busybox
    volumeMounts:
    - mountPath: /data
      name: hostpath-vol
  volumes:
  - name: hostpath-vol
    hostPath:
      path: /data
      type: DirectoryOrCreate

最后还是用了阿里云的 SSD 云盘:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-ssd
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Gi
  storageClassName: alicloud-disk-ssd

资源争抢

有一次一个 Java 服务占满了内存,导致其他服务都受影响。

后来加了资源限制:

resources:
  requests:
    cpu: 1000m
    memory: 2Gi
  limits:
    cpu: 2000m
    memory: 4Gi

但这样又导致了 OOM Kill,因为 Java 的堆内存和容器内存计算方式不太一样。

最后调大了内存限制,同时在 Java 启动参数里加了 -XX:MaxRAMPercentage=75.0,这样 JVM 会根据容器内存限制来调整堆大小。

证书过期

有一次突然所有服务都访问不了,查了一圈发现是证书过期了。

Kubernetes 的证书默认是一年有效期,到期后 API Server 就会拒绝连接。

# 检查证书有效期
kubeadm certs check-expiration

# 更新证书
kubeadm certs renew all

# 重启控制面
systemctl restart kubelet

从那以后,我们在 CI/CD 里加了证书过期检查,提前一个月发告警。

迁移后的效果

迁移完成后,效果还是比较明显的:

  • 自动扩缩容:高峰期自动扩容,低谷期自动缩容,资源利用率提升了 30%+
  • 滚动更新:发布的时候用户无感知,回滚也很方便
  • 监控和日志:Prometheus + Grafana + ELK,问题定位快了很多
  • 团队技能:现在团队里好几个人都能上手 K8s,招聘也容易了

但也有一些问题:

  • 复杂度高:很多概念要理解,学习曲线陡峭
  • 运维成本:集群本身的维护也需要时间和精力
  • 资源消耗:K8s 的组件本身就要占不少资源

对于小团队来说,是不是一定要上 K8s,这是个需要权衡的问题。

经验总结

这次迁移下来,有一些经验值得分享:

  • 不要一上来就上 K8s,Swarm 对于小场景其实够用
  • 迁移前要做好充分的测试,生产环境不能出问题
  • 先搞清楚核心概念,再动手操作,不然容易踩坑
  • 监控和日志要提前规划,不然出问题就瞎了
  • 证书和备份要定期检查,不然哪天集群就挂了

技术选型没有银弹,Swarm 有 Swarm 的好,K8s 有 K8s 的强,关键是要匹配自己的场景。

现在的做法是:开发测试环境还是用 Swarm,简单够用;生产环境用 K8s,追求稳定和功能。

工具本身没有好坏,合适不合适才是关键。


折腾了这么久,容器编排这条路算是走完了第一阶段。下一步是搞服务网格,估计又是一场新的折腾。

版权声明: 本文首发于 指尖魔法屋-把Docker Swarm换到Kubernetes时踩过的坑https://blog.thinkmoon.cn/post/119-container-orchestration-swarm-kubernetes-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!