Flannel工作原理及实现Kubernetes网络分析源码
Flannel是cereos的开源CNI网络插件。下图是Flannel官网提供的一个数据包经过打包、传输、解压的示意图。在此图中,您可以看到两台 Docker0 机器位于不同的网段:10.1.20.1/24 和 10.1.15.1/24。如果从 Web App Frontend1 模块 (10.1.15.2) 连接到另一台主机上的 Backend Service2 模块 (10.1.20.3),则网络数据包将从主机 192.168.0.100 发送到 192.168.0.200。内层容器中的数据包封装在主机的UDP中,外层则包裹着主机的IP和MAC地址。这是一个经典的覆盖网络。由于容器的IP是内部IP,主机之间无法通信,因此容器的网络互通需要发生在主机的网络上。
flannel支持多种网络模式,常用的有vxlan、UDP、hostgw、ipip、gce和阿里云等。 vxlan 和 UDP 的区别在于:vxlan 是内核数据包,而 UDP 是 flaneld 用户态程序数据包,因此 UDP 方法的性能会稍差一些;hostgw政权是东道主政权。容器到另一台主机的网关设置为该主机的 NIC 地址。这和calico很相似,只不过calico是通过BGP声明的,而hostgw是通过central等分发的,所以hostgw是直连模式。不需要通过overlay进行封装和解包,性能相当高。然而,hostgw政权最大的缺点是必须处于两层网络之中。毕竟下一跳路径必须在Neighbor表中,否则将无法访问。
目前生产环境中,最常用的模式是vxlan模式。首先我们看一下运行原理,然后通过源码分析来实现流程。
安装过程很简单,主要分为两步:
第一步安装flannel
yum install flannel或者通过daemonset kubernetes方式运行,为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}}"
复制代码
,然后运行各个节点的flaned程序。?挑一个本网段未被占用的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环境变量和启动脚本以及 mk-docker-opts.sh 设置运行后启动脚本 ExecStartPost 。 ,这个脚本的作用是生成/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.24.host-139.24.246 10.315.245 .24 7 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参数表示强制网关“上线”(尽管没有链路层路由)。否则无法在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表都是持久化的,这说明写入记录是手动维护的。 arp 获取邻居的传统方式是通过广播。如果收到伙伴的arp响应后,伙伴将被标记为可达。如果超过可实现的设置时间后发现合作伙伴无效,则会将其标记为过时。然后延时,探头进入检测状态。如果检测失败,它将被标记为已过时。处于失败状态。我之所以列出arp的基本内容,是因为老版本的flannel并没有使用本文上面的方法,而是使用了一种变通的arp。此时发出的arp表示可达状态,也就是说如果flannel宕机了,如果超过了可达超时时间,那台机器上容器的网络就会中断。让我们简单回顾一下之前的 (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
复制代码
,以便flannel可以接收L3MISS发送到用户空间的内核,并与etcd一起返回该IP地址对应的mac地址并将其设置为可达。从分析可以看出,如果flannel程序被终止,容器之间的通信就会中断,所以这里需要注意。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数据,可以获取网络配置信息,主要涉及后端和网段信息。如果是vxlan,则通过NewManager创建对应的网管,这里使用的是简单的工程模式。首先,每个网络模式管理器将通过 init 进行初始化和注册。
例如vxlan
func init() {
backend.Register("vxlan", New)
复制代码
如果udp 为
func init() {
backend.Register("udp", New)
}
复制代码
其余类似。将施工方法写在图上。这样,根据etcd配置的网络模式,设置并启用适当的网络管理器。
3。注册网络
RegisterNetwork,首先创建flannel.vxlanID网卡,默认vxlanID为1。然后向etcd注册租约,获取相关网段信息。里面有细节。每次运行都应该获得一个新的网段。新版本的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
复制代码
使用该文件设置对接网络。细心的读者可能会注意到,这里的MTU并不是以太网规定的1500。这是因为外部 vxlan 数据包也占用 50 个字节。
当然,运行flannel后,还需要继续监控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中任意主机的增删,更新本地内核转发表。
作者:陈晓宇
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。