容器docker虚拟化的底层实现原理

举报
yd_281320545 发表于 2022/11/04 18:48:50 2022/11/04
【摘要】 GO环境搭建 安装GO环境go官网https://golang.google.cn/dl/下载go1.7.1.linux-amd64.tar.gz,解压到/usr/local目录,suso tar -C /usr/local -xzf go1.7.1.linux-amd64.tar.gz,编辑$HOME/.profile,将export PATH=$PATH:/usr/local/go/b...

GO环境搭建

安装GO环境

go官网https://golang.google.cn/dl/下载go1.7.1.linux-amd64.tar.gz

解压到/usr/local目录,suso tar -C /usr/local -xzf go1.7.1.linux-amd64.tar.gz

编辑$HOME/.profile,将export PATH=$PATH:/usr/local/go/bin添加进去,

执行source $HOME/.profile让设置生效,

给root用户的环境变量添加go,su切换到root,cd然后vim .profile,添加上面的内容,执行生效命令。

打开一个终端,执行go version查看go环境是否配置成功。

tar -C: 解压时修改解压到的目录

.bashrc.profile都是sehll的启动设置文件。.bashrc在系统启动后会自动执行,.profile在用户登录后才运行。

配置GOPATH信息

编辑~/.profile,添加命令export GOPATH=/go到文件,即在根目录设置一个文件作为工作目录。

执行source ~/.profile让设置生效,

执行go env查看go语言的环境是否配置成功。

然后自己手动创建/go和其下面的三个文件夹。

go存放代码的路径时$GOPATH,并且go也是根据这个变量来寻找依赖的包(命令行通过go get就会下载到这个目录下)。老版本GOPATH目录下规定放三个子目录,分别是存放源码的src、存放编译后的生成文件的pkg、存放编译后的可执行文件的bin

Namespace

上一篇文章中记录了linux实现的六种Namespace,使用这些namespace主要使用三个系统调用:使用clone()创建进程时设置进程的namespace属性;使用unshare()将一个进程移出指定的namespace、使用setns()将进程加到一个指定的namespace里面。这篇笔记里重新用go进行实验。

UTS Namespace

可以用这个命名空间来隔离hostname,只需要设置标志为CLONE_NEWUTS即可。

现在src目录下创建main文件夹作为一会编写go代码所属的包。

package main
import (
	"os/exec"
	"syscall"
	"os"
	"log"
)
func main(){
	cmd := exec.Command("sh")	//指定被fork出来的子进程的初始命令
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS,	//设置进程属性为CLONE_NEWUTS
	}
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err:= cmd.Run(); err!=nil{	//Run函数等待子进程结束才退出
		log.Fatal(err)
	}
}

编译go文件go build test.go,运行./test,在新开启的进程空间里修改hostname生效,对重新打开的一个命令行窗口的hostname不生效,证明成功隔离。

b-16429091081032.png

a-16429090995801.png

IPC Namespace

IPC是进程间通信的意思,以此来隔离进程通信空间。在上面代码基础上增加syscall.CLONE_NEWIPC即可。

package main
import (
	"os/exec"
	"syscall"
	"os"
	"log"
)
func main(){
	cmd := exec.Command("sh")	//指定被fork出来的子进程的初始命令
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS | 	syscall.CLONE_NEWIPC,
	}
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err:= cmd.Run(); err!=nil{	//Run函数等待子进程结束才退出
		log.Fatal(err)
	}
}

PID Namespace

隔离pid可以做到像docker里一样进程号从1开始。在上面的代码基础上添加syscall.CLONE_NEWPID

package main
import (
	"os/exec"
	"syscall"
	"os"
	"log"
)
func main(){
	cmd := exec.Command("sh")	//指定被fork出来的子进程的初始命令
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS|syscall.CLONE_NEWIPC|syscall.CLONE_NEWPID,
	}
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err:= cmd.Run(); err!=nil{	//Run函数等待子进程结束才退出
		log.Fatal(err)
	}
}

运行后命令echo $$查看当前shell的id为1,验证成功。

Mount Namespace

被隔离开的两个进程能看到的文件系统是不同的视图,通过mount()umount()只会影响当前mount命名空间的文件系统。mount namespace比chroot更安全。但是在mount()之前是可以看到宿主机文件系统的,并且在根目录下创建文件可以影响到宿主机。

在上面代码基础上添加syscall.CLONE_NEWNS

package main
import (
	"os/exec"
	"syscall"
	"os"
	"log"
)
func main(){
	cmd := exec.Command("sh")	//指定被fork出来的子进程的初始命令
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
	}
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err:= cmd.Run(); err!=nil{	//Run函数等待子进程结束才退出
		log.Fatal(err)
	}
}

运行后执行命令mount -t proc proc /proc,把虚拟文件系统proc挂载到宿主机的/proc目录下,这样以后再查看/proc目录就看到的是子进程被隔离的proc文件系统。

mount -t type 设备名 dir,但是对于proc虚拟文件系统没有设备名,所以设备名设为什么都没有影响。

再命令ps -ef可以看到此时的文件系统已经被隔离开,看不到宿主机的进程信息。

ps -ef就是查看proc虚拟文件系统中的信息来返回结果的。

User Nmaespace

隔离用户组,用syscall.CLONE_NEWUSER可以在子进程自己命名空间中映射为root用户,但是在宿主机看这个进程只是一个普通进程。

package main
import (
	"os/exec"
	"syscall"
	"os"
	"log"
)
func main(){
	cmd := exec.Command("sh")	//指定被fork出来的子进程的初始命令
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER,
	}
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err:= cmd.Run(); err!=nil{	//Run函数等待子进程结束才退出
		log.Fatal(err)
	}
}

运行后输入id,发现此时id已经不是root的0了,证明被隔开。

Network Namespace

被隔离的容器有自己独立的网络设备,clone的时候设置标志CLONE_NEWNET

package main
import (
	"os/exec"
	"syscall"
	"os"
	"log"
)
func main(){
	cmd := exec.Command("sh")	//指定被fork出来的子进程的初始命令
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER | syscall.CLONE_NEWNET,
	}
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err:= cmd.Run(); err!=nil{	//Run函数等待子进程结束才退出
		log.Fatal(err)
	}
}

执行后使用命令ifconfig查看不到网卡信息,打开另一个终端命令ifconfig可以看到当前网卡信息,证明network namespace起作用。

CGroup

CGroup可以对进程做资源上的限制,包括cpu、内存、存储等。

CGroup主要包含三大组件,cgroup(注意是小写)提供进程分组管理的机制,subsystem资源控制模块,hierarchy(层次体系)提供cgroup的树状结构。

CGroup实验

首先创建一个cgroup树,查看其下的内容。

mkdir cgroup-lab
sudo mount -t cgroup -o none,name=cgroup-lab cgroup-lab ./cgroup-lab
ls ./cgroup-lab/

-t cgroup指定挂载的类型

-o none,name=cgroup-lab 指定选项

cgroup-lab即设备名称的位置这个字符串可以是任意值

./cgroup-lab是挂载点

a-16429278664821.png

cgroup.clone_child,cpuset的subsystem会读取这个配置文件,默认值为0;是1的情况下子cgroup会继承父cgroup的cpuset的配置。

cgroup.procs,树中当前节点(cgroup)中的进程组id,根节点的cgroup.procs值包含所有进程组的id。

release_agent是一个路径,用作进程退出后自动清理掉不再使用的cgroup,notify_on_release标识这个cgroup标识的进程组中最后一个进程退出后是否执行release_agent。

tasks,标识cgroup节点包含的进程的id,就是设置这个值来将进程加入一个group中。

在树的根节点创建两个子cgroup,此时会自动装填cgroup节点的配置文件值。

a-16429283849542.png

接下来将当前终端shell的进程加到cgroup-1中,先cd到这个目录下,然后命令sudo sh -c "echo $$ >> tasks",查看cat /proc/$$/cgroup,可以看到被加入到cgroup-1中。

通过subsystem限制cgroup中进程的cpu占用资源,这里利用系统为memory默认创建的hierachy进行实验。

系统为每个subsystem创建一个默认的hierarchy。

使用stress工具模拟高cpu占用进程stress --cpu 1 &

接着将这个进程添加到cpu目录下的cpu-lab这个cgroup中,并做出限制,代码如下

cd /sys/fs/cgroup/cpu
mkdir cpu-lab
cd cpu-lab
sudo sh -c "echo 4551 > cgroup.procs"
sudo sh -c "echo 10000 > cpu.cfs_quota_us"

之后可以看到这个进程的cpu占用率从100%降到了10%(10000/10^6=10%)。

docker使用CGroup限制容器

docker运行一个内存做出限制的容器

sudo docker run -itd -m 128m ubuntu

然后去cgroup查看这个容器的信息

a-16429362209254.png

可以看到docker对于内存的限制措施是在系统的memory下创建一个子cgroup为docker,然后再在docker这个节点下创建每个容器的节点,在这个容器节点下做出限制。

go实现通过CGroup限制容器资源

package main
import (
	"os"
	"fmt"
	"os/exec"
	"path"
	"io/ioutil"
	"syscall"
	"strconv"
)
const cgroupMemoryHierarchyMount = "/sys/fs/cgroup/memory"

func main(){
	if os.Args[0] == "/proc/self/exe" {
		cmd := exec.Command("sh", "-c", `stress --vm-bytes 200m --vm-keep -m 1`)
		cmd.SysProcAttr = &syscall.SysProcAttr{
		}
		cmd.Stdin = os.Stdin
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		//只有创建的子进程才会进入这个大的if,子进程执行cmd.Run是执行stress命令,不会再执行下面
		if err:= cmd.Run(); err!=nil {	
			fmt.Println(err)
			os.Exit(1)
		}
	}
	cmd := exec.Command("/proc/self/exe")	///proc/self/exe 它代表当前程序,即这个程序创建子进程调用自己
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
	}
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err:= cmd.Start(); err!=nil {
		fmt.Println("oh~ error", err)
		os.Exit(1)
	}else{
		fmt.Printf("child pid:%v\n", cmd.Process.Pid)	//fork的子进程在宿主机的pid
		//在/sys/fs/cgroup/memory下创建一个cgroup
		os.Mkdir(path.Join(cgroupMemoryHierarchyMount, "memory-limit-lab"), 0775)	//0775是权限
		//把进程id加入到cgroup中
		ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount, "memory-limit-lab", "tasks"), []byte(strconv.Itoa(cmd.Process.Pid)), 0644)
		//限制cgroup进程的内存占用
		ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount, "memory-limit-lab", "memory.limit_in_bytes"), []byte("100m"), 0644)
		cmd.Process.Wait()
	}
}

docker镜像分层—AUFS

Union File System使用写时复制的机制使得对文件修改操作不会改变原来文件的内容。之后的更稳定可靠的版本称为AUFS。

写时复制:对于一个没有修改的资源,多个实例共享;如果有一个实例修改了这个资源,才会对该资源复制一份并进行修改。

docker镜像使用AUFS原理

老板版本docker中每个镜像都是由很多只读层read-only layer组成,这些镜像都被存储在/var/lib/docker/aufs/diff/下;另外一个目录/var/lib/docker/aufs/layers存储着每个镜像依赖的只读层信息。在pull一个镜像后会下载相关的组成这个镜像的文件镜像,如果在这个镜像上稍作修改,那么会对于修改的部分制作为一个新的文件镜像,并关联一些个没有被修改的文件镜像,而由于只是修改了很小一部分,所以这个利用写时复制机制创建的新镜像的组成部分的文件镜像体积很小。

docker容器使用AUFS原理

启动一个容器时,docker为其创建一个``read-only的init层,和read-write`层来保存容器信息的修改。

容器的read-write层也存储在/var/lib/docker/aufs/diff目录下,因为容器可以转成镜像,镜像可以开启容器,所以这两个东西在某种意义上是等价的。这也解释了关闭容器后不会丢失数据。

如果要删除一个文件file,则aufs机制会在只读层生成一个.wh.file文件来隐藏所有只读层的file文件。

AUFS实验

自己创建一个aufs目录,在其下面创建一下内容

284372a74421b89b484293dd13cc58f.png

把container-layer和image-layer用aufs方式挂载到mnt目录下,注意mount aufs如果没有指定挂载文件的权限,默认第一个目录是read-write权限,后面都是read-only权限。

sudo mount -t aufs -o dirs=./container-layer:./image-layer none ./mnt

效果如下

b4859f5a90c88045243c289b61ede29.png

可以查看挂载信息sudo cat /sys/fs/aufs/si_xxxxx/*

往只读的image-layer.txt文件中写内容echo -e "\nhello world" >> ./mnt/image-layer.txt,查看mnt下的image-layer被改动,但是image-layer目录下的image-layer.txt没有被改动,并且container-layer目录下多了一个image-layer.txt,其中的内容就是改动后的文件内容。

7c02a216d092f19bbdc97a63fc7ed5e.png

b-16430021314353.png

但是对于读写层的container-layer的内容修改,mnt目录和container-layer目录下内容保持一致。

c-16430021778874.png

相关概念

proc文件系统

proc文件系统是由内核提供的,并不真实存在于磁盘上。很多命令就是通过读取这个虚拟的文件系统来获取文件的信息。

/proc/n		pid为n的进程信息
/proc/n/cmdline		n号进程的启动命令
/proc/n/cwd		n号进程当前的工作目录
/proc/n/environ		进程环境变量列表
/proc/n/exe		进程的执行命令文件
/proc/n/fd		进程相关的所有文件描述符
/proc/n/maps	与进程相关的内存映射信息
/proc/n/mem		进程持有的内存
/proc/n/root	进程能看到的根目录
/proc/n/stat	进程的状态
/proc/n/statm	进程占用的内存状态
/proc/n/status	详细进程状态
/proc/self	连接到当前正在运行的进程

chroot、pivot_root

chroot的作用是改变进程的根目录视图,使它不能访问该目录之外的其他文件。

chroot <newroot> [<command><arg>...]
newroot:	要切换到新的root目录
command:	切换root目录后要执行的命令
示例:	sudo chroot /home/jgc/lab/rootfs_lab /bin/bash

但是chroot只是改变当前进程的根目录视图。

pivot_root的功能和chroot类似,但是pivot_root把整个系统切换到一个新的根目录,这样就可以去掉对之前rootfs的依赖,即可以umount卸载挂载点了。

容器与虚拟机比较

容器是共享内核的,其底层原理是多个进程同时运行在一个内核上,利用Namespace把它们隔离开,然后用CGroup限制可用资源。

而虚拟机是共享“硬件”的,每个虚拟机都有自己独立的操作系统。所以,虚拟机是可引导的、绝对安全的隔离技术;而容器是相对脆弱的,不安全的隔离技术。

容器的网络配置

linux是通过网络设备操作和使用网卡,系统安装一个网卡后为其生成一个网络设备,例如eth0;也可以虚拟出一个网络设备,例如veth,bridge。

veth

veth是成对出现的虚拟网络设备,在一端发送数据,在另一端会获取到数据;通常使用veth连接不同的Net Namespace。

下面进行实验,创建两个网络和一对veth,并把veth的两端分别绑定到这两个网络中,并且为两个网络设置ip,最后用一个网络namespace去ping另一个网络namespace。

sudo ip netns add ns1
sudo ip netns add ns2

sudo ip link add veth1 type veth peer name veth2

sudo ip link set veth1 netns ns1
sudo ip link set veth2 netns ns2
查看ns1这个namespace的网络设备
sudo ip netns exec ns1 ip link

配置每个veth的网络地址和路由
sudo ip netns exec ns1 ifconfig veth1 192.168.0.2/24 up
sudo ip netns exec ns2 ifconfig veth2 192.168.0.3/24 up
sudo ip netns exec ns1 route add default dev veth1
sudo ip netns exec ns2 route add default dev veth2
# 用veth一段去ping另一端
sudo ip netns exec ns1 ping -c 1 192.168.0.3

注意ns1是ping不通宿主机的,因为两个ip不在同一网段,并且设备连接两个网段,网络示意图如下。

a-16431863082851.png

bridge与路由表

虚拟bridge相当于交换机,连接不同的网络设备;网络数据包到达bridge时,通过报文中的mac进行广播或者转发。

可以通过定义路由表来决定某个网络的namespace中的数据包的流向。

下面进行实验,创建一个ns1,和一个veth1-veth2,把一端veth1绑定到ns1,再创建一个网桥,网桥一段挂载到veth1,另一端挂载到eth0。

sudo ip netns add ns1
sudo ip link add veth1 type veth peer name veth2
sudo ip link set veth1 netns ns1

sudo brctl addbr br0
sudo brctl addif br0 eth0
sudo brctl addif br0 veth2

sudo ip link set veth2 up
sudo ip link set br0 up
sudo ip netns exec ns1 ifconfig veth1 192.168.0.2/24 up

分别设置ns1的路由和宿主机上的路由
ns1中的所有流量都经过veth1的网络设备流出
sudo ip netns exec ns1 route add default dev veth1
宿主机将192.168.0.0/24网段的数据路由到br0
sudo route add -net 192.168.0.0/24 dev br0

从ns1中ping宿主机地址
sudo ip netns exec ns1 ping -c 1 192.168.200.129

此时ns1可以ping通宿主机,网络示意图如下

b-16431890695992.png

iptables

容器虚拟化常用的两种策略MASQUERADEDNAT来实现容器和宿主机外部的网络通信。

MASQUERADE策略可以将请求包中的源地址转换成一个网络设备的地址。

下面进行实验,先打开ip转发功能,再把源地址为192.168.0.0/24网段来的数据修改为eth0的ip的源地址,这样就可以在ns1中访问宿主机外部的网络。但是这里没有成功,不知原因。

sudo sysctl -w net.ipv4.conf.all.forwarding=1
sudo iptables -t nat -A POSTROUTING -s 192.168.0.0/24 -o eth0 -j MASQUERADE

注意还要关闭防火墙
apt-get install ufw
ufw disable

DNAT也是做网络地址转换,用于把内部网络的端口映射出去。

实验,把宿主机80端口的tcp请求转发到namespace的80端口

sudo iptables -t nat -A PREROUTING -p tcp -m tcp --dport 80 -j DNAT --to-destination 192.168.0.2:80

go实现获取一个未分配的ip

package main

import(
    "fmt"
    "os"
    "path"
    "json"
    "net"
)

const ipamDefaultAllocatorPath = "C:/subnet.json"

//IP Address Management
type IPAM struct{
    //分配ip的文件存放位置
    SubnetAllocatorPath string
    //网段和位图算法的数组map,key是网段,value是分配的位图数组
    Subnets *map[string]string
}

//初始化一个IPAM对象
var ipAllocator = &IPAM{
    SubnetAllocatorPath: ipamDefaultAllocatorPath
}

//json文件转ip分配信息
func (ipam *IPAM) load() error{
    if _, err := os.Stat(ipam.SubnetAllocatorPath); err!=nil{
        return err
    }
    //打开文件
    subnetConfigFile, err := os.Open(ipam.SubnetAllocatorPath)
    defer subnetConfigFile.Close()
    if err!= nil{
        return err
    }
    //读取json格式信息
    subnetJson := make([]byte, 2000)
    n, err := subnetConfigFile.Read(subnetJson)
    if err!=nil{
        return err
    }
    //json格式转换
    err = json.Unmarsha1(subnetJson[:n], ipam.Subnets)
    if err!=nil{
        return err
    }
    return nil
}

//ip分配信息转json文件
func (ipam *IPAM) dump() err{
    ipamConfigFileDir, _ := path.Split(ipam.SubnetAllocatorPath)
    //检查目录是否存在
    if _, err := os.Stat(ipamConfigFileDir); err !=nil {
        if os.IsNotExist(err){
            os.MkdirAll(ipamConfigFileDir, 0644)
        }else{
            return err
        }
    }
    //打开文件
    subnetConfigFile, err := os.OpenFile(ipam.SubnetAllocatorPath, os.O_TRUNC | os.O_WRONLY | os.O_CREATE, 0644)
    defer subnetConfigFile.Close()
    if err!=nil{
        return err
    }
    //存储
    ipamConfigJson, err := json.Marsha1(ipam.Subnets)
    if err !=nil{
        return err
    }
    _, err = subnetConfigFile.Write(ipamConfigJson)
    if err !=nil{
        return err
    }
    return nil
}

//在网段中获取一个可用的ip
func (ipam *IPAM) Allocate(subnet *net.IPNet) (ip net.IP, err error){
    ipam.Subnets = &map[string]string{}
    err = ipam.load()
    if err!=nil{
        fmt.Println(err)
    }
    one, size := subnet.Mask.Size() //返回子网掩码长度和ip总长度
    //如果没分配过这个网段,则初始化网段
    if _, exist := (*ipam.Subnets)[subnet.String()]; !exist {
        //2^(size-one)个可分配ip,每一位0表示可用ip,1表示不可用(已经分配掉)
        (*ipam.Subnets)[subnet.String()] = strings.Repeat("0", 1<<uint8(size-one))
    }
    //遍历网段的位图数组
    for c:= range((*ipam.Subnets)[subnet.String()]){
        if (*ipam.Subnets)[subnet.String()][c] =='0'{
            ipalloc := []byte((*ipam.Subnets)[subnet.String()])
            ipalloc[c] = '1'
            (*ipam.Subnets)[subnet.String()] = string(ipalloc)
            ip = subnet.Ip  //初始ip,比如网段192.168.0.0/24, 这里ip为192.168.0.0
            //处理ip的值,在网段的基础上加上修正
            for t:=uint(4); t>0; t-=1 {
                []byte(ip)[4-t] += uint8(c>>((t-1)*8))
            }
            ip[3] += 1  //ip从1开始
            break
        }
    }
    ipam.dump()
    return
}

//释放ip地址
func (ipam *IPAM) Release(subnet *net.IPNet, ipaddr *net.IP) error{
    ipam.Subnets = &map[string]string{}
    if err := ipam.load(); err !=nil {
        fmt.Println(err)
    }
    c := 0
    releaseIP := ipaddr.To4()   //将ip地址转成4个字节的表示方式
    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
}

//测试分配ip
func TestAllocate(t *testing.T){
    _, ipnet, _ := net.ParseCIDR("192.168.0.0/24")
    ip, _ := ipAllocator.Allocate(ipnet)
    fmt.Println(ip)
}
//测试释放ip
func TestRelease(t *testing.T){
    ip, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
    ipAllocator.Release(ipnet, &ip)
}
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。