Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go编写容器

使用Go语言编写一个极简版的容器Container

作者:奇舞精选

Docker作为一种流行的容器化技术,对于每一个程序开发者而言都具有重要性和必要性,因为容器化相关技术的普及大大简化了开发环境配置、更好的隔离性和更高的安全性,对于部署项目和团队协作而言也更加方便,本文将尝试使用Go语言编写一个极简版的容器

前置知识储备:

Docker 是基于 Linux 容器技术构建的,因此了解 Linux 操作系统的基本原理、命令和文件系统等知识对于理解本文乃至于Docker 源码非常重要。

了解容器技术的基本概念、原理和实现方式对于理解 Docker 源码非常有帮助。可以参考 Docker 官方文档[1]中的容器概述部分,以及相关的教程和文章。

Docker 的源码主要是用 Go 语言编写的,具体可以参考Go 语言官方文档[2]。

[图片来源:Docker架构概览[3]]

什么是容器化

容器化是作为一种虚拟化技术,允许应用程序和其依赖的资源(如库、环境变量等)被封装在一个独立的运行环境中,称为容器。其核心概念主要包括:

容器使用操作系统级别的虚拟化技术,如Linux的命名空间和控制组(cgroup),实现隔离。每个容器都有自己的进程空间、文件系统、网络和用户空间,使得容器之间相互隔离,不会相互干扰。

相比传统的虚拟机(VM),容器更加轻量级。容器共享主机操作系统的内核,因此启动更快、占用更少的资源。

容器可以在不同的环境中运行,包括开发、测试和生产环境。容器以相同的方式运行,不受底层基础设施的影响,提供了更好的可移植性。

容器可以根据需求进行扩展和缩减。容器编排工具(如Kubernetes)可以自动管理容器的部署、伸缩和负载均衡,提供弹性和可扩展性。

"如果创建一个容器就像系统调用 create_container 一样简单就好了"[4]

Guideline

这里我们粗略的估算一下可能涉及到的步骤会有:导入必要的包、main函数、子进程及其命名空间、挂载文件系统、运行子进程命令等。

我们知道真正的容器实现要复杂得多。它可能会涉及更多的命名空间设置、资源限制、文件系统挂载、网络配置等方面的工作。

但是本文,“删繁就简”,主要是为了了解容器的基本原理。

按照这种实现的思路,我们开始一步步用代码实现:

package?main
import?(
?"fmt"
?"os"
?"os/exec"
?"syscall"
)
func?main()?{
?//?根据命令行参数选择执行不同的操作
?switch?os.Args[1]?{
?case?"run":
??parent()?//?执行parent函数
?case?"child":
??child()?//?执行child函数
?default:
??panic("wat?should?I?do")?//?抛出异常,程序无法继续执行
?}
}
func?parent()?{
?cmd?:=?exec.Command("/proc/self/exe",?append([]string{"child"},?os.Args[2:]...)...)
?cmd.Stdin?=?os.Stdin
?cmd.Stdout?=?os.Stdout
?cmd.Stderr?=?os.Stderr
?//?运行命令并检查错误
?if?err?:=?cmd.Run();?err?!=?nil?{
??fmt.Println("ERROR",?err)
??os.Exit(1)
?}
}
func?child()?{
?cmd?:=?exec.Command(os.Args[2],?os.Args[3:]...)
?cmd.Stdin?=?os.Stdin
?cmd.Stdout?=?os.Stdout
?cmd.Stderr?=?os.Stderr
?//?运行命令并检查错误
?if?err?:=?cmd.Run();?err?!=?nil?{
??fmt.Println("ERROR",?err)
??os.Exit(1)
?}
}
func?must(err?error)?{
?//?如果错误不为空,抛出panic异常
?if?err?!=?nil?{
??panic(err)
?}
}

我们从 main.go 开始,读取第一个参数。如果是 "run",我们就运行Parent函数,如果是 "child",我们就运行子方法。父方法运行"/proc/self/exe",这是一个包含当前可执行文件内存映像的特殊文件。

换句话说,我们重新运行自己,但将 child 作为第一个参数传递。

我们可以借此执行另外一个执行用户请求的程序(在 os.Args[2:] 中提供)。有了这个简单的脚手架,我们就可以创建一个容器了。

命名空间

在 Linux 中,命名空间(Namespace)[5]是一种内核功能,用于隔离进程的资源视图。它允许在同一系统上运行的进程具有独立的资源副本,如进程 ID、网络接口、文件系统挂载点等。这种隔离性可以提供更好的安全性和资源管理。 以下是一些常见的 Linux 命名空间类型:

UTS命名空间

Linux UTS Namespace[6]。在 UTS 命名空间中,每个命名空间都有自己的主机名和域名。UTS 命名空间的使用场景包括:容器化和网络隔离等。

要在程序中添加命名空间,我们只需在 parent() 方法的第二行,添加下面的这几行代码,以便于在Go运行子进程时传递给其一些额外的标识。

cmd.SysProcAttr?=?&syscall.SysProcAttr{
?Cloneflags:?syscall.CLONE_NEWUTS?|?syscall.CLONE_NEWPID?|?syscall.CLONE_NEWNS、
}

如果现在运行程序,程序将在 UTS、PID 和 MNT 命名空间内运行。

在 Docker 中,根文件系统是由 Docker 镜像提供的,并且在容器启动时被挂载到容器的根目录上。Docker 根文件系统一般具有分层结构、只读性和写时复制等特性。

现在,虽然我们的进程处于一组孤立的命名空间中,但文件系统看起来与主机相同。为了解决这个问题,我们需要以下四行代码来实现根文件系统:

must(syscall.Mount("rootfs",?"rootfs",?"",?syscall.MS_BIND,?""))
?must(os.MkdirAll("rootfs/oldrootfs",?0700))
????//?将当前目录?`/`?移到?`rootfs/oldrootfs`?并将新的?rootfs?目录交换到?`/`
?must(syscall.PivotRoot("rootfs",?"rootfs/oldrootfs"))
?must(os.Chdir("/"))

所以完整代码如下:

package?main
import?(
?"fmt"
?"os"
?"os/exec"
?"syscall"
)
func?main()?{
?//?根据命令行参数选择执行不同的操作
?switch?os.Args[1]?{
?case?"run":
??parent()?//?执行parent函数
?case?"child":
??child()?//?执行child函数
?default:
??panic("wat?should?I?do")?//?抛出异常,程序无法继续执行
?}
}
func?parent()?{
?cmd?:=?exec.Command("/proc/self/exe",?append([]string{"child"},?os.Args[2:]...)...)
?//?设置子进程的命名空间
?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.Run();?err?!=?nil?{
??fmt.Println("ERROR",?err)
??os.Exit(1)
?}
}
func?child()?{
?//?挂载文件系统
?must(syscall.Mount("rootfs",?"rootfs",?"",?syscall.MS_BIND,?""))
?must(os.MkdirAll("rootfs/oldrootfs",?0700))
?must(syscall.PivotRoot("rootfs",?"rootfs/oldrootfs"))
?must(os.Chdir("/"))
?cmd?:=?exec.Command(os.Args[2],?os.Args[3:]...)
?cmd.Stdin?=?os.Stdin
?cmd.Stdout?=?os.Stdout
?cmd.Stderr?=?os.Stderr
?//?运行命令并检查错误
?if?err?:=?cmd.Run();?err?!=?nil?{
??fmt.Println("ERROR",?err)
??os.Exit(1)
?}
}
func?must(err?error)?{
?//?如果错误不为空,抛出panic异常
?if?err?!=?nil?{
??panic(err)
?}
}

是的,至此,基于golang实现的极简版的容器代码已经有了基本骨架。

Cgroups

Linux Cgroups[7] 在 Docker 容器化中起着重要的作用,它提供了对容器的资源限制和隔离,使得容器可以在共享的宿主机上运行而不会相互干扰:

通过 Cgroups,Docker 可以对容器的资源使用进行限制,如 CPU、内存、磁盘和网络等。这样可以避免容器过度占用宿主机资源,保证系统的稳定性和公平性。

Cgroups 提供了容器级别的资源隔离,每个容器都可以被分配和限制其使用的资源。这样,容器之间的资源使用不会互相干扰,一个容器的问题也不会影响其他容器或宿主机。

Docker 使用 Cgroups 对容器进行管理和监控。通过读取和设置 Cgroups 的属性,Docker 可以实时了解容器的资源使用情况,并可以调整资源限制以满足需求。

在cgroup(控制组)这部分,需要注意Cgroup 的挂载和层级结构等限制。

所以我们将Cgrous这一部分加入到代码实现中来如下:

package?main
import?(
????"fmt"
????"io/ioutil"
????"os"
????"os/exec"
????"strconv"
????"syscall"
)
func?main()?{
????//?创建?cgroup
????err?:=?createCgroup("mycontainer")
????if?err?!=?nil?{
????????fmt.Println("Failed?to?create?cgroup:",?err)
????????return
????}
????defer?func()?{
????????//?退出时删除?cgroup
????????err?:=?deleteCgroup("mycontainer")
????????if?err?!=?nil?{
????????????fmt.Println("Failed?to?delete?cgroup:",?err)
????????}
????}()
????//?限制?CPU?使用率为?50%
????err?=?setCPULimit("mycontainer",?50)
????if?err?!=?nil?{
????????fmt.Println("Failed?to?set?CPU?limit:",?err)
????????return
????}
????//?在容器中运行命令
????cmd?:=?exec.Command("/bin/bash")
????cmd.Stdin?=?os.Stdin
????cmd.Stdout?=?os.Stdout
????cmd.Stderr?=?os.Stderr
????cmd.SysProcAttr?=?&syscall.SysProcAttr{
????????Cloneflags:?syscall.CLONE_NEWNS?|?syscall.CLONE_NEWPID?|?syscall.CLONE_NEWUTS?|?syscall.CLONE_NEWIPC?|?syscall.CLONE_NEWNET,
????????Cgroup:?????"mycontainer",
????}
????err?=?cmd.Run()
????if?err?!=?nil?{
????????fmt.Println("Failed?to?run?command?in?container:",?err)
????}
}
func?createCgroup(name?string)?error?{
????cgroupPath?:=?"/sys/fs/cgroup/cpu/"?+?name
????err?:=?os.Mkdir(cgroupPath,?0755)
????if?err?!=?nil?{
????????return?err
????}
????//?将当前进程加入到?cgroup?中
????err?=?ioutil.WriteFile(cgroupPath+"/tasks",?[]byte(strconv.Itoa(os.Getpid())),?0644)
????if?err?!=?nil?{
????????return?err
????}
????return?nil
}
func?deleteCgroup(name?string)?error?{
????cgroupPath?:=?"/sys/fs/cgroup/cpu/"?+?name
????err?:=?os.Remove(cgroupPath)
????if?err?!=?nil?{
????????return?err
????}
????return?nil
}
func?setCPULimit(name?string,?limit?int)?error?{
????cgroupPath?:=?"/sys/fs/cgroup/cpu/"?+?name
????err?:=?ioutil.WriteFile(cgroupPath+"/cpu.cfs_quota_us",?[]byte(strconv.Itoa(limit*1000)),?0644)
????if?err?!=?nil?{
????????return?err
????}
????return?nil
}

在上面,我们将当前进程加入到新创建的"mycontainer" 的 cgroup,然后,设置该 cgroup 的 CPU 使用率限制为 50%。继而实现在容器中运行一个交互式的 shell。

结语

编写一个容器(container)是一个相当复杂的任务,涉及到许多底层的概念和技术。回顾本文,使用golang一步步“还原”一个mini版的container所需步骤基本如下:

除此之外,还需要考虑到安全性、权限管理、资源限制等多方面因素。

当然,实际的容器实现要更加复杂和完善。在实际项目应用中,我们可能还需要考虑到如文件系统隔离、网络隔离等远比这些复杂的场景。

以上就是使用Go语言编写一个极简版的容器Container的详细内容,更多关于Go编写容器的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文