3、容器化基础:Docker核心概念、Dockerfile编写技巧、镜像仓库管理、容器网络与存储
聊到PaaS平台,容器化是绕不开的基石。说白了,没有Docker,就没有今天这么繁荣的云原生生态。我最早接触Docker还是1.9版本,那时候很多人觉得它就是个轻量级虚拟机。嗯,这个误解到现在还有人信。
这一章,我们就把Docker的核心概念、Dockerfile怎么写、镜像怎么管、网络和存储怎么配,一次性讲透。都是我在生产环境里摸爬滚打出来的经验。
3.1 Docker核心概念:镜像、容器、仓库
这三个概念,你闭着眼睛都得能说清楚。
- 镜像(Image):一个只读的模板,包含了运行应用所需的一切——代码、运行时、库、环境变量、配置文件。你可以把它理解成Java里的类。
- 容器(Container):镜像的运行实例。它是可写的,有生命周期。相当于Java里new出来的对象。
- 仓库(Registry):存放镜像的地方。Docker Hub是官方的,但企业里我们一般用Harbor或自建的。
重要:镜像分层是Docker的精髓。每一层都是只读的,容器层是唯一可写层。这种设计让镜像复用、传输、存储都变得极其高效。
我在项目中遇到过一个问题:有同事把容器当虚拟机用,进去装了一堆东西,然后commit成一个新镜像。结果镜像体积从200MB膨胀到2GB。这种做法,说白了就是反模式。
3.2 Dockerfile编写技巧
Dockerfile写得好不好,直接决定了镜像的质量。我见过太多人把Dockerfile写成了一锅粥。下面是我总结的几个核心技巧。
3.2.1 基础镜像选择
别一上来就FROM ubuntu:latest。你想想看,一个Java应用,用openjdk:11-jre-slim就够了,何必带一堆用不上的工具?
| 基础镜像 | 体积 | 适用场景 |
|---|---|---|
| ubuntu:20.04 | ~200MB | 需要完整系统工具链 |
| alpine:3.15 | ~5MB | 追求极致体积,但注意glibc兼容性 |
| openjdk:11-jre-slim | ~200MB | Java运行时,去掉了编译工具 |
| golang:1.18-alpine | ~300MB | Go编译环境,多阶段构建常用 |
我的习惯:能用slim不用full,能用alpine不用ubuntu。但要注意,alpine用的是musl libc,有些依赖glibc的程序会出问题。我曾经被这个坑过,一个Python的C扩展在alpine上编译了半天。
3.2.2 指令顺序与层缓存
Docker构建时会缓存每一层。如果某层没变,就直接用缓存。所以,把变化最少的指令放在前面。
# 不推荐:先COPY代码,再装依赖
FROM node:14-alpine
COPY . /app
RUN npm install
# 推荐:先装依赖,再COPY代码
FROM node:14-alpine
COPY package.json package-lock.json /app/
RUN npm install
COPY . /app
为什么?因为代码经常改,但package.json不常改。这样改代码时,npm install那层缓存还能用,构建速度能快好几倍。
3.2.3 多阶段构建
这是我最喜欢的功能。你想想看,编译环境需要一堆工具,但运行时只需要编译好的二进制。多阶段构建完美解决了这个问题。
# 第一阶段:编译
FROM golang:1.18-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
# 第二阶段:运行
FROM alpine:3.15
COPY --from=builder /app/myapp /usr/local/bin/
CMD ["myapp"]
最终镜像只有几MB,而不是几百MB。我在做微服务PaaS平台时,所有服务都用这种方式构建,镜像仓库的存储压力小了很多。
3.3 镜像仓库管理
镜像仓库是PaaS平台的“粮仓”。管理不好,后面全是坑。
3.3.1 仓库选型
- Docker Hub:公共仓库,适合开源项目。但拉取有速率限制,企业用不现实。
- Harbor:企业级首选。支持RBAC、镜像复制、漏洞扫描、垃圾回收。我们PaaS平台用的就是它。
- Nexus:如果团队已经用了Nexus做Maven仓库,可以顺便管镜像。
3.3.2 镜像命名与标签规范
没有规范,仓库就是垃圾场。我建议这样命名:
# 格式:仓库地址/项目名/服务名:版本号-环境
registry.paas.com/order-service/order-api:1.2.3-prod
registry.paas.com/user-service/user-svc:2.0.1-staging
注意:永远不要用latest标签。你根本不知道它指向哪个版本。我曾经排查过一个线上问题,发现两个节点的latest镜像不一样,因为构建时间差了10分钟。从那以后,我强制团队必须用语义化版本号。
3.3.3 垃圾回收与清理
镜像越积越多,磁盘迟早会爆。Harbor支持自动垃圾回收,但需要手动触发或定时任务。我一般这样配:
- 保留最近10个版本的镜像
- 超过30天的旧版本自动删除
- 每周日凌晨执行一次垃圾回收
3.4 容器网络
网络是容器化里最容易出问题的地方。Docker提供了几种网络模式,各有各的用途。
3.4.1 网络模式对比
| 模式 | 特点 | 适用场景 |
|---|---|---|
| bridge | 默认模式,容器通过NAT访问外网 | 单机多容器通信 |
| host | 容器直接使用宿主机网络栈 | 性能敏感型应用,如Nginx |
| none | 无网络,只有lo接口 | 安全隔离场景 |
| overlay | 跨主机容器通信,需要Swarm或K8s | 多机集群 |
我个人习惯,单机调试用bridge,生产环境用overlay(配合K8s的CNI插件)。host模式虽然性能好,但端口冲突是个大问题。
3.4.2 自定义网络
默认的bridge网络有个问题:容器之间只能通过IP通信,不能通过服务名。自定义bridge网络可以解决这个问题。
# 创建自定义网络
docker network create --driver bridge my-net
# 启动容器时指定网络
docker run -d --name web --network my-net nginx
docker run -d --name app --network my-net myapp
# 现在app容器里可以直接 ping web
这个特性在微服务架构里特别有用。服务发现都不用额外组件,Docker自带的DNS就能搞定。
3.5 容器存储
容器是短暂的,但数据需要持久化。Docker提供了三种存储方式。
3.5.1 存储方式对比
| 方式 | 特点 | 适用场景 |
|---|---|---|
| Volume | 由Docker管理,存在宿主机特定目录 | 数据库、配置文件等需要持久化的数据 |
| Bind Mount | 直接映射宿主机目录到容器 | 开发环境热更新、日志收集 |
| tmpfs | 存在内存中,容器停止后消失 | 敏感数据、临时缓存 |
3.5.2 Volume的使用
生产环境我强烈推荐用Volume。它比Bind Mount更安全,也更方便迁移。
# 创建Volume
docker volume create mysql-data
# 使用Volume
docker run -d \
--name mysql \
-v mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=root \
mysql:8.0
避坑指南:我曾经把MySQL的数据目录用Bind Mount挂载到宿主机的一个普通目录。结果宿主机磁盘满了,MySQL直接崩溃。后来改用Volume,配合定期备份,再也没出过问题。Volume的存储位置由Docker管理,你不需要关心它在哪,只要知道它叫mysql-data就行。
3.5.3 存储驱动选择
Docker支持多种存储驱动,比如overlay2、aufs、devicemapper。现在主流是overlay2,性能好,稳定性高。
如果你还在用aufs或devicemapper,我建议尽快迁移到overlay2。我在一次性能测试中发现,overlay2的IOPS比devicemapper高了30%以上。
好了,这一章的内容就到这里。Docker的基础打牢了,后面讲K8s和PaaS平台编排的时候,你会觉得轻松很多。下一章我们聊聊容器编排的进阶话题。