0%

一文搞懂Kuberntes CNI 插件 flannel - 知乎

Excerpt

flannel 初始化流程网络插件由各节点上的容器运行时进行调用,containerd 在创建完 Pod 沙箱之后会调用 CNI 插件来对网络进行设置, flannel 网络初始化流程如下: 由于 flannel 是委托来实现的,所以 flannel cni…


网络插件由各节点上的容器运行时进行调用,containerd 在创建完 Pod 沙箱之后会调用 CNI 插件来对网络进行设置,

Untitled

flannel 网络初始化流程如下:

Untitled

由于 flannel 是委托来实现的,所以 flannel cni plugins 的具体工作只是将配置进行读取和补充,具体代码如下:

1
func cmdAdd(args *skel.CmdArgs) error { // 读取配置文件 n, err := loadFlannelNetConf(args.StdinData) fenv, err := loadFlannelSubnetEnv(n.SubnetFile) // 将配置文件传入命令执行方法中 return doCmdAdd(args, n, fenv) }

flannel 如何怎么用

我们先从集群安装的看flannel 如何使用

集群主节点安装完 kubeadmkubectl 后,可以直接通过 kubectl 来安装网络

1
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

应用完成后会在每个 Node 上启动 flanneld 的进程进行网络的维护。

flanneld 启动参数

1
$ ps |grep flannel 1 root 12:57 /opt/bin/flanneld --ip-masq --kube-subnet-mgr flannelFlags.BoolVar(&opts.ipMasq, "ip-masq", false, "setup IP masquerade rule for traffic destined outside of overlay network") flannelFlags.BoolVar(&opts.kubeSubnetMgr, "kube-subnet-mgr", false, "contact the Kubernetes API for subnet assignment instead of etcd.")

可以看到 kubeSubnetMgr 是通过 k8s 的 informer API 来监听事件变化的。

flannel 通过 api-server的 informer 监听Node节点的加入,然后来设置相应的网络

整体流程如下图:

Untitled

flanneld Manager 的实现

接口定义

1
type Manager interface { // GetNetworkConfig 获取网络配置。 GetNetworkConfig(ctx context.Context) (*Config, error) // HandleSubnetFile 处理子网配置,包括IP伪装、子网详细信息和MTU设置。 HandleSubnetFile(path string, config *Config, ipMasq bool, sn net.IPNet, sn6 net.IPNet, mtu int) error // AcquireLease 获取指定属性的租约。 AcquireLease(ctx context.Context, attrs *lease.LeaseAttrs) (*lease.Lease, error) // RenewLease 更新现有租约。 RenewLease(ctx context.Context, lease *lease.Lease) error // WatchLease 监听特定的IPv4和IPv6子网的更改,并将结果发送到提供的通道。 WatchLease(ctx context.Context, sn net.IPNet, sn6 net.IPNet, receiver chan []lease.LeaseWatchResult) error // WatchLeases 监听所有租约的更改,并将结果发送到提供的通道。 WatchLeases(ctx context.Context, receiver chan []lease.LeaseWatchResult) error // CompleteLease 将租约标记为完成,允许管理器执行任何清理操作。 CompleteLease(ctx context.Context, lease *lease.Lease, wg *sync.WaitGroup) error // GetStoredMacAddress 检索存储的MAC地址。 GetStoredMacAddress() string // Name 返回管理器的名称。 Name() string }

kubeSubnetManager — Informer 获取变化

通过 client-go 来实例化 informer ,监听 Node 节点的变化信息

1
func newKubeSubnetManager(ctx context.Context, c clientset.Interface, sc *subnet.Config, nodeName, prefix string) (*kubeSubnetManager, error) { var ksm kubeSubnetManager // 用于接收变化的事件 ksm.events = make(chan lease.Event, scale) // 实例化 informer 的List和Watch方法,用于监听变化 if !ksm.disableNodeInformer { indexer, controller := cache.NewIndexerInformer( // 1.监听Node 的变化 &cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { return ksm.client.CoreV1().Nodes().List(ctx, options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { return ksm.client.CoreV1().Nodes().Watch(ctx, options) }, }, &v1.Node{}, resyncPeriod, // resync的周期 // 2. 处理Node 节点变化的方法 cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { // 推入的事件类型是增加 ksm.handleAddLeaseEvent(lease.EventAdded, obj) }, UpdateFunc: ksm.handleUpdateLeaseEvent, DeleteFunc: func(obj interface{}) { // 推入的事件是移除 ksm.handleAddLeaseEvent(lease.EventRemoved, obj) }, }, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, ) // 3. 初始化后复制给 kubeSubnetManager ksm.nodeController = controller ksm.nodeStore = listers.NewNodeLister(indexer) } return &ksm, nil } func (ksm *kubeSubnetManager) handleAddLeaseEvent(et lease.EventType, obj interface{}) { // 1. 解析Node事件信息 n := obj.(*v1.Node) l, err := ksm.nodeToLease(*n) // 2. 推入到 events channel中 ksm.events <- lease.Event{Type: et, Lease: l} } func (ksm *kubeSubnetManager) handleUpdateLeaseEvent(oldObj, newObj interface{}) { // 1.通过对比新旧对象的annotation是否改变来看是否要推入变更事件 o := oldObj.(*v1.Node) n := newObj.(*v1.Node) if !changed { return // 没有变更直接返回 } // 2. 有变更则推入具体变更,类型是增加 l, err := ksm.nodeToLease(*n) ksm.events <- lease.Event{Type: lease.EventAdded, Lease: l} }

kubeSubnetManager — 消费变更事件

1
func (ksm *kubeSubnetManager) WatchLeases(ctx context.Context, receiver chan []lease.LeaseWatchResult) error { for { select { case event := <-ksm.events: // 将事件传递给外部传入的接收者 receiver <- []lease.LeaseWatchResult{ { Events: []lease.Event{event}, }} } }

这里不直接监听是为了能够通过 context 来控制整个程序的结束,避免了外部关闭了之后 channel 还在继续读取处理

1
package subnet // 传入 receiver channel 来监听 Manager 的变更信息 func WatchLeases(ctx context.Context, sm Manager, ownLease *lease.Lease, receiver chan []lease.Event) { // 1. 监听event事件 leaseWatchChan := make(chan []lease.LeaseWatchResult) go func() { err := sm.WatchLeases(ctx, leaseWatchChan) }() // 2. 将事件写入到receiver中 for watchResults := range leaseWatchChan { for _, wr := range watchResults { var batch []lease.Event if len(wr.Events) > 0 { batch = lw.Update(wr.Events) } else { batch = lw.Reset(wr.Snapshot) } if len(batch) > 0 { receiver <- batch } } } close(receiver) }

上面的事件可以看到先将事件用 LeaseWatcherUpdate 进行了处理,内部是让 Add 的事件变成集合。

1
func (lw *LeaseWatcher) Update(events []Event) []Event { batch := []Event{} for _, e := range events { // 1. 如果和本地子网的租约相同,则不做任何处理直接跳过 if sameSubnet(e.Lease.EnableIPv4, e.Lease.EnableIPv6, *lw.OwnLease, e.Lease) { continue } // 2. 添加事件用于传递给外部 Receiver switch e.Type { case EventAdded: batch = append(batch, lw.add(&e.Lease)) case EventRemoved: batch = append(batch, lw.remove(&e.Lease)) } } return batch } func (lw *LeaseWatcher) add(lease *Lease) Event { // 1. 如果已经有相同的子网,则将原来的数组位置更新成最新的租约 for i, l := range lw.Leases { if sameSubnet(l.EnableIPv4, l.EnableIPv6, l, *lease) { lw.Leases[i] = *lease return Event{EventAdded, lw.Leases[i]} } } // 2. 如果是一个新的租约则直接添加到 leaseWatcher中 lw.Leases = append(lw.Leases, *lease) // 3. 返回刚添加的最后一个租约事件 return Event{EventAdded, lw.Leases[len(lw.Leases)-1]} } func (lw *LeaseWatcher) remove(lease *Lease) Event { // 移除子网,找到相同的网络后直接将对应位置的租约移除 for i, l := range lw.Leases { if sameSubnet(l.EnableIPv4, l.EnableIPv6, l, *lease) { lw.Leases = append(lw.Leases[:i], lw.Leases[i+1:]...) return Event{EventRemoved, l} } } // 移除的子网不会出现不存在的情况,如果有,那证明是有错误的子网 log.Errorf("Removed subnet (%s) and ipv6 subnet (%s) were not found", lease.Subnet, lease.IPv6Subnet) return Event{EventRemoved, *lease} }

kubeSubnetManager — AcquireLease

AcquireLease 函数返回一个包含后端所需重要信息的租约,例如子网。此函数在注册时由后端调用一次。

1
func (ksm *kubeSubnetManager) AcquireLease(ctx context.Context, attrs *lease.LeaseAttrs) (*lease.Lease, error) { // 从缓存中获取节点信息 var cachedNode *v1.Node if ksm.disableNodeInformer { cachedNode, err = ksm.client.CoreV1().Nodes().Get(ctx, ksm.nodeName, metav1.GetOptions{ResourceVersion: "0"}) } else { cachedNode, err = ksm.nodeStore.Get(ksm.nodeName) } // 创建节点的深层副本以确保不修改缓存中的原始节点数据 n := cachedNode.DeepCopy() // 检查节点的 PodCIDR 是否已分配 if n.Spec.PodCIDR == "" {} // 对 BackendData 和 BackendV6Data 进行 JSON 编码 var bd, v6Bd []byte // 初始化 IPv4 和 IPv6 CIDR 变量 var cidr, ipv6Cidr *net.IPNet switch { case len(n.Spec.PodCIDRs) == 0: _, parseCidr, err := net.ParseCIDR(n.Spec.PodCIDR) if err != nil { return nil, err } if len(parseCidr.IP) == net.IPv4len { cidr = parseCidr } else if len(parseCidr.IP) == net.IPv6len { ipv6Cidr = parseCidr } case len(n.Spec.PodCIDRs) < 3: // 处理节点的 IPv4 或 IPv6 CIDR,如果有多个 CIDR,则只处理第一个 IPv4 和第一个 IPv6 CIDR for _, podCidr := range n.Spec.PodCIDRs { _, parseCidr, err := net.ParseCIDR(podCidr) if err != nil { return nil, err } if len(parseCidr.IP) == net.IPv4len { cidr = parseCidr } else if len(parseCidr.IP) == net.IPv6len { ipv6Cidr = parseCidr } } } // 检查节点的注解和租约属性是否一致,如果不一致则更新节点注解 // 更新节点注解 n.Annotations[ksm.annotations.BackendType] = attrs.BackendType // 生成节点更新前后的 JSON 数据 oldData, err := json.Marshal(cachedNode) newData, err := json.Marshal(n) // 生成节点的合并补丁 patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, v1.Node{}) // 应用节点更新 _, err = ksm.client.CoreV1().Nodes().Patch(ctx, ksm.nodeName, types.StrategicMergePatchType, patchBytes, metav1.PatchOptions{}, "status") } // 构造租约 lease := &lease.Lease{ Attrs: *attrs, Expiration: time.Now().Add(24 * time.Hour), } // 如果存在 IPv4 CIDR,则处理 IPv4 租约信息 if cidr != nil && ksm.enableIPv4 { ipnet := ip.FromIPNet(cidr) net, err := ksm.subnetConf.GetFlannelNetwork(&ipnet) // 设置租约的 IPv4 子网信息 lease.Subnet = ip.FromIPNet(cidr) } // 如果存在 IPv6 CIDR,则处理 IPv6 租约信息,与IPv4类似 if ipv6Cidr != nil {} return lease, nil }

LocalManager的实现

监听子网变化,通过ETCD作为底层存储

1
func (m *LocalManager) WatchLeases(ctx context.Context, receiver chan []lease.LeaseWatchResult) error { // 重置子网监听的位置,拿到目前所有生效的快照结果 wr, err := m.registry.leasesWatchReset(ctx) // 写入chan给外面消费 receiver <- []lease.LeaseWatchResult{wr} // 获取下一个监听的版本位置 nextIndex, err := getNextIndex(wr.Cursor) // 从nextIndex的位置开始监听子网的变化信号,然后写入 chan 中 err = m.registry.watchSubnets(ctx, receiver, nextIndex) return nil }

其他监听方法则与 kubeSubnetManager 类似

数据结构的转换 — Lease

v1.Node 转换成了 lease 的结构。从节点获取的信息更新租约,例如 PodCIDR

为什么要做这一层转换呢?因为 Node 中存在很多信息,我们并不需要全部的信息,只需要关注网络相关的内容,并且构建的时候也解析了网络的信息,也检查了IP的合法性。

1
func (ksm *kubeSubnetManager) nodeToLease(n v1.Node) (l lease.Lease, err error) { if ksm.enableIPv4 { // 通过 annotation 获取public IP l.Attrs.PublicIP, err = ip.ParseIP4(n.Annotations[ksm.annotations.BackendPublicIP]) // 获取 annotations 中后端的信息 l.Attrs.BackendData = json.RawMessage(n.Annotations[ksm.annotations.BackendData]) // 解析CIDRs的信息 ,有多个证明有可能存在IPV6的,所以找的时候在IPV4下只找IPV4相关的内容 var cidr *net.IPNet switch { case len(n.Spec.PodCIDRs) == 0: _, cidr, err = net.ParseCIDR(n.Spec.PodCIDR) if err != nil { return l, err } case len(n.Spec.PodCIDRs) < 3: for _, podCidr := range n.Spec.PodCIDRs { _, parseCidr, err := net.ParseCIDR(podCidr) if err != nil { return l, err } if len(parseCidr.IP) == net.IPv4len { cidr = parseCidr break } } } // 格式化子网 l.Subnet = ip.FromIPNet(cidr) l.EnableIPv4 = ksm.enableIPv4 } // ipv6和ipv4处理流程类似 l.Attrs.BackendType = n.Annotations[ksm.annotations.BackendType] return l, nil }

这里通过 switch 的使用来避免了通过 if 进行了长度判断

Backend

Manager 实现

处理子网网络的后端接口定义,通过工厂模式来注册不同的后端处理方法

1
// Manager接口的定义 type Manager interface { GetBackend(backendType string) (Backend, error) } // 初始化子网管理的 func NewManager(ctx context.Context, sm subnet.Manager, extIface *ExternalInterface) Manager { return &manager{ active: make(map[string]Backend), } } func (bm *manager) GetBackend(backendType string) (Backend, error) { bm.mux.Lock() defer bm.mux.Unlock() betype := strings.ToLower(backendType) // 已经运行了则直接返回 if be, ok := bm.active[betype]; ok { return be, nil } // 第一次运行则需要先进行初始化 befunc, ok := constructors[betype] be, err := befunc(bm.sm, bm.extIface) bm.active[betype] = be bm.wg.Add(1) go func() { <-bm.ctx.Done() // 这里如果在删除的时候又调用了GetBackend则会产生竞争,不过最终也是会再被删除,问题不大 bm.mux.Lock() delete(bm.active, betype) bm.mux.Unlock() bm.wg.Done() }() return be, nil } // 注册后端的构造方法 func Register(name string, ctor BackendCtor) { constructors[name] = ctor }

这里后端的选择是通过 /etc/kube-flannel/net-conf.json 的配置文件来标识的,我们进入 flannelPod 可以看到配置文件的内容如下

1
{ "Network": "10.244.0.0/16", "Backend": { "Type": "vxlan" } }

在我自己的集群中使用的网络类型是 vxlan

Network 初始化

vxlan 注册的构造方法

1
func init() { backend.Register("vxlan", New) }

NetWork则是通过工厂模式来选择后端

1
be, err := bm.GetBackend(config.BackendType) // backendType是vxlan bn, err := be.RegisterNetwork(ctx, &wg, config) // 构造网络

注册网络处理具体处理代码如下:

1
func (be *VXLANBackend) RegisterNetwork(ctx context.Context, wg *sync.WaitGroup, config *subnet.Config) (backend.Network, error) { // 解析配置,如果配置非空,解码后覆盖默认配置 cfg := struct { VNI int Port int MTU int GBP bool Learning bool DirectRouting bool }{ VNI: defaultVNI, MTU: be.extIface.Iface.MTU, } if len(config.Backend) > 0 { if err := json.Unmarshal(config.Backend, &cfg); err != nil {} } var dev, v6Dev *vxlanDevice var err error // 当 Flannel 重新启动时,它将从节点注解中获取 MAC 地址以设置 flannel 的 MAC 地址 var hwAddr net.HardwareAddr macStr := be.subnetMgr.GetStoredMacAddress() if macStr != "" { hwAddr, err = net.ParseMAC(macStr) } // 如果启用 IPv4,则初始化 IPv4 VXLAN 设备 if config.EnableIPv4 { devAttrs := vxlanDeviceAttrs{ vni: uint32(cfg.VNI), name: fmt.Sprintf("flannel.%v", cfg.VNI), MTU: cfg.MTU, vtepIndex: be.extIface.Iface.Index, vtepAddr: be.extIface.IfaceAddr, vtepPort: cfg.Port, gbp: cfg.GBP, learning: cfg.Learning, hwAddr: hwAddr, } dev, err = newVXLANDevice(&devAttrs) // 设备是否直接进行路由转发,vxlan的配置下为false,hostgw为true dev.directRouting = cfg.DirectRouting } // 如果启用 IPv6,则初始化 IPv6 VXLAN 设备 if config.EnableIPv6 {} // newSubnetAttrs 构造用于租约的属性,并基于 VXLAN 后端生成相关的 JSON 数据。 // 具体包括了公共 IPv4 和 IPv6 地址、VXLAN VNID、以及与 VXLAN 设备相关的信息。 subnetAttrs, err := newSubnetAttrs(be.extIface.ExtAddr, be.extIface.ExtV6Addr, uint32(cfg.VNI), dev, v6Dev) // 获取租约 lease, err := be.subnetMgr.AcquireLease(ctx, subnetAttrs) // 确保设备具有/32地址,以避免创建广播路由 if config.EnableIPv4 { net, err := config.GetFlannelNetwork(&lease.Subnet) } // 确保 IPv6 设备具有/128地址 if config.EnableIPv6 {} // 创建并返回网络对象 return newNetwork(be.subnetMgr, be.extIface, dev, v6Dev, ip.IP4Net{}, lease, cfg.MTU) } func newSubnetAttrs(publicIP net.IP, publicIPv6 net.IP, vnid uint32, dev, v6Dev *vxlanDevice) (*lease.LeaseAttrs, error) { // 初始化租约属性 leaseAttrs := &lease.LeaseAttrs{ BackendType: "vxlan", } // 处理 IPv4 地址和 VXLAN 设备的信息 if publicIP != nil && dev != nil { // 生成 VXLAN 租约属性的 JSON 数据 data, err := json.Marshal(&vxlanLeaseAttrs{ VNI: vnid, VtepMAC: hardwareAddr(dev.MACAddr()), }) // 设置租约属性中的公共 IPv4 地址和相关的 VXLAN 后端数据 leaseAttrs.PublicIP = ip.FromIP(publicIP) leaseAttrs.BackendData = json.RawMessage(data) } // 处理 IPv6 地址和 VXLAN 设备的信息 ,与IPv4类似 if publicIPv6 != nil && v6Dev != nil {} // 返回构造的租约属性 return leaseAttrs, nil }

总结一下流程:

  1. 解析配置,包括 VNI、端口、MTU和MAC地址信息。
  2. 初始化VXLAN设备
  3. 构造租约并配置flannel租约的网络。
  4. 返回初始化成功的网络对象(Network)

Network 接口定义

1
// Network 接口定义了网络的基本操作。 type Network interface { // Lease 返回与网络关联的租约。 Lease() *lease.Lease // MTU 返回网络的最大传输单元(MTU)。 MTU() int // Run 启动网络服务,并在给定的上下文(context)中运行。 Run(ctx context.Context) }

Network 实现类 — vxlan.network

1
func (nw *network) Run(ctx context.Context) { wg := sync.WaitGroup{} events := make(chan []lease.Event) wg.Add(1) go func() { // 从 kube subnet 组件里监听集群内所有 node 的网络变化 subnet.WatchLeases(ctx, nw.subnetMgr, nw.SubnetLease, events) wg.Done() }() defer wg.Wait() for { // 来自Informer node 资源的变化 evtBatch, ok := <-events nw.handleSubnetEvents(evtBatch) } }

这里的监听方法主要流程如下:

  1. 监听子网网络的租约并发送到events 的channel中
  2. 将event消费出来后发送给 handleSubnetEvents 方法处理

handleSubnetEvents 方法的循环传入的Events,通过 vxlan.device来实现处理 IPv4和IPv6的子网添加和移除

下面的类图总结所有具体实现:

Untitled

vxlan

vxlan是一种网络协议,将虚拟机发出的原始以太报文完整的封装在UDP报文中,然后在外层使用物理网络的IP报文头和以太报文头封装,这样,封装后的报文就像普通IP报文一样,可以通过路由网络转发,这就像给二层网络的虚拟机插上了路由的翅膀,使虚拟机彻底摆脱了二、三层网络的结构限制。

VXLAN的报文:

  • VXLAN Header
    增加VXLAN头(8字节),其中包含24比特的VNI字段,用来定义VXLAN网络中不同的租户。此外,还包含VXLAN Flags(8比特,取值为00001000)和两个保留字段(分别为24比特和8比特)。
  • UDP Header
    VXLAN头和原始以太帧一起作为UDP的数据。UDP头中,目的端口号(VXLAN Port)固定为4789,源端口号(UDP Src. Port)是原始以太帧通过哈希算法计算后的值。
  • Outer IP Header
    封装外层IP头。其中,源IP地址(Outer Src. IP)为源VM所属VTEP的IP地址,目的IP地址(Outer Dst. IP)为目的VM所属VTEP的IP地址。
  • Outer MAC Header
    封装外层以太头。其中,源MAC地址(Src. MAC Addr.)为源VM所属VTEP的MAC地址,目的MAC地址(Dst. MAC Addr.)为到达目的VTEP的路径中下一跳设备的MAC地址。

整个过程可以用以下步骤来总结:

  • 封装: 原始数据包 + VXLAN 头 -> UDP 封装
  • 传输: UDP 封装的数据包在底层网络中传输
  • 解封装: UDP 封装的数据包 -> VXLAN 头 + 原始数据包

Untitled

通过 vxlan 联通后物理网络如下:

Untitled

flannel 会使用vxlan来对网络进行配置

flannel ARP、FDB、Route的作用

ARP

flannel通过VXLAN或其他实现方式创建虚拟网络,其中容器之间的通信通过VTEP(VXLAN Tunnel Endpoint)之间的VXLAN隧道传输。ARP请求和响应在VXLAN网络中传递,用于解析VTEP之间的IP到MAC地址映射。

FDB

fdb 转发表是 forwarding database 的缩写

Flannel会自动处理FDB的更新。当容器启动、迁移或停止时,Flannel会更新FDB,确保数据包能够正确地通过VXLAN网络转发到目标容器。

Route

路由用于决定数据包从源到目的地的路径。在容器网络中,路由配置确保数据包能够正确地从源容器传递到目标容器或外部网络。

Flannel会在底层网络协议栈中配置路由信息,确保VXLAN网络中的数据包按照正确的路径进行转发。这包括在主机之间和容器之间的路由。

vxlan会解决幂等问题,内部过程会解决冲突

vxlan.device的实现

vxlan.device是通过包装了 netlink 实现了网络配置。

vxlanDeviceAttrs 用于存储 VXLAN 设备的属性,包括 VNI、设备名称、MTU、VTEP 等。

vxlanDevice 结构体包含一个 netlink.Vxlan 类型的链接和一个表示是否进行直接路由的标志。

ConfigureConfigureIPv6 方法用于配置 VXLAN 设备的 IPv4 和 IPv6 地址。

AddFDB, AddV6FDB, DelFDB, DelV6FDB, AddARP, AddV6ARP, DelARP, DelV6ARP 方法用于添加或删除 VXLAN 设备的 FDB(Forwarding Database)和 ARP 表项,这些方法使用 netlink 包来配置 Linux内核的 FDB和ARP表项。

Netlink 是 Linux 是用户空间用来与内核空间通信的接口,它可以用来添加、删除和设置IP地址和路由。底层是使用 netlink.VXlan

问题

Untitled

在部署服务的时候发现启动 Pod 初始化网络报了 /run/flannel/subnet.env 文件不存在,从上面初始化的流程可以看到,这个文件是在 flanneld 启动的时候初始化从 api-server 拉取信息后生成的,所以在集群中重启了 flanneld 的 Pod这个问题就解决了

Refrence

  1. 源码分析 kubelet pod 生成 coredns resolv.conf 配置原理
  2. 源码分析 kubernetes CNI flannel 容器网络插件的设计实现原理
  3. https://github.com/flannel-io/flannel
  4. Linux Netlink for go
  5. https://docs.kernel.org/userspace-api/netlink/intro.html
  6. VXLAN 是什么