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
, 如果c
为8
, 表明第8个ip地址, 8用32位表示的话为0.0.0.8
, 如何得到的呢?0(8>>24).0(8>>16).0(8>>8).8(8>>0)
另外ip = subnet.IP
为192.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 00010011
为0.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.1
和192.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]---网络实现测试
网友评论