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
方法便是通過該結構來執行具體的邏輯
cgroupData 的 path
方法可以獲取子系統的系統路徑
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 的包,這個包並沒有什麼黑魔法,只是對子系統
下的配置文件進行查詢和修改