第3章:容器技术选型——Docker核心概念与实战

各位同学,欢迎来到第三章。这一章我们聊聊容器技术选型。说实话,容器技术是PaaS平台的基石,选对了,后面事半功倍;选错了,后面全是坑。我个人在早期做PaaS时,就因为在容器运行时上踩过坑,导致线上频繁重启,那叫一个惨。

好,咱们直接进入正题。这一章我会带你吃透Docker的核心概念,再聊聊Dockerfile怎么写才专业,最后对比一下主流的容器运行时。嗯,内容有点多,但都是干货。

3.1 Docker核心概念:镜像、容器、仓库

这三个概念,说白了就是「做菜的三个步骤」。

  • 镜像(Image):就是菜谱。它定义了你要做什么菜、用什么料、怎么炒。镜像是只读的,不能改。
  • 容器(Container):就是炒出来的那盘菜。它是镜像的运行实例,可以读写,可以启动、停止、删除。
  • 仓库(Registry):就是菜谱库。你把做好的镜像推到仓库里,别人就能拉下来用。

我在项目中遇到过一个问题:有同事把镜像当容器用,直接在容器里改配置,然后commit成新镜像。这种做法其实很不推荐。为什么呢?因为这样会丢失构建的可重复性。你想想看,如果哪天容器挂了,你还能复现出同样的镜像吗?

核心原则:镜像应该是不可变的。任何修改都应该通过Dockerfile重新构建,而不是在容器里手动改。

3.2 Dockerfile最佳实践

写Dockerfile,我建议你记住几个原则。这些原则是我在几十次构建失败后总结出来的。

3.2.1 选择合适的基础镜像

别一上来就用ubuntu:latest。我见过一个项目,基础镜像1.2GB,结果里面90%的包都用不上。你想想看,每次拉取、构建、部署都要多花多少时间?

我个人习惯用alpine或者slim版本。比如:

FROM python:3.9-slim
# 或者
FROM alpine:3.15

3.2.2 减少层数,合并RUN命令

Dockerfile的每一行都会生成一个层。层多了,镜像就大,构建也慢。我建议把相关的命令合并到一个RUN里:

# 不推荐
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim

# 推荐
RUN apt-get update && \
    apt-get install -y curl vim && \
    rm -rf /var/lib/apt/lists/*

嗯,这里要注意:最后一定要清理缓存。否则镜像里会残留一堆没用的包文件,白白占用空间。

3.2.3 使用.dockerignore

这个文件很多人会忽略。我曾经见过一个项目,把整个node_modules目录都打包进了镜像,结果镜像大小直接飙到2GB。你想想看,这多离谱?

正确的做法是:

# .dockerignore
node_modules
.git
*.md
.gitignore

3.2.4 多阶段构建

这个技巧特别实用。比如你要编译一个Go程序,编译环境需要golang,但运行环境只需要一个二进制文件。多阶段构建就能完美解决:

# 第一阶段:编译
FROM golang:1.18 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp

# 第二阶段:运行
FROM alpine:3.15
WORKDIR /app
COPY --from=builder /app/myapp .
CMD ["./myapp"]

这样最终镜像只有几十MB,而不是几百MB。我在生产环境中一直用这个方案,效果非常好。

小技巧:如果你用多阶段构建,记得给每个阶段起个名字(比如AS builder),这样COPY的时候更清晰。

3.3 Docker Compose编排入门

单机环境下,Docker Compose是编排多容器应用的最佳选择。说白了,它就是用一个YAML文件定义你的应用服务。

我举个例子,一个典型的Web应用,包含前端、后端、数据库:

version: '3.8'
services:
  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    depends_on:
      - backend

  backend:
    build: ./backend
    ports:
      - "8080:8080"
    environment:
      - DB_HOST=db
      - DB_PORT=5432
    depends_on:
      - db

  db:
    image: postgres:13
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

你看,一个文件就把三个服务串起来了。启动只需要一条命令:

docker-compose up -d

我在项目中遇到过一个问题:有人把Compose文件写得特别复杂,一个文件里定义了十几个服务。结果每次启动都要等半天,而且出了问题很难排查。我的建议是:一个Compose文件不要超过5个服务,如果服务太多,拆分成多个文件。

避坑指南:我曾经在生产环境用Compose管理有状态服务(比如数据库),结果容器重启后数据丢了。后来才意识到,有状态服务一定要用volume持久化数据。切记!

3.4 容器运行时对比:runc、containerd、CRI-O

这部分内容比较底层,但作为PaaS平台的架构师,你必须了解。容器运行时,说白了就是真正负责「跑容器」的那个组件。

运行时 特点 适用场景 我的评价
runc OCI标准实现,轻量级 Docker默认运行时 稳定可靠,但功能单一
containerd 工业级容器运行时,支持镜像管理 Kubernetes默认运行时 推荐生产环境使用
CRI-O 专为Kubernetes设计,轻量安全 Kubernetes专用场景 适合对安全性要求高的场景

我简单解释一下它们的区别:

  • runc:它是OCI标准的参考实现。Docker早期就是直接调用runc来创建容器的。但runc只负责「创建容器」这一件事,其他事情(比如拉取镜像、管理网络)都不管。
  • containerd:它是从Docker里拆分出来的。说白了,它把runc包了一层,增加了镜像管理、存储管理等能力。现在Kubernetes默认就用containerd。
  • CRI-O:它是红帽主导的项目,专门为Kubernetes的CRI接口设计的。它比containerd更轻量,但生态相对小一些。

我在项目中遇到过一个问题:早期我们用的Docker版本比较老,底层还是runc。后来升级到containerd后,发现性能提升了20%左右。为什么会这样?因为containerd对镜像层做了缓存优化,减少了重复拉取。

我的建议:如果你在搭建PaaS平台,直接选containerd作为容器运行时。它成熟、稳定、性能好,而且Kubernetes官方推荐。除非你对安全性有极端要求,否则不用考虑CRI-O。

3.5 本章小结

好,这一章的内容就到这里。我们讲了Docker的三大核心概念,Dockerfile的最佳实践,Docker Compose的入门用法,以及容器运行时的对比。嗯,内容确实不少,但这些都是PaaS平台的基石。

下一章,我们会聊聊容器网络。到时候我会分享一些我在生产环境中遇到的网络坑,保证让你少走弯路。

公众号:蓝海资料掘金营,微信deep3321