ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Docker核心概念拆解:镜像分层、网络排障与数据持久化实战

Docker核心概念拆解:镜像分层、网络排障与数据持久化实战 Docker 核心概念这东西我一开始是吃了亏才愿意回炉重造的。当时接手一个项目要把跑着的 MySQL 容器整个搬到新机器图省事直接docker commit打了个“备份镜像”结果拖过去启动之后账户全乱、数据时好时坏最后花了整整一天去修。问题出在哪不是命令写错而是镜像和容器的关系压根没想清楚。后来陆陆续续把镜像分层、网络模型、数据卷、Compose 编排这些概念逐个抠透再回头看那些报错——virtualization support not detected、docker network 不通、容器一删数据就没了——全是同一个原因你以为你在操作容器其实你停在了概念的外围。这篇文章不打算把官方文档抄一遍而是从实际经验和排障视角把 Docker 最核心的几个概念拆开讲清楚镜像与容器的真正边界、分层文件系统为什么影响构建和体积、容器网络是怎么“不通”的、数据持久化三条路线怎么选以及 Docker Desktop 和 Docker Engine 报错背后的底层逻辑。适合刚装完 Docker 但总觉得它是黑盒的人也适合已经用docker run跑过几个服务、却被生产环境问题卡住的开发者。1. 一次部署事故的复盘镜像和容器差在哪里1.1 容器不是镜像快照而是镜像之上的“可写层”很多人会把镜像和容器理解成“模板和实例”的关系方向没错但不够精确。更贴近实际行为的描述是镜像是一个只读的静态文件集合容器是这堆文件之上多出来的一层可写空间再加上一套独立的进程状态。用生活里的例子类比镜像是一张压好的光盘容器是播放器里正在播放的画面。光盘内容怎么播都不变但你随时可以暂停、快进、退出播放过程中产生的临时进度、缓冲数据跟你手里的光盘没有任何关系。对应到 Docker 里docker run启动容器的那一刻引擎在镜像所有层之上挂载一个容器可写层进程对文件系统的所有修改都落在这个可写层。容器stop之后可写层还在start还能接着用但rm删除容器时可写层也会一并清理镜像则始终原封不动。当初我docker commit犯的错就是把这个可写层连同里面混乱的临时文件、日志、未刷盘的中间状态一起固化成了新镜像。这种镜像看起来能用实际携带了大量不该有的状态迁移之后大概率出问题。社区常说“不要用docker commit做镜像交付”不是因为它完全不能用而是它把容器运行时的“意外”当成了“配置”这正是镜像和容器概念边界模糊最容易踩的坑。1.2 容器生命周期背后是 namespace 和 cgroup再往深一层看容器不是一个独立操作系统而是宿主机上的普通进程只不过被 namespace 和 cgroup 隔离出了独立视角。PID namespace 让容器内进程以为自己是 1 号进程Mount namespace 让容器看到独立的文件系统挂载表Network namespace 让容器拥有自己的网络栈和 IP而 cgroup 负责限制 CPU、内存这些资源的使用量。所以docker run的真实动作是创建一系列 namespace、配置 cgroup、把镜像层挂载为只读根文件系统、叠加可写层然后启动进程。这也是为什么容器启动速度远远快于虚拟机——没有完整硬件虚拟化只是进程级别的隔离。搞清楚生命周期常用命令就很好背了操作命令说明创建但不起动docker create分配文件系统、网络配置不执行进程运行新容器docker runcreate start 的简化版停止/启动docker stop/docker start停止后容器还在可再次启动进入运行中容器docker exec -it 容器 bash开一个额外进程进入容器删除容器docker rm -f 容器-f强制删运行中的容器查看读写层变化docker diff 容器列出容器可写层相对镜像的文件变更镜像删不掉、容器删不掉这类“看起来报错很奇怪”的问题也适合在这个概念层面解释正在被容器使用的镜像、被其他镜像依赖的上层镜像都不能直接删除docker rmi删的是镜像层结构和容器删除是两套生命周期。2. 镜像不只是“打包好的程序”分层机制与构建缓存2.1 OverlayFS 与 Copy-on-Write 到底怎么工作镜像之所以能做到底层共享、传输高效靠的是分层结构。一个镜像由若干只读层叠加而成每层对应 Dockerfile 里的一条指令读取文件时从上往下查修改文件时先把目标文件从下层复制到上层再改这个过程叫Copy-on-Write而把所有层叠加成统一视图的机制在 Linux 上最常用的是 OverlayFS存储驱动 overlay2。这套机制解释了非常多看似离谱的现象。比如你在容器里删掉一个 50MB 的文件镜像体积不会变小因为删除只是在上层做一个“白化标记”下层的真实数据还在。再比如两个容器共享同一个镜像时底层镜像在宿主机上只存一份每个容器各写各的可写层互不干扰这也是容器能快速批量创建的成本基础。用docker history 镜像可以看到每一层的大小和创建指令用docker inspect 镜像能看到 RootFS.Layers 的哈希列表。如果一个镜像体积异常大别急着猜多半是某一层里塞了不该塞的大文件或者删除动作没有真正发生。2.2 Dockerfile 指令顺序、构建缓存与多阶段构建Docker 构建时会按 Dockerfile 指令顺序生成层每一层如果它的父层没变、指令没变、相关上下文没变就会命中构建缓存。指令顺序因此不只是风格问题直接决定构建速度。最典型的优化是先拷贝依赖描述文件再安装依赖最后才拷贝源码FROM node:20 AS builder WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY src ./src RUN npm run build FROM nginx:alpine AS release COPY --frombuilder /app/dist /usr/share/nginx/html第二段FROM nginx:alpine AS release是多阶段构建把编译阶段需要的 node 环境、npm 缓存全部留在第一个阶段最终镜像只需要 nginx 和构建产物。这种做法的收益是实打实的一个带完整构建工具链的镜像可能几个 GB多阶段构建后往往只有几十 MB。如果你在生产环境拉过 Gerrit、GitLab 这类大型应用镜像会被镜像体积吓到其原理也离不开层与依赖的取舍。另外.dockerignore非常重要。docker build会把整个上下文发给 daemon项目里若有node_modules、.git、日志文件不忽略的话每次构建都白白传输大量数据而且任何文件变化都会让后续 COPY 的缓存失效。2.3 镜像下载慢问题可能出在 registry 镜像源镜像分层还有个直接后果同一个基础镜像的不同应用镜像之间可以共享底层下载。比如 MySQL、Redis、GitLab 都用debian或alpine基础镜像本地已经有这些层的话新拉镜像往往只需要下载增量部分。至于热搜里常见的“docker镜像下载慢”绝大多数情况不是网速问题而是没有配置 registry mirror。Docker 默认从 Docker Hub 拉取镜像在国内就是不稳定。比较靠谱的方案是配置镜像加速器Linux 下编辑/etc/docker/daemon.json{ registry-mirrors: [ https://docker.m.daocloud.io ] }改完执行sudo systemctl daemon-reload sudo systemctl restart docker。Docker Desktop 则是在 Settings 的 Docker Engine 配置区直接改 JSON。配好之后重新拉镜像速度差别非常明显。注意镜像加速只影响拉取不影响你 push 到自己公司内部仓库——私有仓库是另一个概念对应的是 Registry 服务。3. 容器网络为什么“默认不通”一次连线排障记录3.1 默认 bridge 网络的瓶颈有 IP 也连不上很多人第一次用容器部署 MySQL启动后在本机输入localhost:3306连不上第一反应是 MySQL 配置问题其实问题在网络概念没理顺。默认情况下容器跑在名为bridge的虚拟网络里宿主机上会有一块docker0网桥容器被分配类似172.17.0.x的内部 IP。这个 IP 只在宿主机和容器之间连通外部客户端根本不知道这个地址。要让外部访问必须在docker run时做端口映射docker run -d --name mysql8 -p 3306:3306 -e MYSQL_ROOT_PASSWORDsecret mysql:8.0-p 3306:3306的意思是宿主机 3306 端口收到的流量转发到容器 3306。只写-p 3306时宿主机端口是随机分配写127.0.0.1:3306:3306就只监听本机回环地址外部机器访问不到。默认 bridge 还有一个非常隐蔽的坑容器之间能用 IP 互通但直接用容器名当主机名解析不出来。默认 bridge 不支持内建 DNS你起一个 MySQL 一个 RedisRedis 里配mysql8:3306去连它解析不了表现就是“网络不通”。3.2 自定义网络与容器间 DNSRedis 主从的实战解决容器互访最干净的做法是创建自定义 bridge 网络docker network create app-network docker run -d --name redis-master --network app-network redis:7 docker run -d --name redis-replica --network app-network redis:7 \ redis-server --replicaof redis-master 6379自定义网络会启用 Docker 内建的 DNS 解析容器名就是可用的主机名。同一网络里的容器之间互相访问再也不用猜 IP重启后 IP 变了也不影响配置。Redis 主从、MySQL 主从、应用连数据库这类“怎么都不通”的问题九成是没把服务放进同一个自定义网络。不同网络模式适用的场景差别很大我直接列一个对照表网络模式指定方式特点典型场景bridge--network bridge默认NAT 隔离需端口映射单机多容器隔离部署host--network host直接用宿主机网络栈无隔离性能敏感、需要大量端口none--network none只有回环接口离线计算、安全隔离任务container--network container:名称与指定容器共享网络栈服务间用 localhost 通信3.3 网络排障顺序先分清哪一段断了遇到“docker 网络不通”我的排查链路基本固定按顺序来在容器内部测回环和本服务docker exec 容器 curl 127.0.0.1:端口排除服务本身没起来。在宿主机上测映射端口ss -lntp | grep 端口确认端口监听在0.0.0.0还是127.0.0.1。测容器间互访docker exec A ping B容器名排查自定义网络是否配置。看宿主机防火墙iptables -L -n、firewalld或ufw status。Docker 默认通过 iptables 做端口转发有些发行版的防火墙规则会拦截 docker0 网桥的转发流量。确认是不是“假网络不通”比如客户端报failed to connect to the docker api at npipe:////./pipe/dockerDesktopLinuxEngine这其实是 Docker 客户端找不到 daemon属于引擎状态问题不是容器网络问题我在第六节专门展开。排查时注意一个容易误判的点容器内curl 127.0.0.1能通不代表宿主机能通宿主机能通不代表外部能通。每一段都有独立的防火墙和 IP 归属必须一层层缩小范围。4. 掉电后数据还在吗Volume、Bind Mount 与 tmpfs 的选型4.1 容器可写层的“假持久化”容器一删数据就没了这个现象第一次遇到时很容易让人怀疑 Docker 有 bug。实际上容器可写层天生就是临时的——它跟着容器生命周期走。docker stop不会清掉可写层所以很多人以为数据“已持久化”但docker rm一旦执行可写层连同里面所有数据一起销毁。生产环境里状态数据绝对不能只落在容器可写层。常见做法是把数据目录挂载到宿主机Docker 提供三条路Volume、Bind Mount、tmpfs。三者的核心区别是数据存在哪、生命周期由谁管理。方案挂载来源生命周期适合场景VolumeDocker 管理的目录在/var/lib/docker/volumes下独立于容器docker volume rm 才删数据库、应用数据、备份Bind Mount宿主机任意路径比如/data/mysql跟随宿主机文件开发热更新、挂配置文件tmpfs内存文件系统不落磁盘容器停止即清空缓存、密钥等敏感数据4.2 MySQL 容器为什么要用具名 Volume部署 MySQL 或 Redis最稳妥的是具名 Volume。命令是docker run -d --name mysql8 \ -v mysql-data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORDsecret \ mysql:8.0mysql-data这个卷由 Docker 统一管理位置对使用者透明。好处是备份恢复非常方便一条命令就能打包卷内容docker run --rm \ -v mysql-data:/data \ -v $(pwd):/backup \ alpine tar czf /backup/mysql-data.tar.gz -C /data .恢复时把容器数据目录先清掉再解压回去。Bind Mount 的写法是-v /data/mysql:/var/lib/mysql适合你明确要知道文件落在哪个路径的场景但容易碰到权限问题——容器内进程的 uid 和宿主机目录属主对不上MySQL 就会报权限错误目录起不来。规规矩矩用具名 VolumeDocker 会在创建卷时按容器需要的属主初始化目录这类权限问题会少很多。4.3 tmpfs 与开发热更新--tmpfs /tmp表示把容器/tmp挂成内存盘写入的内容不落宿主机磁盘。适合放缓存和临时文件容器停掉内容就没了但启动速度快而且不污染磁盘。开发阶段用 Bind Mount 挂源码目录很常见docker run -d --name web-dev \ -v $(pwd)/src:/app/src \ -p 8080:3000 \ node:20 node server.js宿主机改代码容器内立刻能读到配合 nodemon 之类的工具就直接热更新了。生产环境千万不要把整个源码目录用 Bind Mount 挂进去一是低效二是宿主机文件变动可能影响容器运行生产环境的代码应该固化在镜像里。选型的判断标准很简单代码是不可变镜像的一部分数据是可变的独立存储。5. 从多条 docker run 到 Compose 编排服务的依赖管理5.1 为什么单条 docker run 撑不起真实项目单容器跑一个服务很容易真实项目往往是 N 个服务数据库、缓存、后端、任务队列。全用docker run手动管理你会被三件事逼疯容器启动顺序靠记、网络要靠手动指定、同一套配置没法版本化。今天在测试机敲的命令明天到生产环境要重新敲一遍而且稍不留神端口就冲突了。这时候需要的是声明式编排工具Docker Compose 就是最主流的一个。它把容器配置写进一个 YAML 文件用docker compose up一键拉起一组服务。核心思路是把之前docker run的参数翻译成结构化配置让依赖关系、网络、存储、环境变量全部成为可复用的代码。5.2 services、networks、volumes 三个关键词一个典型的 Compose 文件长这样services: gitlab: image: gitlab/gitlab-ce:latest container_name: gitlab restart: unless-stopped hostname: gitlab.example.com ports: - 80:80 - 443:443 - 2222:22 volumes: - gitlab-config:/etc/gitlab - gitlab-logs:/var/log/gitlab - gitlab-data:/var/opt/gitlab shm_size: 256m redis: image: redis:7 restart: unless-stopped ports: - 6379:6379 volumes: - redis-data:/data volumes: gitlab-config: gitlab-logs: gitlab-data: redis-data:顶层volumes是声明卷服务里volumes:下挂载的是路径。networks同理如果 Compose 不声明网络Docker 会自动创建默认项目网络同一个 Compose 文件里的服务天然用服务名互访。这就是为什么 Compose 编排的微服务之间互相调用基本不需要配置 IP。GitLab 这类重量级应用用 Compose 部署尤其合适配置、日志、数据分别放到三个具名卷升级时只要换镜像版本再docker compose up -d数据不丢。如果服务之间有严格依赖比如后端必须先于任务队列启动可以用depends_on。5.3 depends_on 不是万能顺序保证depends_on只是控制容器启动顺序不是“等服务就绪”。比如services: app: image: my-app depends_on: - mysql mysql: image: mysql:8.0Docker 只会先启动 mysql再启动 app但如果 MySQL 还没完成初始化、端口还没监听app 里的连接照样失败。要真正等“就绪”得配合healthcheck把它理解成“启动顺序依赖”而非“健康依赖”。这也是新手最容易误用的配置出现“明明 depends_on 了还是连不上”的报错不要怀疑 Docker 执行错了而是健康检查没到位。另外restart: unless-stopped是我最常用的重启策略除了手动 stop容器异常退出和宿主机重启后都会自动拉起。生产部署时把它当成标配能避免大量“服务掉了没人发现”的尴尬。6. Docker Desktop 为什么会“起不来”引擎、daemon 和虚拟化后端6.1 你敲的 docker 命令只是个客户端很多人对docker有一个隐藏误解以为你敲的命令就是 Docker 本身。实际上docker只是 CLI 客户端真正干活的是一个后台守护进程dockerd两者的通信在 Linux 上默认走/var/run/docker.sock在 Windows 上走命名管道npipe:////./pipe/dockerDesktopWindowsEngine之类。docker run、docker pull这些命令都是客户端把请求发给守护进程去执行。所以当你看到failed to connect to the docker api at npipe:////./pipe/dockerDesktopLinuxEngine这类报错时第一反应不应该是“容器出了问题”而是“客户端根本没找到 daemon”。常见原因Docker Desktop 其实没启动、退到了托盘、WSL2 后端崩了。Linux 上对应的是Cannot connect to the Docker daemon先跑systemctl status docker看守护进程是不是挂了。6.2 虚拟化检测失败与 WSL2 后端的准备Docker Desktop 在 Windows 上不是直接跑 Linux 容器的它需要一个轻量虚拟机。新版默认用 WSL2 后端也可以切 Hyper-V。如果你启动时报virtualization support not detected本质是宿主机没有提供虚拟化能力给 Docker Desktop排查顺序一般是这样打开任务管理器性能标签里确认“虚拟化”已启用没启用就去 BIOS 打开 Intel VT-x 或 AMD SVM。PowerShell 以管理员身份启用必要功能dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart重启后执行wsl --set-default-version 2再启动 Docker Desktop。WSL2 后端对老系统不友好Win10 版本太旧、BIOS 没开虚拟化、或者同时装了 Hyper-V 冲突都会让 Docker Desktop 一直转圈然后报错。遇到这类问题把 Windows 更新补到较新版本通常是成本最低的解法。至于热搜里那个汉化包asxez/dockerdesktop-cn我只能说界面翻译不影响引擎行为。装了就图个菜单顺手但排障时还是建议切回英文界面因为所有报错日志和文档都基于英文术语中文界面反而容易让你找不到对应的配置项。6.3 Linux 上的权限与 daemon 配置Linux 安装 Docker 后最常见的两个坑一个是权限一个是启动。权限报错通常是permission denied while trying to connect to the Docker daemon socket原因是当前用户不在docker组里。解决sudo usermod -aG docker $USER newgrp docker重新登录后普通用户就能直接用docker命令不需要每次都sudo。注意docker组等同于宿主机 root 权限给用户加这个组要谨慎别在多人共用的机器上随手乱加。启动失败的话先看服务状态和日志sudo systemctl enable --now docker sudo journalctl -u docker -n 50常见原因包括磁盘空间不足、SELinux/AppArmor 策略拦截、daemon.json写错格式。修改了/etc/docker/daemon.json后如果 Docker 起不来多半是 JSON 语法有问题或者镜像加速地址不可用先用dockerd --validate之类的工具看看别反复重启硬试。最后分享一个我自己的排查习惯遇到 Docker 玄学问题先回到概念层问三个问题——当前数据写在哪个层网络请求走的是哪一段客户端连的是哪一个 daemon把这三个问题想清楚绝大多数“莫名其妙”的报错其实都有非常直接的答案。Docker 核心概念不难难的是愿意别把它当黑盒。
返回列表