尧图精选

从Namespace到Cgroups:Docker容器原理与MySQL、Redis实战踩坑指南

🕒 发布时间:2026/9/14 17:15:51 📁 来源:尧图网络
聊到 Docker很多人第一步是背命令第二步是照抄别人的docker run第三步是遇到容器起不来、数据丢失、网络不通时彻底傻眼。我有过这种经历装好 Docker Desktop敲了一行docker run -d -p 8080:80 nginx浏览器立刻能访问当时只觉得神奇但要我说清“Docker 原理”嘴里只剩下“容器就是一个独立的环境”这种空话。这篇文章不打算罗列命令而是从镜像分层、命名空间、控制组、网络与存储这些底层机制一路讲到 Windows 上 Docker Desktop 的虚拟化问题、MySQL 8.0 容器化、Redis 主从编排的真实踩坑点。适合两种人看一是刚接触容器想建立完整知识框架的新手二是已经用了一阵子 Docker但总觉得哪里没想通想补上最后一块拼图的开发者。1. 容器这东西到底是怎么“骗”过操作系统的我第一次真正理解容器是在一台 Ubuntu 服务器上同时跑了 20 个 Redis 容器宿主机的内存只多了不到 200MB。那一刻我才意识到容器不是虚拟机它不会为每个应用准备完整内核而是在“共用同一个内核”的前提下让每个进程以为自己独占了一台机器。这套“骗术”的核心就是 Linux 的 Namespace 和 Cgroups外加镜像分层。1.1 从虚拟机到容器为什么会有 Docker虚拟机的思路是模拟硬件再在模拟出来的硬件上装完整操作系统。一个 CentOS 虚拟机通常要占好几 GB 磁盘、1GB 内存起步。Hypervisor 负责“翻译”虚拟机的 CPU 指令、内存访问这个翻译动作本身就是性能损耗。容器绕开了硬件模拟直接调用宿主机内核的系统调用。Docker 不创建新系统它只是把宿主机内核的资源再切分、再隔离。也就是说你在 Windows 上用 Docker Desktop 跑的 Linux 容器本质是通过一个轻量级 Linux 虚拟机比如 WSL2提供 Linux 内核然后在那个内核上跑容器。这也是为什么 Windows 上的 Docker 必须开启虚拟化功能否则内核都起不来。理解这层关系后很多问题就通了为什么容器里的uname -r和宿主机一样因为用的是同一个内核。为什么容器里不能随便换内核模块因为内核不是它的。为什么容器启动快因为不需要从磁盘加载操作系统只需要启动一个进程。1.2 Namespace给进程造一个“单间”Namespace 是 Linux 内核提供的一种资源隔离能力。它让进程只能看到被分配给你的那部分系统资源而不是整个全局资源。你可以把它想成公司工位隔板一立每个人看到的区域就是自己的工位其实整层楼还是共用的。Docker 创建容器时会为容器里的进程设置几个关键的 NamespacePID Namespace容器里的进程 PID 从 1 开始看不到宿主机的其他进程。Network Namespace容器有自己的网卡、IP、路由表、iptables 规则像是独立的小机房。Mount Namespace容器只能看到挂载到它视图里的文件系统宿主机其他目录对它来说不存在。UTS Namespace容器有自己的 hostname。IPC Namespace容器有自己的消息队列、信号量等进程间通信资源。User Namespace可以映射用户权限比如让容器里的 root 实际上是宿主机上的普通用户。Cgroup Namespace隔离控制组视图让容器里的进程看不到宿主机的 cgroup 层级。正是靠这些 Namespace 的叠加容器内进程才产生了“我独占一台机器”的错觉。你可以在容器里执行ps aux看到 PID 1 是应用进程感觉非常干净这就是 PID Namespace 的效果。但注意这些隔离大多数情况下是逻辑隔离不是完整的安全边界。内核漏洞一旦被利用容器是有可能穿透这种隔离的所以生产环境不能为了图省事就--privileged满天飞。1.3 Cgroups给进程定一个“饭量”Namespace 解决的是“看得见什么”Cgroups 解决的是“能用多少”。你可以把 Cgroups 理解成食堂的分餐制不管你多能吃最上面控制你的人只会给你打固定量的饭。Cgroups 是 Linux 内核的另一个机制用来限制、记录和隔离进程组的资源使用。Docker 对 Cgroups 的使用主要体现在这些子系统上cpu限制 CPU 时间片。memory限制内存使用量超过会触发 OOM。blkio限制块设备的读写速率。pids限制进程数量。举个例子用下面的命令启动一个最多用 100MB 内存、最多用 0.5 个 CPU 核的容器docker run -d --name demo --memory100m --cpus0.5 nginx在宿主机上你还能找到对应的 Cgroups 条目比如查看容器的完整 IDdocker inspect demo | grep -i cgroup到/sys/fs/cgroup/memory/docker/id/memory.limit_in_bytes里能看到数字被写成了104857600。这就是 Cgroups 在背后做限制。很多人问“容器为什么不会把宿主机内存打爆”其实答案很朴素如果每个容器都设置了内存上限那么所有容器的总和就是资源天花板。如果不设置容器能用多少内存完全看它自己的行为一旦失控就可能拖垮宿主机。我在实际项目中见过一个没设内存限制的容器线程爆炸后把整台服务器 OOM 重启从那以后我养成了习惯生产容器一律通过 compose 文件或运行参数写死内存上限。1.4 镜像的分层存储为什么一个镜像能到处跑Docker 镜像不是一个大 ISO 文件它是由很多只读层叠加而成的。每一层对应 Dockerfile 里的一条指令层和层之间用 UnionFS联合文件系统合并对外看起来是一个完整的文件系统。打个比方镜像就像千层蛋糕。FROM 那层是底座RUN 安装的依赖是中间层COPY 的代码是顶层。不管你把这个蛋糕从开发机推到生产服务器层的内容都不会变所以体积、哈希都稳定能到处复用。docker pull时你会看到输出写着Pull complete每拉一层都会显示对应哈希。如果在多台机器上拉同一个镜像只要本地已经存在某一层它就不会重复传输这就是分层节省带宽的原理。容器也依赖这个设计。当我们从镜像启动容器时Docker 不会复制整个镜像而是在这些只读层之上创建一个可写层容器运行时所有文件变更都写在这一层。进程要读文件时UnionFS 会让它先看可写层再一层层往下找。这样同一个镜像被同时启动成几十个容器共享的是底层只读层磁盘占用不会成倍增长。镜像层不可变这一点特别重要。你容器里改了/etc/nginx/nginx.conf这个修改只存在于当前容器的可写层里一旦docker rm修改就随容器一起消失。想持久化必须用卷Volume或重新构建镜像。这个教训我后面讲数据持久化时还会再展开。2. 从 Dockerfile 到容器一次干净的“孵化”过程搞懂镜像分层后再看 Dockerfile 就容易多了。很多新手以为 Dockerfile 就是“写一堆 Linux 命令”这个理解不够准确。每条指令都会产生一个镜像层而层的顺序、内容直接影响镜像体积、构建速度和容器的行为。2.1 Dockerfile 的每一行都在干什么下面这个例子是典型的 Node.js 应用镜像FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm install --registryhttps://registry.npmjs.org COPY . . EXPOSE 3000 CMD [node, index.js]逐行解释FROM指定基础镜像。它是所有层的底座用什么基础镜像基本决定了上层能装什么东西。WORKDIR设置工作目录。它会自动创建目录并影响后续指令的执行路径。COPY package*.json ./把宿主机上下文里的文件复制进镜像。注意我特意把package.json和源码分开复制是为了利用分层缓存。如果你把整个目录先 COPY 进去再 RUN npm install那么只要源码有任何修改依赖缓存就会全部失效每次构建都要重新装依赖。RUN npm install在镜像里执行命令会创建一个新层。这里有个细节RUN 层会保留所有临时文件如果你在一条 RUN 里下载、解压、删除临时文件能有效控制体积。COPY . .复制源码。EXPOSE只是声明容器内服务监听的端口不会真的做端口映射更多的像是“产品说明书”。CMD定义容器启动时的默认命令。CMD 可以被docker run后面的命令覆盖。另一个关键指令是ENTRYPOINT。CMD 可以当成默认参数被覆盖ENTRYPOINT 则定义容器的主程序二者经常配合使用。比如ENTRYPOINT [nginx, -g] CMD [daemon off;]这样docker run nginx -t时-t会作为参数传给 nginx覆盖掉 CMD而不是替换掉主程序。2.2 容器启动时引擎究竟做了什么执行docker run -d --name web -p 8080:80 nginx后Docker 引擎并非只是“启动 nginx”它要完成一系列步骤检查本地是否存在 nginx 镜像不存在则从 registry 拉取。基于镜像创建容器的可写层。按照网络配置创建 Network Namespace并分配 IP。解析端口映射写入 iptables 规则。设置 Cgroups 资源限制。把镜像里配置的挂载点挂到宿主机目录或卷。在执行环境里启动容器的 PID 1 进程。其中任何一步出了问题容器都会启动失败。排查时不要只看应用日志还要看docker inspect里的 State 字段和事件日志。我在团队里反复强调容器启动失败不等于应用报错可能是网络、权限、挂载、内核能力这些“外围因素”导致的。一个典型的例子是容器需要绑定 80 端口但宿主机 80 端口已被占用docker run会直接报端口冲突不细看还以为是应用配置错了。2.3 为什么容器启动快写时复制与 UnionFS启动容器为什么比启动虚拟机快因为容器不需要引导操作系统只需要创建一个可写层并运行进程。但这还不够真正让它高效的是写时复制Copy-on-Write机制。写时复制的核心思想是只有在需要修改数据时才复制什么都不改就不动。容器启动时可写层是空的进程读取文件时直接读共享的底层镜像层完全不占额外空间。只有当进程要写文件时UnionFS 才会把那个文件从底层复制到可写层然后再修改。这带来两个好处容器启动几乎不耗时可以秒级扩容。多个容器共享底层镜像内存页缓存也能共享宿主机的整体资源利用率更高。反过来这也解释了为什么容器不适合存状态数据。你频繁写文件可写层会不断变大而且这些数据随容器生命周期而生灭一旦容器重建数据就没了。所以设计上应该把应用视为“无状态牲畜”把数据放到卷里而不是留在容器里。2.4 基础镜像为什么越做越小Alpine 带来的思考基础镜像的选择直接影响部署效率。Ubuntu 基础镜像一般 70MB 左右CentOS 更大而 Alpine 只有几 MB。Alpine 之所以小是因为它用 BusyBox 和 musl libc 替代了传统的 GNU 工具链和 glibc很多常用命令都以 symlink 方式提供体积被压到极致。但体积小不等于零成本。musl libc 和 glibc 在解析 DNS、处理多线程、某些数学库上有细微差别。比如有些依赖原生 Node.js 模块或 Python wheel 的镜像如果只提供 glibc 版本在 Alpine 里可能跑不起来需要额外装编译工具链反而让镜像更大。我的经验是能用 Alpine 优先用 Alpine但跑机器学习模型、需要复杂原生依赖的服务还是选 Debian slim 更省心。这个选择不只看体积还要看软件生态和线上无人值守情况。Google 的 distroless 镜像是另一个方向连 shell、包管理器都不带只有运行环境安全和体积都向生产靠拢但排查问题时没有 bash 也会让你抓狂。最终选型要看团队运维习惯。3. 容器网络和数据最容易被“坑”的两个地方原理看再多真正动手时最容易出问题的永远是网络和数据。我接手过的项目里有一半的容器故障来自网络配置错误另一半来自数据没做持久化。这两块必须单独拿出来聊。3.1 五种网络模式生产上到底选哪个Docker 默认的网络驱动是 bridge但生产环境远不止这一种。下面这张表是我自己整理的基本能覆盖大多数场景网络模式一句话说明适用场景注意点bridge容器通过虚拟网桥互连默认模式单机多容器通信端口映射容器间可以用 IP 或 link 名访问host容器直接使用宿主机网络栈无独立 IP追求最低网络延迟或绑定宿主机端口多端口会直接占用宿主机不能映射none容器无网络只有回环接口隔离计算任务或手动配置网络默认没有网络需自行设置container复用另一个容器的网络栈sidecar 模式如日志采集、流量代理两个容器共享 IP 和端口空间overlay跨宿主机容器网络VXLAN 等隧道技术封装多机集群、docker swarm、k8s pod需要额外配置集群存储和网络键单机开发时bridge 是最常用的。多个容器通过自定义 bridge 网络连在一起不用刻意记住 IP直接用容器名做 DNS 解析。比如docker compose up之后web容器里直接curl mysql:3306能通靠的就是自定义 bridge 自带的 DNS 解析。host 网络在低延迟场景有优势因为没有 NAT 层的转发开销。但它把容器和宿主机网络耦合得太深跟“隔离”的理念相悖而且端口冲突问题会让人非常头疼我一般在需要频繁暴露端口、或要求 UDP 组播场景下才用。3.2 端口映射背后的 iptables/NAT 机制-p 8080:80这个参数看起来简单背后其实是一整套 NAT 规则。Docker 会在宿主机的 iptables 的DOCKER链里写入 DNAT 规则把访问宿主机 8080 端口的数据包目标地址改写为容器的 IP:80然后通过 Docker 网桥转发到容器。这里有个容易踩的坑如果你自己在宿主机上写了很严格的 iptables 规则比如默认 DROP 所有转发流量那么即便-p写了端口映射外部也访问不到容器。你排查时docker ps看到端口映射是正常的ss -lntp也可能没看到 NodePort但流量就是进不去最后往往是 iptables 的 FORWARD 链拦住了。还有个更隐蔽的问题Docker 会动态修改 iptables有时它会把你手动设置的规则覆盖掉。所以我的建议是别在宿主机层面跟 Docker 抢 iptables 控制权除非你非常清楚规则顺序。生产上需要精细控制流量时应该用反向代理Nginx、Traefik或服务网格而不是试图自己在宿主机上手工拼规则。3.3 容器里的数据怎么不丢Volume、Bind Mount、tmpfs容器是可丢弃的但数据不能丢。Docker 提供了三种挂载方式Volume由 Docker 管理存放在/var/lib/docker/volumes/下。持久性最好推荐用于数据库数据、应用配置。Bind Mount把宿主机任意目录挂到容器目录。适合开发环境改代码即时生效但跨平台时路径差异要注意。tmpfs内存文件系统容器停止即清空。适合存储临时文件比如认证 token、缓存。以 MySQL 为例最稳妥的做法是挂载一个 volume 到/var/lib/mysqldocker run -d \ --name mysql8 \ -e MYSQL_ROOT_PASSWORDyourpass \ -p 3306:3306 \ -v mysql_data:/var/lib/mysql \ mysql:8.0这里的mysql_data是命名的 volume即使容器被删掉、重建数据依然在。如果你直接把卷挂成了/var/lib/mysql但宿主机目录权限不对MySQL 可能启动就报权限错误这在后面实操里会展开。Bind Mount 在开发时很爽因为你改了宿主机代码容器里立刻就能看到。但要注意容器内运行的用户 UID 可能和宿主机用户不同导致文件“Permission denied”。我经常在团队里说先搞清楚容器进程以什么用户运行再去想权限问题不要一上来就--privileged那样迟早会出事。3.4 跨主机通信Calico 和 Overlay 网络的一点理解单机容器网络好理解跨主机容器通信就要思考“容器 A 在机器1容器 B 在机器2两者 IP 还不能冲突怎么通信”最朴素的方法是 Overlay 网络把容器流量封装在宿主机之间的 VXLAN 隧道里。Docker Swarm 内置的 overlay 网络用的就是这个机制。Calico 的做法不一样。它不走 VXLAN 数据面封装而是用 BGP 协议把每个节点的容器网段路由信息广播出去底层是纯三层路由。数据包从容器 A 发出宿主机内核根据路由表直接把包转发到机器2不需要隧道封装性能接近宿主机网络。如果你在用 KubernetesCalico 是常见的 CNI 插件之一。它的 BGP 模式适合对性能有要求的集群但要求底层网络允许 BGP 协议传输。VXLAN 模式则更通用能跨二层网络跑但封装解封装会带来一点 CPU 开销。两种模式不是替代关系更多是适用场景不同。理解了这一点后面看 Calico 的配置就不容易懵。4. Windows 上跑 Docker Desktop虚拟化、WSL2 和权限的“连环坑”热搜里最多的 Docker 问题几乎全部围绕 Windows。作为一个在 Windows 上被 Docker Desktop 折磨过很久的人我太懂那种“明明按教程做的就是不启动”的感觉了。这些问题单独看都是小事连起来却能把人卡死。4.1 为什么总是报“virtualization support not detected”Docker Desktop 在 Windows 上依赖虚拟化技术。如果启动时提示virtualization support not detected最常见的原因有三个第一CPU 虚拟化被 BIOS/UEFI 禁用了。Intel 的 VT-x 或 AMD 的 SVM 没打开。你去 BIOS 里找到 Intel Virtualization Technology 或 SVM Mode把它设为 Enabled 就行。第二Windows 的虚拟机平台组件没启用。在“启用或关闭 Windows 功能”里勾选“虚拟机平台”和“适用于 Linux 的 Windows 子系统”。第三Windows 自带 Hyper-V 和第三方虚拟化软件冲突比如旧版本 VMware、VirtualBox 占用了虚拟化特性。排查顺序我一般是这样先看任务管理器里“性能”页CPU 部分是否有“虚拟化已启用”再看 Windows 功能里虚拟化相关的选项是否勾选最后才是 BIOS 设置。80% 的问题出在前两步。4.2 WSL2 还是 Hyper-V选哪个更合理Docker Desktop 支持两种后端WSL2 和 Hyper-V。Hyper-V 是 Type 1 hypervisor镜像、容器都跑在专门创建的虚拟机里。WSL2 虽然也基于虚拟化平台但它启动的是轻量级实用工具内存管理、启动速度、跨版本升级都更顺手。我的建议是优先用 WSL2。它支持docker命令直接跑在 WSL2 的 Linux 发行版里和 Linux 服务器上的体验几乎一致。你在 PowerShell 里装的 Docker Desktop底层其实是把 Docker daemon 跑在了 WSL2 的发行版里。这样你在 Windows 相关工具链和容器命令间切换很自然也不容易碰到 Hyper-V 独占资源的问题。但也有例外。如果项目要求必须在 Hyper-V 场景下测试嵌套虚拟化或者需要用旧版 Boot2Docker 兼容方案可以切回 Hyper-V。大多数现代开发场景WSL2 是更舒服的选择。4.3 Windows 下把 Docker 用得顺手的几个设置WSL2 后端有个特点Docker 镜像默认存放在 WSL2 发行版虚拟磁盘ext4.vhdx里路径一般在%LOCALAPPDATA%\Docker\wsl\disk\docker_data.vhdx。这个文件会在不知不觉中膨胀。镜像拉多了、容器建多了虚拟磁盘能占到几十 GB。如果 C 盘空间紧张最简单的办法是把 WSL2 虚拟磁盘迁移到 D 盘网上很多教程都有本质是把发行版导出再导入。另一个实用技巧是在.wslconfig里限制资源[wsl2] memory4GB processors2 swap2GB这样不会让 WSL2 无限制吞掉整机内存。如果你打开很多 IDE、浏览器、数据库客户端这个限制能救你一把。再就是镜像加速配置。Docker Desktop 的 Settings - Docker Engine 里可以直接编辑 registry-mirrors。配置前先确认加速源是否稳定避免以后拉镜像时莫名其妙失败。我见过有人把加速源配置错结果所有docker pull都超时反而是关了加速源就好了。4.4 “需要来自 Administrators 的权限才能删除”到底是什么原理很多人误以为这是 Docker 的问题其实是 Windows 的 NTFS 权限设计。Docker Desktop 在安装时会创建服务、虚拟磁盘、网络适配器等资源某些目录和文件默认只允许 Administrators 或 SYSTEM 账户访问。当你的 Windows 用户虽然属于管理员组但 UAC 没有提权时资源管理器里删除这些文件就会弹出“你需要来自 Administrators 的权限才能删除”。这既是安全设计也是提醒不要随意删 Docker 数据目录尤其是docker_data.vhdx。真需要清理正确方法是退出 Docker Desktop。在 PowerShell管理员中执行wsl --shutdown。如果想彻底重置 Docker 数据可以直接备份后再删%LOCALAPPDATA%\Docker或 WSL2 发行版。不要用资源管理器强删容易把文件占用、权限状态搞得一团糟。如果你不是管理员又想使用 DockerDocker Desktop 安装时默认会创建一个docker-users组把用户加进去即可不需要直接给管理员权限。Windows 上折腾 Docker第一原则永远是先确认进程是以谁的身份运行再决定接下来怎么操作。5. 用原理驱动实操MySQL 8.0 与 Redis 主从的容器化部署原理说了这么多还是得落到真实场景。热搜里“docker 安装 mysql8.0 并使用”“docker 安装 redis 主从”“docker compose”这些词正好是容器化最典型的两个服务。我把它们的完整部署思路讲一遍每一步都会说明为什么这样做。5.1 跑 MySQL 8.0目录挂载、时区、字符集一次说清MySQL 容器化的最大坑是数据持久化和时区。直接跑一个mysql:latest很容易但重启容器后数据没丢这要求你把数据目录放到 volume 里。下面是我在生产环境用的一套命令docker volume create mysql_data docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDYourStrongPass \ -e TZAsia/Shanghai \ -v mysql_data:/var/lib/mysql \ -v /etc/localtime:/etc/localtime:ro \ mysql:8.0 \ --character-set-serverutf8mb4 \ --collation-serverutf8mb4_unicode_ci \ --default-time-zone08:00解释几个关键点-v mysql_data:/var/lib/mysql数据持久化。这是最重要的参数。-e TZAsia/Shanghai和--default-time-zone08:00解决时区问题。否则你SELECT NOW()得到的可能是 UTC 时间。--character-set-serverutf8mb4和--collation-serverutf8mb4_unicode_ci保证中文和 emoji 存储正常。记住utf8 在 MySQL 里并不是真正的万国码必须用 utf8mb4。-v /etc/localtime:/etc/localtime:ro让容器内时间跟宿主机一致。这个挂载在 Linux 上很常用但 Windows 上路径不一定存在需要按实际情况调整。有段时间我排查一个“数据库连上了但非常慢”的问题最后发现是宿主机防火墙对 3306 端口做了限速同时 MySQL 容器开启了skip-name-resolve但客户端连接还是触发反向 DNS 解析。容器本身没问题但层层堆叠的网络配置影响很大。所以排查线上问题时不要只盯着容器参数宿主机网络、安全组、云厂商策略同样要查。5.2 用 docker compose 编排 Redis 一主两从Redis 主从复制的容器化特别适合用 docker compose 管理因为主从结构需要多个容器协同还要共享网络。下面是一份最小可用的docker-compose.ymlservices: redis-master: image: redis:7-alpine container_name: redis-master command: [redis-server, --port, 6379, --appendonly, yes] ports: - 6379:6379 volumes: - redis-master-data:/data networks: - redis-net redis-replica-1: image: redis:7-alpine container_name: redis-replica-1 command: [redis-server, --port, 6379, --replicaof, redis-master, 6379, --appendonly, yes] depends_on: - redis-master volumes: - redis-replica-1-data:/data networks: - redis-net redis-replica-2: image: redis:7-alpine container_name: redis-replica-2 command: [redis-server, --port, 6379, --replicaof, redis-master, 6379, --appendonly, yes] depends_on: - redis-master volumes: - redis-replica-2-data:/data networks: - redis-net volumes: redis-master-data: redis-replica-1-data: redis-replica-2-data: networks: redis-net: driver: bridge这里有三点值得说明第一从节点--replicaof后面写的是redis-master而不是 IP。这依赖 compose 自动创建的 DNS 解析容器间用服务名互相访问。假如你手动写 IP服务重启后 IP 可能变化主从关系就断了。第二我把每个容器都挂载了独立的 volume。Redis 主从节点各有自己的数据目录不共享否则重启后可能出现 AOF 文件冲突。虽然 Redis 主从不是强一致存储但数据目录独立才不会互相踩踏。第三我开了--appendonly yes。虽然性能会有一点损耗但能避免进程重启后丢几秒数据。如果你只是做缓存可以不开 AOF全内存模式更快。到底是“缓存”还是“存储”决定 Redis 容器该怎么配置这个定位问题值得先想清楚。启动命令docker compose up -d查看主从状态docker exec -it redis-master redis-cli info replication正确输出里connected_slaves:2就是两台上线了。5.3 容器健康检查与依赖启动顺序depends_on只能控制容器启动顺序不能保证依赖服务“已经就绪”。比如你的应用容器在 MySQL 容器启动但还没初始化完成时就连数据库照样报Connection refused。这时需要给 MySQL 容器加健康检查。在 compose 文件里可以给 mysql 服务加上healthcheck: test: [CMD, mysqladmin, ping, -h, 127.0.0.1, -u, root, -pYourStrongPass] interval: 10s timeout: 5s retries: 5 start_period: 30s然后在应用服务里用depends_on: mysql8: condition: service_healthy这样应用容器只有在 MySQL 健康检查通过后才会启动从根上避免“应用起来了但数据库还没就绪”的竞态问题。这个技巧用得好比在应用代码里写睡眠等待高好几个层次。5.4 重启策略让容器自己活过来容器进程崩溃时Docker 不会自动拉起除非你设置重启策略。compose 文件里用restart字段Docker 命令对应--restart参数。常见取值策略行为建议no不自动重启调试期默认on-failure非正常退出时重启适合一次性任务always无论退出码都重启适合常驻服务但在本机要小心无限重启unless-stopped手动 stop 后不重启其他情况重启适合绝大多数服务我一般用unless-stopped既保证服务在机器重启后自动恢复也保留“我手动停了它就别再起来”的控制权。always策略有个坑你在容器里写了个无限循环导致 CPU 100%然后docker restart它很快又会重启可能掩盖问题本身。所以重启策略要和健康检查、日志监控配合使用而不是一设了之。6. 容器问题的“三板斧”与几个容易忽略的冷知识前面五章基本覆盖了原理和常见场景最后一个部分来点可复用的排查思路。容器排查没那么神秘核心就三板斧再加上几个冷知识能解决 90% 的日常问题。6.1 日志、exec、inspect 三板斧第一板斧是docker logs。看容器输出注意区分标准输出和标准错误很多应用默认把日志写到文件而不是 stdout这时候docker logs可能帮不上忙需要进容器看文件。第二板斧是docker exec -it container sh。进容器里面手工执行命令。容器里常缺少各种调试工具所以我一般会先确认是不是 Alpine 镜像如果是得先apk add curl bash之类的。需要注意的是进入容器时你的 shell 是在容器当前 Namespace 里的看到的东西和容器内进程一致而不是宿主机视角。第三板斧是docker inspect。它输出容器完整配置包括挂载、端口、网络、环境变量、健康检查状态、启动命令。排查时重点看这几个字段docker inspect container --format{{json .State}} docker inspect container --format{{json .NetworkSettings.Ports}} docker inspect container --format{{json .Mounts}}如果State里有ExitCode: 0但容器还是退出了说明进程主动结束如果ExitCode: 137多半是被 OOM 杀掉或手动 killExitCode: 139是段错误ExitCode: 1要看应用日志。这些退出码能帮你快速缩小范围。6.2 为什么容器删了文件体积还不变镜像分层是不可变的你在可写层里删文件只是形成一个层标记“这个文件被删了”底层的文件还在镜像里。这就是为什么有些镜像删了很多东西体积却一点没变小。要真正瘦身必须在构建阶段就处理在同一个 RUN 命令里安装、清理、删除临时文件并且避免使用包含旧文件的层。如果你用的是旧镜像重新构建时加了新的层旧数据不会自动消失这时候可以尝试docker system prune -a docker builder prune -a后者清理的是构建缓存。构建缓存虽然是好东西但也会占用大量磁盘。在 CI 机器上我经常见到跑了几个月后磁盘莫名其妙满了一查全是 Build Cache。定期清理是必要的。6.3 容器里的 PID 1 和僵尸进程Linux 系统里 PID 1 有很多特殊职责最核心的就是收养孤儿进程。在容器里如果你的应用进程不是真正的 init 系统也不具备孤儿进程回收能力子进程变成僵尸后就会一直停留在进程表里时间长了会积累大量僵尸进程影响容器健康。典型场景是容器里跑 bash 脚本脚本再拉起子进程如果主进程没等待子进程结束子进程就可能成为僵尸。Docker 提供了--init参数会在容器内注入一个轻量级 init 进程tini负责信号转发和僵尸进程回收。docker run --init -d --name demo nginx在 compose 文件里对应的是init: true如果你发现容器内出现很多defunct进程先看看有没有加--init。这个冷知识平时不起眼但线上容器跑久了僵尸进程累积到一定数量会让应用变得很卡。6.4 docker exec 和 docker attach 的区别以及 IDE 打包镜像经常有人把docker exec和docker attach搞混。docker attach是连接容器的标准输入、输出和错误流相当于把自己的终端接到容器主进程上。如果你在 attach 模式按下 CtrlC可能会把 SIGINT 发给容器主进程大部分直接退出。docker exec是在容器里新起一个进程和你打个洞钻进容器里做事情一样不会影响主进程。日常排查用docker exec就好attach 用途相对特殊。至于 IDA 里一键打包 Docker 镜像这个功能本质还是帮你在后台执行docker build -t ... .只是把上下文目录、Dockerfile 位置通过插件面板图形化了。如果你理解了前面 Dockerfile 的分层原理IDEA 打包镜像出问题时还是得回到 Dockerfile 本身去排查。7. 写到最后的一点个人体会我见过太多人把 Docker 当成“黑盒”能用就算会不能用了就重装。但真正能解决线上问题的人往往是那些理解了 Namespace 和 Cgroups、清楚镜像分层和网络转发规则、知道数据必须放在卷里的人。原理不是纸上谈兵它决定了你在容器报错时是胸有成竹还是听天由命。我自己最深的体会是别急着上 Kubernetes先把 Docker 的底层机制搞明白。你可以拿一台开发机开一个alpine容器在里面跑ps aux、mount、ip addr再把宿主机的/sys/fs/cgroup翻一翻。这些东西比任何教程都直观。踩过的坑越多对原理的理解就越扎实。容器就像积木规则清楚了搭什么都能搭出来。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →