前言

 在上一篇文章中 《“深入浅出”来解读Docker网络核心原理》 大家了解了Docker中libnetwrok提供的4种驱动,它们各有千秋,但实际上每一种方式都有一定的局限性。假设需要运营一个数据中心的网络,我们有许多的宿主机,每台宿主机上运行了成千上万个Docker容器,如果使用4种网络驱动的话会是怎么样的呢,我们来分析一下:

  1. 使用host驱动可以让容器与宿主机共用同一个网络栈,这么做看似解决了网络问题,可实际上并未使用network namespace的隔离,缺乏安全性。
  2. 使用Docker默认的bridge驱动,容器没有对外IP,只能通过NAT来实现对外通信。这种方式不能解决跨主机容器间直接通信的问题,难以满足复杂场景下的业务需求。
  3. 使用overlay驱动,可以用于支持跨主机的网络通信,但必须配合swarm进行配置和使用才能实现跨主机的网络通信。
  4. 使用null驱动实际上不进行任何网络配置。

 可见,为了实现数据中心大量容器间的跨主机网络通信,为了更灵活地实现容器间网络的共享与隔离,也为了在管理成千上万个容器时可以更加自动化地进行网络配置,我们需要来了解更高级的网络方案。

 本文及后期的文章将通过一些工具和额外的操作来突破Docker网络原有的限制,实现一些更高级的功能来满足实际运用中的复杂需求。

把Linux network namespace玩起来

 在上一篇文章中已经介绍过了linux network namespace,在本文中我们将从实践的角度来了解如何在linux系统下操作linux network namespace。

 ip是linux系统下一个强大的网络配置工具,它不仅可以替代一些传统的网络管理工具,如ifconfig、route等,还可以实现更丰富的功能。下面将介绍如何使用ip命令来管理network namespace。

使用ip netns来操作network namespace

ip netns命令是用来操作network namespace的指令,具体使用方法如下。

  • 创建一个network namespace:
#创建一个名为net-test的network namespace
[root@ganbing ~]# ip netns add net-test
  • 列出系统中已存在的network namespace:
[root@ganbing ~]# ip netns ls
net-test
  • 删除一个network namespace:
[root@ganbing ~]# ip netns delete net-test
  • 在network namespace中执行一条命令:

#命令格式 ip netns exec <network nameapce name> <command>

#比如显示net-test namespace的网卡信息,路由信息

[root@ganbing ~]# ip netns exec net-test ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
     link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
 
 [root@ganbing ~]# ip netns exec net-test route -n
 Kernel IP routing table
 Destination     Gateway         Genmask         Flags Metric Ref    Use Iface

其实,你如果觉得ip netns exec 来执行命令比较麻烦,还可以使用启动一个shell来配合: #命令格式 ip netns exec <network nameapce name> bash

这样,就可以在上面执行命令,就好像使用者进入了这个network namespace中;如果要退出这个bash,则输入exit即可。

使用ip为network namespace配置网卡

 当使用ip netns add命令创建了一个network namespace后,就拥有了一个独立的网络空间,可以根据需求来配置该网络空间,如添加网卡,配置IP,设置路由等。下面以之前建立的名为net-test的network namespace为例来演示如何进行这些操作。

 当使用ip命令创建一个network namespace时,会默认创建一个回环设备(loopback interface:lo)。该设备默认不启动,最好将其启动。

[root@ganbing ~]# ip netns exec net-test ip link set dev lo up

在主机上创建两张虚拟网卡veth-1 和 veth-2:

[root@ganbing ~]# ip link add veth-1 type veth peer name veth-2

将veth-2设备添加到net-test这个network namespace中,veth-1留在宿主机中:

[root@ganbing ~]# ip link set veth-2 netns net-test

现在net-test这个network namespace就有两块网卡了(lo和veth-2),验证看一下:

[root@ganbing ~]# ip netns exec net-test ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
222: veth-2@if223: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000
    link/ether 92:24:fd:44:c6:00 brd ff:ff:ff:ff:ff:ff link-netnsid 0

接下来可以为网卡分配IP并启动网卡:

#在主机上为veth-1配置IP并启动
[root@ganbing ~]# ip addr add 10.0.0.1/24 dev veth-1
[root@ganbing ~]# ip link set dev veth-1 up

#为net-test中的veth-2配置IP并启动
[root@ganbing ~]# ip netns exec net-test ip addr add 10.0.0.2/24 dev veth-2
[root@ganbing ~]# ip netns exec net-test ip link set dev veth-2 up

 给两张网卡配置了IP后,会在各自的network namespace中生成一条路由,用ip route 或者 route -n查看:

#在主机中查看路由
[root@ganbing ~]# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
...
10.0.0.0        0.0.0.0         255.255.255.0   U     0      0        0 veth-1
...

#在net-test中查看路由
[root@ganbing ~]# ip netns exec net-test route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
10.0.0.0        0.0.0.0         255.255.255.0   U     0      0        0 veth-2

 上面这两条路由表明的意义是目的地址 10.0.0.0/24网络的IP包分别从veth-1和veth-2发出。

 现在net-test这个network namespace有了自己的网卡、IP地址、路由表等信息,就相当于成了一台小型的“虚拟机”了。测试一下它的连通性,来检查配置是否正确。

 从主机的veth-1网卡ping net-test的veth-2网卡:

 从net-test的veth-2网卡ping主机的veth-1网卡:

将两个network namespace连接起来

 很多时候,想搭建一个复杂的网络环境来测试数据,往往受困于没有足够的资源来创建虚拟机。我们掌握了配置network namespace后,便可以解决这个问题。可以在一台普通的机器上,以简单的方式创建多个相互隔离的network namespace,然后通过网卡、网桥等虚拟设备将它们连接起来,组成想要的网络拓扑。

 下面我们来演示一个简单的例子,将两个network namespace通过veth pair设备连起来。过程如下:

1、创建两个network namespace ns1、ns2,名称可自行定义:

[root@ganbing ~]# ip netns add ns1
[root@ganbing ~]# ip netns add ns2
[root@ganbing ~]# ip netns ls
ns2
ns1

2、创建veth pair设备veth-a,veth-b:

[root@ganbing ~]# ip link add veth-a type veth peer name veth-b

3、将网卡分别放到两个network namespace中:

[root@ganbing ~]# ip link set veth-a netns ns1
[root@ganbing ~]# ip link set veth-b netns ns2

4、启动这两个网张:

[root@ganbing ~]# ip netns exec ns1 ip link set dev lo up
[root@ganbing ~]# ip netns exec ns1 ip link set dev veth-a up
[root@ganbing ~]# ip netns exec ns2 ip link set dev lo up
[root@ganbing ~]# ip netns exec ns2 ip link set dev veth-b up

5、分配IP:

[root@ganbing ~]# ip netns exec ns1 ip addr add 10.0.0.1/24 dev veth-a
[root@ganbing ~]# ip netns exec ns2 ip addr add 10.0.0.2/24 dev veth-b

6、验证连通

 通过veth pair设备连接起来的两个network namespace就好像直接通过网线连接起来的两台机器,它的拓扑图如下所示:

 大家想一下,如果有更多的network namespace需要连接怎么办?是不是就需要引入虚拟网桥了,就如同Docker网络一样。

使用ip命令配置Docker容器网络

 在上一篇文章 <“深入浅出”来解读Docker网络核心原理> 介绍过,Docker是使用Linux namespace技术进行资源隔离的,网络也是如此。当用默认网络模式(bridge模式)启动一个Docker容器时,一定是在主机上新建了一个Linux network namespace。我们可以按照在network namespace中配置网络的方法来配置Docker 容器的网络。

 首先,启动一个名为test1的Docker容器:

[root@ganbing ~]# docker run -itd --name test1 busybox

 然后,使用ip netns list命令查看是否可以看到新建的network namespace。执行命令后发现并没有看到新建的network namespace。这并不代表Docker容器没有创建network namespace,只是ip netns 命令无法查看而以,这个与ip netns命令工作方式有关。

 当使用ip netns命令创建了两个network namespace(ns1、ns2)后,会在/var/run/netns目录下看到ns1和ns2:

[root@ganbing ~]# ls -la /var/run/netns/
total 0
drwxr-xr-x  2 root root   80 Mar 19 18:25 .
drwxr-xr-x 40 root root 1240 Mar 19 15:08 ..
-r--r--r--  1 root root    0 Mar 19 18:22 ns1
-r--r--r--  1 root root    0 Mar 19 18:22 ns2

 ip netns list命令在/var/run/netns目录下查找network namespace。由于Docker创建的network namespace并不在此目录下创建任何选项,因此,需要一些额外的操作来使ip命令可以操纵Docker创建的network namespace。

 Linux下的每一个进程都会属于一个特定的network namespace,来看一下不同network namespace环境中/pro/$PID/ns目录下有何区别。

#/proc/self 链接到当前正在运行的进程
[root@ganbing ~]# ls -la /proc/self/ns/
......
lrwxrwxrwx 1 root root 0 Mar 19 19:17 net -> net:[4026531956]
......

#在ns1和ns2中
[root@ganbing ~]# ip netns exec ns1 ls -la /proc/self/ns
......
lrwxrwxrwx 1 root root 0 Mar 19 19:18 net -> net:[4026533018]
......

[root@ganbing ~]# ip netns exec ns2 ls -la /proc/self/ns
lrwxrwxrwx 1 root root 0 Mar 19 19:18 net -> net:[4026533116]

 从上面可以发现,不同network namespace中的进程有不同的net:[]号码发配。这些号码代表着不同的network namespace,拥有相同net:[]号码的进程属于同一个network namesapce。只要将代表Docker创建的network namesapce的文件链接到/var/run/netns目录下,就可以使用ip netns命令操作了,步骤方法如下:

1、用docker inspect查看test1容器的PID

[root@ganbing ~]# docker inspect  --format '{{.State.Pid}}' test1
17037

2、如果/var/run/netns目录不存在,就要手工创建(一般都有),然后在/var/run/netns目录下创建软链接,指向test1容器的network namespace

[root@ganbing ~]# ln -s /proc/17037/ns/net  /var/run/netns/test1

3、测试是否成功

[root@ganbing ~]# ip netns list
test1
ns2
ns1

[root@ganbing ~]# ip netns  exec test1 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
226: eth0@if227: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

 完成上面的配置后,就可以自行配置Docker的网络环境了。除了ip netns命令外,还有一些工具可以进入linux network namespace,比如nsenter。但需要额外的安装这个工具。

把pipework玩起来

 Docker现有的网络比较简单,扩展性和灵活性都不能满足很多复杂的应用场景。很多时候都需要自定义Docker容器的网络。比如,为了使容器各节点之间通信、各节点和本地主机之间的通信,比较简单的做法就是将Docker容器网络配置到本地主机网络的网段中。我们来看一下怎么实现。

将Docker容器配置到本地网络环境中

 如果想要使Docker容器和容器主机处于同一网络,那么容器和主机应该处在一个二层网络中。就是把两台机器连在同一个交换机上,或者连在不同的级联交换机上。在虚拟场影 下,虚拟网桥可以将容器连在一个二层网络中,只要将主机的网卡桥接到虚拟网桥中,就能将容器和主机的网络连起来,再给Docker容器分配一个本地局域网IP就OK了。

我们来通个一个例子分析一下这个过程 :本地网络为 172.18.18.0/24,网关为 172.18.18.1,宿主机IP为172.18.18.34(网卡ens160),要在这台宿主机上启动一个名为test的Docker容器,并给它配置IP为 172.18.18.36。由于并不需要Docker提供的网络,所以用--net=none参数来启动容器。操作如下:

1、启动一个test容器

[root@docker ~]# docker run -itd --name test01 --network none busybox
39ea5fac5ebb8bd25372d04efb6b662a18cd6fdf85105c22df0796087d776280

2、创建一个供容器连接的网桥br0

[root@docker ~]# brctl addbr br0
[root@docker ~]# ip link set dev br0

3、将主机ens160网卡桥接到br0上,并把ens160的IP配置在br0上。由于笔者是远程操作服务器,所以执行这一步的时候会导致网络断开,因此这里放在一条命令执行

[root@docker ~]# ip addr add 172.18.18.34/24 dev br0; \
> ip addr del 172.18.18.34/24 dev ens160; \
> brctl addif br0 ens160; \
> ip route del default; \
> ip route add default via 172.18.18.1 dev br0

4、找到test01的PID

[root@docker ~]# docker inspect  --format '{{.State.Pid}}' test01
4557

5、将容器的network namespace添加到/var/run/netns/目录下

[root@docker ~]# mkdir /var/run/netns
[root@docker netns]# ln -s /proc/4557/ns/net /var/run/netns/test01

6、创建用于连接网桥和Docker容器的网卡设备

#将veth-a连接到br0网桥中
[root@docker ~]# ip link add veth-a type veth peer name veth-b
[root@docker ~]# brctl addif br0 veth-a
[root@docker ~]# ip link set dev veth-a up

#将veth-b放在test的network namespace中,重命令eth0,并为其配置IP和默认路由
[root@docker ~]# ip netns exec test01 ip link set dev lo up
[root@docker ~]# ip link set veth-b netns test01
[root@docker ~]# ip netns exec test01 ip link set dev veth-b name eth0
[root@docker ~]# ip netns exec test01 ip link set eth0 up
[root@docker ~]# ip netns exec test01 ip addr add 172.18.18.36/24 dev eth0
[root@docker ~]# ip netns exec test01 ip route add default via 172.18.18.1

7、查看一下test01的网卡情况,并测试

[root@docker ~]# ip netns exec test01 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
9: eth0@if10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP qlen 1000
    link/ether 66:fa:71:ba:0e:fb brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.18.18.36/24 scope global eth0
       valid_lft forever preferred_lft forever
[root@docker ~]# ip netns exec test01 route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.18.18.1     0.0.0.0         UG    0      0        0 eth0
172.18.18.0     0.0.0.0         255.255.255.0   U     0      0        0 eth0

 完成配置扣,Docker容器和宿主机连接的网络图如下所示:

 现在test01容器可以与本地主机相互访问,并且test01容器可以通过本地网络的网关172.18.18.1访问外部网络。

来解析一下pipework

 从上面的过程可以发现,配置Docker容器的网络是相当繁琐的。如果需要经常自定义Docker网络,可以把上面的步骤编写成shell脚本,这样方便操作。事实上,目前已有了一个这样的工具解脱我们繁琐的步骤,就是由Docker公司工程师Jerome Petazzoni在Githu上发布的pipework的工具。pipwork号称是容器的SDN解决方案,可以在复场景下将容器连接起来。其实随着Docker网络的不断改进,piipwork工具的很多功能会被Docker原生支持,因此pipework当初只是过渡方案之一而以,大家只要知道了解就行了。下面来看一下pipework的功能。

*** 支持linux网桥连接到容器并配置容器IP** 1、下载pipework

[root@docker ~]# git clone https://github.com/jpetazzo/pipework

2、将pipework脚本放处指定的目录,/usr/local/bin

[root@docker ~]# cp ./pipework/pipework /usr/local/bin/

3、对test01容器进行配置

[root@docker /]# docker run -itd --name  test01 --network none busybox
[root@docker /]# pipework  br0 test01 172.18.18.36/24@172.18.18.1

上面配置命令操作如下:

  • 查看主机是否存在br0网桥,不存在就创建;
  • 向test01中加入一块名为eth1的网卡,并配置IP172.18.18.36/24;
  • 若test01中已有默认路由,就删除,把172.18.18.1设为默认路由l
  • 将test01容器连接到之前创建的网桥上br0;

这个过程和之前采用ip命令配置的过程类似,pipework其实就是用shell写的代码。

pipework其实还有其它的很多功能,比如还支持open vswitch、支持dhcp获取容器的IP等等,本文只要是和大家了解一下它的作用和功能,其它详细的功能就不作介绍了。

看到这里的朋友应该对network namespace和pipework有了更好的理解