docker容器启动过程--简易版实现

本篇文章为学习笔记,代码来源于:https://github.com/pibigstar/go-docker

1. 命令参数

cmdArray中存放的是定义之外的参数(Flag中的参数属于定义过的参数),在之后调用init命令时使用。比如./go-docker run -d -v /root/test:/test --name test busybox shsh属于定义之外的参数,会被存放在cmdArray

// 创建namespace隔离的容器进程
// 启动容器
var runCommand = cli.Command{
	Name:  "run",
	Usage: "Create a container with namespace and cgroups limit",
	Flags: []cli.Flag{
		cli.BoolFlag{
			Name:  "ti",
			Usage: "enable tty",
		},
		cli.StringFlag{
			Name:  "m",
			Usage: "memory limit",
		},
		cli.StringFlag{
			Name:  "cpushare",
			Usage: "cpushare limit",
		},
		cli.StringFlag{
			Name:  "cpuset",
			Usage: "cpuset limit",
		},
		cli.StringFlag{
			Name:  "v",
			Usage: "docker volume",
		},
		cli.BoolFlag{
			Name:  "d",
			Usage: "detach container",
		},
		cli.StringFlag{
			Name:  "name",
			Usage: "container name",
		},
		cli.StringSliceFlag{
			Name:  "e",
			Usage: "docker env",
		},
		cli.StringFlag{
			Name:  "net",
			Usage: "container network",
		},
		cli.StringSliceFlag{
			Name:  "p",
			Usage: "port mapping",
		},
	},
	Action: func(ctx *cli.Context) error {
		if len(ctx.Args()) < 1 {
			return fmt.Errorf("missing container args")
		}

		res := &subsystem.ResourceConfig{
			MemoryLimit: ctx.String("m"),
			CpuSet:      ctx.String("cpuset"),
			CpuShare:    ctx.String("cpushare"),
		}
		// cmdArray 为容器运行后,执行的第一个命令信息
		// cmdArray[0] 为镜像名, .Tail() 是去掉第一个后的全部参数
		var cmdArray []string
		for _, arg := range ctx.Args().Tail() {
			cmdArray = append(cmdArray, arg)
		}

		tty := ctx.Bool("ti")
		detach := ctx.Bool("d")

		if tty && detach {
			return fmt.Errorf("ti and d paramter can not both provided")
		}

		containerName := ctx.String("name")
		volume := ctx.String("v")
		net := ctx.String("net")
		// 要运行的镜像名
		imageName := ctx.Args().Get(0)
		envs := ctx.StringSlice("e")
		ports := ctx.StringSlice("p")

		Run(cmdArray, tty, res, containerName, imageName, volume, net, envs, ports)
		return nil
	},
}

2. 程序运行

func Run(cmdArray []string, tty bool, res *subsystem.ResourceConfig, containerName, imageName, volume, net string, envs, ports []string) {
	containerID := container.GenContainerID(10)
	if containerName == "" {
		containerName = containerID
	}
	parent, writePipe := container.NewParentProcess(tty, volume, containerName, imageName, envs)
	if parent == nil {
		logrus.Errorf("failed to new parent process")
		return
	}
	if err := parent.Start(); err != nil {
		logrus.Errorf("parent start failed, err: %v", err)
		return
	}
	// 记录容器信息
	err := container.RecordContainerInfo(parent.Process.Pid, cmdArray, containerName, containerID)
	if err != nil {
		logrus.Errorf("record container info, err: %v", err)
	}

	// 添加资源限制
	cgroupMananger := cgroups.NewCGroupManager("go-docker")
	// 删除资源限制
	defer cgroupMananger.Destroy()
	// 设置资源限制
	cgroupMananger.Set(res)
	// 将容器进程,加入到各个subsystem挂载对应的cgroup中
	cgroupMananger.Apply(parent.Process.Pid)

	// 设置网络
	if net != "" {
		// 初始化容器网络
		err = network.Init()
		if err != nil {
			logrus.Errorf("network init failed, err: %v", err)
			return
		}
		containerInfo := &container.ContainerInfo{
			Id:          containerID,
			Pid:         strconv.Itoa(parent.Process.Pid),
			Name:        containerName,
			PortMapping: ports,
		}
		if err := network.Connect(net, containerInfo); err != nil {
			logrus.Errorf("connect network, err: %v", err)
			return
		}
	}

	// 设置初始化命令
	sendInitCommand(cmdArray, writePipe)

	if tty {
		// 等待父进程结束
		err := parent.Wait()
		if err != nil {
			logrus.Errorf("parent wait, err: %v", err)
		}
		// 删除容器工作空间
		err = container.DeleteWorkSpace(containerName, volume)
		if err != nil {
			logrus.Errorf("delete work space, err: %v", err)
		}
		// 删除容器信息
		container.DeleteContainerInfo(containerName)
	}
}

func sendInitCommand(comArray []string, writePipe *os.File) {
	command := strings.Join(comArray, " ")
	logrus.Infof("command all is %s", command)
	_, _ = writePipe.WriteString(command)
	_ = writePipe.Close()
}

2.1 过程示意图

整个过程可以看作一个引导进程,由它去创建和启动init进程,init进程中运行着容器。

  • init进程需要优先启动,获取它的PID,从而对该进程进行网络配置和信息记录
  • WorkSpace实际上是容器的工作目录,在init进程中会将文件系统切换到该目录下
  • 资源限制作用于引导进程,而init进程是它的子进程,将继承父进程的资源限制,子进程也可以修改限制,但不能超过父进程的限制

mermaid-diagram-2023-08-12-213437

实践中,程序无法实现后台运行,容器所在进程会在主进程结束后被1号进程接管,随后马上就会被结束掉。

TODO:我目前还不知道如何创建后台进程,之后有机会再来完善这个功能。

2.2 资源释放

初始化进程创建的资源:

  • 子进程:init

  • 工作空间:读写层和挂载点

  • 容器信息记录:/var/run/docker/config.json

  • 虚拟网卡

前台运行时,可以通过ctrl+c来结束init进程的执行,之后程序将删除工作空间和容器信息。因为虚拟网卡在容器的命名空间中,所以容器init停止后,该资源将不复存在;资源限制也是创建在init进程中的文件系统,也会随进程结束而无效。

3. 创建父进程

父进程实际上是运行docker init命令:切换文件系统后执行/proc/self/exe init

通过创建管道,阻塞了进程的运行,但获取到了进程的PID以进行后续配置。启动但阻塞是因为资源还没有完全初始化,但后面的初始化需要用到进程的PID,在该进程的命名空间中进行配置,初始化结束后关闭writePipe,进程将继续运行。(pipe写入端未关闭,读取端将陷入阻塞)

parent, writePipe := container.NewParentProcess(tty, volume, containerName, imageName, envs)
if parent == nil {
    logrus.Errorf("failed to new parent process")
    return
}
if err := parent.Start(); err != nil {
    logrus.Errorf("parent start failed, err: %v", err)
    return
}
// 创建一个会隔离namespace进程的Command
func NewParentProcess(tty bool, volume, containerName, imageName string, envs []string) (*exec.Cmd, *os.File) {
	readPipe, writePipe, _ := os.Pipe()
	// 调用自身,传入 init 参数,也就是执行 initCommand
	cmd := exec.Command("/proc/self/exe", "init")
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |
			syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,
	}
	if tty {
		cmd.Stdin = os.Stdin
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
	} else {
		// 把日志输出到文件里
		logDir := path.Join(common.DefaultContainerInfoPath, containerName)
		if _, err := os.Stat(logDir); err != nil && os.IsNotExist(err) {
			err := os.MkdirAll(logDir, os.ModePerm)
			if err != nil {
				logrus.Errorf("mkdir container log, err: %v", err)
			}
		}
		logFileName := path.Join(logDir, common.ContainerLogFileName)
		file, err := os.Create(logFileName)
		if err != nil {
			logrus.Errorf("create log file, err: %v", err)
		}
		cmd.Stdout = file
	}
	// 设置额外文件句柄
	cmd.ExtraFiles = []*os.File{
		readPipe,
	}
	// 设置环境变量
	cmd.Env = append(os.Environ(), envs...)
	err := NewWorkSpace(volume, containerName, imageName)
	if err != nil {
		logrus.Errorf("new work space, err: %v", err)
	}
	// 指定容器初始化后的工作目录
	cmd.Dir = common.MntPath
	return cmd, writePipe
}

3.1 创建WorkSpace

  • 只读层和读写层属于主机的命名空间下,挂载点位于容器内
  • 只读层存放的是镜像文件
  • 读写层是对只读层的增量修改,并不会修改只读层的数据
  • 在挂载点中写入的数据实际上写入到了读写层
  • 挂载关系:imagePathwriteLayPath联合挂载到了mntPathmntPath被挂载到了hostPath
// 创建容器运行时目录
func NewWorkSpace(volume, containerName, imageName string) error {
	// 1. 创建只读层
	err := createReadOnlyLayer(imageName)
	if err != nil {
		logrus.Errorf("create read only layer, err: %v", err)
		return err
	}
	// 2. 创建读写层
	err = createWriteLayer(containerName)
	if err != nil {
		logrus.Errorf("create write layer, err: %v", err)
		return err
	}
	// 3. 创建挂载点,将只读层和读写层挂载到指定位置
	err = CreateMountPoint(containerName, imageName)
	if err != nil {
		logrus.Errorf("create mount point, err: %v", err)
		return err
	}
	// 4. 设置宿主机与容器文件映射
	mountVolume(containerName, imageName, volume)
	return nil
}
3.1.1 创建只读层
// 根据镜像创建只读层
func createReadOnlyLayer(imageName string) error {
	imagePath := path.Join(common.RootPath, imageName)
	_, err := os.Stat(imagePath)
	if err != nil && os.IsNotExist(err) {
		err := os.MkdirAll(imagePath, os.ModePerm)
		if err != nil {
			logrus.Errorf("mkdir image path, err: %v", err)
			return err
		}
	}
	// 解压 /root/imageName.tar
	imageTarPath := path.Join(common.RootPath, fmt.Sprintf("%s.tar", imageName))
	if _, err = exec.Command("tar", "-xvf", imageTarPath, "-C", imagePath).CombinedOutput(); err != nil {
		logrus.Errorf("tar image tar,path: %s, err: %v", imageTarPath, err)
		return err
	}
	return nil
}
3.1.2 创建读写层
// 创建读写层
func createWriteLayer(containerName string) error {
	writeLayerPath := path.Join(common.RootPath, common.WriteLayer, containerName)
	_, err := os.Stat(writeLayerPath)
	if err != nil && os.IsNotExist(err) {
		err = os.MkdirAll(writeLayerPath, os.ModePerm)
		if err != nil {
			logrus.Errorf("mkdir write layer, err: %v", err)
			return err
		}
	}
	return nil
}
3.1.3 创建挂载点
func CreateMountPoint(containerName, imageName string) error {
	mntPath := path.Join(common.MntPath, containerName)
	_, err := os.Stat(mntPath)
	if err != nil && os.IsNotExist(err) {
		err := os.MkdirAll(mntPath, os.ModePerm)
		if err != nil {
			logrus.Errorf("mkdir mnt path, err: %v", err)
			return err
		}
	}

	// 将宿主机上关于容器的读写层和只读层挂载到 /root/mnt/容器名 里
	writeLayPath := path.Join(common.RootPath, common.WriteLayer, containerName)
	imagePath := path.Join(common.RootPath, imageName)
	dirs := fmt.Sprintf("dirs=%s:%s", writeLayPath, imagePath)
	cmd := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", mntPath)
	if err := cmd.Run(); err != nil {
		logrus.Errorf("mnt cmd run, err: %v", err)
		return err
	}
	return nil
}
3.1.4 挂载卷

宿主机和容器的文件映射

func mountVolume(containerName, imageName, volume string) {
	if volume != "" {
		volumes := strings.Split(volume, ":")
		if len(volumes) > 1 {
			// 创建宿主机中文件路径
			parentPath := volumes[0]
			if _, err := os.Stat(parentPath); err != nil && os.IsNotExist(err) {
				if err := os.MkdirAll(parentPath, os.ModePerm); err != nil {
					logrus.Errorf("mkdir parent path: %s, err: %v", parentPath, err)
				}
			}

			// 创建容器内挂载点
			containerPath := volumes[1]
			containerVolumePath := path.Join(common.MntPath, containerName, containerPath)
			if _, err := os.Stat(containerVolumePath); err != nil && os.IsNotExist(err) {
				if err := os.MkdirAll(containerVolumePath, os.ModePerm); err != nil {
					logrus.Errorf("mkdir volume path path: %s, err: %v", containerVolumePath, err)
				}
			}

			// 把宿主机文件目录挂载到容器挂载点中
			dirs := fmt.Sprintf("dirs=%s", parentPath)
			cmd := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", containerVolumePath)
			cmd.Stdout = os.Stdout
			cmd.Stderr = os.Stderr
			if err := cmd.Run(); err != nil {
				logrus.Errorf("mount cmd run, err: %v", err)
			}
		}
	}
}

3.2 进程运行的时机

在刚刚启动init进程时,由于pipe的写入端没有关闭,程序会阻塞在ioutil.ReadAll(pipe)

// 本容器执行的第一个进程
// 使用mount挂载proc文件系统
// 以便后面通过`ps`等系统命令查看当前进程资源的情况
func RunContainerInitProcess() error {
	cmdArray := readUserCommand()
	if cmdArray == nil || len(cmdArray) == 0 {
		return fmt.Errorf("get user command in run container")
	}
	// 挂载
	err := setUpMount()
	if err != nil {
		logrus.Errorf("set up mount, err: %v", err)
		return err
	}

	// 在系统环境 PATH中寻找命令的绝对路径
	path, err := exec.LookPath(cmdArray[0])
	if err != nil {
		path = cmdArray[0]
	}

	err = syscall.Exec(path, cmdArray[0:], os.Environ())
	if err != nil {
		return err
	}
	return nil
}

func readUserCommand() []string {
	// 指 index 为 3的文件描述符,
	// 也就是 cmd.ExtraFiles 中 我们传递过来的 readPipe
	pipe := os.NewFile(uintptr(3), "pipe")
	bs, err := ioutil.ReadAll(pipe)
	if err != nil {
		logrus.Errorf("read pipe, err: %v", err)
		return nil
	}
	msg := string(bs)
	return strings.Split(msg, " ")
}

当主进程(引导进程)执行到sendInitCommand(cmdArray, writePipe)时,由于writePipe.Close()ioutil.ReadAll(pipe)将返回数据,init进程继续运行。

func Run(cmdArray []string, tty bool, res *subsystem.ResourceConfig, containerName, imageName, volume, net string, envs, ports []string) {
	...
	// 记录容器信息
	// 添加资源限制
	// 设置网络
    
	// 设置初始化命令
	sendInitCommand(cmdArray, writePipe)

	if tty {
		...
	}
}

func sendInitCommand(comArray []string, writePipe *os.File) {
	command := strings.Join(comArray, " ")
	logrus.Infof("command all is %s", command)
	_, _ = writePipe.WriteString(command)
	_ = writePipe.Close()
}

4. 记录容器信息

便于之后通过docker命令查看容器信息(运行情况)

// 记录容器信息
func RecordContainerInfo(containerPID int, cmdArray []string, containerName, containerID string) error {
	info := &ContainerInfo{
		Pid:        strconv.Itoa(containerPID),
		Id:         containerID,
		Command:    strings.Join(cmdArray, ""),
		Name:       containerName,
		CreateTime: time.Now().Format("2006-01-02 15:04:05"),
		Status:     common.Running,
	}

	dir := path.Join(common.DefaultContainerInfoPath, containerName)
	_, err := os.Stat(dir)
	if err != nil && os.IsNotExist(err) {
		err := os.MkdirAll(dir, os.ModePerm)
		if err != nil {
			logrus.Errorf("mkdir container dir: %s, err: %v", dir, err)
			return err
		}
	}

	fileName := fmt.Sprintf("%s/%s", dir, common.ContainerInfoFileName)
	file, err := os.Create(fileName)
	if err != nil {
		logrus.Errorf("create config.json, fileName: %s, err: %v", fileName, err)
		return err
	}

	bs, _ := json.Marshal(info)
	_, err = file.WriteString(string(bs))
	if err != nil {
		logrus.Errorf("write config.json, fileName: %s, err: %v", fileName, err)
		return err
	}

	return nil
}

/var/run/docker/$name/config.json中记录的容器信息如下:

containerInfo

5. 添加资源限制

5.1 Cgroup机制

借助Linux Cgroup来实现对一组进程及子进程的资源限制、控制和统计,资源包括CPU、内存、存储、网络等。

Cgroup 完成资源限制主要通过下面三个组件:

  • cgroup: 是对进程分组管理的一种机制
  • subsystem: 是一组资源控制的模块
  • hierarchy: 把一组 cgroup 串成一个树状结构 (可让其实现继承)

可以将/sys/fs/cgroup中的模块看作subsystem,在subsystem中创建的目录看作cgroup,创建的目录继承父目录的属性,看作hierarchy

/sys/fs/cgroup下的目录属于subsystem(资源控制模块),比如memory用来控制内存使用。

sh5

举个例子:/sys/fs/cgroup/memory,在该路径下创建的目录,将会继承其属性,system.sliceuser.slice即为创建的目录。将PID写入tasks文件中即可将对应进程添加到该cgroup

sh7

/sys/fs/cgroup/memory/user.slice

sh8

通过向memory.limit_in_bytes写入数据来限制内存使用

sh11

5.2 数据结构

每个资源(memory、cpu、cpuset)都实现了Subsystem接口

// 资源限制配置
type ResourceConfig struct {
	// 内存限制
	MemoryLimit string
	// CPU时间片权重
	CpuShare string
	// CPU核数
	CpuSet string
}

/**
将cgroup抽象成path, 因为在hierarchy中,cgroup便是虚拟的路径地址
*/
type Subystem interface {
	// 返回subsystem名字,如 cpu,memory
	Name() string
	// 设置cgroup在这个subSystem中的资源限制
	Set(cgroupPath string, res *ResourceConfig) error
	// 移除这个cgroup资源限制
	Remove(cgroupPath string) error
	// 将某个进程添加到cgroup中
	Apply(cgroupPath string, pid int) error
}

var (
	Subsystems = []Subystem{
		&MemorySubSystem{},
		// 设置tasks时,这两个必须同时设置
		&CpuSubSystem{},
		&CpuSetSubSystem{},
	}
)

5.3 设置资源限制

// 添加资源限制
cgroupMananger := cgroups.NewCGroupManager("go-docker")
// 删除资源限制
defer cgroupMananger.Destroy()
// 设置资源限制
cgroupMananger.Set(res)
// 将容器进程,加入到各个subsystem挂载对应的cgroup中
cgroupMananger.Apply(parent.Process.Pid)

通过CGroupManager对多种资源进行统一配置

type CGroupManager struct {
	Path string
}

func NewCGroupManager(path string) *CGroupManager {
	return &CGroupManager{Path: path}
}

func (c *CGroupManager) Set(res *subsystem.ResourceConfig) {
	for _, subsystem := range subsystem.Subsystems {
		err := subsystem.Set(c.Path, res)
		if err != nil {
			logrus.Errorf("set %s err: %v", subsystem.Name(), err)
		}
	}
}

func (c *CGroupManager) Apply(pid int) {
	for _, subsystem := range subsystem.Subsystems {
		err := subsystem.Apply(c.Path, pid)
		if err != nil {
			logrus.Errorf("apply task, err: %v", err)
		}
	}
}

func (c *CGroupManager) Destroy() {
	for _, subsystem := range subsystem.Subsystems {
		err := subsystem.Remove(c.Path)
		if err != nil {
			logrus.Errorf("remove %s err: %v", subsystem.Name(), err)
		}
	}
}

只举一个例子:对memory资源的限制

type MemorySubSystem struct {
	apply bool
}

func (*MemorySubSystem) Name() string {
	return "memory"
}

func (m *MemorySubSystem) Set(cgroupPath string, res *ResourceConfig) error {
	subsystemCgroupPath, err := GetCgroupPath(m.Name(), cgroupPath, true)
	if err != nil {
		logrus.Errorf("get %s path, err: %v", cgroupPath, err)
		return err
	}
	if res.MemoryLimit != "" {
		m.apply = true
		// 设置cgroup内存限制,
		// 将这个限制写入到cgroup对应目录的 memory.limit_in_bytes文件中即可
		err := ioutil.WriteFile(path.Join(subsystemCgroupPath, "memory.limit_in_bytes"), []byte(res.MemoryLimit), 0644)
		if err != nil {
			return err
		}
	}
	return nil
}

func (m *MemorySubSystem) Remove(cgroupPath string) error {
	subsystemCgroupPath, err := GetCgroupPath(m.Name(), cgroupPath, false)
	if err != nil {
		return err
	}
	return os.RemoveAll(subsystemCgroupPath)
}

func (m *MemorySubSystem) Apply(cgroupPath string, pid int) error {
	if m.apply {
		subsystemCgroupPath, err := GetCgroupPath(m.Name(), cgroupPath, false)
		if err != nil {
			return err
		}
		tasksPath := path.Join(subsystemCgroupPath, "tasks")
		err = ioutil.WriteFile(tasksPath, []byte(strconv.Itoa(pid)), os.ModePerm)
		if err != nil {
			logrus.Errorf("write pid to tasks, path: %s, pid: %d, err: %v", tasksPath, pid, err)
			return err
		}
	}
	return nil
}

6. 网络配置

6.1 数据结构

区分网络、网络端点、网络驱动接口:网络设备由名称、IP段和驱动名称唯一确定,驱动用来连接不同的网络设备。网络端点可以理解为容器内部网络与主机网络的连接点,配置了端口映射。eth0docker0都属于网络设备。

// 网络
type Network struct {
	Name    string
	IpRange *net.IPNet
	Driver  string
}

// 网络端点
type Endpoint struct {
	ID          string           `json:"id"`
	Device      netlink.Veth     `json:"dev"`
	IPAddress   net.IP           `json:"ip"`
	MacAddress  net.HardwareAddr `json:"mac"`
	Network     *Network
	PortMapping []string
}

// 网络驱动接口
type NetworkDriver interface {
	// 驱动名
	Name() string
	// 创建网络
	Create(subnet string, name string) (*Network, error)
	// 删除网络
	Delete(network Network) error
	// 连接容器网络端点到网络
	Connect(network *Network, endpoint *Endpoint) error
	// 从网络上移除容器网络端点
	Disconnect(network Network, endpoint *Endpoint) error
}

网络设备可以通过ip link show查看,如下图所示。

  • loeth0是默认存在的网络接口
  • cni-podman0host命名空间下的网络接口,用于桥接host和container,属于上面的Network
  • veth45a9497e@if2container命名空间下的网络接口,也称虚拟网卡(不直接和物理网卡相绑定),会随着容器进程的创建而创建,作用在容器进程中的命名空间。通过设置master cni-podman0关联到host命名空间

sh3

  • cni-podman0可用于多个容器,不会自动删除;veth和容器命名空间绑定,容器一旦停止,将不复存在(一个进程对应一个命名空间)
  • Driver用于创建Veth,建立容器和主机的联系
  • 之后通过ip link set $link netns $ns将这个Veth移动到容器的命名空间下

6.2 运行流程

// 设置网络
if net != "" {
    // 初始化容器网络
    err = network.Init()
    if err != nil {
        logrus.Errorf("network init failed, err: %v", err)
        return
    }
    containerInfo := &container.ContainerInfo{
        Id:          containerID,
        Pid:         strconv.Itoa(parent.Process.Pid),
        Name:        containerName,
        PortMapping: ports,
    }
    if err := network.Connect(net, containerInfo); err != nil {
        logrus.Errorf("connect network, err: %v", err)
        return
    }
}
6.2.1 初始化网络
  • 假设host存在一个名为bridge的网络接口,存入bridgeDriver

  • 遍历/var/run/docker/network/network/,载入其中的网络设备(创建的网络设备会存放在该目录下)

  • 梳理一下:此时拥有了driversnetworks,即网络设备和其对应的驱动(网络设备在软件层面上对物理网卡的描述;驱动是将不同网络设备关联起来的一段程序)

// 初始化网络驱动
func Init() error {
	var bridgeDriver = BridgeNetworkDriver{}
	drivers[bridgeDriver.Name()] = &bridgeDriver

	if _, err := os.Stat(common.DefaultNetworkPath); err != nil && os.IsNotExist(err) {
		if err = os.MkdirAll(common.DefaultNetworkPath, os.ModePerm); err != nil {
			return err
		}
	}
	// 递归遍历目录
	err := filepath.Walk(common.DefaultNetworkPath, func(nwPath string, info os.FileInfo, err error) error {
		if strings.HasSuffix(nwPath, "/") {
			return nil
		}
		_, nwName := path.Split(nwPath)
		nw := &Network{
			Name: nwName,
		}

		if err := nw.load(nwPath); err != nil {
			logrus.Errorf("error load network: %s", err)
		}

		networks[nwName] = nw
		return nil
	})

	if err != nil {
		logrus.Errorf("file path walk, err: %v", err)
		return err
	}
	logrus.Infof("networks: %v", networks)

	return nil
}
6.2.2 连接网络
// 连接网络
func Connect(networkName string, containerInfo *container.ContainerInfo) error {
	network, ok := networks[networkName]
	if !ok {
		return fmt.Errorf("no Such network: %s", networkName)
	}

	// 分配容器IP地址
	ip, err := ipAllocator.Allocate(network.IpRange)
	if err != nil {
		return err
	}

	// 创建网络端点
	ep := &Endpoint{
		ID:          fmt.Sprintf("%s-%s", containerInfo.Id, networkName),
		IPAddress:   ip,
		Network:     network,
		PortMapping: containerInfo.PortMapping,
	}
	// 调用网络驱动挂载和配置网络端点
	if err = drivers[network.Driver].Connect(network, ep); err != nil {
		return err
	}
	// 给容器的namespace配置容器网络设备IP地址
	if err = configEndpointIpAddressAndRoute(ep, containerInfo); err != nil {
		return err
	}

	// 配置端口映射
	err = configPortMapping(ep, containerInfo)
	if err != nil {
		logrus.Errorf("config port mapping, err: %v", err)
		return err
	}
	return nil
}
1 配置网络端点

创建并启动一个虚拟网络接口vethveth与主机中的bridge接口建立了关联,发送到veth的数据将被转发到bridge接口,由该接口通过物理网卡发送到互联网。veth之后会被移动到容器的命名空间下。

func (d *BridgeNetworkDriver) Connect(network *Network, endpoint *Endpoint) error {
	bridgeName := network.Name
	br, err := netlink.LinkByName(bridgeName)
	if err != nil {
		return err
	}

	la := netlink.NewLinkAttrs()
	la.Name = endpoint.ID[:5]
	la.MasterIndex = br.Attrs().Index

	endpoint.Device = netlink.Veth{
		LinkAttrs: la,
		PeerName:  "cif-" + endpoint.ID[:5],
	}

	if err = netlink.LinkAdd(&endpoint.Device); err != nil {
		logrus.Errorf("add endpoint device, err: %v", err)
		return err
	}

	if err = netlink.LinkSetUp(&endpoint.Device); err != nil {
		logrus.Errorf("add endpoint device: %v", err)
		return err
	}
	return nil
}
2 配置容器网络IP

进入到容器的网络namespace中,启动容器中的虚拟网卡veth,并配置默认路由(目的地+网关),代码中表示:所有路径的请求都统一转发到网关ep.Network.IpRange.IP

// 给容器的namespace配置容器网络设备IP地址
func configEndpointIpAddressAndRoute(ep *Endpoint, cinfo *container.ContainerInfo) error {
	peerLink, err := netlink.LinkByName(ep.Device.PeerName)
	if err != nil {
		logrus.Errorf("fail config endpoint: %v", err)
		return err
	}
	defer enterContainerNetns(&peerLink, cinfo)()

	interfaceIP := *ep.Network.IpRange
	interfaceIP.IP = ep.IPAddress

	if err = setInterfaceIP(ep.Device.PeerName, interfaceIP.String()); err != nil {
		return fmt.Errorf("%v,%s", ep.Network, err)
	}

	if err = setInterfaceUP(ep.Device.PeerName); err != nil {
		return err
	}

	if err = setInterfaceUP("lo"); err != nil {
		return err
	}

	_, cidr, _ := net.ParseCIDR("0.0.0.0/0")

	defaultRoute := &netlink.Route{
		LinkIndex: peerLink.Attrs().Index,
		Gw:        ep.Network.IpRange.IP,
		Dst:       cidr,
	}

	if err = netlink.RouteAdd(defaultRoute); err != nil {
		return err
	}

	return nil
}
3 进入容器NS
  • 主要是通过系统调用实现,在网络配置结束后,将回到之前的namespace
  • 进入的是容器的namespace,即init进程的namespace;之前的namespace是主进程
func enterContainerNetns(enLink *netlink.Link, cinfo *container.ContainerInfo) func() {
	f, err := os.OpenFile(fmt.Sprintf("/proc/%s/ns/net", cinfo.Pid), os.O_RDONLY, 0)
	if err != nil {
		logrus.Errorf("error get container net namespace, %v", err)
	}

	nsFD := f.Fd()
	runtime.LockOSThread()

	// 修改veth peer 另外一端移到容器的namespace中
	if err = netlink.LinkSetNsFd(*enLink, int(nsFD)); err != nil {
		logrus.Errorf("set link netns, err: %v", err)
	}

	// 获取当前的网络namespace
	origns, err := netns.Get()
	if err != nil {
		logrus.Errorf("get current netns, err: %v", err)
	}

	// 设置当前进程到新的网络namespace,并在函数执行完成之后再恢复到之前的namespace
	if err = netns.Set(netns.NsHandle(nsFD)); err != nil {
		logrus.Errorf("error set netns, %v", err)
	}
	return func() {
		netns.Set(origns)
		origns.Close()
		runtime.UnlockOSThread()
		f.Close()
	}
}
4 配置端口映射
  • 端口映射是为了从主机访问容器内部,以便通过主机暴露服务,由ip+port唯一确定,运行时只需输入ex_port:in_port是因为ip在程序运行时都是已知的
  • 容器将请求发送到主机,使用主机的网络下载数据,是通过配置网卡实现的,由虚拟网卡将报文转发到主机的物理网卡上
// 配置端口映射关系
func configPortMapping(ep *Endpoint, cinfo *container.ContainerInfo) error {
	for _, pm := range ep.PortMapping {
		portMapping := strings.Split(pm, ":")
		if len(portMapping) != 2 {
			logrus.Errorf("port mapping format error, %v", pm)
			continue
		}
		iptablesCmd := fmt.Sprintf("-t nat -A PREROUTING -p tcp -m tcp --dport %s -j DNAT --to-destination %s:%s",
			portMapping[0], ep.IPAddress.String(), portMapping[1])
		cmd := exec.Command("iptables", strings.Split(iptablesCmd, " ")...)
		//err := cmd.Run()
		output, err := cmd.Output()
		if err != nil {
			logrus.Errorf("iptables Output, %v", output)
			continue
		}
	}
	return nil
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值