很多人跑了好几个月 Dockerdocker run、docker build、docker-compose up用得飞起但被问到“容器到底是怎么隔离的、容器之间又是怎么通信的”反而容易卡壳。这篇是 Docker 系列里偏内核的一篇我不打算把命令手册再抄一遍而是围绕两个核心问题展开容器凭什么能各看各的进程和文件容器里的网络请求又是怎么走到宿主机外面去的如果你已经掌握了 Docker 的常用操作想往底层走一步这篇会非常对胃口。镜像、容器、网络、存储这四块里面网络最抽象也最容易出问题单独讲透很有必要。后面无论你是要调生产环境的问题还是要去学容器编排平台这篇文章讲的原理都是最底层的地基。1. 从 Namespace 到 cgroups容器的“独立”到底独立在哪很多人以为容器就是“轻量虚拟机”这个说法从使用角度讲没问题但从原理上讲非常误导。虚拟机的隔离是硬件级别的虚拟机有自己独立的虚拟 CPU、独立内存、独立磁盘设备宿主机的问题通常不会直接蔓延到虚拟机。而容器恰恰相反它和宿主机共享同一个内核没有独立的硬件抽象层。容器里跑着的进程本质上就是宿主机上的一个普通进程只是被一批内核机制“包装”得看起来像独立系统。这一层包装的主要成分就是 Linux 命名空间。1.1 七种命名空间逐一拆解进程的障眼法Linux 命名空间Namespace是一种将内核全局资源包装成独立实例的机制。常见的有 Mount、PID、Network、UTS、IPC、User、Cgroup 七种。创建容器时Docker 会为容器主进程申请一套全新的命名空间然后在这个命名空间内启动进程。进程以为自己看到了完整的系统实际上看到的只是内核为它虚拟出来的一角。PID 命名空间对“容器是普通进程”这一点体现得最直接。你在容器内执行ps aux会看到 PID 1 是容器主进程进程列表非常干净但回到宿主机上执行docker top 容器名会看到这个进程的真实 PID可能是个几百甚至几千的数值。同一个进程在不同命名空间里呈现出来的身份不同这就是命名空间的障眼法。再往下看Mount 命名空间管挂载点视图每个容器能看到独立的文件系统挂载树容器内改/etc/hosts、/etc/hostname不会影响宿主机。Network 命名空间让容器拥有独立的网卡、路由表、iptables 规则。UTS 命名空间管主机名IPC 命名空间管进程间通信User 命名空间则允许容器内用非 root 用户映射到宿主机普通用户属于安全加固的重要方向。Docker 在创建容器进程时通过 clone 系统调用传入一组CLONE_NEW*标志位一次性创建出多个命名空间。之后容器内派生的进程都会继承这套命名空间所以容器内所有进程共享同一套“隔离视角”。这套机制解释了一个常见现象容器内 PID 1 退出后整个容器就结束了因为 PID 1 进程是这套命名空间的持有者它没了命名空间也没了。1.2 cgroups 资源限制容器内存和 CPU 是如何被卡住的命名空间解决的是“看得见/看不见”的问题cgroups 解决的是“能用多少”的问题。Namespace 给每个容器一套独立视图但这不代表容器可以无限占用宿主机资源。如果没有资源限制一个容器就可能吃满全部 CPU 和内存把宿主机和其他容器一起拖死。cgroups 把这些进程按层级分组对 CPU、内存、IO、网络带宽等资源做配额和统计。内存限制的原理值得细说。给容器设置-m 512m时cgroup 的内存子系统会为该容器关联一个内存控制组。当容器内进程累计使用超过阈值内核会触发回收回收不掉就进入 OOM 流程把占用最高的进程杀掉。这里有一个容易误读的点在容器里执行free -m看到的依然是宿主机全部内存因为/proc/meminfo里的数字没有按 cgroup 重算。要查看真实限制得去读/sys/fs/cgroup/memory/memory.limit_in_bytes这类 cgroup 文件。这也是很多新手在容器里用free判断内存结果和预期对不上的原因。CPU 限制则分两种权重型和限额型。docker run -c 512设置的是 CPU 份额同一宿主机上如果有多个容器争抢 CPU份额高者分得更多但当 CPU 空闲时低份额容器也能跑满。而--cpus1.5这类参数是硬性的使用上限无论空闲与否都不允许超过。生产环境如果要保证单个容器占用不失控通常建议用--cpus这类硬限制而不是只靠份额约束。1.3 rootfs 与写时复制文件系统层面的隔离真相文件系统隔离除了 Mount 命名空间保证挂载视图独立还有一层关键依赖rootfs。每个容器拥有自己的根文件系统这个根文件系统是从镜像的一堆只读层叠加出来的最上层挂一个可写层。修改文件时文件会先按需复制到可写层再修改这就是写时复制。容器删除时可写层被丢弃镜像本身不受影响。这里最常见的误区是以为容器里的文件系统是“完整独立的磁盘”于是放心地在容器里保存关键数据。实际上容器可写层依赖宿主机磁盘容器一删数据就没了。持久化要么挂卷要么挂宿主机目录否则数据只能活在容器生命周期内。这也是隔离和安全的典型错位隔离的是视图不等同于数据永久性。最后必须强调一句命名空间带来的隔离是“逻辑隔离”不是“安全边界”。所有容器共享宿主机内核一旦某个容器拿到了特权能力理论上存在影响宿主机的可能。所以生产环境要避免使用特权模式启动容器也不要随意把宿主机的/proc、/sys或 Docker socket 挂进容器。这个点在容器环境里是首要检查项没有之一。2. Docker 网络通信链路先彻底搞懂 veth、bridge 与 iptables网络是容器里最贴近使用直觉的部分但问题一旦出现没有原理支撑排查基本靠蒙。我会从默认 bridge 模式讲起把一条请求从容器内到容器外、从外部到容器内的完整链路拆开每一步都讲清楚为什么这么设计。2.1 默认 bridge 模式veth pair 与 docker0 的完整链路在默认安装下Docker daemon 会在宿主机上创建一个名为 docker0 的网桥默认网段通常是 172.17.0.0/16docker0 自己的地址是 172.17.0.1。每启动一个容器Docker 会创建一对 veth 虚拟网卡一头放进容器的 Network Namespace命名为 eth0另一头挂在 docker0 上。veth 是成对出现的虚拟以太网卡数据从一头进去必然从另一头出来可以理解成一根虚拟网线。容器里的 eth0 会被分配 172.17.0.2 之类的 IP默认路由指向 172.17.0.1也就是 docker0。当容器发起对外访问时数据包从容器 eth0 出发经 veth 对到达 docker0。docker0 连接着宿主机内核的网络协议栈数据包再走上层路由与 iptables 处理。如果目标是外部 IP宿主机启用 IP 转发后将包转发到物理网卡出去。这里有个容易被忽略的设计细节Docker 桥接模式用的是 NAT不是纯二层桥接。容器默认网段是私有网段外部网络根本不知道 172.17.0.2 这个地址。数据包出去前iptables 的地址伪装规则会把源地址改写为宿主机物理网卡地址回来后 NAT 表反向记录映射关系再把应答包送回容器。整个机制与家用路由器让多台设备共享一个公网 IP 上网完全一样。理解了这一点你就知道为什么容器默认 IP 在外网“不存在”也就能理解端口映射的必要性了。2.2 端口映射与 DNAT外部流量如何进入容器外部网络要访问容器无法直接用 172.17.0.2 这种私有地址只能通过宿主机端口转发。docker run -p 8080:80本质上是向 iptables 的 nat 表 DOCKER 链插入一条 DNAT 规则凡是到达宿主机 8080 端口的 TCP 包目标地址被改成容器 IP 的 80 端口。这条规则生效后外部访问链路是请求到宿主机 IP:8080iptables DNAT 把目标改写为 172.17.0.2:80宿主机路由到 docker0经过 veth 对进入容器 eth0最终交给容器内服务。容器发出的响应包经过 NAT 反向转换后对客户端表现为宿主机 8080 端口发出的数据。实操中你可以用iptables -t nat -vnL DOCKER直接看到这些规则。有个容易卡住的点如果在宿主机手动清空了 iptables 规则Docker 的端口映射、容器间通信、容器外网访问三件事几乎同时失效。Docker daemon 启动时会重建自己的规则链但如果你不是通过重启 Docker 来恢复就需要手动加载相关规则或干脆重启容器。这个坑我见过太多次后面排查部分会专门讲。2.3 host 模式与 none 模式两种极端场景的适用边界host 模式直接把容器放进宿主机的 Network Namespace不分配独立 IP也不做 NAT。它的性能开销最小但端口冲突风险很高容器里监听 80宿主机就不能再监听 80。适合对网络延迟敏感、且端口规划完全可控的场景比如一些高性能网关或对时延有要求的中间件。我一般不建议默认选 host除非你能确定端口不会撞车。none 模式则不给容器配置任何网络接口除了 loopback。容器完全与外界隔离应用无法对外提供服务也无法访问外网。它的价值主要在两类场景一是对网络敏感的批量计算任务完全不希望容器偷偷联网二是自定义网络时先把网络功能全部拆掉再手动用底层命令配置避免 Docker 的自动配置干扰你的方案。从使用频率来说bridge 模式占绝大多数host 模式偶尔用none 模式更多出现在特殊网络方案里。一个常见认知误区是“host 模式更快所以应该默认用”实际上大部分应用不是网络瓶颈bridge 模式多出来的 NAT 开销微乎其微。为了那一点点性能提升去承担端口冲突和隔离缺失的风险不划算。3. 动手验证不用 Docker 也能复现容器隔离和网络原理理论讲完我们来做几组实操。这三组操作不依赖任何第三方工具在你自己的 Linux 机器上就能完成。它们能帮你把前面讲过的概念全部落一遍也让后续排查问题时有更清晰的直觉。3.1 亲手拉开一个隔离空间unshare 与 ip netnsLinux 的 util-linux 提供了一个unshare命令可以在不启动 Docker 的情况下直接创建新的命名空间。执行sudo unshare --pid --fork --mount-proc bash这会创建一个新的 PID 命名空间并把/proc挂载为新的实例。你在这个 shell 里执行ps aux会看到进程列表非常短PID 1 就是这个 shell 自身。如果此时在另一个终端用pstree -p观察会发现这个 shell 在宿主机里有另一个真实 PID。这正是 PID 命名空间的直观体验。想退出就执行exit所有命名空间会随最后一个进程退出而清理。这也是容器生命周期的缩影容器主进程退出容器即结束原理完全相同。注意一下普通用户运行unshare往往会因为权限不足而失败sudo 是必要前提。ip netns是另一套方便的工具专门管理网络命名空间。先建一个命名空间sudo ip netns add ns1 sudo ip netns exec ns1 ip addr执行后看到 ns1 里只有 loopback且状态是 down说明一个新的网络命名空间默认是空的。这与创建一个未配置网络的容器非常相似。3.2 手工搭建 veth 与 bridge把 Docker 的封装拆开看再创建一对 veth 和一个网桥sudo ip link add br-test type bridge sudo ip link add veth0 type veth peer name veth1 sudo ip link set veth1 netns ns1 sudo ip link set veth0 master br-test sudo ip addr add 172.20.0.1/24 dev br-test sudo ip link set br-test up sudo ip link set veth0 up sudo ip netns exec ns1 ip addr add 172.20.0.2/24 dev veth1 sudo ip netns exec ns1 ip link set veth1 up sudo ip netns exec ns1 ip route add default via 172.20.0.1解释每一条br-test网桥相当于 Docker 的 docker0。veth0是半根虚拟网线veth1是另半根。把veth1塞进ns1后命名空间里就出现了一个新的网络接口把veth0挂到br-test相当于把网线插到交换机端口上。再给网桥分配 172.20.0.1给ns1里的接口分配 172.20.0.2添加默认路由一个三层互通的最小网络就成型了。测试一下sudo ip netns exec ns1 ping 172.20.0.1能通。这条链路ns1 → veth1 → veth0 → br-test → 宿主机协议栈和 Docker 的默认 bridge 模式几乎同构。区别只是 Docker 把这套流程集成化、自动化了。到这里你已经手动实现了 Docker 网络模块百分之八十的核心逻辑剩下的 DNS、负载均衡、端口映射都是在这个基础上封装出来的。3.3 用一条 NAT 规则让“容器”访问外网到这里ns1只能和宿主机互通。要让ns1访问外部网络需要打开 IP 转发并加入一条地址伪装规则sudo sysctl -w net.ipv4.ip_forward1 sudo iptables -t nat -A POSTROUTING -s 172.20.0.0/24 ! -o br-test -j MASQUERADE这条 iptables 规则的意思凡是源地址属于 172.20.0.0/24、且不是从 br-test 出去的包出去前都把源地址改成宿主机出口网卡的地址。加上这条后在ns1里ping 外部IP如果网络放行就能通了。如果这时候不通优先检查宿主机外网网卡的流量走向以及系统防火墙是否拦截了转发。还有一个隐蔽的坑你在宿主机上执行过iptables -F的话这条规则会被清掉转发链路直接断开ip_forward即使开着也没用。这就是 Docker 网络最容易踩的雷系统防火墙、NAT 规则、内核转发三个环节任何一环被外部改动容器网络都会出问题。4. 从单机到跨主机overlay 网络与 macvlan 方案怎么选默认 bridge 模式解决单机容器通信没问题但一旦进入多主机部署问题立刻出现宿主机 A 上的容器无法直接通过容器 IP 访问宿主机 B 上的容器因为彼此的 docker0 网桥不在同一个二层网络。于是有了真正的 overlay 网络和 macvlan 方案。4.1 overlay 与 VXLAN 封装跨主机通信的原理overlay 网络本质上是在宿主机物理网络之上叠一层虚拟网络。容器 IP 不参与实际路由数据包要先经过封装再通过宿主机网络传输。Docker 默认使用 VXLAN 技术把容器发出的完整报文封装进宿主机 UDP 报文里通过宿主机网络在节点之间转发。理解封装最简单的方式是类比快递容器 A 要发数据给容器 B内部报文写的是 A 的容器 IP 到 B 的容器 IP。宿主机把这封信塞进一个信封信封上写的是宿主机 A 的物理 IP 到宿主机 B 的物理 IP。B 宿主机收到信封后拆封再投递给容器 B。对容器 A 和 B 来说完全感知不到中间发生了封装过程只觉得自己和对方在同一个二层网络里。这里必须注意 MTU 问题。VXLAN 封装的报文头额外占用几十字节如果宿主机物理 MTU 是 1500那么容器网络 MTU 必须下调到 1450 或更低否则大包直接丢掉。很多跨主机容器网络不通的案例最后定位到 MTU 不匹配。Docker 在 overlay 驱动上会自动适配部分场景但如果你自建 VXLAN 或与其他网络设备联动一定要核对两侧 MTU。4.2 macvlan让容器直接拥有物理地址macvlan 是另一种跨主机网络方案思路完全不同它不封装而是让宿主机物理网卡在二层模式下虚拟出多个 MAC 地址每个容器分配其中一个 MAC并直接使用外部局域网的同网段 IP。容器就像直接插在交换机上的一台独立主机外部设备可以直接访问容器 IP。macvlan 性能比 veth 加 NAT 的模式好因为少了 NAT 转换转发路径更短适合对时延敏感且需要容器 IP 直接被外部网络看到的场景。但它有两个明显的坑。第一宿主机无法通过 macvlan 接口直接访问容器因为物理网卡被虚拟出多个 MAC 后宿主机自身的 MAC 不再绑定。第二很多基础网络环境不允许一个物理口出现大量虚拟 MAC交换机的安全策略会直接封端口。另外macvlan 依赖的网卡必须支持混杂模式一般物理网卡支持无线网卡多数不行。选型结论很直接单机且需要端口映射用 bridge 最省心多机且对网络隔离和扩展有要求overlay 优先容器需要参与外部二三层路由、追求低延迟再看 macvlan。macvlan 这类方案不是每个人都有条件踩坑先把 overlay 吃透已经能覆盖绝大多数生产场景。4.3 网络选型对比与安全加固建议三种模式放在一起看更清楚网络模式适用场景跨主机性能特点主要风险bridge单机容器、端口映射否有少量 NAT 开销iptables 被外部改动后故障host延迟敏感、端口可控否性能最好端口冲突、隔离弱overlay多机容器编排是有封装开销MTU 不匹配、调试成本macvlan容器需直连外部网络是接近物理性能宿主机无法访问容器、交换机限制容器网络的安全问题一半出在“默认太开放”。在默认 bridge 网络里同一网络的所有容器默认可以互相访问。生产环境如果多个服务跑在同一台机器又不希望某个容器能扫其他容器端口最简单的做法就是创建多个自定义 bridge 网络按业务拆分让不同业务的容器处于不同网络。自定义 bridge 网络还带来一个极好用的能力DNS 解析。Docker 会向自定义网络内的容器注入一个内嵌 DNS 服务容器之间可以直接用容器名互访。默认 docker0 没有这个能力只能用旧机制手动指定别名体验差很多。所以只要是正式项目我都建议创建自定义网络不要让容器直接依赖默认 docker0。安全加固层面有几个动作值得长期坚持避免使用特权模式启动容器不要挂载 Docker socket容器对外端口尽量收敛需要对外访问的服务通过统一的入口网关暴露敏感服务使用 internal 网络完全去掉外网出口宿主机上的防火墙规则先备份再变更。这一套组合下来能挡掉绝大多数利用容器网络弱配置发起的攻击。5. 容器网络问题排查指南高频故障与修复实录网络问题在容器环境里出现频率很高。我把这些年实际遇到的高频问题整理成一份排查清单每个问题都附带定位思路能省下大量试错时间。5.1 容器无法访问外网怎么办现象容器内访问外部 IP 不通但容器之间通信正常。第一步检查宿主机 IP 转发sysctl net.ipv4.ip_forward如果输出为 0改成 1echo 1 /proc/sys/net/ipv4/ip_forward第二步检查 iptables。最常见的坑是手动执行过iptables -F清空了规则导致 NAT 链断裂。清空后容器失去外网能力但 docker0 内的通信仍然正常这个现象非常有迷惑性。恢复方式是重启 Docker 服务或者直接重启受影响容器让 daemon 重新生成策略链。如果业务不允许重启至少要重建地址伪装规则把源地址转换补回去。5.2 端口映射失效的完整排查步骤现象宿主机上访问宿主机端口没反应容器内直接访问服务端口正常。先确认端口是否被其他进程占用用ss -lntp | grep 8080查一下。再确认容器状态docker ps -a有时候端口绑定没报错但容器已经退出映射自然不生效。如果这两项正常继续看 iptablesiptables -t nat -vnL DOCKER找到对应的 DNAT 规则是否存在。如果不存在很可能是规则链被外部工具覆盖。还有一种常见情况启动容器时用的是-p 8080:80但容器内服务实际监听的是 IPv6 或不同端口先用docker exec 容器 ss -lnt核对容器内的监听状态。排查要按“宿主端口 → 规则链 → 容器内监听”的顺序一层层走跳步容易漏。5.3 容器内 DNS 解析异常的处理路径现象容器里访问域名报无法解析。先看容器内/etc/resolv.conf自定义网络里应该指向 127.0.0.11这是 Docker 内置 DNS 服务。如果文件内容异常指向了宿主机不通的 DNS 或残留旧配置多半是挂载了宿主机 resolv.conf 或容器创建后宿主 DNS 被改过。排查思路按顺序走检查容器网络模式确认宿主机 DNS 配置在容器内用nslookup或dig解析看请求是否到达 127.0.0.11。如果是自定义网络且容器内访问外部域名失败还要确认容器是否有外网路由DNS 请求和业务流量走的是同一张网。不要忽略一个细节桥接网络出去的 DNS 请求也会被 NAT 转换如果部署在特殊网络环境里需要把宿主机 DNS 与容器 DNS 的一致性一起检查。5.4 容器 IP 动态变化与固定 IP 方案现象容器重启后 IP 变了配置文件里写死的旧 IP 失效。Docker 容器默认不保证固定 IP要固定 IP 必须在创建时指定。自定义网络场景下可以这样做docker network create --subnet172.30.0.0/24 app-net docker run --network app-net --ip 172.30.0.10 ...千万别想在生产环境靠容器 IP 做服务发现IP 本身就不该成为依赖。正确的做法是依赖 Docker 内置 DNS通过容器名互访或者接入服务注册中心。这一套在自定义 bridge 网络里已经支持得很完整没必要去绕远路。5.5 高频问题速查表现象优先排查项常用命令/动作容器无法访问外网宿主机 ip_forward、iptables NAT 链sysctl net.ipv4.ip_forward; iptables -t nat -vnL POSTROUTING端口映射不生效宿主端口占用、DNAT 规则、容器内监听ss -lntp; iptables -t nat -vnL DOCKER; docker exec 容器 ss -lnt容器间无法通信是否同网络、自定义网络 DNS、路由docker network inspect; docker exec 容器 ping 容器名容器 DNS 解析失败/etc/resolv.conf、宿主 DNS、外网路由cat /etc/resolv.conf; nslookup 域名容器 IP 重启变化未指定固定 IP、依赖 IP 访问docker network create 指定 subnet; docker run --ip; 改用容器名手动清 iptables 后网络全断规则链被破坏备份原规则; 重启 Docker 或重建规则链这张表覆盖了我遇到过的绝大多数容器网络问题。如果你的现象不在表里回到原理层去推容器内到容器外关键在 veth、网桥、路由、NAT外部到容器内关键在端口映射、DNAT。顺着这条路径一段一段测定位速度一定比乱猜快。最后说点个人习惯。我每次排查容器网络问题都先看宿主机四个地方网卡地址、路由表、iptables NAT 规则、内核转发开关十秒跑完一半问题已经暴露。然后再进容器看网卡配置、路由、DNS 配置。按这个顺序走很少做无用功。Docker 的隔离和网络这两层原理看起来门槛高其实动手玩一遍 unshare 和 veth 就通了。理解到这里后面再看容器编排里的网络插件、服务发现和入口网关你会发现它们解决的都是同一批底层问题的更高层抽象。
阅读完成 · 觉得有帮助?