Runc 與 Cgroups

Runc 可以算是啓動創建容器的最後一步,其中設置 Cgroups,隔離 namespaces,配置網絡,掛載相應的卷 等一系列操作
在這裏插入圖片描述本文將主要講 runc 是如何去操作系統中的 Cgroups,實現對資源的限制和管理的

Runc 支持三種方式來限制管理資源,分別是使用 Cgroups V1, Cgroups V2, Systemd
本文將主要講解 Cgroups V1, 關於 Cgroups V1 相關的基本概念可以參考
Linux Cgroups V1 介紹與使用

Cgroup Manager

Cgroup Manager 是 runc 實現對系統的 cgroup 操作抽象
實現了設置資源的配置,PID 加入到指定控制組控制組的銷燬,控制組進程的暫停恢復,獲取配置,獲取統計信息等操作

type Manager interface{
	Apply(pid int) error			// 將 pid 加入到控制組中
	Set(container *config.Config) error	// 設置控制組的配置
	GetCgroups()(*configs.Cgroup, error)	// 獲取控制組的配置
	
	GetPids()([]int, error)			// 返回控制組(cgroup) 中的 PID, 不包括子控制組
	GetAllPids()([]int, error)		// 返回控制組和它的子控制組的所有 PID
	GetStats() (*Stats, error)		// 獲取控制組統計信息
	
	Destroy() error					// 刪除控制組
	
	GetPaths()map[string]string		// 獲取保存 cgroup 狀態文件的路徑
	GetUnifiedPath()(string, error)	// 如果容器組沒有掛載任何控制器(子系統), 則返回值同 GetPaths,否則,返回 error

	Freeze(state configs.FreezerState) error	// 任務的暫停和恢復
}

GetStats 方法會返回控制組中的統計信息,記錄了 CPU, Memroy, Blkio 之類的一些狀態

type Stats struct {
    CpuStats    CpuStats    `json:"cpu_stats,omitempty"`
    MemoryStats MemoryStats `json:"memory_stats,omitempty"`
    PidsStats   PidsStats   `json:"pids_stats,omitempty"`
    BlkioStats  BlkioStats  `json:"blkio_stats,omitempty"`
    // the map is in the format "size of hugepage: stats of the hugepage"
    HugetlbStats map[string]HugetlbStats `json:"hugetlb_stats,omitempty"`
}

Set 方法在設置控制組 資源的時候需要傳遞 config.Config實際上該方法只會使用 Config.Cgroup.Resources 中的數據

// Config 定義容器的配置
type Config struct {
	// ....
	Cgroups *Cgroup `json: "cgroup"`
	// ....
}

type Cgroup struct {
        Path string `json:"path"`	// cgroup 路徑,相對於`層級`的地址
        ScopePrefix string `json:"scope_prefix"`	// ScopePrefix describes prefix for the scope name
        Paths map[string]string		// 所屬各個控制器的 cgroup 路徑,需要注意該路徑是絕對路徑
        
        // 包含了各個子系統資源的設置
        *Resources
}
type Resources struct {
    AllowAllDevices *bool `json:"allow_all_devices,omitempty"`
    AllowedDevices []*Device `json:"allowed_devices,omitempty"`
    DeniedDevices []*Device `json:"denied_devices,omitempty"`
    Devices []*Device `json:"devices"`
    Memory int64 `json:"memory"`
    MemoryReservation int64 `json:"memory_reservation"`
    MemorySwap int64 `json:"memory_swap"`
    KernelMemory int64 `json:"kernel_memory"`
    KernelMemoryTCP int64 `json:"kernel_memory_tcp"`
    CpuShares uint64 `json:"cpu_shares"`
    CpuQuota int64 `json:"cpu_quota"`
    CpuPeriod uint64 `json:"cpu_period"`
    CpuRtRuntime int64 `json:"cpu_rt_quota"`
    CpuRtPeriod uint64 `json:"cpu_rt_period"`
    CpusetCpus string `json:"cpuset_cpus"`
    CpusetMems string `json:"cpuset_mems"`
    PidsLimit int64 `json:"pids_limit"`
    BlkioWeight uint16 `json:"blkio_weight"`
    BlkioLeafWeight uint16 `json:"blkio_leaf_weight"`
    BlkioWeightDevice []*WeightDevice `json:"blkio_weight_device"`
    BlkioThrottleReadBpsDevice []*ThrottleDevice `json:"blkio_throttle_read_bps_device"`
    BlkioThrottleWriteBpsDevice []*ThrottleDevice `json:"blkio_throttle_write_bps_device"`
    BlkioThrottleReadIOPSDevice []*ThrottleDevice `json:"blkio_throttle_read_iops_device"`
    BlkioThrottleWriteIOPSDevice []*ThrottleDevice `json:"blkio_throttle_write_iops_device"`
    Freezer FreezerState `json:"freezer"`
    HugetlbLimit []*HugepageLimit `json:"hugetlb_limit"`
    OomKillDisable bool `json:"oom_kill_disable"`
    MemorySwappiness *uint64 `json:"memory_swappiness"`
    NetPrioIfpriomap []*IfPrioMap `json:"net_prio_ifpriomap"`
    NetClsClassid uint32 `json:"net_cls_classid_u"`
    CpuWeight uint64 `json:"cpu_weight"`
    CpuMax string `json:"cpu_max"`
}

config.Cgroup 需要注意一下,其中 Path 字段是相對於層級掛載路徑的控制器路徑,層級的概念在Cgroups V1 中已經解釋

比如,我的控制組系統中路徑是 /sys/fs/cgroup/cpu/iceber/cgroup1
其中/sys/fs/cgroup/cpu 是 Cgroups 的一個層級,他綁定了 cpu 這個子系統/controller
config.Cgroup.Path 就應該是 /iceber/cgroup1

confi.Cgroup.Paths 這個是直接提供每個子系統控制組的系統路徑,他會屏蔽掉 Path 字段的作用

Cgroups V1 Manager

// libcontainer/cgroups/fs/apply_raw.go

type Manager struct {
        mu       sync.Mutex
        Cgroups  *configs.Cgroup
        Rootless bool // ignore permission-related errors
        Paths    map[string]string // 記錄各個子系統下控制組的路徑
}

我們使用 Manager 直接初始化相應字段就可以了

manger := &Manager{
	Cgroups: configCgroup,
}

manager.Cgroups 實際會使用到的字段是 configs.Cgroup.Path,configs.Cgroups.Paths,通過這兩個字段來找到子系統控制組路徑

manager.Cgroups.Resources 字段可能也會使用到
比如 Freeze 方法就會利用到 manager.Cgroups.Resources.Freezer 字段

apply 方法 將 pid 加入到控制組

apply 的作用是根據 manager.Cgroups.Path/Paths 將 pid 參數加入子系統控制組

getCgroupData 函數會返回 cgroupData,而 subsystem 接口的 Apply 方法便是通過該結構來執行具體的邏輯
cgroupDatapath 方法可以獲取子系統的系統路徑

func (m *Manager) Apply(pid int) (err error) {
        if m.Cgroups == nil {
                return nil
        }
        m.mu.Lock()
        defer m.mu.Unlock()

        var c = m.Cgroups
        d, err := getCgroupData(m.Cgroups, pid)
        if err != nil {
                return err
        }
		// m.Paths 記錄了各個 subsystem 下控制組的路徑
        m.Paths = make(map[string]string)

		// 判斷 cgroups 配置中是否指定子系統下控制組的路徑
        if c.Paths != nil {
                for name, path := range c.Paths {
                		// 檢查子系統是否掛載正常
                        _, err := d.path(name)
                        if err != nil {
                                if cgroups.IsNotFound(err) {
                                        continue
                                }
                                return err
                        }
                        m.Paths[name] = path
                }
                // 將 pid 加入到指定控制組中
                return cgroups.EnterPid(m.Paths, pid)
        }
		// 根據cgroups.Path 來獲取相應子系統下控制組路徑
        for _, sys := range m.getSubsystems() {
        		// 獲取子系統下控制組路徑
                p, err := d.path(sys.Name())
                if err != nil {
                        // 由於安全原因,devices 必須存在
                        if cgroups.IsNotFound(err) && sys.Name() != "devices" {
                                continue
                        }
                        return err
                }
                m.Paths[sys.Name()] = p
				// 執行具體子系統的 Apply 邏輯
                if err := sys.Apply(d); err != nil {
                        // handle error
                }
        }
        return nil
}

我們現在來看一下 getCgroupData 具體做了什麼
注意:cgroupData 其實只針對 manager.Cgroups.Path 有效,因爲manager.Cgroups.Paths 已經指定了完整系統路徑

/*
eg: cgroupData 中的數據是這樣的
{
	root: "sys/fs/cgroup"
	innerPath: "/parentCgroup/cgroup1"
	pid: 10000
	config: *config.Cgroup
}
*/
type cgroupData struct {
        root      string	// Cgroups 的掛載目錄
        innerPath string	// 子系統下控制組的相對目錄
        config    *configs.Cgroup	// cgroups 的配置,包含了各個子系統的資源配置,會在子系統設置相關資源時使用
        pid       int		// 加入到控制組的 PID
}

func getCgroupData(c *configs.Cgroup, pid int) (*cgroupData, error) {
		// getCgroupRoot 會通過從 /proc/self/mountinfo 查詢 Cgroups 掛載點的目錄
        root, err := getCgroupRoot()
        if err != nil {
                return nil, err
        }
		// 要求配置中提供配置組的路徑,Path 是相對於 root
        if (c.Name != "" || c.Parent != "") && c.Path != "" {
                return nil, fmt.Errorf("cgroup: either Path or Name and Parent should be used")
        }
        
        // 對路徑進行安全處理
        cgPath := libcontainerUtils.CleanPath(c.Path)
        cgParent := libcontainerUtils.CleanPath(c.Parent)
        cgName := libcontainerUtils.CleanPath(c.Name)

		// 默認使用 Path, 而 Parent 和 Name 其實已經廢除
        innerPath := cgPath
        if innerPath == "" {
                innerPath = filepath.Join(cgParent, cgName)
        }
        return &cgroupData{
                root:      root,
                innerPath: innerPath,
                config:    c,
                pid:       pid,
        }, nil
}

Apply 方法最終會調用各個子系統Apply 方法,那我們現在來看一下子系統的接口定義,以及以 cpu 子系統 爲例的 Apply 邏輯

subsystem(子系統)/controller(控制器)

Cgroups 中使用 子系統/控制器 來管理和限制具體的資源

runc 提供了 subsystem 接口,定義了控制器需要提供給 Manager 使用的方法

type subsystem interface {     
        Name() string	// 返回子系統的名字  
        // 創建 cgroupData 指定的控制組,並將 cgroupData.pid 加入到該控制組
        Apply(*cgroupData) error	
        // 設置控制組路徑的資源配置
        Set(path string, cgroup *configs.Cgroup) error	
        
        GetStats(path string, stats *cgroups.Stats) error	 // 獲取指定控制組路徑下的資源統計信息
        Remove(*cgroupData) error	// 移除 cgroupData 中指定的控制組  
}

// 聲明 Manger 會使用的所有子系統
 var subsystemsLegacy = subsystemSet{
                &CpusetGroup{},
                &DevicesGroup{},
                &MemoryGroup{},
                &CpuGroup{},
                &CpuacctGroup{},
                &PidsGroup{},
                &BlkioGroup{},
                &HugetlbGroup{},
                &NetClsGroup{},
                &NetPrioGroup{},
                &PerfEventGroup{},
                &FreezerGroup{},
                &NameGroup{GroupName: "name=systemd", Join: true},
}

我們現在來看一看 cpu 子系統的 Apply 方法做了哪些事情

type CpuGroup struct {}
func (s *CpuGroup) Apply(d *cgroupData) error {
        // 獲取 cpu 子系統的控制組地址
        path, err := d.path("cpu")
        if err != nil && !cgroups.IsNotFound(err) {
                return err
        }
        return s.ApplyDir(path, d.config, d.pid)
}

func (s *CpuGroup) ApplyDir(path string, cgroup *configs.Cgroup, pid int) error {
        if path == "" {
                return nil
        }
        // 確保路徑存在
        if err := os.MkdirAll(path, 0755); err != nil {
                return err
        }
        // 在添加進程加入控制組之前,設置實時資源配置
        // 因爲如果進程已經進入 SCHED_RR 模式並且沒有設置 RT 帶寬,再次添加會失敗
        if err := s.SetRtSched(path, cgroup); err != nil {
                return err
        }
        // 將 pid 加入到控制組中
        return cgroups.WriteCgroupProc(path, pid)
}

可以看到,實際 Apply 做了兩件事,第一件是創建控制組的目錄,第二件事就是把 PID 加入到控制組

我們順勢看一下資源配置是如何設置的,其實就是簡單地對控制組下相應配置文件進行修改,並沒有什麼黑魔法

func (s *CpuGroup) SetRtSched(path string, cgroup *configs.Cgroup) error {
        if cgroup.Resources.CpuRtPeriod != 0 {
                if err := fscommon.WriteFile(path, "cpu.rt_period_us", strconv.FormatUint(cgroup.Resources.CpuRtPeriod, 10)); err != nil {
                        return err
                }
        }
        if cgroup.Resources.CpuRtRuntime != 0 {
                if err := fscommon.WriteFile(path, "cpu.rt_runtime_us", strconv.FormatInt(cgroup.Resources.CpuRtRuntime, 10)); err != nil {
                        return err
                }
        }
        return nil
}

func (s *CpuGroup) Set(path string, cgroup *configs.Cgroup) error {
        if cgroup.Resources.CpuShares != 0 {
                if err := fscommon.WriteFile(path, "cpu.shares", strconv.FormatUint(cgroup.Resources.CpuShares, 10)); err != nil {
                        return err
                }
        }
        if cgroup.Resources.CpuPeriod != 0 {
                if err := fscommon.WriteFile(path, "cpu.cfs_period_us", strconv.FormatUint(cgroup.Resources.CpuPeriod, 10)); err != nil {
                        return err
                }
        }
        if cgroup.Resources.CpuQuota != 0 {
                if err := fscommon.WriteFile(path, "cpu.cfs_quota_us", strconv.FormatInt(cgroup.Resources.CpuQuota, 10)); err != nil {
                        return err
                }
        }
        return s.SetRtSched(path, cgroup)
}

Manager 再分析

Freeze 暫停/恢復 控制組 中任務

實際是操作 freezer 子系統,修改控制組中 freezer.state 文件

func (m *Manager) Freeze(state configs.FreezerState) error {
        if m.Cgroups == nil {
                return errors.New("cannot toggle freezer: cgroups not configured for container")
        }
		// 獲取 Freeze subsystem 的控制組地址
        paths := m.GetPaths()
        dir := paths["freezer"]
        prevState := m.Cgroups.Resources.Freezer
        m.Cgroups.Resources.Freezer = state
        // 獲取 subsystem,修改 freezer.state 文件
        freezer, err := m.getSubsystems().Get("freezer")
        if err != nil {
                return err
        }
        err = freezer.Set(dir, m.Cgroups)
        if err != nil {
                m.Cgroups.Resources.Freezer = prevState
                return err
        }
        return nil
}

總結

實際上 runc 相當於使用了一個方便操作系統 Cgroups 的包,這個包並沒有什麼黑魔法,只是對子系統下的配置文件進行查詢和修改

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章