flannel
是 coreos 开源的 Kubernetes CNI
实现。它使用 etcd
或者 Kubernetes API
存储整个集群的网络配置。每个 kubernetes节点上运行 flanneld
组件,它从 etcd
或者 Kubernetes API
获取集群的网络地址空间,并在空间内获取一个 subnet
,该节点上的容器 IP都从这个 subnet
中分配,从而保证不同节点上的 IP不会冲突。flannel通过不同的 backend
来实现跨主机的容器网络通信,目前支持 udp
, vxlan
, host-gw
等一系列 backend实现。
源码地址:https://github.com/flannel-io/flannel
子网管理器,以下简称sm
在main方法中会初始化sm: sm, err := newSubnetManager(ctx)
这里kube子网管理为例:
func NewSubnetManager(ctx context.Context, apiUrl, kubeconfig, prefix, netConfPath string, setNodeNetworkUnavailable, useMultiClusterCidr bool) (subnet.Manager, error) {
var cfg *rest.Config
var err error
// Try to build kubernetes config from a master url or a kubeconfig filepath. If neither masterUrl
// or kubeconfigPath are passed in we fall back to inClusterConfig. If inClusterConfig fails,
// we fallback to the default config.
cfg, err = clientcmd.BuildConfigFromFlags(apiUrl, kubeconfig)
if err != nil {
return nil, fmt.Errorf("fail to create kubernetes config: %v", err)
}
c, err := clientset.NewForConfig(cfg)
if err != nil {
return nil, fmt.Errorf("unable to initialize client: %v", err)
}
// The kube subnet mgr needs to know the k8s node name that it's running on so it can annotate it.
// If we're running as a pod then the POD_NAME and POD_NAMESPACE will be populated and can be used to find the node
// name. Otherwise, the environment variable NODE_NAME can be passed in.
nodeName := os.Getenv("NODE_NAME")
if nodeName == "" {
podName := os.Getenv("POD_NAME")
podNamespace := os.Getenv("POD_NAMESPACE")
if podName == "" || podNamespace == "" {
return nil, fmt.Errorf("env variables POD_NAME and POD_NAMESPACE must be set")
}
pod, err := c.CoreV1().Pods(podNamespace).Get(ctx, podName, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("error retrieving pod spec for '%s/%s': %v", podNamespace, podName, err)
}
nodeName = pod.Spec.NodeName
if nodeName == "" {
return nil, fmt.Errorf("node name not present in pod spec '%s/%s'", podNamespace, podName)
}
}
netConf, err := os.ReadFile(netConfPath)
if err != nil {
return nil, fmt.Errorf("failed to read net conf: %v", err)
}
sc, err := subnet.ParseConfig(string(netConf))
if err != nil {
return nil, fmt.Errorf("error parsing subnet config: %s", err)
}
if useMultiClusterCidr {
err = readFlannelNetworksFromClusterCIDRList(ctx, c, sc)
if err != nil {
return nil, fmt.Errorf("error reading flannel networks from k8s api: %s", err)
}
}
sm, err := newKubeSubnetManager(ctx, c, sc, nodeName, prefix, useMultiClusterCidr)
if err != nil {
return nil, fmt.Errorf("error creating network manager: %s", err)
}
sm.setNodeNetworkUnavailable = setNodeNetworkUnavailable
if sm.disableNodeInformer {
log.Infof("Node controller skips sync")
} else {
go sm.Run(context.Background())
log.Infof("Waiting %s for node controller to sync", nodeControllerSyncTimeout)
err = wait.Poll(time.Second, nodeControllerSyncTimeout, func() (bool, error) {
return sm.nodeController.HasSynced(), nil
})
if err != nil {
return nil, fmt.Errorf("error waiting for nodeController to sync state: %v", err)
}
log.Infof("Node controller sync successful")
}
return sm, nil
}
上面的逻辑大致如下:
通过配置得到的kubeconfig获取到pod访问客户端
通过节点环境变量获取到节点名称,如果没有则通过pod详情获取到节点名称
通过client-go库方法机制对集群中node进行监听,因为flannel是根据node来划分网段的
根据监听到的node的事件,放入到sm的events channel中
在main方法中,进行了以下操作:
bm := backend.NewManager(ctx, sm, extIface)
be, err := bm.GetBackend(config.BackendType)
if err != nil {
log.Errorf("Error fetching backend: %s", err)
cancel()
wg.Wait()
os.Exit(1)
}
bn, err := be.RegisterNetwork(ctx, &wg, config)
通过上面会得到这样一个接口实例:
type Network interface {
Lease() *subnet.Lease
MTU() int
Run(ctx context.Context)
}
目前支持的backend类型有allpc,awsvpc,gce,hostgw,udp和vxlan。
以vxlan为例:
func (nw *network) Run(ctx context.Context) {
wg := sync.WaitGroup{}
log.V(0).Info("watching for new subnet leases")
events := make(chan []subnet.Event)
wg.Add(1)
go func() {
subnet.WatchLeases(ctx, nw.subnetMgr, nw.SubnetLease, events)
log.V(1).Info("WatchLeases exited")
wg.Done()
}()
defer wg.Wait()
for {
evtBatch, ok := <-events
if !ok {
log.Infof("evts chan closed")
return
}
nw.handleSubnetEvents(evtBatch)
}
}
上面代码逻辑大致如下:
调用SubnetManager.WatchLeases()监听整个集群网络的变更事件
根据不同事件刷新路由表,arp表和fdb表等。
与flannel相关的几个虚拟网络上设备:
flannel.1:这是一个vxlan设备。也就是耳熟能详的vteh设备,负责网络数据包的封包和解封。
cni0:是一个linux bridge,用于连接同一个宿主机上的pod。
vethf12090da@if3:容器内eth0网卡的对端设备,从名字上看,在容器内eth0网卡的编号应为3。
VxLAN的设计思想是:
在现有的三层网络之上,“覆盖”一层虚拟的、由内核VxLAN模块负责维护的二层网络,使得连接在这个VxLAN二层网络上的“主机”(虚拟机或容器都可以),可以像在同一个局域网(LAN)里那样自由通信。
为了能够在二层网络上打通“隧道”,VxLAN会在宿主机上设置一个特殊的网络设备作为“隧道”的两端,叫VTEP
VTEP原理如下:
flannel.1设备,就是VxLAN的VTEP,即有IP地址,也有MAC地址
容器服务的IP包,会先出现在docker0网桥,再路由到本机的flannel.1设备进行处理,
为了能够将“原始IP包”封装并发送到正常的主机,源VTEP设备收到原始IP包后,在上面加上一个目的MAC地址(也就是VTEP设备的MAC地址),封装成数据桢,发送给目的VTEP设备 ,封装过程只是加了一个二层头,不会改变“原始IP包”的内容,
Linux会再加上一个VxLAN头,VxLAN头里有一个重要的标志叫VNI,它是VTEP识别某个数据桢是不是应该归自己处理的重要标识。在Flannel中,VNI的默认值是1,这也是为什么宿主机的VTEP设备都叫flannel.1的原因
一个flannel.1设备只知道另一端flannel.1设备的MAC地址,却不知道对应的宿主机地址是什么。在linux内核里面,网络设备进行转发的依据,来自FDB的转发数据库
https://juejin.cn/post/6994825163757846565
http://just4coding.com/2021/11/03/flannel/