Docker 踩坑记

Docker 踩坑记

Scroll Down

docker-tutorial-mac

踩坑描述

docker run -d --network=host image

相信了解 Docker 的同学对这个命令应该一点都不会不陌生,这是一个非常简单的运行镜像的命令,只不过稍微有些区别的是网络使用了 host 模式。

例子:

docker run -d \
  --privileged=true \
  --network host \
  --name myredis \
  redis redis-server \
  --appendonly yes  

但令人很奇怪的是,容器的状态是 running,而且进去容器中的发现 redis 服务也确实是正常运行的,但是在宿主机通过 127.0.0.1 或者 localhost 却无法访问容器中的 redis 服务。

Amazing!这就让人有点百思不得其解。

But,我发现在 Linux 机器上是可以通过宿主机的 ip 访问。

Emmm......这 Mac 和 Linux 还有区别对待的吗?

Docker Desktop 原理

其实官网已经声明 host 模式只适用于 Linux,对于 Mac 和 Windows 上是不支持的。

sfm-002

为什么会不支持呢?那我们得先了解 Docker for Mac 的原理。

众所周知, Docker 的核心是利用 Linux 的 NamespaceCgroups 来实现资源的隔离和限制,容器共享宿主机内核,简单来说,Docker 最初就是基于 Linux 量身定做的产品,所以 Mac 本身是没法运行 Docker 容器。

不过换个思路,我们可以跑虚拟机,最早还没有 Docker for Mac 的时候,就是通过 docker-machine 在 Virtual Box 或者 VMWare 直接起一个 Linux 的虚拟机,然后在主机上用 Docker Client 操作虚拟机里的 Docker Server。

所以, Docker for Mac 其实本质上就是在本地跑了一个虚拟机来运行 Docker,只不过 Hypervisor 采用的是 xhyve

这也是为什么我们在 macOS 上没有看到 docker0 网桥,因为这个接口实际上是在虚拟机中。

聊聊 Docker 网络

对于这个问题有如此的困惑,其实还是要归咎于我们对 Docker 网络的不熟悉。

大多数公司都是有自己的一套成熟的 CICD 平台,而在实际的业务场景中,开发同学也只需要知道在平台界面上如何操作打包发布,遇到操作打包发布失败的就@运维同学场外支援。所以对容器的原理这块的可能本身就接触的比较少,更不用说容器网络了。

本着学海无涯的精神,小年决定带大家重新捋一捋容器网络这一块的知识。

006ARE9vgy1fr8idcuufzj309q09ojrs

Docker 的网络隔离是基于 Linux 的 Network Namespace 实现,不同的网络模式都与 Network Namespace 紧密关联。

Docker容器网络有四种模式:

1. host

使用宿主机的默认网络命名空间,共享一个网络栈。简单来说,就是共用宿主机的ip、端口。

上面也提到过,Docker Desktop For Mac 因为其本身借助虚拟机实现的,所以 host 模式对于 Mac、Windows 是不起效果的。

2. bridge(默认)

Docker 会默认创建一个 docker0 的虚拟网桥,此主机上启动的 Docker 容器如果没有指定网络模式会连接到这个虚拟网桥上。

Linux 可以通过 brctl show 命令查看,Mac 中是没有 brctl 命令的,通过 ifconfig 也可以看到 docker0 的配置。

3. none

使用 none 模式,Docker 容器拥有自己的 Network Namespace,但是并不为 Docker 容器进行任何网络配置。也就是说,这个 Docker 容器没有网卡、IP、路由等信息。需要我们自己为 Docker 容器添加网卡、配置 IP 等。

4. container

这个模式指定新创建的容器和已经存在的一个容器共享一个 Network Namespace,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。

通过 docker network ls 命令我们可以看到,Docker 本身默认支持三种网络模型,并且在创建容器的时候可以通过 --network 来指定使用的网络模型。

[root@server1 ~]# docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
bc1bf3817c35        bridge              bridge              local
bde8994f7873        host                host                local
66bd8d3ad9f2        none                null                local

对于这几种网络模式其实还是比较好容易理解,我们重点关注 bridge 模式,bridge 模式的底层实现,其实就是借助于bridge(网桥)一个虚拟网络设备而实现的,作为默认的网络模式,必然是有它的理由。

那么,我们先从虚拟网络设备说起

虚拟网络设备 vs 物理网络设备

Linux 内核中有一个网络设备管理层,处于网络设备驱动协议栈之间,负责衔接它们之间的数据交互。驱动不需要了解协议栈的细节,协议栈也不需要了解设备驱动的细节。

对于一个网络设备来说,就像一个管道(pipe)一样,有两端,从其中任意一端收到的数据将从另一端发送出去。

比如一个物理网卡 eth0,它的两端分别是内核协议栈(通过内核网络设备管理模块间接的通信)和外面的物理网络,从物理网络收到的数据,会转发给内核协议栈,而应用程序从协议栈发过来的数据将会通过物理网络发送出去。

dfm-007

虚拟网络设备和物理网络设备没有区别,都是网络设备,都能配置IP,也归内核的网络设备管理模块管理。

虚拟网络设备的一端也是协议栈,而另一端是什么取决于虚拟网络设备的驱动实现。

tun/tap

tun/tap 虚拟网络设备一端连着协议栈,另外一端连着用户空间的应用程序

也就是说,协议栈发给 tun/tap 的数据包能被这个应用程序读取到,当然应用程序能直接向 tun/tap 发送数据包。

dfm-003

这里假设物理网卡eth0 IP 为 10.32.0.11,tun0 为一个 tun/tap 设备,IP 配置为 192.168.3.11.

  1. 应用程序 A 通过 Socket A 发送了一个数据包,假设这个数据包的目的IP地址是 192.168.3.1
  2. socket A 将这个数据包丢给网络协议栈
  3. 协议栈根据数据包本地路由规则和目的IP,匹配到数据包应该由 tun0 出去,于是将数据包丢给 tun0
  4. tun0 收到数据包之后,将数据包丢给了应用程序B
  5. 应用程序B收到数据包之后,构造一个新的数据包,将原来的数据包嵌入在新的数据包中,最后通过 socket B 将数据包转发出去,这时候新数据包的源地址变成了eth0 的地址,而目的IP地址变成了一个其它的地址,比如是10.33.0.1.
  6. socket B 将数据包丢给协议栈
  7. 协议栈根据本地路由规矩,将数据包交给 eth0
  8. eth0 通过物理网络将数据包发送出去

简单总结一下,原本要应用程序 A 发送的网络数据包,经由 tun0 转发到应用程序B 处理后,再转发出去。

由此来看, tun/tap 可以将协议栈中的数据包转发给应用程序B, 而应用程序B 可以对数据包做自定义的处理,比如数据压缩、加密等功能,我们比较熟悉和常见的场景就是 VPN

tun 与 tap 的区别

  • tun 设备是一个虚拟的端到端 IP 层设备,也就是说收发 tun 设备的用户程序只能读写 IP 网络数据包(三层)
  • tap 设备是一个虚拟的链路层设备,收发 tap 设备的用户程序能读写链路层数据包(二层)

veth

veth设备是成对出现的,一端连着内核协议栈,另一端是两个设备彼此相连。一个设备收到协议栈的数据发送请求后,会将数据发送到另一个设备上去。

sdm-004

由于它的这个特性,常常被用于构建虚拟网络拓扑。例如连接两个不同的网络命名空间(netns),连接 docker 容器,连接网桥(Bridge)等,其中一个很常见的案例就是 OpenStack Neutron 底层用它来构建非常复杂的网络拓扑。

bridge

bridge (网桥),跟其他虚拟网络设备一样,bridge也可以配置 IP、MAC 地址等。

除此之外,与其他网络设备不同的是,bridge 还是一个虚拟交换机,和物理交换机有类似的功能。

对于普通的网络设备,只有两端,数据从一端进从另一端出。而 bridge 有多个端口,数据可以从多个端口进,从多个端口出。

bridge 一端连接着协议栈,另外一端有多个端口,数据在各个端口间转发数据包是基于 MAC 地址

常见的实际场景

虚拟机

典型的虚拟机网络是通过 tun/tap 将虚拟机内的网卡与宿主机的 br0 连接起来,虚拟机发出去的数据包先经过 br0,然后再交给 eth0 发送出去,数据包都不需要经过宿主机的协议栈,实现真实交换机的效果,效率也高。

sdm-005

容器

咳咳......重点内容,敲黑板!!!做好笔记

上面说过 Docker 的核心是利用 Linux 的 NamespaceCgroups 来实现资源的隔离和限制,而容器之间的网络正是使用了 Network Namespace 实现隔离。在默认的网络模式 bridge 模式中,每个容器都有自己单独的网络协议栈,通讯模式与上面的虚拟机差不多,但是它采用的是 veth-pair 的方式。

dfm-006

那么可能有人问,怎么可以找到宿主机上的 veth 与容器中对应的 eth0 呢?

这里小年教大家点小技巧:

步骤一:容器里执行,ip link show eth0 命令

dfm-008

然后可以看到 13813: eth0@if13814,其中 13813是 eth0 接口的 index, 13814就是它的 veth pair 的 index。

步骤二:宿主机执行 ip link show | grep 13814,这样就能找到容器对应的 veth-pair 关系。

dfm-009

解决办法

办法总比困难多,这个问题很早就有人碰到了,网上大神们也提供了一些解决方法,具体的操作这里就不展开讲,有兴趣的话就参考下面链接:

https://pjw.io/articles/2018/04/25/access-to-the-container-network-of-docker-for-mac/

https://docs.docker.com/desktop/mac/networking/

参考

https://blog.51cto.com/ganbing/2087598

https://morven.life/posts/networking-2-virtual-devices/

https://segmentfault.com/a/1190000009491002

https://segmentfault.com/a/1190000009249039

普通的改变,将改变普通

我是宅小年,一个在互联网低调前行的小青年

关注公众号「宅小年」,个人博客 📖 edisonz.cn,阅读更多分享文章