用Golang手写一个Container

news2024/11/25 18:41:08

本文作者系360奇舞团前端开发工程师

5008582b3364861a2cfeb9f5f2606473.jpeg

前言

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

前置知识储备:

  • Linux 基础知识

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

  • 容器技术基础

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

  • Go 语言基础

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

d30b075b17790f2ff7d6cf6176d8f423.jpeg[图片来源:Docker架构概览[4]]

什么是容器化

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

  • 隔离性

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

  • 轻量性

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

  • 可移植性

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

  • 可扩展性

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

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

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

  • PID命名空间:每个进程在 PID 命名空间中都有一个唯一的进程 ID。不同的 PID 命名空间中的进程 ID 可以重复,因此进程在其所属的命名空间中可以认为是唯一的。

  • 网络命名空间:每个网络命名空间都有自己的网络设备、IP 地址、路由表和防火墙规则。这使得在不同的网络命名空间中可以进行网络隔离和配置。

  • 文件系统命名空间:文件系统命名空间允许在不同的命名空间中使用不同的文件系统视图。这意味着一个进程可以在一个命名空间中看到的文件和目录,在另一个命名空间中可能是不可见的。

  • UTS 命名空间:UTS 命名空间用于隔离主机名和域名。每个 UTS 命名空间可以有自己独立的主机名,这在容器化环境中非常有用。

  • IPC 命名空间:IPC 命名空间用于隔离不同进程之间的进程间通信(IPC)机制,如信号量、消息队列和共享内存等。

  • 用户命名空间:用户命名空间允许在不同命名空间中重新映射用户和组 ID。这提供了更好的用户隔离和权限管理。通过使用这些命名空间,可以创建独立的容器环境,每个容器都有自己的资源副本,从而实现更好的隔离和资源管理。

UTS命名空间

Linux UTS Namespace[7]。在 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[8] 在 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所需步骤基本如下:

  1. 了解容器技术和相关概念:在开始编写mini容器之前,强烈建议先了解一些容器技术的基本原理,如命名空间(namespaces)、控制组(cgroups)、文件系统隔离等。

  2. 选择编程语言和库:之所以选择使用 Golang 进行容器的编写,因为它提供了强大的并发和系统编程能力。同时,还可以使用一些相关的库,如os/execsyscall

  3. 创建容器的基本结构:首先创建出一个基本的容器结构,该结构将包含容器的信息,如 ID、进程 ID、文件系统等。

  4. 设置容器的命名空间:使用 Golang 的syscall包,设置容器的命名空间,如 PID 命名空间、网络命名空间等。这样可以将容器中的进程与主机系统的进程隔离开来。

  5. 设置容器的文件系统:创建一个文件系统,可以是一个文件夹或镜像文件,用于存储容器内的文件和目录。这里我们可以借助于 Golang 的osio/ioutil包来操作文件系统。

  6. 启动容器中的进程:使用os/exec包,在容器的命名空间中启动一个新的进程, 并指定要运行的可执行文件和参数。

  7. 设置容器的网络:如果想让容器具有网络连接能力,我们还需要设置容器的网络命名空间,并进行相关网络配置。这可能涉及到创建虚拟网络设备、配置 IP 地址等。

  8. 处理容器的生命周期:需要考虑到容器的创建、启动、停止和销毁等生命周期事件。这可能涉及到信号处理、资源清理等操作。

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

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

参考资料

[1]

unsplash.com: https://unsplash.com/

[2]

Docker 官方文档: https://docs.docker.com/

[3]

Go 语言官方文档: https://golang.org/doc/

[4]

Docker 源码分析: https://zhuanlan.zhihu.com/p/302786713

[5]

Build Your Own Container Using Less than 100 Lines of Go: https://www.infoq.com/articles/build-a-container-golang/

[6]

Linux 命名空间概述 - 官方文档 : https://www.kernel.org/doc/html/latest/admin-guide/namespaces/index.html

[7]

UTS 命名空间 - Linuxize: https://lwn.net/Articles/531114/

[8]

Linux Cgroups: https://www.kernel.org/doc/Documentation/cgroup-v1/cgroups.txt

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

eef698c111b9b4b7efcd1748f8de2727.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1090514.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【git的使用方法】——上传文件到gitlab仓库

先进入到你克隆下来的仓库的目录里面 比如:我的仓库名字为zhuox 然后将需要上传推送的文件拷贝到你的克隆仓库下 这里的话我需要拷贝的项目是t3 输入命令ls,就可以查看该文件目录下的所有文件信息 然后输入git add 文件名 我这边输入的是 &#x…

LLMs的终局是通用人工智能AGI总结 生成式AI和大语言模型 Generative AI LLMs

终于学完了 生成式AI和大语言模型 Generative AI & LLMs. LLMs 解决了如下问题: 对NLP的不能够理解长句子,解决方案 自注意力机制Transformers architecture Attention is all you need大模型算力不够,解决方案 LLMs 缩放法则和计算最…

服务器使用u盘安装麒麟系统报错“dracut-initqueue timeout”,/dev/root does not exist

最近使用u盘安装麒麟系统,发现找不到u盘引导程序,提示dracut-initqueue timeout或者/dev/root does not exist 解决方法,先确定启动u盘所在盘符,使用 blkid 命令,我这边显示启动u盘所在盘符是 /dev/sdd4 blkid重启服…

基于Linux安装Hive

Hive安装包下载地址 Index of /dist/hive 上传解压 [rootmaster opt]# cd /usr/local/ [rootmaster local]# tar -zxvf /opt/apache-hive-3.1.2-bin.tar.gz重命名及更改权限 mv apache-hive-3.1.2-bin hivechown -R hadoop:hadoop hive配置环境变量 #编辑配置 vi /etc/pro…

LLMs AWS Sagemaker JumpStart

现在您已经探讨了使用LLM构建应用程序的基础知识,我想向您展示一项名为Amazon Sagemaker JumpStart的AWS服务,它可以帮助您快速进入生产并进行大规模操作。 以下是您在先前视频中探讨的应用程序堆栈。正如您所看到的,构建一个LLM驱动的应用程…

Macos数字音乐库:Elsten Software Bliss for Mac

Elsten Software Bliss for Mac是一款优秀的音乐管理软件,它可以帮助用户自动化整理和标记数字音乐库,同时可以自动识别音乐信息并添加标签和元数据。 此外,Bliss还可以修复音乐库中的问题,例如重复的音乐文件和缺失的专辑封面等…

深耕全面预算管理 拥抱企业数字未来

随着世界数字未来的不断发展,我国也正经历着一场更大范围、更深层次的科技变革。企业面对构建内部生态平衡体系的艰巨任务,对于其信息化部署也提出了更高的要求。增强预算编制的全面性,启动预算管理一体化改革成为了我国企业提高数字化水平的…

Rocket Typist pro for mac 「Macos文本快速输入工具」

Rocket Typist Pro是一款在Mac上使用的文本快速输入工具,它可以帮助用户更快速、更准确地输入文本。 这款软件的设计非常简单、高效,它通过使用短语或宏,可以快速插入文本,减少重复性工作,提高工作效率。 Rocket Typ…

华为校招机试题- 机器人活动区域-2023年

题目描述: 现有一个机器人,可放置于 M N的网格中任意位置,每个网格包含一个非负整数编号。当相邻网格的数字编号差值的绝对值小于等于 1 时,机器人可在网格间移动 问题:求机器人可活动的最大范围对应的网格点数目。 说明: 1)网格左上角坐标为 (0, 0),右下角坐标为 (m-…

Vue 的响应式数据 ref的使用

ref 是 vue 提供给我们用于创建响应式数据的方法。 ref 常用于创建基本数据&#xff0c;例如&#xff1a;string、number、boolean 等。 ref 还是通过 Object.defineProperty 的 get 与 set 方法&#xff0c;实现的响应式数据。 ref 创建基本数据&#xff1a; <template…

springboot 通过url下载文件并上传到OSS

DEMO流程 传入一个需要下载并上传的url地址下载文件上传文件并返回OSS的url地址 springboot pom文件依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w…

【【萌新的SOC学习之基于BRAM的PS和PL数据交互实验】】

萌新的SOC学习之基于BRAM的PS和PL数据交互实验 基于BRAM的PS和PL的数据交互实验 先介绍 AXI BRAM IP核控制器的简介 AXI BRAM ip核 是xilinx提供的一个软核 这个ip核被设计成 AXI的一个从机接口 用于AXI互联的集成 系统的主设备和本地的RAM进行通信 &#xff08;我们可以通过这…

大数据分析/开发项目实战班

大数据分析/开发项目实战班采用新型教学模式&#xff0c;让学生“学有所用&#xff0c;学能所用”&#xff0c;角色演练开展项目式教学&#xff0c;将产业项目与教学知识结合&#xff0c;突出学生的主体性&#xff0c;打破传统教学壁垒。 大数据分析/开发项目实战班介绍&#x…

ubuntu下yolov6 tensorrt模型部署

文章目录 ubuntu下yolov6 tensorrt模型部署一、Ubuntu18.04环境配置1.1 安装工具链和opencv1.2 安装Nvidia相关库1.2.1 安装Nvidia显卡驱动1.2.2 安装 cuda11.31.2.3 安装 cudnn8.21.2.4 下载 tensorrt8.4.2.41.2.5 下载仓库TensorRT-Alpha并设置 二、从yolov6源码中导出onnx文…

Linux高性能服务器编程 学习笔记 第十三章 多线程编程

早期Linux不支持线程&#xff0c;直到1996年&#xff0c;Xavier Leroy等人开发出第一个基本符合POSIX标准的线程库LinuxThreads&#xff0c;但LinuxThreads效率低且问题多&#xff0c;自内核2.6开始&#xff0c;Linux才开始提供内核级的线程支持&#xff0c;并有两个组织致力于…

【灵动 Mini-G0001开发板】+Keil5开发环境搭建+ST-Link/V2程序下载和仿真+4颗LED100ms闪烁。

我们拿到手里的是【灵动 Mini-G0001开发板】 如下图 我们去官网下载开发板对应资料MM32G0001官网 我们需要下载Mini—G0001开发板的库函数与例程&#xff08;第一手学习资料&#xff09;Keil支持包&#xff0c; PCB文件有需要的&#xff0c;可以自行下载。用户指南需要下载&a…

在Mission Planner上校准外置GPS罗盘

环境 windows 11 pixhawk 2.4.8 GPS M8N Mission Planner 1.3.80 前提 已经校准pixhawl自带的加速度计 根据提示&#xff0c;转动pixhawk&#xff0c;按空格键进行下一个步骤&#xff0c;成功后提示success 校准GPS罗盘 pixhawk飞控支持使用双罗盘&#xff08;也就是内置…

【LeetCode热题100】--394.字符串解码

394.字符串解码 思路&#xff1a; 定义两个栈&#xff0c;用于存放数字和字符如果是遇到’[&#xff0c;则数字和字母进栈如果遇到’]&#xff0c;则出栈&#xff0c;并拼接成一个字符串注意考虑多个数字在一起的情况 class Solution {public String decodeString(String s) …

MySQL的index merge(索引合并)导致数据库死锁分析与解决方案 | 京东云技术团队

背景 在DBS-集群列表-更多-连接查询-死锁中&#xff0c;看到9月22日有数据库死锁日志&#xff0c;后排查发现是因为mysql的优化-index merge&#xff08;索引合并&#xff09;导致数据库死锁。 定义 index merge(索引合并)&#xff1a;该数据库查询优化的一种技术&#xff0…

每日leetcode_775全局倒置与局部倒置

每日leetcode_755全局倒置与局部倒置 记录自己的成长&#xff0c;加油。 题目出处&#xff1a;775. 全局倒置与局部倒置 - 力扣&#xff08;LeetCode&#xff09; 题目 题目简要&#xff1a; 全局倒置&#xff1a;左边的大于右边的&#xff08;不需要紧挨着&#xff09; 局部…