在计算机世界里,我们总爱开玩笑说:“只要一出问题,肯定是网络(的锅)!” 这句玩笑话背后,其实藏着一个朴素的道理:网络是所有应用的生命线,没有网络,应用就成了孤岛,毫无用处。
在 Docker 的早期,网络功能确实让人头疼。但时过境迁,现在的 Docker 网络已经设计得相当优雅和强大。 在这一节我们会一起探索 Docker 网络的基础知识,比如它背后的核心设计理念——容器网络模型(CNM),以及它的标准实现 libnetwork。

Docker 的世界里运行着成千上万的容器化应用,这些应用需要彼此沟通,有些是容器间的直接对话,有些则需要和外部世界(比如传统的虚拟机或物理服务器)交流。这就好比一个繁忙的大都市,需要有四通八达的交通网络,才能让居民(应用)自由往来。
幸运的是,Docker 提供了一整套成熟的网络解决方案。无论是容器之间的“局域网”,还是连接外部世界的“高速公路”,Docker 都能轻松应对。这背后,都离不开一个叫做 容器网络模型(CNM) 的基石。
CNM 是一个开放、可插拔的架构设计。你可以把它想象成一个通用的“插座标准”,而 libnetwork 就是 Docker 官方出品的“插座”,它实现了 CNM 的所有核心功能。 不同的 网络驱动(Drivers) 就像是各式各样的“插头”,可以插在这个“插座”上,来实现不同的网络类型,比如单机桥接网络、多机覆盖网络,或是连接到现有的 VLAN。
Docker 网络的设计哲学,可以归结为三大核心组件:
凡事都要从设计开始。CNM 就是 Docker 网络的设计规范,它定义了构成 Docker 网络的三个基本元素:
下面这张图清晰地展示了这三者的关系:
从图中可以看到,容器 A 通过一个端点连接到了网络 A。而容器 B 则更特殊一些,它有两个端点,分别连接到了网络 A 和网络 B。因为容器 A 和 B 都连接在网络 A 上,所以它们之间可以互相通信。
一个端点就像一根网线,它只能连接到一个网络。如果一个容器需要同时加入多个网络,那么它就需要多个端点。
如果说 CNM 是设计图纸,那么 libnetwork 就是按照图纸建造出来的实体建筑。它是一个开源、跨平台的项目,不仅被 Docker 使用,也服务于其他项目。
在 Docker 的早期,所有的网络代码都堆积在 Docker 守护进程(daemon)里,这使得守护进程变得臃肿不堪,难以维护。 后来,社区决定将网络部分的代码剥离出来,重构成一个独立的项目,这就是 libnetwork。如今,所有 Docker 的核心网络功能都由 libnetwork 提供。
除了实现 CNM 的核心组件,libnetwork 还提供了服务发现、入口负载均衡以及网络的控制和管理功能。
如果说 libnetwork 负责管理和控制,那么驱动程序就负责具体的数据传输。网络的连接、隔离、创建等实际工作,都是由驱动程序完成的。
Docker 自带了几个核心的网络驱动,我们称之为“原生驱动”,包括 bridge、overlay 和 macvlan,它们可以满足绝大多数常见的网络需求。当然,第三方也可以编写自己的网络驱动,来满足更复杂的定制化需求。
最简单、最常见的 Docker 网络类型就是单主机桥接网络。从它的名字我们就能看出两个关键点:
在 Linux 系统上,Docker 使用 bridge 驱动来创建这种网络;在 Windows 上,则使用 nat 驱动。尽管名字不同,但它们的工作原理和效果几乎是一样的。
每台安装了 Docker 的主机,都会有一个默认的桥接网络。在 Linux 上它叫 bridge,在 Windows 上叫 nat。默认情况下,你创建的所有新容器都会自动连接到这个网络。
让我们通过 docker network ls 命令来看一下:
|# 在 Linux 上 $ docker network ls NETWORK ID NAME DRIVER SCOPE 333e184cd343 bridge bridge local # 在 Windows 上 > docker network ls NETWORK ID NAME DRIVER SCOPE 095d4090fa32 nat nat local
默认的 bridge 网络虽然方便,但功能上有所限制,比如它不支持通过容器名进行服务发现。因此,在实际开发中,我们通常会创建自己的桥接网络。
让我们来创建一个新的桥接网络,并连接容器进行测试:
创建一个新的桥接网络
|$ docker network create --driver bridge my-local-net
创建并连接两个容器
|$ docker run -d --name c1 --network
切记,默认的 bridge 网络不支持通过容器名进行服务发现。这个功能只在用户自定义的桥接网络中才可用。
桥接网络还有一个重要的功能,叫做 端口映射(Port Mapping)。它可以将容器的端口映射到主机的某个端口上,这样外部世界就可以通过访问主机的端口来访问容器内的服务了。
这个过程就像是给公寓里的某个房间(容器)装了一个门铃(主机端口),外卖员(外部请求)只需要按门铃,就能找到正确的房间。
覆盖网络(Overlay Network)是为多主机环境设计的。它能够跨越多个 Docker 主机,将分布在不同主机上的容器连接到同一个网络中,就好像它们都在一台机器上一样。这种网络类型非常适合大规模的容器集群通信。 假设你有一个大型的购物中心,里面有很多不同的商店分布在不同的楼层和区域。为了让顾客能够方便地在各个商店之间穿梭,购物中心建立了一个统一的导航系统。无论顾客在哪个位置,都能通过这个系统找到任何一家商店,并且可以轻松地在商店之间移动。
覆盖网络就是这个“购物中心导航系统”的概念。它创建了一个虚拟的网络层,覆盖在多个物理主机之上,让分布在不同机器上的容器能够像在同一个网络中一样相互通信。
覆盖网络使用了一种叫做"隧道技术"的方法来实现跨主机的通信。当容器A(在主机1上)想要和容器B(在主机2上)通信时,数据包会经过以下路径:
首先,容器A的数据包被封装在一个特殊的隧道包中,然后通过物理网络发送到主机2。主机2接收到隧道包后,会解封装出原始数据包,并转发给容器B。这个过程对容器来说是透明的,它们感觉就像在同一个网络中直接通信一样。
在Docker Swarm模式下,创建覆盖网络非常简单:
|$ docker network create -d overlay my-overlay-network
这个命令会创建一个名为my-overlay-network的覆盖网络。-d overlay参数指定使用overlay驱动程序。
覆盖网络带来了许多重要的优势。首先是跨主机通信,容器可以跨越物理主机边界进行通信,这对于分布式应用来说是必不可少的。其次是网络隔离,不同的覆盖网络之间是完全隔离的,这提供了良好的安全性。
覆盖网络还支持服务发现,容器可以通过服务名称相互发现,而不需要知道具体的IP地址。最后是自动扩展,当你在Swarm集群中添加新的节点时,覆盖网络会自动扩展到新节点上。
覆盖网络在微服务架构中特别有用。想象一下,你有一个电商应用,包含用户服务、订单服务、支付服务、库存服务等多个微服务。这些服务可能分布在不同的主机上,但它们需要相互通信来完成一个完整的业务流程。
通过覆盖网络,你可以将所有相关的服务连接到同一个网络中,让它们能够通过服务名称相互访问。这样,用户服务就可以直接调用订单服务,订单服务可以调用支付服务,而所有这些通信都在同一个虚拟网络中进行,既安全又高效。
虽然覆盖网络提供了强大的功能,但它也有一些性能开销。由于数据包需要经过封装和解封装的过程,会有一定的延迟和带宽开销。对于对网络性能要求极高的应用,可能需要考虑其他网络方案。
覆盖网络是Docker Swarm模式下的默认网络类型,特别适合需要跨主机通信的分布式应用。它提供了良好的隔离性和服务发现功能,是构建大规模容器集群的重要工具。
在很多场景下,我们需要让容器化的应用和传统的物理网络或 VLAN 直接通信。比如,一个应用正在逐步容器化,一部分服务在容器里,另一部分还在物理服务器上。
为了解决这个问题,Docker 提供了 macvlan 驱动。它可以为每个容器分配一个独立的 IP 地址和 MAC 地址,让容器在网络上看起来就像一台真实的物理机或虚拟机。
macvlan 的性能非常好,因为它不需要端口映射或额外的桥接。但它的缺点也很明显:它要求主机的物理网卡开启“混杂模式”(promiscuous mode),而这在很多公司的网络环境和公有云平台上是不被允许的。
macvlan 功能很强大,但使用前请务必和你的网络管理员确认,是否允许在网络中开启网卡的混杂模式。
除了提供网络连接,libnetwork 还提供了重要的网络服务,其中之一就是 服务发现(Service Discovery)。 服务发现允许容器和 Swarm 服务通过名字互相定位,只要它们在同一个网络中。这背后的功臣是 Docker 内置的 DNS 服务器和每个容器里的 DNS 解析器。
这个过程就像一个自动更新的电话簿。每当一个容器启动时,它就会把自己的名字和 IP 地址注册到 Docker 的 DNS 服务器上。当另一个容器想要通过名字访问它时,就会去查询这个 DNS 服务器,找到对应的 IP 地址,然后建立连接。
在 Swarm 模式下,Docker 提供了一种叫做 入口负载均衡(Ingress Load Balancing) 的机制,让外部可以方便地访问集群中的服务。
当你发布一个服务并暴露端口时,Docker 会默认使用 ingress 模式。这会在整个 Swarm 集群的所有节点上“发布”这个端口。
外部流量可以访问任何一个 Swarm 节点的这个端口,然后 Docker 的路由网格(Routing Mesh)会自动将请求转发到正在运行该服务副本的容器上,并实现负载均衡。
这种机制极大地简化了服务的外部访问和扩展,因为你不需要关心服务具体运行在哪个节点上,路由网格会为你处理好一切。
Docker 网络的核心是 容器网络模型(CNM),它定义了沙盒、端点和网络这三个基本构建块。libnetwork 是 CNM 的官方实现,它提供了核心的网络功能、服务发现和负载均衡。而 驱动程序 则扩展了 libnetwork,提供了不同类型的网络实现。
在容器 c2 中测试连通性
现在你的终端已经进入了 c2 容器的命令行。尝试通过名字 ping c1 容器:
|/ # ping c1 PING c1 (172.19.0.2): 56 data bytes 64 bytes from 172.19.0.2: seq=0 ttl=64 time=0.102 ms ...
成功了!这是因为在我们自己创建的 my-local-net 网络中,Docker 内置的 DNS 服务会自动为容器注册名字,使得它们可以通过名字互相访问。