当前位置: 代码迷 >> 综合 >> Docker:11---Docker的网络通信方式:Docker内部网络、Docker Networking、Docker链接
  详细解决方案

Docker:11---Docker的网络通信方式:Docker内部网络、Docker Networking、Docker链接

热度:41   发布时间:2024-02-01 17:57:53.0
  • Docker容器间的网络通信有以下几种方法:
    • ①Docker的内部网络
    • ②从Docker 1.9及之后的版本开始,可以使用Docker Networking以及docker network命令
    • ③Docker链接。一个可以将具体容器链接到一起来进行通信的抽象层
  • 不推荐使用第一种方法,Docker的内部网络这种解决方案并不是灵活、强大
  • 两种比较现实的连接Docker容器的方式是Docker Networking和Docker链接(Docker link)。如果用户正在使用Docker 1.9或者更新的版本,推荐使用Docker Networking,如果使用的是Docker 1.9之前的版本,应该选择Docker链接
  • 但是我们更加推荐使用Docker Networking而不是Docker链接,原因如下:
    • Docker Networking可以将容器连接到不同宿主机上的容器
    • 通过Docker Networking连接的容器可以在无须更新连接的情况下,对停止、启动或者重启容器。而使用Docker链接,则可能需要更新一些配置,或者重启相应的容器来维护Docker容器之间的链接
    • 使用Docker Networking,不必事先创建容器再去连接它。同样,也不必关心容器的运行顺序,读者可以在网络内部获得容器名解析和发现

 一、Docker内部连接

  • 第一种方法涉及Docker自己的网络栈。前面一系列文章中,我们看到的Docker容器都是公开端口并绑定到本地网络接口的,这样可以把容器里的服务在本地Docker宿主机所在的外部网络上(比如,把容器里的80端口绑定到本地宿主机的更高端口上)公开。除了这种用法,Docker这个特性还有种用法我们没有见过,那就是内部网络

宿主机的“docker0”接口(虚拟的以太网桥)

  • 在宿主机中安装完Docker之后,会在宿主机中创建一个新的网络接口,名为docker0
  • 在宿主机中输入下面的命令查看接口情况,如下所示:
    • 可以看到,docker0接口有符合RFC1918的私有IP地址,范围是172.16~172.30
    • 此docker0接口的地址是172.18.0.1,是这个Docker网络的网关地址,也是当前宿主机中所有Docker容器的网关地址
    • 备注:Docker会默认使用172.17.x.x作为子网地址。如果这个子网被占用了,Docker会在172.16~172.30这个范围内尝试创建别的子网(下图是我们的一台云服务器,其172.17.x.xIP被占用了,因此使用了172.18.x.x的地址)
ip a show docker0

  • 接口docker0是一个虚拟的以太网桥,用于连接容器和本地宿主机网络

宿主机的“vethxxx”接口

  • 在宿主机中每使用“docker run”命令运行一个容器,就会在宿主机中创建一个以“veth”开头的网络接口
  • 如下图所示:
    • 可以看到当前宿主机中运行着两个docker容器
    • 通过ifconfig命令可以查看到有两个以“veth”开头的网络接口
sudo docker psifconfig

Docker容器的“eth0”接口

  • 容器创建成功之后,容器内部会创建一个名为eth0的接口,用于与宿主机之间进行通信
  • 例如,下面我们启动一个容器,并查看其网络接口
    • 可以看到Docker给容器分配的IP地址为172.18.0.4
sudo docker run -t -i ubuntu /bin/bash# 第一次创建容器,没有ifconfig、ip这样的命令,需要输入下面的命令安装
apt-get update
apt-get install net-toolsifconfig

  • 详情参阅下面的工作原理

Docker内部网络工作原理

  • 上面已经介绍了这么多的接口,我们来介绍一下Docker内部网络是如何工作的
  • 第一步:Docker每创建一个容器就会创建一组互联的网络接口,这组接口就像管道的两端(一端发送,另一端接收)
    • 在宿主机中叫“vethxxx”接口
    • 在Docker容器中叫eth0接口

  • 第二步:然后宿主机中还有一个名为docker0的虚拟子网,这个子网由宿主机和所有Docker容器共享
    • 可以把docker0看成是虚拟网线,其一端连接宿主机的“veth”接口,另一端连接容器的eth0接口
    • 因此可以进行通信

路由演示案例

  • 上面我们介绍了工作原理,现在我们从容器内跟踪对外通信的路由,看看是如何建立连接的
  • 第一步:启动一个容器,在容器中输入下面的命令安装traceroute路由工具
apt-get -yqq updateapt-get install -yqq traceroute

  • 第二步:输入下面的命令让容器访问百度
    • 可以看到容器第一跳的地址是宿主机网络上docker0接口的网络IP 172.18.0.1
traceroute baidu.com

Docker的防火墙规则和NAT配置

  • 不过Docker网络还有一部分配置才能允许建立连接:防火墙规则和NAT配置。这些配置允许Docker在宿主机网路和容器间路由
  • 在宿主机中输入下面的命令可以查看宿主机的IPTables NAT配置,如下所示:
    • 这里有几个值得注意的IPTables规则。首先,我们注意到,容器默认是无法访问的。从宿主网络与容器通信时,必须明确指定打开的端口
sudo iptables -t nat -L -n

  • 下面我们以DNAT(即目标NAT)为例,这个规则把容器里的访问路路由到宿主机的32774端口上
  • 第一步:我们宿主机中当前有一个运行着redis服务的容器,如下所示:
    • 容器的4379端口映射到了宿主机的32774端口上

  • 第二步:查看redis容器的网络,命令如下:
    • 可以看到redis容器的IP地址为172.18.0.3
    • 并且使用了docker0接口作为网关地址
    • 容器的6379端口被映射到了本地宿主机的32774端口
# 查看所有配置
sudo docker inspect redis# 过滤只查看IP地址
sudo docker inspect -f '{{ .NetworkSettings.IPAddress }}' redis

  • 第三步:因为容器的6379端口被映射到了本地宿主机的32774端口,所以我们可以在宿主机中通过下面的命令来访问容器中的redis服务
redis-cli -h 127.0.0.1 -p 32774

  • 第四步:但是,因为运行在本地的Docker宿主机上,所以可以不使用映射后的端口,可以直接使用172.18.0.3和4379端口对容器的redis进行访问,如下所示:
redis-cli -h 172.18.0.3 -p 6379

这种联网方式不适用于实际开发

  • 这种联网方式有两大问题:
    • 第一:要在应用程序里对容器的IP地址做硬编码
    • 第二:如果重启容器,Docker可能会改变容器的IP地址
  • 因此,在实际开发中,如果容器提供服务,其重启后IP地址改变,那么就需要不断的更新,这种方式不是很灵活

二、Docker Networking

  • 容器之间的连接用网络创建,这被称为Docker Networking,也是Docker 1.9发布版本中的一个新特性。Docker Networking允许用户创建自己的网络,容器可以通过这个网上互相通信。实质上,Docker Networking以新的用户管理的网络补充了现有的docker0.更重要的是,现在容器可以跨越不同的宿主机来通信,并且网络配置可以更灵活地定制。Docker Networking也和Docker Compose以及Swarm进行了集成(Docker Compose以及Swarm后面文章会介绍)
  • Docker Networking支持也是可插拔的,也就是说可以增加网络驱动以支持来自不同网络设备提供商(如Cisco和VMware)的特性拓扑和网络框架
  • 下面是一个演示案例,演示了将两个容器同时连接到一个名为app的网络中:
    • redis容器:提供一个Redis服务端
    • sinatra_redis容器:提供sinatra Web服务,该Web服务会连接Redis,将Redis作为后端数据库,其连接的redis数据库就是上面redis容器中的redis数据库
    • 为了将这个两个容器放在一个网络中,我们在宿主机创建了一个新的Docker网络,名为app
    • 这个演示案例在前面文章介绍过的,镜像和配置文件请参阅:https://blog.csdn.net/qq_41453285/article/details/107448159

第一步(创建新的Docker网络)

  • 想要使用这种方式,需要先为Docker创建一个网络,然后在这个网络下启动容器
  • 创建网络的命令如下所示:
    • 此处创建了一个桥接网络,名为app
    • 命令返回新创建的网络的网络ID
sudo docker network create app

  • 使用下面的命令可以查看新创建的网络,如下所示:
    • 可以看到这个新网络是一个本地的桥接网络(非常像docker0网络)
    • 而且当前还没有容器运行在这个网络上
sudo docker network inspect app

  • 备注:除了可以创建运行于单个主机之上的桥接网络,我们还可以创建一个overlay网络overlay网络允许我们跨多台宿主机进行通信。详细文档可参阅:https://docs.docker.com/network/network-tutorial-overlay/
  • 可以通过下面的命令列出的当前宿主机中所有的网络,如下所示:
sudo docker network ls

  • 附加:下面的命令是用来删除一个Docker网络的
sudo docker network rm NETWORK ID

第二步(启动redis容器,并加入到app新网络)

  • 上面我们已经成功的创建了一个名为app的网络,现在我们将redis容器加入到这个新网络中
  • 命令如下,就是在运行的时候将容器加入app网络中:
    • -d:将容器以守护进程的方式在宿主机中运行
    • --net:指定了这个新容器运行在哪个网络中,此处为我们上面创建的那个app网络
    • --name:容器的名字叫db
    • 最后一个选项:镜像的名称
sudo docker run -d --net=app --name db dongshao/redis
  • 查看新创建的容器:
sudo docker ps

  • 现在我们再次输入下面的命令来查看app网络的信息,如下所示:
    • 可以看到在这个网络下新增了一个容器,其有Mac、IP等地址,IP地址为172.19.0.2
sudo docker inspect app

第三步(启动webapp_redis容器,并加入到app新网络)

  • 现在我们来启动webapp_redis容器,这个容器包含带有连接redis服务端的sinatra Web服务
  • 在sinatra目录(注意)下,输入下面的命令启动webapp_redis容器:
    • -d:将容器以守护进程的模式在宿主机中运行
    • -p:将容器的4567端口映射到任意一个宿主机的端口上
    • --name:将容器命名为webapp_redis
    • -t:告诉Docker为要创建的容器分配一个伪tty终端。这样新创建的容器就可以提供一个交互式shell
    • -i:保证容器STDIN是开启的
    • -v:
      • 将宿主机的目录作为卷,挂在到容器中。关于“卷”的知识参阅前文:https://blog.csdn.net/qq_41453285/article/details/107397371
      • 此处我们将宿主机当前路径($PWD)下的webapp_redis目录挂载到容器的/opt/webapp目录下,上面我们的Dockerfile文件已经介绍了,容器的/opt/webapp会作为sinatra Web服务的根环境目录
      • 也就是说,访问/opt/webapp下的内容就会访问到宿主机当前路径下的webapp_redis目录
    • dongshao/sinatra_redis:使用到的镜像的名称
    • /bin/bash:容器启动一个shell
sudo docker run -p 4567 --net=app --name webapp_redis -t -i -v $PWD/webapp_redis:/opt/webapp dongshao/sinatra_redis /bin/bash

  • 查看新创建的容器:
sudo docker ps

  • 现在我们在宿主机中输入下面的命令来查看app网络的信息,如下所示:
    • 可以看到在这个网络下新增了一个容器,其有Mac、IP等地址,IP地址为172.19.0.3
sudo docker inspect app

  • 因为我们的db容器和webapp_redis容器都运行在同一个网络之下,因此我们可以在webapp_redis容器中输入下面的命令来ping通db容器

  • 上面能ping的原理是:
    • Docker会将容器的名称作为域名,每一个容器对应一个域名,域名就是它们的容器名
    • 因此通过ping+容器名就可以ping通容器
    • 容器会把这些域名信息保存在/etc/hosts文件中,但是不知道为什么我这次配置的时候没有。正确的情况应该是该文件中有两条记录,一条为“172.19.0.2 db”,另一条为“172.19.0.2 db.app”(前面的为IP,后面的为域名,就是因为这些两个条目,我们上面才能ping通的,不知道为什么我这个文件没有)

  • 上面我们sinatra Web源码中连接的Redis服务端的主机名就是“db”,就是上面我们那个名为“db”的容器

第三步(对Sinatra Web服务进行验证)

  • 上面我们只是启动了sinatra_redis容器,但是没有运行里面的webapp
  • 我们执行下面的命令,启动sinatra_redis容器中的Sinatra Web服务,并且最后使用“&”符号,让其在后台运行
nohup /opt/webapp/bin/webapp &

  • 服务启动之后,在宿主机中查看一下sinatra_redis容器的4567端口映射到了当前宿主机的哪个端口上,如下图所示:
    • sinatra服务的默认端口为4567
    • 容器的4567端口映射到了宿主机的32776端口上

  • 我们可以在网页中尝试访问这个Web服务,显示成功

  • 可以尝试发送POST请求:
    • 我们的sinatra Web服务可以接受以/json结尾的POST请求,并将客户端的请求参数存放到其连接的redis容器的redis数据库中
    • 下面我们发送了一个POST请求,并发送了两个请求参数“name”和“status”
    • sinatra会将POST请求的信息保存到redis数据库,并以JSON的形式再返回给客户端
curl -i -H 'Accept: application/json' -d 'name=Foo&status=Bar' http://localhost:32776/json

  • 查看Redis数据库的内容:
    • 因为sinatra会将数据存放到其连接的redis容器的redis数据库中
    • 我们可以根据Docker的“内部网络”特性(参阅:https://blog.csdn.net/qq_41453285/article/details/107454201),让宿主机直接通过私有地址连接redis容器(IP为172.19.0.2)
    • 连接上之后查看里面的数据,可以看到服务端成功的保存了数据到params键中

  • 我们再次发送请求,获取redis的数据:
    • 输入下面的数据,sinatra会将redis数据库中的数据返回给客户端
curl -i http://localhost:32776/json

附加命令(向网络中加入/删除容器)

  • 上面我们创建了一个名为app的网络
  • 加入容器:
    • 一个容器在“docker run”运行的时候,可以执行--net选项来将其加入到一个网络中
    • Docker还提供了下面的命令让一个已有容器加入到新的网络中
    • 加入宿主机中有一个名为db2的容器,那么可以输入下面的命令让其加入到名为app的网络中
sudo docker network connect app db2
  • 加入完成之后可以输入下面的命令,可以看到容器从网络中移除了
sudo docker network inspect app
  • 删除容器:
    • 将一个容器从指定网络中移除的命令如下所示
    • 下面是将db2容器的网络从app网络中移除的命令
docker network disconnect app db2

三、Docker链接

  • 连接容器的另一种选择就是使用Docker链接。在Docker 1.9之前,这是首选的容器连接方式,并且只有在运行1.9之前版本的情况下才推荐这种方式。让一个容器链接到另一个容器是一个简单的过程,这个过程要引用容器的名字
  • 考虑到还在使用低于Docker 1.9版本的用户,我们来看看Docker链接是如何工作的
  • 还是接着上面的演示案例,我们将sinatra_redis和redis两个容器进行链接

第一步

  • 首先将上面那两个容器先删除,我们需要创建新的容器进行测试了
# 查看当前主机中所有的Docker容器
sudo docker ps -a# 删除前面使用到的两个容器
sudo docker rm webapp_redis db# 再次查看,删除成功
sudo docker ps -a

第二步(创建redis容器)

  • 输入下面的命令启动一个容器:
    • -d:将容器以守护进程的方式在宿主机中运行
    • --name:新容器的名字为redis
    • dongshao/redis:容器所使用的镜像
# 启动容器
sudo docker run -d --name redis dongshao/redis# 查看容器
sudo docker ps -a

  • 该容器启动之后,其镜像的Dockerfile中配置了,会自动启动redis服务

第三步(创建webapp_redis容器,并关联redis容器)

  • 输入下面的命令启动一个webapp_redis容器,命令如下:
    • -p:将容器的4567端口映射到任意一个宿主机的端口上
    • --name:将容器命名为webapp_redis
    • --link:创建两个容器间的“客户-服务”链接。前面为链接的容器的名字(也就是我们上面的redis容器),后面的为链接的别名(取名为db)。在这个演示案例中:
      • webapp容器是“客户”。redis容器是“服务”,并且为这个服务取别名为db
      • 别名让我们可以一致的访问容器公开的信息,而无须关注底层容器的名字
      • 链接让服务容器有能力与客户容器通信,并且能分享一些连接细节,这些细节有助于应用程序配置并使用这个链接
    • -t:告诉Docker为要创建的容器分配一个伪tty终端。这样新创建的容器就可以提供一个交互式shell
    • -i:保证容器STDIN是开启的
    • -v:
      • 将宿主机的目录作为卷,挂在到容器中。关于“卷”的知识参阅前文:https://blog.csdn.net/qq_41453285/article/details/107397371
      • 此处我们将宿主机当前路径($PWD)下的webapp_redis目录挂载到容器的/opt/webapp目录下,上面我们的Dockerfile文件已经介绍了,容器的/opt/webapp会作为sinatra Web服务的根环境目录
      • 也就是说,访问/opt/webapp下的内容就会访问到宿主机当前路径下的webapp_redis目录
    • dongshao/sinatra_redis:使用到的镜像的名称
    • /bin/bash:容器启动一个shell
sudo docker run -p 4567 --name webapp_redis --link redis:db -t -i -v $PWD/webapp_redis:/opt/webapp dongshao/sinatra_redis /bin/bash

  • 在宿主机中查看新创建的容器,如下所示:
sudo docker ps -a

  • 备注:可以把多个容器链接在一起。比如,如果想让这个Redis实例服务于多个Web应用程序,可以把每个Web应用程序的容器和同一个redis容器链接在一起,如下所示:
sudo docker -p 4567 --name webapp2 --link redis:dbsudo docker -p 4567 --name webapp3 --link redis:db
  • 提示:容器链接目前只能工作于同一台Docker宿主机中,不能链接位于不同Docker宿主机上的容器。多于多宿主机网络环境,需要使用Docker Networking,或者使用我们在后面会讨论的“Docker Swarm”,Docker Swarm可以用于完成多台宿主机上的Docker守护进程之间的编排

备注知识点(链接的安全性)

  • 连接也能得到一些安全上的好处。注意,启动Redis容器时,并没有使用-p标志公开redis容器的任何端口。因为不需要这么做,通过把容器链接在一起,可以让客户容器直接访问任意服务容器的公开端口(即客户端webapp_redis容器可以连接到服务redis容器的6379端口)。更妙的是,只有使用--link标志链接到这个容器的容器才能连接到这个端口。容器的端口不需要对本地宿主机公开,现在我们已经拥有一个非常安全的模型。通过这个安全模型,就可以限制容器化应用程序被攻击面,减少应用暴露的网络
  • 提示:如果用户希望,处于安全原因(或其他原因),可以强制Dcoerk只允许有链接的容器之间互相通信。为此,可以在启动Docker守护进程时加上--ice=false标志,关闭所有没有链接的容器间的通信
  • 容器链接之后,会把链接信息写入以下地方:
    • /etc/hosts文件
    • 包含连接信息的环境变量中

第四步(查看/etc/hosts)

  • 先在Docker容器中输入下面的命令来看看/etc/hosts文件,如下所示:
    • 倒数第2行:为链接的redis容器的网络信息,分别为(redis容器的IP、从该连接的别名衍生的主机名db(--link选项中的别名)、redis容器的主机名(默认为容器的ID)、容器的名称)
    • 最后一行:为自己的IP、自己的主机名(默认为容器的ID)
cat /etc/hosts

  • 备注:上面主机的名称都是容器的ID,可以在“docker run”的时候通过-h或者--hostname选项来指定容器的主机名
  • 我们的域名文件中有了链接容器的域名配置,因此可以输入下面的命令来ping通redis容器:
# 通过link别名ping
ping db# 通过容器主机名ping
ping d827ec93ea34# 通过容器名称ping
ping redis# 通过IP ping
ping 172.18.0.2

  • 附加知识(--add-host选项):
    • 该选项用来在创建容器时,在容器的/etc/hosts文件中添加一条记录
    • 例如,我们在运行容器时,想要把宿主机的IP和和域名添加到容器的/etc/hosts文件中,那么可以输入下面的命令
sudo docker run -p 4567 --add-host=docker:111.229.177.161 --name webapp2 --link redis:db ...
  • /etc/hosts文件的更新:前面提到过,如果容器重启了,那么容器的IP就会变化,从Docker 1.3开始,如果被连接的容器重启了,那么/etc/hosts文件的IP地址就会自动更新

第五步(查看环境变量)

  • 可以在容器中输入下面的命令来查看环境变量,如下所示:
env

  • 可以看到有一些以“DB”开头的环境变量:
    • Docker在连接webapp_redis和redis容器时,自动创建了这些以DB开头的环境变量。以DB开头是因为DB是创建连接时的别名
    • 这些自动创建的环境变量包含以下信息:
      • 子容器的名字
      • 容器里运行的服务所使用的协议、IP和端口号
      • 容器里运行的不同服务所指定的协议、IP和端口号
      • 容器里由Docker设置的环境变量的值
  • 具体的变量会因为容器的配置不同而有所不同(如容器的Dockerfile中右ENV和EXPOSE指令定义的内容)。重要的是,这些变量包含一些我们可以在应用程序中用来进行持久的容器间链接的信息

第六步(将这两个容器关联)

  • 此时我们的两个容器都配置好了,现在我们可以来将两个容器进行链接了
  • 有两种方法来链接容器:
    • 方法一:使用环境变量里的一些连接信息
    • 方法二:使用DNS和/etc/hosts信息
  • 方法一:修改app.rb源文件,将其源码修改为下面的代码
    • 这里使用Ruby的URI模板来解析DB_ROOT环境变量,让我们使用解析后的宿主机和端口数出来配置Redis连接信息
    • 我们的应用程序现在就可以使用该连接信息来找到在以链接容器中的Redis了
    • 这种抽象模式避免了我们在代码中对Redis的IP地址和端口进行硬编码,但是它仍是一种简陋的服务发现方式
require 'uri'...
uri = URI.parse(ENV['DB_PORT'])
redis = Redis.new(:host => uri.host, :port => uri.port)
...
  • 方法二:还有一种方法就是更灵活的本地DNS。修改app.rb源文件,将其源码修改为下面的代码
    • 我们的应用程序会在本地查找名为db的宿主机,找到/etc/hosts文件里的相关项并解析宿主机到正确的IP地址
    • 这也解决了硬编码IP地址的问题
redis = Redis.new(:host => 'db', :port => '6379')
  • 附加知识(--dns、--dns-search):
    •  也可以在docker run命令中加入--dns或者--dns-search标志来为某个容器单独配置DNS
    • 你可以设置本地DNS解析的路径和搜索域。在https://docs.docker.com/network/上可以找到更详细的配置信息
    • 如果没有这两个标志,Docker会根据宿主机的信息来配置DNS解析。可以在/etc/resolv.conf文件中查看DNS解析的配置情况
  • 就是不演示,自己修改源码运行吧