專案原始碼:點選檢視專案原始碼
前面兩天我們瞭解完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
陣列,我們定義了兩個執行命令runCommand
,initCommand
,這兩個命令定義在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
}
文章會首發於我微信公眾號上,掃碼關注,及時獲取最新內容
本作品採用《CC 協議》,轉載必須註明作者和本文連結