AI摘要

本文是作者三年容器化实践经验的总结,包括Docker和Kubernetes的使用过程中遇到的各种问题和解决方案。作者分享了Docker镜像构建、网络连接、权限问题、构建缓存等常见问题的处理方法,以及Kubernetes中YAML配置、资源调度、服务暴露、ConfigMap更新、日志管理等方面的坑和解决方法。最后,作者提出了一个本地开发工作流方案,并强调容器化是一种手段而非目标,建议读者从Docker开始,逐步尝试Kubernetes,优化开发工作流,让容器化成为助力而非负担。

直到现在,我还记得第一次在自己的MacBook上成功运行docker run hello-world时,那个命令行里跳出来的“Hello from Docker!”带给我的那种奇妙的兴奋感。那感觉就像第一次学会骑自行车——你知道这只是一个开始,前方是全新的世界。

但很快,这种兴奋就被现实打得粉碎。我亲手打包的第一个Spring Boot应用,在Docker容器里怎么也连不上本地的MySQL。那个红色的连接异常日志,像一盆冷水浇醒了我的美梦。

三年过去了,我从一个连容器和虚拟机都分不清的新手,到能在团队里带着新人趟过容器化的坑。这篇记录,就是这三年里那些让我凌晨三点还在查文档、那些“啊哈!”顿悟瞬间的真实合集。这不是一篇完美的教程,而是一份“伤痕文学”——希望我的伤,能让你走得顺一点。

第一章:Docker篇——当“在我的机器上能运行”成为诅咒

坑1:镜像构建的“空间恐惧症”

我的第一个Dockerfile简单得近乎天真:

FROM ubuntu:latest
RUN apt-get update && apt-get install -y openjdk-8-jdk
COPY . /app
WORKDIR /app
CMD ["java", "-jar", "myapp.jar"]

构建镜像,2.7GB。是的,你没看错,2.7GB。上传到仓库要20分钟,同事下载时看我的眼神都不对劲了。

我学到的:

  • ubuntu:latest是个巨兽。请用openjdk:8-jre-slimalpine版本,它们能轻量到让你感动。
  • 每个RUN命令都会产生一层镜像。一定要合并命令,并且记得清理apt缓存:
RUN apt-get update && apt-get install -y --no-install-recommends \
    openjdk-8-jdk \
    && rm -rf /var/lib/apt/lists/*

坑2:那个永远连不上的“localhost”

我在application.yml里写的数据库地址是jdbc:mysql://localhost:3306/mydb。容器启动成功,但应用死活连不上数据库。我花了整整一个下午,在Stack Overflow上各种尝试,直到看到一句话:“容器有自己的网络命名空间,localhost指向的是容器自己,不是你的宿主机。”

解决方案有几个,取决于场景:

  1. 开发时:用host.docker.internal(Docker Desktop)或172.17.0.1(Linux docker0网桥)代替localhost
  2. 更好的做法:永远不要在配置里写死主机名。用环境变量:
spring:
  datasource:
    url: jdbc:mysql://${DB_HOST:localhost}:3306/mydb

然后运行容器时:docker run -e DB_HOST=host.docker.internal myapp

坑3:权限的幽灵——那些“Permission Denied”

我把一个本地的日志目录挂载到容器里:-v ./logs:/app/logs。应用启动失败,报“Permission denied”。原来,容器内的进程通常以非root用户运行(这是好事!),但宿主机上的./logs目录属于我的用户,容器用户没权限写。

解决办法:

  1. 简单粗暴但危险:让容器以root运行(--user root),不推荐。
  2. 在Dockerfile里创建用户并设置合适的UID:
RUN groupadd -g 1000 appuser && useradd -u 1000 -g appuser appuser
USER appuser
  1. 或者,在宿主机上设置目录权限为777(仅用于开发)。

坑4:构建缓存的“薛定谔”状态

有时候我只是改了源码里的一行注释,重新构建镜像,却发现Docker又从apt-get update开始执行,慢得要死。有时候我希望它完全不使用缓存,它却固执地用了旧缓存。

我学到的构建缓存原理:

  • Docker的构建是一层层的,每一层都有哈希。
  • 如果某一层没变化,Docker就会复用缓存。
  • 一旦某一层发生了变化,它之后的所有层都会重新构建

所以,聪明的Dockerfile应该把不常变的内容放在前面:

# 1. 基础镜像
FROM openjdk:11-jre-slim

# 2. 安装依赖(不常变)
COPY requirements.txt .
RUN pip install -r requirements.txt  # 这层不常变

# 3. 复制应用代码(常变)
COPY . /app  # 这层一变,后面的层都会重建

如果想完全禁用缓存:docker build --no-cache .

第二章:Kubernetes篇——从“这玩意真酷”到“这玩意真复杂”

当我第一次用minikube在本地启动Kubernetes集群,看到dashboard上那些跳动的Pod时,感觉像是拿到了未来科技的钥匙。但很快,现实就来教我做人。

坑5:YAML地狱与“复制粘贴工程师”

我的第一个Deployment YAML是从网上抄的。改了镜像名,应用了,然后Pod一直CrashLoopBackOff。我看日志,日志说“配置文件找不到”。原来我从网上抄的YAML里,挂载了一个不存在的configMap。

血的教训:

  • 不要盲目复制YAML。理解每一行是干什么的。
  • 从最简单的开始:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp:latest
        ports:
        - containerPort: 8080
  • kubectl explain命令!这是你的救星。不知道某个字段什么意思?kubectl explain deployment.spec.template.spec.containers

坑6:那些永远“Pending”的Pod

我写了一个完美的Deployment,kubectl apply之后,Pod状态一直是Pendingkubectl describe pod一看,事件里写着“Insufficient cpu/memory”。

原来:

  • 我的minikube默认只给了2GB内存,而我的Pod请求了1.5GB内存,但节点上还有其他系统Pod在运行。
  • 资源请求(requests)和限制(limits)是两回事
resources:
  requests:
    memory: "512Mi"
    cpu: "250m"  # 250毫核
  limits:
    memory: "1Gi"
    cpu: "500m"

requests是调度依据,limits是运行限制。别乱写,写小了Pod可能被OOMKilled,写大了Pod可能永远调度不出去。

坑7:Service的“我有三种Type,但都不如人意”

我想让外部能访问我的应用。于是:

  1. NodePortkubectl get svc看到端口是32456,用minikube ip:32456访问,成功了!高兴了五分钟,然后发现重启minikube后IP变了,端口也变了。
  2. LoadBalancer:在minikube里,这其实只是NodePort的别名,真正的云上LoadBalancer是要钱的!
  3. 最后发现开发环境的神器:port-forward

    kubectl port-forward svc/myapp-service 8080:80

    现在访问localhost:8080就能访问服务了。但这只是临时的,断开命令就没了。

坑8:ConfigMap的“更新了但没完全更新”

我在ConfigMap里改了个配置,然后kubectl apply -f config.yaml。满怀期待地重启Pod,发现配置没变!原来,挂载为Volume的ConfigMap,更新后不会自动注入到已运行的Pod中。我必须删除Pod,让Deployment重建它。

但有个例外:如果通过环境变量envFrom引用ConfigMap,那Pod启动后就固定了,ConfigMap更新不会影响。

变通方案(开发环境可用):

  1. 直接kubectl edit configmap my-config,改完保存。
  2. 删除Pod:kubectl delete pod mypod-xxxxx
  3. 新Pod会读取新的ConfigMap。

坑9:那些“消失”的日志

kubectl logs mypod,发现日志只从一小时前开始。更早的日志呢?消失了。原来,容器默认的日志驱动是写到标准输出,但磁盘空间有限,会自动轮转清理。

几个选择:

  1. kubectl logs --previous看上一个容器的日志(如果Pod重启了)。
  2. 配置日志收集(EFK/Loki栈),这对开发环境太重了。
  3. 对于开发,最重要的习惯是:在出问题时第一时间kubectl logs,别等

第三章:本地开发工作流——寻找甜蜜点

经历了这些坑后,我意识到需要一个顺手的本地开发工作流。

我的当前方案:

  1. 用Docker Compose处理依赖:MySQL、Redis、RabbitMQ这些中间件,用docker-compose.yml一键启动。

    version: '3'
    services:
      mysql:
        image: mysql:8.0
        environment:
          MYSQL_ROOT_PASSWORD: root
        ports:
          - "3306:3306"
  2. 应用本身用Kubernetes,但用Telepresence或k8d:这让我能在本地IDE里调试代码,但代码能访问K8s集群里的其他服务。这是开发体验的质的飞跃。
  3. 用Skaffold自动化:在代码更改时自动重建镜像、更新K8s部署。在skaffold.yaml里配置:

    apiVersion: skaffold/v2beta3
    kind: Config
    build:
      artifacts:
      - image: myapp
        context: .
    deploy:
      kubectl:
        manifests:
        - k8s/*.yaml

    然后运行skaffold dev,就可以专注于写代码了。

最后的一点真心话

三年容器化之路,我最深的体会是:容器化不是目标,而是一种手段。它的目标是可重复、可移植、可扩展的软件交付。

不要为了用Kubernetes而用Kubernetes。如果你的应用只是一个简单的Spring Boot服务,也许用Docker Compose就足够了。只有当你有多个服务需要编排、需要自动伸缩、需要复杂的部署策略时,K8s的光芒才会真正显现。

从今天开始,你可以:

  1. 从Docker开始,把你的应用容器化。
  2. 在本地用minikube或kind(Kubernetes in Docker)搭个集群玩玩。
  3. 尝试把应用部署上去,遇到坑,解决它。
  4. 优化你的开发工作流,让容器化不是负担,而是助力。

这条路有很多坑,但每填平一个坑,你就比昨天的自己更强了一点。而且,当你第一次看到你的应用在K8s上自动扩缩容,或者你只用一条命令就把整个环境复制给新同事时,你会觉得,这一切都是值得的。

现在,如果你要开始你的容器化之路,我的建议是:从一个小应用开始,给自己一个周末的时间,允许自己犯错,允许自己谷歌。因为每一个踩过这些坑的人,都曾是那个对着“Connection refused”一筹莫展的新手。

版权声明 ▶ 本网站名称:黄磊的博客
▶ 本文标题:容器化之路:Docker & Kubernetes入门避坑指南
▶ 本文链接:https://www.huangleicole.com/backend-related/88.html
▶ 转载本站文章需要遵守:商业转载请联系站长,非商业转载请注明出处!!

如果觉得我的文章对你有用,请随意赞赏