Docker高级网络实践

本文涉及的产品
公网NAT网关,每月750个小时 15CU
简介: Docker中libnetwork提供的4种驱动,它们各有千秋,但实际上每一种方式都有一定的局限性。

Docker中libnetwork提供的4种驱动,它们各有千秋,但实际上每一种方式都有一定的局限性。假设需要运营一个数据中心的网络,我们有许多宿主机,每台宿主机上运行了数百个甚至上千个Docker容器,使用4种网络驱动的具体情况如下。

❏ 使用host驱动可以让容器与宿主机共用同一个网络栈,这么做看似解决了网络问题,可实际上并未使用network namespace的隔离,缺乏安全性。

❏ 使用Docker默认的bridge驱动,容器没有对外IP,只能通过NAT来实现对外通信。这种方式不能解决跨主机容器间直接通信的问题,难以满足复杂场景下的需求。

❏ 使用overlay驱动,可以用于支持跨主机的网络通信,但必须要配合Swarm进行配置和使用才能实现跨主机的网络通信。

❏ 使用null驱动实际上不进行任何网络设置。

玩转Linux network namespace

ip是Linux系统下一个强大的网络配置工具,它不仅可以替代一些传统的网络管理工具,如ifconfig、route等

1.使用ip netns命令操作network namespac

ip netns命令是用来操作network namespace的指令

  • 创建一个名为nstest的network namespace .
  • 列出系统中已存在的network namespace:

删除一个network namespace:

在network namespace中执行一条命令:

root@foobar-server:/home/vagrant# ip netns exec 23531 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
    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
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
12: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether e6:91:07:39:fd:59 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.95/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::e491:7ff:fe39:fd59/64 scope link
       valid_lft forever preferred_lft forever

2.使用ip命令为network namespace配置网卡

使用ip netns add命令创建了一个network namespace后,就拥有了一个独立的网络空间,可以根据需求来配置该网络空间,如添加网卡、配置IP、设置路由规则等.


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


ip netns exec nstest ip link set dev lo up

在主机上创建两张虚拟网卡veth-a和veth-b。

ip link add veth-a type veth peer name veth-b

将veth-b设备添加到nstest这个network namespace中,veth-a留在主机中

ip link set veth-b netns nstest

nstest这个network namespace就有了两块网卡lo和veth-b

ip netns exec nstest ip link
lo: <LOOPBACK, UP, LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
veth-b: <BROADCAST, MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000
    link/ether 72:01:ad:c5:67:84 brd ff:ff:ff:ff:ff:ff

为网卡分配IP并启动网卡

# 在主机上为veth-a配置IP并启动
 ip addr add 10.0.0.1/24 dev veth-a
ip link set dev veth-a up
# 为nstest中的veth-b配置IP并启动
 ip netns exec nstest ip addr add 10.0.0.2/24 dev veth-b
 ip netns exec nstest ip link set dev veth-b up

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

# 在主机中
 ip route
...
10.0.0.0/24 dev veth-a  proto kernel  scope link  src 10.0.0.1
# 在nstest network namespace中
 ip netns exec nstest ip route
10.0.0.0/24 dev veth-b  proto kernel  scope link  src 10.0.0.2

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

测试一下连通性

从主机的veth-a网卡ping nstest network namespace的veth-b网卡

ping 10.0.0.2

从nstest network namespace的veth-b网卡ping主机的veth-a网卡

ping 10.0.0.1

3.将两个network namespace连接起来

创建相互隔离的networknamespace,然后通过网卡、网桥等虚拟设备将它们连接起来,组成想要的拓扑网络。

# 创建两个network namespace ns1, ns2
 ip netns add ns1
 ip netns add ns2
# 创建veth pair设备veth-a, veth-b
 ip link add veth-a type veth peer name veth-b
# 将网卡分别放到两个namespace中
 ip link set veth-a netns ns1
 ip link set veth-b netns ns2
# 启动两张网卡
 ip netns exec ns1 ip link set dev veth-a up
 ip netns exec ns2 ip link set dev veth-b up
# 分配IP
 ip netns exec ns1 ip addr add 10.0.0.1/24 dev veth-a
 ip netns exec ns2 ip addr add 10.0.0.2/24 dev veth-b
# 验证连通
 ip netns exec ns1 ping 10.0.0.2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_req=1 ttl=64 time=0.054 ms
...

通过veth pair设备连接起来的两个network namespace就好像直接通过网线连接起来的两台机器。

如果有更多network namespace需要连接,那就有必要引入虚拟网桥了,就如同Docker的网络一样。

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

Docker正是使用Linux namespaces技术进行资源隔离的,网络也是如此隔离的。当用默认网络模式(bridge模式)启动一个Docker容器时,一定是在主机上新创建了一个Linux network namespace。


配置Docker容器的网络

启动一个名为con4的Docker容器

docker run -itd --name con4 ubuntu:14.04 /bin/bash

解决ip netns list 无法查看docker 创建的netns.

ip netns list命令在/var/run/netns目录下查找network namespace。由于Docker创建的network namespace并不在此目录下创建任何项。


不同network namespace环境中/proc/$PID/ns目录下都有不同的netns. 这几个网络是相互隔离起来的。

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

# 用docker inspect查看从con4容器的PID
 docker inspect ——format '{{ .State.Pid }}' con4
31203
# 若不存在/var/run/netns目录,则创建
 mkdir -p /var/run/netns
# 在/var/run/netns目录下创建软链接,指向test1容器的network namespace
 ln -s /proc/31203/ns/net /var/run/netns/con4
# 测试是否成功
 ip netns list
ns1
ns2
con4
 ip netns exec con4 ip link
1: lo: <LOOPBACK, UP, LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
52: eth0: <BROADCAST, UP, LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen
    1000
    link/ether 02:42:ac:11:00:04 brd ff:ff:ff:ff:ff:ff

pipework原理解析

Docker现有的网络模式比较简单,扩展性和灵活性都不能满足很多复杂应用场景的需求。很多时候用户都需要自定义Docker容器的网络,而非使用Docker默认创建的IP和NAT规则。


使用Docker容器来安装部署Cloud Foundry时,为了方便Cloud Foundry各节点之间以及各节点和本地主机之间的通信,一个简单的做法就是将Docker容器网络配置到本地主机网络的网段中。

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

如果想要使Docker容器和容器主机处于同一个网络,那么容器和主机应该处在一个二层网络中。能想到的场景就是把两台机器连在同一个交换机上,或者连在不同的级联交换机上。


在虚拟场景下,虚拟网桥可以将容器连在一个二层网络中,只要将主机的网卡桥接到虚拟网桥中,就能将容器和主机的网络连接起来。构建完拓扑结构后,只需再给Docker容器分配一个本地局域网IP就大功告成了。


网络拓扑如图


本地网络为10.10.103.0/24


网关为10.10.103.254


有一台IP地址为10.10.103.91/24的主机(网卡为eth0)

6ba7fabdbb814d81b33cf81788b19efb.png

# 启动一个名为test1的Docker容器
 docker run -itd --name test1 --net=none ubuntu /bin/bash
# 创建一个供容器连接的网桥br0
 brctl addbr br0
 ip link set br0 up
# 将主机eth0桥接到br0上,并把eth0的IP配置在br0上。由于是远程操作,会导致网络断开,因此这里放在
一条命令中执行
 ip addr add 10.0.2.15/24 dev br0; \
      sudo ip addr del 10.0.2.15/24 dev eth0; \
    sudo brctl addif br0 eth0; \
    sudo ip route del default; \
    sudo ip route add default via 10.0.2.254 dev br0
# 找到test1的PID,保存到pid中
$ pid=$(sudo docker inspect --format='{{ .State.Pid }}' test1)
# 将容器的network namespace添加到/var/run/netns目录下
 mkdir -p /var/run/netns
 ln -s /proc/$pid/ns/net /var/run/netns/$pid
# 创建用于连接网桥和Docker容器的网卡设备
# 将veth-a连接到br0网桥中
 ip link add veth-a type veth peer name veth-b
 brctl addif br0 veth-a
 ip link set veth-a up
# 将veth-b放到test1的netwrok namespace中,重命名为eth0,并为其配置IP和默认路由
 ip link set veth-b netns $pid
 ip netns exec $pid ip link set dev veth-b name eth0
 ip netns exec $pid ip link set eth0 up
 ip netns exec $pid ip addr add 10.0.2.95/24 dev eth0
 ip netns exec $pid ip route add default via 10.0.2.254

现在test1容器可以很方便地实现与本地主机相互访问,并且test1容器可以通过本地网络的网关10.10.103.254访问外部网络。

2. 使用pipework配置docker 网络

配置Docker容器的网络是相当烦琐的。如果需要经常自定义Docker网络,可考虑提炼上述过程,编写成shell脚本,方便操作。


这个过程由Docker公司工程师Jerome Petazzoni在GitHub上发布的名为pipework的工具可以实现。pipework号称是容器的SDN解决方案,可以在复杂场景下将容器连接起来。它既支持普通的LXC容器,也支持Docker容器。


下面是启动一个容器不使用docker任何网络模型,通过pipework + iptables 命令去实现宿主机和容器、容器和容器、容器和外部的通信

 docker run -it --rm --net none --name test1 ubuntu:14.04 bash

用了很多ip命令来配置test1容器的IP地址和网关,用pipework工具则可以很方便地完成配置,具体示例如下:

# 下载pipework
 git clone https://github.com/jpetazzo/pipework
# 将pipework脚本放入PATH环境变量所指定的目录下,如/usr/local/bin/
cp ~/pipework/pipework /usr/local/bin/
# 完成test1的配置
 ./pipework br0 test1 192.168.1.10/24@192.168.1.254

这一行配置命令执行的操作如下:

❏ 查看主机中是否存在br0网桥,不存在就创建;

❏ 向test1容器中加入一块名为eth1的网卡,并配置IP地址为192.168.1.10/24;

❏ 若test1中已经有默认路由,则删掉,把192.168.1.254设为默认路由的网关;

❏ 将test1容器连接到之前创建的网桥br0上。

这是容器能访问外部网络吗?

答案是不能,没有相应的 NAT 地址伪装规则,而这个 NAT 地址伪装规则在默认情况下会由 Docker 自动添加。在 Docker 宿主机上添加相应的 iptables 规则。

iptables -t nat -A POSTROUTING -s 192.168.0.0/16 -j MASQUERADE

这条命令是加入 NAT 表 (-t nat) 中的POSTROUTING 链 (-A POSTROUTING),将源地址为 192.168.0.0/16 的数据包进行地址转换,转变为 NAT 转换节点的地址。这里指的是服务器的IP地址。

换句话说,当从一个 IP 地址为 192.168.0.0/16 的 Docker 容器发送到外部网络的数据包时,这条规则将会允许这些数据包通过该主机网络接口并自动更改来源地址为该主机所拥有的 IP 地址。这通常用于特定场景下的网络地址转换和隐藏,比如在 Docker 网络中使用 NAT 的情况下将容器内的私有 IP 地址变为公网 IP,以使其能够与外部网络通信。


具体地,通过 -j MASQUERADE 指定了一种地址转换方式,即源 IP 地址 NAT,也称 Source NAT 。此操作将更改数据包的源地址为该接口的公有 IP,这使得返回流量能够回到原始 IP。

3、使用网络命令配置docker 网络

假如不使用这个pipework,自己实现这个过程呢?这种逆向功能有助于深入理解docker 容器的网络


下面是启动一个容器不使用docker任何网络模型,通过命令去实现宿主机和容器、容器和容器、容器和外部的通信.


需要掌握的网络命令

  • ip
  • brctl
  • route
  • iptables

宿主机上的操作:

  • 创建网桥并设置为ip地址(网关的地址)
$ sudo brctl addbr br0
$ sudo ip addr add 192.168.1.254/24 dev br0
$ sudo ip link set dev br0 up
  • 创建虚拟的veth pair, 并将一端和br0连接

来创建一个 veth 对 veth_host、veth_container,并将 veth_host连接到网桥 br0 上

$ sudo ip link add veth_host type veth peer name veth_container # 创建veth pair
$ sudo brctl addif br0 veth_host # 将veth_host 插入br0
$ sudo ip link set veth_host up # 启动veth_host设备

ip -d link show 查看所有网络接口详细信息

brctl show 查看网桥信息

$ ip -d link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT \
group default
 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 promiscuity 0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state \
UNKNOWN mode DEFAULT group default qlen 1000
 link/ether 08:00:27:98:a7:ad brd ff:ff:ff:ff:ff:ff promiscuity 0
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state \
DOWN mode DEFAULT group default
 link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff promiscuity 0
 bridge
6: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode \
DEFAULT group default
 link/ether ee:7d:7e:f7:6f:18 brd ff:ff:ff:ff:ff:ff promiscuity 0
 bridge
8: veth_host: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master br0 \
state UP mode DEFAULT group default qlen 1000
 link/ether ee:7d:7e:f7:6f:18 brd ff:ff:ff:ff:ff:ff promiscuity 1
 veth
$ brctl show
bridge name bridge id STP enabled interfaces
br0 8000.ee7d7ef76f18 no foo
docker0 8000.000000000000 not

容器里面的操作:

ip netns 默认查询network ns的地址是/var/run/netns。 因此建一个软连接指向docker 容器的ns

$ cd /var/run
$ sudo ln -s /var/run/docker/netns netns
$ sudo ip netns
c785553b22a1
$ NID=$(sudo ip netns)

宿主机可以看到容器的ns了。容器的操作都是在宿主机上切换ns进入容器的ns进行设定。

713fdbfaf2514c9b9c43dcea128ad410.png

  • 增加一个网络接口
$ sudo ip link set veth_conntainer netns $NID # 将刚才创建的veth pair中的veth_conntainer虚拟设备放入容器的ns
$ sudo ip netns exec $NID ip link set dev veth_conntainer  name eth1 #  进入ns 并将"bar"的接口重命名为 "eth1。  这是模拟docker引擎处理虚拟网络接口的过程。
$ sudo ip netns exec $NID ip link set eth1 address 12:34:56:78:9a:bc # 为eth1 添加ip地址
$ sudo ip netns exec $NID ip link set eth1 up
  • 设置路由信息
$ sudo ip netns exec $NID ip addr add 192.168.1.1/24 dev eth1 # 为eth1 设置ip地址
$ sudo ip netns exec $NID ip route add default via 192.168.1.254 #  为eth1 设置gateway

如果你想访问容器外部网络,就要添加如下的 IP NAT 地址伪装规则。

$ sudo iptables -t nat -A POSTROUTING -s 192.168.0.0/24 -j MASQUERADE

pipework源代码

# IFACE保存传进去的第一个参数,即br0
IFNAME=$1
# 判断主机中是否存在br0,并判断其类型
if [ -d /sys/class/net/$IFNAME/bridge ]
    then
      IFTYPE=bridge
      BRTYPE=linux
...
# 若主机中不存在br0,则从名称头部“br”判断它为Linux网桥,并创建br0
case "$IFNAME" in
br*)
    IFTYPE=bridge
    BRTYPE=linux
    ;;
...
[ $IFTYPE = bridge ] && [ ! -d /sys/class/net/$IFNAME ] && {
[ $BRTYPE = linux ] && {
    (ip link add dev $IFNAME type bridge > /dev/null 2>&1) || (brctl addbr $IFNAME)
    ip link set $IFNAME up
}
 ..
# CONTAINER_IFNAME中保存了需要添加到Docker容器中网卡的名称,默认为eth1
# 可以使用-i参数自定义,如pipework br0-i eth2 ...
if [ "$2" = "-i" ]; then
    CONTAINER_IFNAME=$3
    shift 2
fi
...
CONTAINER_IFNAME=${CONTAINER_IFNAME:-eth1}
# IPADDR保存要配置的IP地址,GATEWAY保存网关信息
# IP地址一定要指定子网掩码。网关使用“@”符号接在IP地址后,也可以不指定
IPADDR=$3
...
if echo $IPADDR | grep -q @
then
    GATEWAY=$(echo $IPADDR | cut -d@ -f2)
    IPADDR=$(echo $IPADDR | cut -d@ -f1)
else
    GATEWAY=
fi
# GUESTNAME保存要配置的容器的名称,即test1
# 通过Docker容器名称test1找到容器的PID,并最终保存到NSPID中
GUESTNAME=$2
...
DOCKERPID=$(docker inspect ——format='{{ .State.Pid }}' $GUESTNAME
...
if [ $DOCKERPID ]; then
    NSPID=$DOCKERPID
# 将Docker容器的network namespace软链接到/var/run/netns目录下
ln -s /proc/$NSPID/ns/net /var/run/netns/$NSPID
# 创建veth pair设备,名称分别为LOCAL_IFNAME和GUEST_IFNAME,并将LOCAL_IFNAME放到之前创建的br0网桥上
LOCAL_IFNAME="v${CONTAINER_IFNAME}pl${NSPID}"
GUEST_IFNAME="v${CONTAINER_IFNAME}pg${NSPID}"
ip link add name $LOCAL_IFNAME mtu $MTU type veth peer name $GUEST_IFNAME mtu $MTU
brctl addif $IFNAME $LOCAL_IFNAME
ip link set $LOCAL_IFNAME up
# 将GUEST_IFNAME放入Docker容器中,并重命名为eth1
ip link set $GUEST_IFNAME netns $NSPID
ip netns exec $NSPID ip link set $GUEST_IFNAME name $CONTAINER_IFNAME
# 给Docker容器新增加的网卡配置IP地址和网关
ip netns exec $NSPID ip addr add $IPADDR dev $CONTAINER_IFNAME
[ "$GATEWAY" ] && {
ip netns exec $NSPID ip route delete default >/dev/null 2>&1 && true
}
ip netns exec $NSPID ip link set $CONTAINER_IFNAME up
[ "$GATEWAY" ] && {
ip netns exec $NSPID ip route get $GATEWAY >/dev/null 2>&1 || \
ip netns exec $NSPID ip route add $GATEWAY/32 dev $CONTAINER_IFNAME
ip netns exec $NSPID ip route replace default via $GATEWAY
}

pipework跨主机通信

  • weave

你想要为你在跨越多个数据中心的、规模从单台到数千台的主机上运行的容器创建一个网 络。该网络具备自动 IP 地址分配功能,并且集成基于 DNS 的服务发现。

  • flannel覆盖网络
  • 在多台Docker主机中使用Docker Network
  • calico

参考

[图灵程序设计丛书].Docker经典实例.pdf

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
目录
相关文章
|
9天前
|
存储 安全 数据安全/隐私保护
Docker进阶:网络配置与服务编排
【10月更文挑战第17天】随着微服务架构的流行,Docker作为容器化技术的领导者,在企业级应用部署中扮演着重要角色。掌握Docker的高级特性,特别是网络配置和服务编排,对于构建高效、可维护的分布式系统至关重要。本文将深入探讨Docker的网络配置选项、容器间通信机制、端口映射技术以及使用Docker Compose进行多容器应用部署的最佳实践。
32 8
|
5天前
|
Docker 容器
docker swarm启动服务并连接到网络
【10月更文挑战第16天】
10 5
|
5天前
|
调度 Docker 容器
docker swarm创建覆盖网络
【10月更文挑战第16天】
9 5
|
6天前
|
负载均衡 应用服务中间件 数据安全/隐私保护
docker swarm 创建 Swarm 模式下的网络
【10月更文挑战第14天】
13 6
|
6天前
|
负载均衡 网络协议 关系型数据库
docker swarm 使用网络启动服务
【10月更文挑战第15天】
13 4
|
7天前
|
应用服务中间件 nginx Docker
docker swarm创建覆盖网络
【10月更文挑战第14天】
11 3
|
6天前
|
数据安全/隐私保护 Docker 容器
docker swarm创建网络
【10月更文挑战第15天】
7 1
|
7天前
|
Docker 容器
docker swarm 在服务中使用网络
【10月更文挑战第14天】
9 2
|
8天前
|
Kubernetes 持续交付 Docker
探索DevOps实践:利用Docker与Kubernetes实现微服务架构的自动化部署
【10月更文挑战第18天】探索DevOps实践:利用Docker与Kubernetes实现微服务架构的自动化部署
38 2
|
12天前
|
机器学习/深度学习 人工智能 监控
深入理解深度学习中的卷积神经网络(CNN):从原理到实践
【10月更文挑战第14天】深入理解深度学习中的卷积神经网络(CNN):从原理到实践
41 1