七天用Go寫個docker(第三天)

項目源碼:點擊查看項目源碼

前面兩天我們瞭解完docker原理之後,今天我們動手把項目的結構給搭起來,先總體看一下項目結構

整個文件調用過程如下

我們最終達到的效果實現下面這個命令,該命令會啓動一個隔離的容器,並在該容器中運行第一個命令爲 top

go-docker run -ti top

main.go

程序的入口,主要是接收命令行參數,接收命令行參數處理使用的第三方工具包爲github.com/urfave/cli,日誌打印採用的github.com/sirupsen/logrus

package main

import (
	"github.com/sirupsen/logrus"
	"github.com/urfave/cli"
	"os"
)

const usage = `go-docker`

func main() {
	app := cli.NewApp()
	app.Name = "go-docker"
	app.Usage = usage

	app.Commands = []cli.Command{
		runCommand,
		initCommand,
	}
	app.Before = func(context *cli.Context) error {
		logrus.SetFormatter(&logrus.JSONFormatter{})
		logrus.SetOutput(os.Stdout)
		return nil
	}
	if err := app.Run(os.Args); err != nil {
		logrus.Fatal(err)
	}
}

這裏主要關注Commands數組,我們定義了兩個運行命令runCommandinitCommand,這兩個命令定義在Command.go文件中,看一下文件內容

command.go

package main

import (
	"fmt"

	"github.com/sirupsen/logrus"
	"github.com/urfave/cli"

	"go-docker/cgroups/subsystem"
	"go-docker/container"
)

// 創建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",
		},
	},
	Action: func(context *cli.Context) error {
		if len(context.Args()) < 1 {
			return fmt.Errorf("missing container args")
		}
		tty := context.Bool("ti")

		res := &subsystem.ResourceConfig{
			MemoryLimit: context.String("m"),
			CpuSet:      context.String("cpuset"),
			CpuShare:    context.String("cpushare"),
		}
		// cmdArray 爲容器運行後,執行的第一個命令信息
		// cmdArray[0] 爲命令內容, 後面的爲命令參數
		var cmdArray []string
		for _, arg := range context.Args() {
			cmdArray = append(cmdArray, arg)
		}
		Run(cmdArray, tty, res)
		return nil
	},
}

// 初始化容器內容,掛載proc文件系統,運行用戶執行程序
var initCommand = cli.Command{
	Name:  "init",
	Usage: "Init container process run user's process in container. Do not call it outside",
	Action: func(context *cli.Context) error {
		logrus.Infof("init come on")
		return container.RunContainerInitProcess()
	},
}

run命令主要就是啓動一個容器,然後對該進程設置隔離,init是run命令中調用的,不是我們自身通過命令行調用的,這裏我們主要關注Run(cmdArray, tty, res)函數即可,它接收我們傳遞過來的參數,tty表示是否前臺運行,對應docker的 -ti 命令,Run函數寫在了run.go文件中

run.go

package main

import (
	"os"
	"strings"

	"github.com/sirupsen/logrus"

	"go-docker/cgroups"
	"go-docker/cgroups/subsystem"
	"go-docker/container"
)

func Run(cmdArray []string, tty bool, res *subsystem.ResourceConfig) {
	parent, writePipe := container.NewParentProcess(tty)
	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
	}
	// 添加資源限制
	cgroupMananger := cgroups.NewCGroupManager("go-docker")
	// 刪除資源限制
	defer cgroupMananger.Destroy()
	// 設置資源限制
	cgroupMananger.Set(res)
	// 將容器進程,加入到各個subsystem掛載對應的cgroup中
	cgroupMananger.Apply(parent.Process.Pid)

	sendInitCommand(cmdArray, writePipe)
	parent.Wait()
}

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

基本上對docker初始化要做的事情都放在了這個文件中,主要是啓動一個容器,然後對該容器做一些資源限制,這裏需要關注的是 container.NewParentProcess(tty),它會給我們返回一個被namesapce隔離的進程。這個函數在 process.go文件裏

process.go

package container

import (
	"os"
	"os/exec"
	"syscall"
)

// 創建一個會隔離namespace進程的Command
func NewParentProcess(tty bool) (*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
	}
	cmd.ExtraFiles = []*os.File{
		readPipe,
	}
	return cmd, writePipe
}

這個函數會通過 /proc/self/exe init來調用自身我們定義的initCommand命令,然後給該進程設置隔離信息。看一下我們的initCommand幹了什麼事,這個命令的內容在init.go 文件裏。

init.go

package container

import (
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"strings"
	"syscall"

	"github.com/sirupsen/logrus"
)

// 本容器執行的第一個進程
// 使用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 {
		logrus.Errorf("look %s path, err: %v", cmdArray[0], err)
		return err
	}

	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, " ")
}

func setUpMount() error {
	// systemd 加入linux之後, mount namespace 就變成 shared by default, 所以你必須顯示
	//聲明你要這個新的mount namespace獨立。
	err := syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, "")
	if err != nil {
		return err
	}
	//mount proc
	defaultMountFlags := syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV
	err = syscall.Mount("proc", "/proc", "proc", uintptr(defaultMountFlags), "")
	if err != nil {
		logrus.Errorf("mount proc, err: %v", err)
		return err
	}

	return nil
}

看着很多,其實沒幹多少事,就是設置下掛載點,然後運行容器啓動後的第一個命令也就是 top 命令,其實真個容器的隔離已經完成了,那我們再拐回去看下資源限制做了那些東西,資源限制全部放在了cgroup文件夾中

subsystem.go

資源限制接口,Apply將進程ID添加到tasks中,即將此進程加入cgroup中,Set則對某個資源進行限制,Remove 則爲移除該cgroup,都比較簡單,就是創建文件,寫文件罷了,理解原理之後,寫起來很輕鬆。

package 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{},
		&CpuSubSystem{},
		&CpuSetSubSystem{},
	}
)

manager.go

資源限制管理器

package cgroups

import (
	"github.com/sirupsen/logrus"
	"go-docker/cgroups/subsystem"
)

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.go

內存限制實例

package subsystem

import (
	"io/ioutil"
	"os"
	"path"
	"strconv"

	"github.com/sirupsen/logrus"
)

type MemorySubSystem struct {
}

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 != "" {
		// 設置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, true)
	if err != nil {
		return err
	}
	return os.RemoveAll(subsystemCgroupPath)
}

func (m *MemorySubSystem) Apply(cgroupPath string, pid int) error {
	subsystemCgroupPath, err := GetCgroupPath(m.Name(), cgroupPath, true)
	if err != nil {
		return err
	}
	tasksPath := path.Join(subsystemCgroupPath, "tasks")
	err = ioutil.WriteFile(tasksPath, []byte(strconv.Itoa(pid)), 0644)
	if err != nil {
		logrus.Errorf("write pid to tasks, path: %s, pid: %d, err: %v", tasksPath, pid, err)
		return err
	}
	return nil
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章