• 【博客438】Kubernetes IPAM分配IP原理


    Kubernetes IPAM分配IP原理

    IPAM是k8s cni插件中负责分配ip的一类插件,其实现有dhcp,host-local等

    在这里插入图片描述

    IPAM host-local分配IP原理

    Kube-controller-manager为每个节点分配一个podCIDR。从podCIDR中的子网值中为节点上的Pod分配IP地址。由于所有节点上的podCIDR是不相交的子网,因此它允许为每个pod分配唯一的IP地址。

    先看pod创建过程:

    在这里插入图片描述

    1、kubelet收到创建Pod的事件,调用CRI接口,Docker或者其他的容器运行时发起对Pod的创建;
    
    2、后端Docker创建网络空间network namespace;
    
    3、CRI调用CNI插件,会传入刚刚创建好的网络空间network namespace;
    
    4、CNI插件会读取Node节点/etc/cni/net.d路径下的配置文件,配置Pod网络,
         实现Pod网络到Node节点网络的打通,同时拿到Pod的IP;
    
    5、Docker创建Pause容器,每一个Pod在启动时都会有一个基础的Pause容器,
       把Pause容器加入到网络空间中,后续其他的容器都会公用Pause容器的网络空间。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    host-local为啥叫host-local

    host-local插件从address ranges 中分配IP,将分配的结果存在本地机器,所以这也是为什么叫做host-local

    毕竟如果没有地方进行纪录来进行比较,就没有办法每次都回传一个没有被用过的IP地址。

    host-local example

    dataDir的变数会指定要用哪个资料夹作为host-local记录用过的资讯,预设值是/var/lib/cni/networks/ .

    {
        "ipam": {
            "type": "host-local",
            "ranges": [
                [
                    {
                        "subnet": "10.10.0.0/16",
                        "rangeStart": "10.10.1.20",
                        "rangeEnd": "10.10.3.50",
                        "gateway": "10.10.0.254"
                    },
                    {
                        "subnet": "172.16.5.0/24"
                    }
                ],
                [
                    {
                        "subnet": "3ffe:ffff:0:01ff::/64",
                        "rangeStart": "3ffe:ffff:0:01ff::0010",
                        "rangeEnd": "3ffe:ffff:0:01ff::0020"
                    }
                ]
            ],
            "routes": [
                { "dst": "0.0.0.0/0" },
                { "dst": "192.168.0.0/16", "gw": "10.10.5.1" },
                { "dst": "3ffe:ffff:0:01ff::1/64" }
            ],
            "dataDir": "/run/my-ipam-path"
        }
    }
    
    // 没有特别指定dataDir,所有的档案都会存放在/var/lib/cni/networks/里面
    $ sudo find /var/lib/cni/networks/ -type f
    /var/lib/cni/networks/last_reserved_ip.0
    /var/lib/cni/networks/10.10.1.1
    /var/lib/cni/networks/10.10.1.2
    /var/lib/cni/networks/10.10.1.3
    
    我们可以观察到,每个被用过的IP都会产生一个以该IP为名的文件,该档案中的内容非常简单,
    就是使用的container ID
    
    还可以观察到一个名为last_reserved_ip的文件,用来记住每个range目前分配的最后一个IP是哪个
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    在k8s node上进行验证

    $ kubectl describe nodes | grep PodCIDR
    PodCIDR:                     10.244.0.0/24
    PodCIDR:                     10.244.1.0/24
    PodCIDR:                     10.244.2.0/24
    
    $ sudo cat /run/flannel/subnet.env
    FLANNEL_NETWORK=10.244.0.0/16
    FLANNEL_SUBNET=10.244.0.1/24
    FLANNEL_MTU=1450
    FLANNEL_IPMASQ=true
    
    $ sudo ls /var/lib/cni/networks/cbr0
    10.244.0.2  10.244.0.3  10.244.0.8  last_reserved_ip.0  lock
    
    $ sudo cat /var/lib/cni/networks/cbr0/10.244.0.8
    2d39d5afb81e56314a7fd6bdd57c9ccf6d02c32b556273cfb6b9bb8a248c851b
    
    $ sudo docker ps  --no-trunc | grep $(sudo cat /var/lib/cni/networks/cbr0/10.244.0.8)
    2d39d5afb81e56314a7fd6bdd57c9ccf6d02c32b556273cfb6b9bb8a248c851b   k8s.gcr.io/pause:3.1 "/pause" Up 4 hours  k8s_POD_k8s-udpserver-6576555bcb-7h8jh_default_87196597-ccda-4643-ac5d-85343a3b6c90_0
    
    解析:
    * 每个节点上都有一个PodCIDR的栏位,代表的是该节点可以使用的网段
    * 节点是对应到的PodCIDR是10.244.0.0/24,与/run/flannel/subnet.env里面的数值一致。
    * 查看下ipam分配ip后为每个ip生成的记录文件,内容是一个id
    * docker查看,可以发现是pod里对应的container的id
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    host-local如何知道上一次分配到ip是哪个

    host-local会在宿主机生成一个记录文件:/var/lib/cni/networks/last_reserved_ip.0

    cni官方文档对host-local的简述

    host-local IPAM plugin allocates ip addresses out of a set of address ranges. It stores the state locally on the host filesystem, therefore ensuring uniqueness of IP addresses on a single host. The allocator can allocate multiple ranges, and supports sets of multiple (disjoint) subnets. The allocation strategy is loosely round-robin within each range set.

    下文对官方文档中的 round-robin 算法原理进行剖析

    IP分配算法:round-robin

    轮询调度(Round Robin Scheduling)算法就是以轮询的方式依次将请求调度不同的服务器,即每次调度执行i = (i + 1) mod n,并选出第i台服务器。 算法的优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度。

    host-local会根据参数给予的IP范围,依序回传一个没有被使用过的IP, 这个运作原理非常的符合我们真正的需求,每次有POD产生的时候都可以得到一个没有被使用过的IP地址,避免重复同时又能够使用。

    IPAM host-local round-robin分配

    源码剖析:

    // 分配一个ip
    func (a *IPAllocator) Get(id string, ifname string, requestedIP net.IP) (*current.IPConfig, error) {
    	a.store.Lock()
    	defer a.store.Unlock()
    
    	var reservedIP *net.IPNet
    	var gw net.IP
    
        // 如果请求ip不为空,则查看请求的ip是否满足分配的条件
    	if requestedIP != nil {
    		if err := canonicalizeIP(&requestedIP); err != nil {
    			return nil, err
    		}
    
    		r, err := a.rangeset.RangeFor(requestedIP)
    		if err != nil {
    			return nil, err
    		}
    
    		if requestedIP.Equal(r.Gateway) {
    			return nil, fmt.Errorf("requested ip %s is subnet's gateway", requestedIP.String())
    		}
    
    		reserved, err := a.store.Reserve(id, ifname, requestedIP, a.rangeID)
    		if err != nil {
    			return nil, err
    		}
    		if !reserved {
    			return nil, fmt.Errorf("requested IP address %s is not available in range set %s", requestedIP, a.rangeset.String())
    		}
    		reservedIP = &net.IPNet{IP: requestedIP, Mask: r.Subnet.Mask}
    		gw = r.Gateway
        // 否则分配一个新的未使用的ip回去
    	} else {
            ...
            ...
            // 获取迭代器,迭代器指向上一个分配的ip
    		iter, err := a.GetIter()
    		if err != nil {
    			return nil, err
    		}
    		for {
    		    // 迭代器的下一个ip就是要分配出去的ip
    			reservedIP, gw = iter.Next()
    			if reservedIP == nil {
    				break
    			}
    
    			reserved, err := a.store.Reserve(id, ifname, reservedIP.IP, a.rangeID)
    			if err != nil {
    				return nil, err
    			}
    
    			if reserved {
    				break
    			}
    		}
    	}
    
    	if reservedIP == nil {
    		return nil, fmt.Errorf("no IP addresses available in range set: %s", a.rangeset.String())
    	}
    
    	return ¤t.IPConfig{
    		Address: *reservedIP,
    		Gateway: gw,
    	}, nil
    }
    
    // 获取迭代器
    // 迭代器指向的ip就是上一个分配的ip
    func (a *IPAllocator) GetIter() (*RangeIter, error) {
    	iter := RangeIter{
    		rangeset: a.rangeset,
    	}
    
    	// Round-robin by trying to allocate from the last reserved IP + 1
    	startFromLastReservedIP := false
    
        // 迭代器指向的ip就是上一个分配的ip
        // 原理是读取本地的/var/lib/cni/networks/last_reserved_ip.0
    	lastReservedIP, err := a.store.LastReservedIP(a.rangeID)
    
    	// Find the range in the set with this IP
    	// 如果存在上一个分配的ip,则迭代器指向上一个分配的ip,否则指向第一个ip
    	if startFromLastReservedIP {
    		for i, r := range *a.rangeset {
    			if r.Contains(lastReservedIP) {
    				iter.rangeIdx = i
    				// We advance the cursor on every Next(), so the first call
    				// to next() will return lastReservedIP + 1
    				iter.cur = lastReservedIP
    				break
    			}
    		}
    	} else {
    		iter.rangeIdx = 0
    		iter.startIP = (*a.rangeset)[0].RangeStart
    	}
    	return &iter, nil
    }
    
    // 从迭代器获取其下一个ip作为分配的ip
    // 迭代器指向的ip是上一次分配的ip,其下一个ip就是round-robin算法的下一个ip
    func (i *RangeIter) Next() (*net.IPNet, net.IP) {
    	r := (*i.rangeset)[i.rangeIdx]
    
        // 如果是第一次分配,则取第一个ip
    	if i.cur == nil {
    		i.cur = r.RangeStart
    		i.startIP = i.cur
    		if i.cur.Equal(r.Gateway) {
    			return i.Next()
    		}
    		return &net.IPNet{IP: i.cur, Mask: r.Subnet.Mask}, r.Gateway
    	}
    
        // 如果到了末端,则重头开始
    	if i.cur.Equal(r.RangeEnd) {
    	    // 这里是round-robin算法的实现
    		i.rangeIdx += 1
    		i.rangeIdx %= len(*i.rangeset)
    		r = (*i.rangeset)[i.rangeIdx]
    
    		i.cur = r.RangeStart
        // 如果没到末端,则取下一个ip
    	} else {
    		i.cur = ip.NextIP(i.cur)
    	}
    
    	if i.startIP == nil {
    		i.startIP = i.cur
    	} else if i.cur.Equal(i.startIP) {
    		// IF we've looped back to where we started, give up
    		return nil, nil
    	}
    
    	if i.cur.Equal(r.Gateway) {
    		return i.Next()
    	}
    
    	return &net.IPNet{IP: i.cur, Mask: r.Subnet.Mask}, r.Gateway
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143

    总结:每次从本地读取/var/lib/cni/networks/last_reserved_ip.0文件,获取上一次分配的ip,然后其下一个ip就是要分配出去的,如果没有上一次分配的ip,则获取第一个ip分配出去

    k8s如何保证pod ip不重复

    1、kubernetes会针对每个node去标示一个名为PodCIDR的值,代表该Node可以使用的网段是什么,且分配的时候保证为每个pod分配不同的段

    2、flannel的Pod 会去读取node中的PodCIDR,并且将该资讯写道/run/flannel/subnet.env中,
    flannel CNI收到任何创建Pod的请求时,会去读取/run/flannel/subnet.env,flannel CNI会调用bridge CNI,bridge CNI会调用host-local CNI,host-local CNI使用round-robin保证同一个node上的pod的ip不重复

    为什么使用round-robin,而不是每次取最小的未分配的ip

    因为CNI回收ip和docker回收容器两个操作不是一个原子性的操作!

    假设有一个pod用了10.244.1.1,然后这个pod被删除了,然后CNI插件释放这个ip,docker释放这个容器,但是有可能docker还没完全释放这个容器的时候,CNI先完成了ip的释放并且这时候又有新的pod创建出来,那么新的pod可能用了10.244.1.1这个ip,但是旧的正在被释放容器也在用这个ip,就可能出现冲突和数据混乱

  • 相关阅读:
    【python】懒人福利,通过Python的JIRA库操作JIRA,自动批量提交关闭bug,提高效率
    动态分区算法(头歌实验)第1关:首次适应算法。第2关:最佳适应算法。
    PaddleOCR ‘could not create a primitive descriptor for a reorder primitive‘异常处理
    百度网盘开放接口
    BERT论文精读
    java毕业设计大学生心愿墙系统Mybatis+系统+数据库+调试部署
    13 Go的错误处理
    Django(4)表单
    php使用lunar实现农历、阳历、节日等功能
    从小公司功能测试到一线大厂自动化测试,薪资翻倍,我做到了...
  • 原文地址:https://blog.csdn.net/qq_43684922/article/details/126184031