[mydocker]---容器地址分配

2019-05-12  本文已影响0人  nicktming

1. 准备工作

1.1 准备环境

root@nicktming:~/go/src/github.com/nicktming/mydocker# git clone https://github.com/nicktming/mydocker.git
root@nicktming:~/go/src/github.com/nicktming/mydocker# git checkout code-5.8
root@nicktming:~/go/src/github.com/nicktming/mydocker# git checkout -b dev-6.1

2. 容器地址分配

根据前面几篇文章的铺垫, 从这篇文章就开始准备实现网络部分的一些功能. 首先需要解决的是如何分配容器地址, 因为一台主机上可以启动很多容器, 正常情况下(比如bridge模型)每个容器都会有属于自己的ip地址.所以需要有一套机制来管理容器地址的分配与释放.

2.1 bitmap算法介绍

bitmap-01.png

由上图可知网络192.168.0.0/24的前缀占24位, 所以主机号占了8位, 因此该网络最多可分配2的8次方 256位, 除了192.168.0.0, 所以可分配255个ip, 从192.168.0.1~192.168.0.255中的ip地址都是属于该网络.

由于需要给容器分配地址, 显然需要在机器上保存哪些ip地址已经被分配, 哪些没有分配, 所以可以用bitmap来做这个事情而且效率也比较快.
从上图可知, 该map会维护一个数组长度为255(在该例子中), 数组下标加1的值就是该网络中的第几个ip, 比如下标为5, 该下标代表的ip地址为192.168.0.5.

然后数组里面的值标记该位置的ip是否被分配, 0表示没有被分配, 1表示已经被分配.

2.2 数据结构定义

关于网络地址信息都会保存在宿主机的/var/run/mydocker/network/ipam/subnet.json位置.

IPAM中的Subnets属性是一个map类型, key为某个网络比如192.168.0.0/24, value为该网络对应的ip地址分配情况表, 是一个string,其实就是把一个char数组转化成一个string.

{"192.168.0.0/24":"0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}
该map表示192.168.0.2已经被分配出去了, 其余的ip未分配出去.

const ipamDefaultAllocatorPath = "/var/run/mydocker/network/ipam/subnet.json"

type IPAM struct {
    SubnetAllocatorPath string
    Subnets *map[string]string
}

var ipAllocator = &IPAM{
    SubnetAllocatorPath: ipamDefaultAllocatorPath,
}

2.3 持久化和加载数据

持久化数据 dump, 很简单就是把IPAM中的Subnets持久化到IPAM的SubnetAllocatorPath(/var/run/mydocker/network/ipam/subnet.json), 因为Subnets里面保存着容器网络的地址分配情况.

func (ipam *IPAM) dump() error {
    // ipam.SubnetAllocatorPath 文件夹与文件分离开
    ipamConfigFileDir, _ := path.Split(ipam.SubnetAllocatorPath)
    if _, err := os.Stat(ipamConfigFileDir); err != nil {
        // 如果不存在 就逐级生成文件夹
        if os.IsNotExist(err) {
            os.MkdirAll(ipamConfigFileDir, 0644)
        } else {
            return err
        }
    }
    // O_CREATE int = syscall.O_CREAT  // create a new file if none exists.
    // O_WRONLY int = syscall.O_WRONLY // open the file write-only.
    // O_TRUNC  int = syscall.O_TRUNC  // truncate regular writable file when opened.
    // 如果该文件不存在 就创建一个
    subnetConfigFile, err := os.OpenFile(ipam.SubnetAllocatorPath, os.O_TRUNC | os.O_WRONLY | os.O_CREATE, 0644)
    defer subnetConfigFile.Close()
    if err != nil {
        return err
    }
    // 将ipam.Subnets的内容持久化
    // 也就是将所有网络的分配情况保存到该文件中
    ipamConfigJson, err := json.Marshal(ipam.Subnets)
    if err != nil {
        return err
    }

    _, err = subnetConfigFile.Write(ipamConfigJson)
    if err != nil {
        return err
    }

    return nil
}

加载数据, 就是把(/var/run/mydocker/network/ipam/subnet.json)里面的内容加载到IPAM中的Subnets中, 方便操作.

func (ipam *IPAM) load() error {
    // 如果该文件不存在 返回
    if _, err := os.Stat(ipam.SubnetAllocatorPath); err != nil {
        log.Printf("load error err:%v\n", err)
        if os.IsNotExist(err) {
            return nil
        } else {
            return err
        }
    }
    // 打开该文件
    subnetConfigFile, err := os.Open(ipam.SubnetAllocatorPath)
    defer subnetConfigFile.Close()
    if err != nil {
        return err
    }
    // 将该文件的内容读到subnetJson中
    subnetJson := make([]byte, 2000)
    n, err := subnetConfigFile.Read(subnetJson)
    if err != nil {
        return err
    }

    log.Printf("n:%d\n", n)
    log.Println(subnetJson)

    // 将subnetJson中内容加载到ipam.Subnets中
    err = json.Unmarshal(subnetJson[:n], ipam.Subnets)
    if err != nil {
        log.Printf("Error dump allocation info, %v", err)
        return err
    }
    return nil
}

2.4 地址分配

地址分配分为三个步骤:

1. 先从/var/run/mydocker/network/ipam/subnet.json中加载数据到ipam的subnets, 如果该文件不存在, subnets是一个空map,里面什么网络信息都没有.
2. 根据bitmap分配ip
3. 将已经有数据的subnets持久化到/var/run/mydocker/network/ipam/subnet.json中.

func (ipam *IPAM) Allocate(subnet *net.IPNet) (ip net.IP, err error) {
    // 存放网段中地址分配信息的数组
    // 无论ipamDefaultAllocatorPath是否存在都先new一个
    ipam.Subnets = &map[string]string{}

    // 从文件中加载已经分配的网段信息
    err = ipam.load()
    if err != nil {
        log.Printf("Error dump allocation info, %v", err)
    }
    // 得到网络号
    _, subnet, _ = net.ParseCIDR(subnet.String())

    log.Printf("Allocate subnet:%s, ipam.Subnets:%v\n", subnet, ipam.Subnets)
    // one表示前缀的个数 size表示ip地址的个数 ipv4==>size=32
    one, size := subnet.Mask.Size()

    log.Printf("Allocate one:%d, size:%d\n", one, size)

    // 如果该网络还不在ipam.Subnets中, 则初始化一个
    // 那怎么知道该网络有多少个ip地址呢 size-one就表示主机号占的位数 2的(size-one)方就有多少个主机ip
    if _, exist := (*ipam.Subnets)[subnet.String()]; !exist {
        (*ipam.Subnets)[subnet.String()] = strings.Repeat("0", 1 << uint8(size - one))
    }

    log.Printf("Allocate one:%s\n", (*ipam.Subnets)[subnet.String()])

    for c := range((*ipam.Subnets)[subnet.String()]) {
        // 如果第c个ip没有被分配 则分配
        if (*ipam.Subnets)[subnet.String()][c] == '0' {
            ipalloc := []byte((*ipam.Subnets)[subnet.String()])
            ipalloc[c] = '1'
            (*ipam.Subnets)[subnet.String()] = string(ipalloc)

            // 查一下c 用32位如何表示
            ip = subnet.IP
            for t := uint(4); t > 0; t-=1 {
                []byte(ip)[4-t] += uint8(c >> ((t - 1) * 8))
            }
            ip[3]+=1
            break
        }
    }
    // 持久化数据
    ipam.dump()
    return
}

这里需要做几点说明:

1. one, size := subnet.Mask.Size()
比如subnet为192.168.0.0/24 那子网掩码就是255.255.255.0, 另外前缀个数为24, 由于这是个ipv4, ip地址位数是32, 所以主机号占了32-24=8位, 那么主机个数为2的8次幂为256. 所以one=24, size=32.

2. []byte(ip)[4-t] += uint8(c >> ((t - 1) * 8)) 这个部分是为了找到被分配的ip, 可以看到网络如果是192.168.0.0/24, 表明地址从192.168.0.1~192.168.0.255, 如果c8, 表明第8个ip地址, 8用32位表示的话为0.0.0.8, 如何得到的呢? 0(8>>24).0(8>>16).0(8>>8).8(8>>0)
另外ip = subnet.IP192.168.0.0, 所以依次对应加起来就是192.168.0.8, ip[3]+=1==>192.168.0.9. 所以分配的地址会是192.168.0.9.

再加一个例子比如网络为172.16.0.0/12, 所以该网络有2的20次幂个ip地址, 从172.00010000.0.0~172.00011111.255.255, 此时如果c为65555表示为00000000 00000001 00000000 000100110.1.0.19, 挨个加上就得到172.17.0.19, 所以最终分配的ip地址为172.17.0.20.

测试

func Test002(t *testing.T)  {
    hostip, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
    log.Printf("ip: %s ipnet ip:%s, mask:%s\n", hostip, ipnet.IP, ipnet.Mask)
    ip, _ := ipAllocator.Allocate(ipnet)
    log.Printf("alloc ip : %v\n", ip)
}

结果

root@nicktming:~/go/src/github.com/nicktming/mydocker/network# pwd
/root/go/src/github.com/nicktming/mydocker/network
root@nicktming:~/go/src/github.com/nicktming/mydocker/network# ls 
ipam.go  ipam_test.go
root@nicktming:~/go/src/github.com/nicktming/mydocker/network# ls -l /var/run/mydocker
ls: cannot access /var/run/mydocker: No such file or directory
root@nicktming:~/go/src/github.com/nicktming/mydocker/network# go test -v ipam_test.go -test.run Test002
=== RUN   Test002
2019/05/04 19:08:58 ip: 192.168.0.1 ipnet ip:192.168.0.0, mask:ffffff00
2019/05/04 19:08:58 load error err:stat /var/run/mydocker/network/ipam/subnet.json: no such file or directory
2019/05/04 19:08:58 Allocate subnet:192.168.0.0/24, ipam.Subnets:&map[]
2019/05/04 19:08:58 Allocate one:24, size:32
2019/05/04 19:08:58 Allocate one:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
2019/05/04 19:08:58 dump ipamConfigFileDir:/var/run/mydocker/network/ipam/
2019/05/04 19:08:58 MkdirAll
2019/05/04 19:08:58 dump ipamConfigJson:{"192.168.0.0/24":"1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}
2019/05/04 19:08:58 alloc ip : 192.168.0.1
--- PASS: Test002 (0.00s)
PASS
ok      command-line-arguments  0.003s
root@nicktming:~/go/src/github.com/nicktming/mydocker/network# cat /var/run/mydocker/network/ipam/subnet.json 
{"192.168.0.0/24":"1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}
root@nicktming:~/go/src/github.com/nicktming/mydocker/network# 
root@nicktming:~/go/src/github.com/nicktming/mydocker/network# 
root@nicktming:~/go/src/github.com/nicktming/mydocker/network# go test -v ipam_test.go -test.run Test002
root@nicktming:~/go/src/github.com/nicktming/mydocker/network# cat /var/run/mydocker/network/ipam/subnet.json 
{"192.168.0.0/24":"1100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}

可以看到分配了两个地址192.168.0.1192.168.0.2

2.5 地址释放

理解了地址分配, 地址释放就是一个反向的, 就不多说了.

func (ipam *IPAM) Release(subnet *net.IPNet, ipaddr *net.IP) error {
    ipam.Subnets = &map[string]string{}

    _, subnet, _ = net.ParseCIDR(subnet.String())

    err := ipam.load()
    if err != nil {
        log.Printf("Error dump allocation info, %v", err)
    }

    c := 0
    releaseIP := ipaddr.To4()
    releaseIP[3]-=1
    for t := uint(4); t > 0; t-=1 {
        c += int(releaseIP[t-1] - subnet.IP[t-1]) << ((4-t) * 8)
    }

    ipalloc := []byte((*ipam.Subnets)[subnet.String()])
    ipalloc[c] = '0'
    (*ipam.Subnets)[subnet.String()] = string(ipalloc)

    ipam.dump()
    return nil
}

测试

func Test003(t *testing.T)  {
    hostip, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
    log.Printf("ip: %s ipnet ip:%s, mask:%s\n", hostip, ipnet.IP, ipnet.Mask)
    ipAllocator.Release(ipnet, &hostip)
}

结果

root@nicktming:~/go/src/github.com/nicktming/mydocker/network# cat /var/run/mydocker/network/ipam/subnet.json 
{"192.168.0.0/24":"1100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}root@nicktming:~/go/src/github.com/nicktming/mydocker/network# 
root@nicktming:~/go/src/github.com/nicktming/mydocker/network# go test -v ipam_test.go -test.run Test003
root@nicktming:~/go/src/github.com/nicktming/mydocker/network# cat /var/run/mydocker/network/ipam/subnet.json 
{"192.168.0.0/24":"0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}
root@nicktming:~/go/src/github.com/nicktming/mydocker/network# 

3. 参考

1. 自己动手写docker.(基本参考此书,加入一些自己的理解,加深对docker的理解)

4. 全部内容

mydocker.png

1. [mydocker]---环境说明
2. [mydocker]---urfave cli 理解
3. [mydocker]---Linux Namespace
4. [mydocker]---Linux Cgroup
5. [mydocker]---构造容器01-实现run命令
6. [mydocker]---构造容器02-实现资源限制01
7. [mydocker]---构造容器02-实现资源限制02
8. [mydocker]---构造容器03-实现增加管道
9. [mydocker]---通过例子理解存储驱动AUFS
10. [mydocker]---通过例子理解chroot 和 pivot_root
11. [mydocker]---一步步实现使用busybox创建容器
12. [mydocker]---一步步实现使用AUFS包装busybox
13. [mydocker]---一步步实现volume操作
14. [mydocker]---实现保存镜像
15. [mydocker]---实现容器的后台运行
16. [mydocker]---实现查看运行中容器
17. [mydocker]---实现查看容器日志
18. [mydocker]---实现进入容器Namespace
19. [mydocker]---实现停止容器
20. [mydocker]---实现删除容器
21. [mydocker]---实现容器层隔离
22. [mydocker]---实现通过容器制作镜像
23. [mydocker]---实现cp操作
24. [mydocker]---实现容器指定环境变量
25. [mydocker]---网际协议IP
26. [mydocker]---网络虚拟设备veth bridge iptables
27. [mydocker]---docker的四种网络模型与原理实现(1)
28. [mydocker]---docker的四种网络模型与原理实现(2)
29. [mydocker]---容器地址分配
30. [mydocker]---网络net/netlink api 使用解析
31. [mydocker]---网络实现
32. [mydocker]---网络实现测试

上一篇下一篇

猜你喜欢

热点阅读