共计 6467 个字符,预计需要花费 17 分钟才能阅读完成。
Flannel 是 cereos 开源的 CNI 网络插件,下图 flannel 官网提供的一个数据包经过封包、传输以及拆包的示意图,从这个图片中可以看出两台机器的 docker0 分别处于不同的段:10.1.20.1/24 和 10.1.15.1/24,如果从 Web App Frontend1 pod(10.1.15.2)去连接另一台主机上的 Backend Service2 pod(10.1.20.3),网络包从宿主机 192.168.0.100 发往 192.168.0.200,内层容器的数据包被封装到宿主机的 UDP 里面,并且在外层包装了宿主机的 IP 和 mac 地址。这就是一个经典的 overlay 网络,因为容器的 IP 是一个内部 IP,无法从跨宿主机通信,所以容器的网络互通,需要承载到宿主机的网络之上。
flannel 支持多种网络模式,常用的是 vxlan、UDP、hostgw、ipip 以及 gce 和阿里云等,vxlan 和 UDP 的区别是:vxlan 是内核封包,而 UDP 是 flanneld 用户态程序封包,所以 UDP 的方式性能会稍差;hostgw 模式是一种主机网关模式,容器到另外一个主机上容器的网关设置成所在主机的网卡地址,这个和 calico 非常相似,只不过 calico 是通过 BGP 声明,而 hostgw 是通过中心的 etcd 分发,所以 hostgw 是直连模式,不需要通过 overlay 封包和拆包,性能比较高,但 hostgw 模式最大的缺点是必须是在一个二层网络中,毕竟下一跳的路由需要在邻居表中,否则无法通行。
在实际的生产环境中,最常用的还是 vxlan 模式,我们先看工作原理,然后通过源码解析实现过程。
安装的过程非常简单,主要分为两步:
第一步安装 flannel
yum install flannel 或者通过 kubernetes 的 daemonset 方式启动, 配置 flannel 用的 etcd 地址
第二步配置集群网络
curl -L http://etcdurl:2379/v2/keys/flannel/network/config -XPUT -d value="{\"Network\":\"172.16.0.0/16\",\"SubnetLen\":24,\"Backend\":{\"Type\":\"vxlan\",\"VNI\":1}}"
然后启动每个节点的 flanned 程序。
一、工作原理
1、容器的地址如何分配
Docker 容器启动时通过 docker0 分配 IP 地址,flannel 为每个机器分配一个 IP 段,配置在 docker0 上,容器启动后就在本段内选择一个未占用的 IP,那么 flannel 如何修改 docker0 网段呢?
先看一下 flannel 的启动文件 /usr/lib/systemd/system/flanneld.service
[Service]
Type=notify
EnvironmentFile=/etc/sysconfig/flanneld
ExecStart=/usr/bin/flanneld-start $FLANNEL_OPTIONS
ExecStartPost=/opt/flannel/mk-docker-opts.sh -k DOCKER_NETWORK_OPTIONS -d /run/flannel/docker
文件里面指定了 flannel 环境变量和启动脚本和启动后执行脚本 ExecStartPost 设置的 mk-docker-opts.sh,这个脚本的作用是生成 /run/flannel/docker,文件内容如下:
DOCKER_OPT_BIP="--bip=10.251.81.1/24"
DOCKER_OPT_IPMASQ="--ip-masq=false"
DOCKER_OPT_MTU="--mtu=1450"
DOCKER_NETWORK_OPTIONS="--bip=10.251.81.1/24 --ip-masq=false --mtu=1450"
而这个文件又被 docker 启动文件 /usr/lib/systemd/system/docker.service 所关联,
[Service]
Type=notify
NotifyAccess=all
EnvironmentFile=-/run/flannel/docker
EnvironmentFile=-/etc/sysconfig/docker
这样便可以设置 docker0 的网桥了。
在开发环境中,有三台机器,分别分配了如下网段:
host-139.245 10.254.44.1/24
host-139.246 10.254.60.1/24
host-139.247 10.254.50.1/24
2、容器如何通信
上面介绍了为每个容器分配 IP,那么不同主机上的容器如何通信呢,我们用最常见的 vxlan 举例,这里有三个关键点,一个路由,一个 arp,一个 FDB。我们按照容器发包的过程,逐一分析上面三个元素的作用,首先容器出来的数据包会经过 docker0,那么下面是直接从主机网络出去,还是通过 vxlan 封包转发呢?这是每个机器上面路由设定的。
#ip route show dev flannel.1
10.254.50.0/24 via 10.254.50.0 onlink
10.254.60.0/24 via 10.254.60.0 onlink
可以看到每个主机上面都有到另外两台机器的路由,这个路由是 onlink 路由,onlink 参数表明强制此网关是“在链路上”的(虽然并没有链路层路由),否则 linux 上面是没法添加不同网段的路由。这样数据包就能知道,如果是容器直接的访问则交给 flannel.1 设备处理。
flannel.1 这个虚拟网络设备将会对数据封包,但下面一个问题又来了,这个网关的 mac 地址是多少呢?因为这个网关是通过 onlink 设置的,flannel 会下发这个 mac 地址,查看一下 arp 表
# ip neig show dev flannel.1
10.254.50.0 lladdr ba:10:0e:7b:74:89 PERMANENT
10.254.60.0 lladdr 92:f3:c8:b2:6e:f0 PERMANENT
可以看到这个网关对应的 mac 地址,这样内层的数据包就封装好了
还是最后一个问题,外出的数据包的目的 IP 是多少呢?换句话说,这个封装后的数据包应该发往那一台机器呢?难不成每个数据包都广播。vxlan 默认实现第一次确实是通过广播的方式,但 flannel 再次采用一种 hack 方式直接下发了这个转发表 FDB
# bridge fdb show dev flannel.1
92:f3:c8:b2:6e:f0 dst 10.100.139.246 self permanent
ba:10:0e:7b:74:89 dst 10.100.139.247 self permanent
这样对应 mac 地址转发目标 IP 便可以获取到了。
这里还有个地方需要注意,无论是 arp 表还是 FDB 表都是 permanent,它表明写记录是手动维护的,传统的 arp 获取邻居的方式是通过广播获取,如果收到对端的 arp 相应则会标记对端为 reachable,在超过 reachable 设定时间后,如果发现对端失效会标记为 stale,之后会转入的 delay 以及 probe 进入探测的状态,如果探测失败会标记为 Failed 状态。之所以介绍 arp 的基础内容,是因为老版本的 flannel 并非使用本文上面的方式,而是采用一种临时的 arp 方案,此时下发的 arp 表示 reachable 状态,这就意味着,如果在 flannel 宕机超过 reachable 超时时间的话,那么这台机器上面的容器的网络将会中断,我们简单回顾试一下之前 (0.7.x) 版本的做法,容器为了为了能够获取到对端 arp 地址,内核会首先发送 arp 征询,如果尝试
/proc/sys/net/ipv4/neigh/$NIC/ucast_solicit
此时后会向用户空间发送 arp 征询
/proc/sys/net/ipv4/neigh/$NIC/app_solicit
之前版本的 flannel 正是利用这个特性,设定
# cat /proc/sys/net/ipv4/neigh/flannel.1/app_solicit
3
从而 flanneld 便可以获取到内核发送到用户空间的 L3MISS, 并且配合 etcd 返回这个 IP 地址对应的 mac 地址,设置为 reachable。从分析可以看出,如果 flanneld 程序如果退出后,容器之间的通信将会中断,这里需要注意。Flannel 的启动流程如下图所示:
Flannel 启动执行 newSubnetManager,通过他创建后台数据存储,当前有支持两种后端,默认是 etcd 存储,如果 flannel 启动指定“kube-subnet-mgr”参数则使用 kubernetes 的接口存储数据。
具体代码如下:
func newSubnetManager() (subnet.Manager, error) {
if opts.kubeSubnetMgr {return kube.NewSubnetManager(opts.kubeApiUrl, opts.kubeConfigFile)
}
cfg := &etcdv2.EtcdConfig{Endpoints: strings.Split(opts.etcdEndpoints, ","),
Keyfile: opts.etcdKeyfile,
Certfile: opts.etcdCertfile,
CAFile: opts.etcdCAFile,
Prefix: opts.etcdPrefix,
Username: opts.etcdUsername,
Password: opts.etcdPassword,
}
// Attempt to renew the lease for the subnet specified in the subnetFile
prevSubnet := ReadCIDRFromSubnetFile(opts.subnetFile, "FLANNEL_SUBNET")
return etcdv2.NewLocalManager(cfg, prevSubnet)
}
通过 SubnetManager,结合上面介绍部署的时候配置的 etcd 的数据,可以获得网络配置信息,主要指 backend 和网段信息,如果是 vxlan,通过 NewManager 创建对应的网络管理器,这里用到简单工程模式,首先每种网络模式管理器都会通过 init 初始化注册,
如 vxlan
func init() {backend.Register("vxlan", New)
如果是 udp
func init() {backend.Register("udp", New)
}
其它也是类似,将构建方法都注册到一个 map 里面,从而根据 etcd 配置的网络模式,设定启用对应的网络管理器。
3、注册网络
RegisterNetwork,首先会创建 flannel.vxlanID 的网卡,默认 vxlanID 是 1. 然后就是向 etcd 注册租约并且获取相应的网段信息,这样有个细节,老版的 flannel 每次启动都是去获取新的网段,新版的 flannel 会遍历 etcd 里面已经注册的 etcd 信息,从而获取之前分配的网段,继续使用。
最后通过 WriteSubnetFile 写本地子网文件,
# cat /run/flannel/subnet.env
FLANNEL_NETWORK=10.254.0.0/16
FLANNEL_SUBNET=10.254.44.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true
通过这个文件设定 docker 的网络。细心的读者可能发现这里的 MTU 并不是以太网规定的 1500,这是因为外层的 vxlan 封包还要占据 50 Byte。
当然 flannel 启动后还需要持续的 watch etcd 里面的数据,这是当有新的 flannel 节点加入,或者变更的时候,其他 flannel 节点能够动态更新的那三张表。主要的处理方法都在 handleSubnetEvents 里面
func (nw *network) handleSubnetEvents(batch []subnet.Event) {
. . .
switch event.Type {// 如果是有新的网段加入(新的主机加入)case subnet.EventAdded:
. . .// 更新路由表
if err := netlink.RouteReplace(&directRoute); err != nil {log.Errorf("Error adding route to %v via %v: %v", sn, attrs.PublicIP, err)
continue
}
// 添加 arp 表
log.V(2).Infof("adding subnet: %s PublicIP: %s VtepMAC: %s", sn, attrs.PublicIP, net.HardwareAddr(vxlanAttrs.VtepMAC))
if err := nw.dev.AddARP(neighbor{IP: sn.IP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)}); err != nil {log.Error("AddARP failed:", err)
continue
}
// 添加 FDB 表
if err := nw.dev.AddFDB(neighbor{IP: attrs.PublicIP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)}); err != nil {log.Error("AddFDB failed:", err)
if err := nw.dev.DelARP(neighbor{IP: event.Lease.Subnet.IP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)}); err != nil {log.Error("DelARP failed:", err)
}
continue
}// 如果是删除实践
case subnet.EventRemoved:
// 删除路由
if err := netlink.RouteDel(&directRoute); err != nil {log.Errorf("Error deleting route to %v via %v: %v", sn, attrs.PublicIP, err)
} else {log.V(2).Infof("removing subnet: %s PublicIP: %s VtepMAC: %s", sn, attrs.PublicIP, net.HardwareAddr(vxlanAttrs.VtepMAC))
// 删除 arp if err := nw.dev.DelARP(neighbor{IP: sn.IP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)}); err != nil {log.Error("DelARP failed:", err)
}
// 删除 FDB
if err := nw.dev.DelFDB(neighbor{IP: attrs.PublicIP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)}); err != nil {log.Error("DelFDB failed:", err)
}
if err := netlink.RouteDel(&vxlanRoute); err != nil {log.Errorf("failed to delete vxlanRoute (%s -> %s): %v", vxlanRoute.Dst, vxlanRoute.Gw, err)
}
}
default:
log.Error("internal error: unknown event type:", int(event.Type))
}
}
}
这样 flannel 里面任何主机的添加和删除都可以被其它节点所感知到,从而更新本地内核转发表。
作者:陈晓宇
来源:宜信技术学院