概述

说到host-gw,基本都是想起Flannel提供性能最高网络模式,最基础和最简单容器网络模式。Flannel 一种纯三层网络模式 host-gw。顾名思义,host-gw 是一种主机网关模式,每个主机会维护一张路由表,记录发往某目标容器子网的数据包的下一跳 IP 地址(也就是子网所在宿主机的 IP)。宿主机将下一跳目的主机的 MAC 地址作为目的地址,通过二层网络把包发往目的主机。目的主机收到后,会直接转发给对应容器。所以 host-gw 模式下,数据包直接以容器 IP 包的形式在网络中传递,每个宿主机就是通信链路中的网关。例如

  • 容器网络段是: 10.239.0.0/16
  • 主机网络段是: 172.1.0.0/16
  • 每个主机分别占用一个24位网段: Host 1: 10.239.0.0/24 , Host 2: 10.239.1.0/24, Host3: 10.239.2.0/24 .. HostN: 10.239.(n-1).0/24
  • 容器网络规模描述:每台host机器最多可以支持254容器IP, 整个容器网络可以支持255台主机

    优点

    没有经过二层转换,效率高,cpu占用低下。

    缺点

    所有容器节点主机网段必须是相同网段。跨网段主机不能使用host-gw方式组网。

    原因

    跨主机间容器间需要通过Host机器是通过路由表进行路由转发。例如当前主机网络段是 172.1.0.0/16 , 如果有一台机器网络段是 172.2.0.0/16 机器加入这个容器段是不可以的。IP地址不在同一各网络段,不能够通过主机直接路由:

例如在网络段是 172.2.0.0/16 主机段下面添加路由命令失败

  1. ip route add 10.239.x.0/24 via 172.2.x.y dev eth0

可以做什么

通过熟悉 host-gw 原理可以做

  • 配置路由表实现多台机器docker容器网络进行通信。
  • 配置路由表实现k8s集群外机器和k8s网络容器网段进行通信。除了加密网络其他都可以通过这个方法实现和容器网络通信

    容器网络架构

    网络命名空间隔离容器

    容器网络和Host机器网络通过网络命名空间进行隔离。简单的说把容器虚拟网卡,以及需要路由配置,iptables放到指定容器网络空间里面。主机(Host)默认有 root namespace , 主机所有设备,包括 ip 命令新创建网络设备默认放到 root namespace 上。如图:
    容器网络概述-1.jpg

    同一主机內容器网络空间对外通信

    如果我们无法和某个专有的网络栈通信,那么它看上去就没什么用。幸运的是,Linux提供了好用的工具——虚拟Ethernet设备。从man veth可以看到,veth pair设备是虚拟Ethernet设备。他们可以作为网络命名空间之间的通道(tunnel),从而创建连接到另一个命名空间里的物理网络设备的桥梁,但是也可以作为独立的网络设备使用。veth-pair在linux成对创建到 root namspace ,创建以后把对一端移动到container命名空间,连接两个命名空间。移动到容器一端一般重命名为 eth0 , 我们在图里面称为 ceh0 。接口对两端可以认为是直连两张网卡。如图:
    容器网络概述-veth.jpg

    容器网络空间之间通信

    可以把多个容器容器网络空间想像为多台虚拟主机。像物理网络一样,多台主机需要通过交换机组成网络。连接命名空间的这个虚拟交换机可以通过linux上网桥实现。Linux网桥作用类似于网络switch。它会在连接到其上的接口间转发网络包。并且因为它是switch,它是在L2层完成这些转发的。

接入交换机以后,把容器网络命名空间默认路由配置交换机 br0 ip地址,所有容器可以之间可以互相连接了。
连接多个容器网络空间.jpg
Master 代表把veth对接入到br0上, 这一端接口接入网桥以后就不能配置ip地址了。这个 br0 相当与docker里面 docker0 或者k8s 集群flannel网络组件 cni0

Host主机和容器命名空间通信

Host主机和容器命名空间是怎么通信呢。例如host主机网络段是 172.1.1.0/24 ,容器网络段是 10.239.0.0/24 ,处于不同的网络段,需要通过路由器进行通信。需要在主机添加一条路由

  1. $ ip route add 10.239.0.0/24 via br0

当然这条路由在添加网桥时候自动添加了。如图:
主机网络和容器网络.jpg

容器命名空间和Host通信

容器和主机也是怎么通信呢?例如host主机网络段是 172.1.1.0/24 ,容器网络是 10.239.0.0/24,不在一个网段上,走容器命名空间路由表默认路由项目 Default via br0 , 进入路由器 b0 以后在 root namespace 上,根据根命名空间路由表选项进行路由。主机每个网卡都配置下面一条路由,可以通过ip命令去查到, ip 不同可能不一样。

  1. $ ip route
  2. ...
  3. 172.1.1.0/24 dev eth0 proto kernel scope link src 172.1.1.10
  4. // 172.1.1.10 不同主机不一样

主机网络和容器网络-2.png

跨主机上容器网络空间通信

在k8s集群上,经常使用多台主机组成大容器网络,主机之间容器怎么样通信呢?首先,遵循不同网段主机进行通信时候需要通过路由器进行转发。其次,Host可以认为本主机所有容器和其他主机上通信路由器, br0 连接本机容器交换机器一端, eth0 相当于连接其他网络段一端。第三, 集群每台主机都负责一个网段,每台主机都包含集群容器网络所有ip段路由表。

第三条也是 flannel 设计容器网络精髓,之后所有容器网络组件都是在这个设计基础上扩展。精髓在于:

  • 简化IP 分配控件: 每台主机只需保存本机ip分配表,无需多台机器进行同步,不会由于整个集群容器数过大导致导致申请大并发问题
  • 路由配置简单: 路由条数和主机规模成正比,和容器数目无关。

跨机器路由条数和集群主机数目成正比,本机网段路由到 br0 ,其他路由就跳到对应网段主机上。黄色虚线框部分就是对应主机跨机容器网络通信路由如图:
跨主机网络和容器网络.jpg

容器网段数据包的路由流向

在同一台机器上主机访问容器,不同主机上容器进行访问时候都会用到路由表,对于路由表配置应该怎么去理解。

主机上访问本机容器

主机上本机器上容器使用下面路由:

  1. $ip route
  2. ...
  3. 10.239.0.0/24 via br0 proto kernel scope link src 10.239.0.1
  4. ...
  • kernel: the route was installed by the kernel during autoconfiguration。
  • via: 网段10.239.0.0/24 数据包通过 br0 发送的
  • src: 原地地址: 10.239.0.1

本机 ping 10.239.0.2 相当于用 br0 接口向容器发送 ping 而不是从 eth0 发送 ping 数据包。如图:
主机和本机容器通信.jpg
注意
主机 root network namespace 执行任何程序发送网络数据是,可以使用网络空间内任意网络接口,不限于 eth0

主机上访问跨主机容器

  1. $ ip route
  2. ...
  3. 10.239.1.0/24 via 172.1.1.2 dev eth0 // 172.1.1.2 Host2 ip 地址
  4. ...

这个路由可以理解为:

  • via: 指定一下一条路由地址 172.1.1.2 (Host2 ip地址)网段必须和 eth0 一致
  • dev: 指定通过设备 eth0 发送

这个路由配置2层封包过程分析:

Host1机器上 ping Host2 机器上容器,icmp封包和路由过程。例如

  1. host1> ping 10.239.1.2

数据包组装过程如下:
跨机器路由封包过程.jpg

发送数据包路由传递过程

主机访问跨主机容器.jpg

对端回应数据包路由传递过程

主机访问跨主机容器回应.jpg

注意

  • 虚线表示数据包不是通过网络设备转发,不能用tcpdump工具抓包。数据包通过Host2 br0接口进入Root network Namespace, 是路由阶段路时候对二层封包源mac地址修改的,并且由Host2 eth0 发送。
  • 经过路由以后链路层[eth]源地址,目标地址有所有变换。

解释

  • 同一网络命名空间所有网络接口都是共享同一个网络栈,不存在网络接口间数据包传输。网络空间内的物理接口/虚拟接口都可以作网络空间数据入口,也做可以为网络空间出口。数据可以在不同接口进出。
  • 不同网络空间不同网络堆栈,两个空间瓦网络包隔离的,必须通过网络接口传输 以太网包

跨主机容器之间访问

主机1上容器和主机2上容器进行访问。主机1上容器通过默认路由先把包传送到br0上数据包进入,主机1 root network namspace ,余下流程基本和主机上访问其他主机容器一致, 如图

发送过程
跨主机容器互相访问.jpg

返回过程
跨主机容器互相访问回应.jpg

注意

  • 跨主机容器访问过程中,容器的源ip和目标ip没有经过转换.
  • 经过路由器后, 只有链路层源地址和目标地址被替换

容器访问集群外部的世界

容器之间通信都是使用容器地址的,都是考每台机器上的host-gw路由表去处理,如果在容器集群外机器,这些机器当然没有配置这个host-gw路由表,目标服务器无法将包发回容器. 怎么办法呢.需要一个地址,把容器网络地址转化为主机地址.地址伪装是通过 root network namespaceiptables 配置
容器和外部世界连接.jpg

容器IP伪装(容器IP替换主机ip)由多条规则组成

# 容器网络段(container to container) 10.239.0.0/16 网段内部通信不做snat伪装
$ iptables -t nat -A POSTROUTING -s 10.239.0.0/16 -d 10.239.0.0/16 -j RETURN

# 容器网络段访问外部主机( container to external) 10.239.0.0/16 网段出去外网,把容器地址转换为主机地址
$ iptables -t nat -A POSTROUTING -s 10.239.0.0/16 ! -d 224.0.0.0/4 -j MASQUERADE

# 其他机器通过Host机器跳转本主机对应的容器网络网段请求不用做地址伪装
$ iptables -t nat -A POSTROUTING ! -s 10.239.0.0/16 -d 10.239.0.0/24 -j RETURN

# 其他机器通过Host机器跳转容器网络段,当时不是当前Host网络容器网络段,需要进行地址伪装
$ iptables -t nat -A POSTROUTING ! -s 10.239.0.0/16 -d 10.239.0.0/16 -j MASQUERADE

之前路由表(host-gw)只会替换Mac地址,现在源IP都被替换,容器IP地址被替换为主机地址过程称为 SNAT ,这个过程iptables POSTROUTING 事件被接获, 但是远程服务器返回到主机时候, ICMP 目标地址是主机,怎么到达容器呢?不用担心,有 contract 表跟踪,再进行把 ICMP 目标地址Host1 ip转换为容器1 IP 地址.这个过程叫 DNAT

所有其他ICMP消息(例如EchoEcho Reply )都具有一个标识符字段,该标识符字段可以在逻辑上充当“端口号”的代理,以便您的NAT实现可以进行必要的重写。实际上,RFC 792包含支持以下解释的语言:

echo发送方可以使用标识符和序列号来帮助将回复与回声请求进行匹配。例如,标识符可以像TCP或UDP中的端口一样用于标识会话,并且序列号可以在发送的每个回显请求中递增。回显器在回显应答中返回这些相同的值。

下面数据包发送和路由过程
容器访问集群外部主机.jpg

数据包返回过程
容器访问集群外部主机返回.jpg

容器访问外网时候,一出一入都需要进行 NAT , 和容器之间通信效率是差一些. 都是通过 contrack 连接表进行跟踪的.

实践

手动实现两台机器docker组成容器集群,两台机器容器可以直接通过容器网段通信

两台机器docker容器间互相通信

通过host-gw原理和简单路由命令就是可以实现实现2台机器docker上容器互相通信.测试机器可以使用vagrant生成, 注意host-gw容器间ip地址是不进行地址转换,在openstack一些vm上,网络底层是有网络段限制导致不能通信,建议使用物理机器/vagrant生长虚拟机器.

下面两台vm配置方别如下:

vm1:

  • IP地址: 10.5.7.60
  • Docker 容器网段是: 172.17.0.1/24

vm2:

  • IP地址: 10.5.7.61
  • Docker 容器网段是: 172.17.1.0/24

网络规划如下
容器网络模式: host-gw 介绍 - 图16

图中黄色虚线是需要手动去配置路由表项目

指定docker容器网段

docker默认使用172.17.0.0/16网段,host-gw每台机器都需要独立网段的,需要配置docker网段,配置方式如下.

vm1 配置如下:

vim /etc/docker/daemon.json
{
    "bip": "172.17.0.1/24"
    "iptables": false
}

vm2 配置如下

vim /etc/docker/daemon.json
{
    "bip": "172.17.1.1/24",
    "iptables": false
}

配置完毕以后需要重新启动docker

配置路由表

根据上图, 两台机器都需要把对方网段加入路由表中, 如果多台机器话需要自己之外其他所有节点网段都配置上了.

vm1 添加路由配置如下:

vm1> $ ip route add 172.17.1.0/24 via 10.5.7.61 dev eth1

测试命令:
在 vm1上否能够和vm2 docker0 网卡连通

vm1> ping 172.17.1.1
PING 172.17.1.1 (172.17.1.1) 56(84) bytes of data.
64 bytes from 172.17.1.1: icmp_seq=1 ttl=64 time=0.451 ms
64 bytes from 172.17.1.1: icmp_seq=2 ttl=64 time=0.274 ms

vm2 添加路由配置如下:

vm2> $ ip route add 172.17.0.0/24 via 10.5.7.60 dev eth1

在 vm2上否能够和vm2 docker0 网卡连通

vm2> ping 172.17.0.1
PING 172.17.0.1 (172.17.0.1) 56(84) bytes of data.
64 bytes from 172.17.0.1: icmp_seq=1 ttl=64 time=0.337 ms
64 bytes from 172.17.0.1: icmp_seq=2 ttl=64 time=0.297 ms

容器可以连通测试通过

测试效果

vm2 机器启动nginx

vm2> docker run -d -name nginx nginx
vm2> docker inspect nginx // 获取对应ip地址 172.17.1.3

vm1 启动busybox

vm1> docker run -it  --rm busybox sh
vm1 busybox> wget 172.17.1.3
Connecting to 172.17.1.3 (172.17.1.3:80)
saving to 'index.html'
...

参考: