第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