cgroup 掛載失敗是什么鬼???

問題
線上 k8s 集群在進行容器創(chuàng)建時報如下錯誤:
Failed create pod sandbox: rpc error: code = Unknown desc = failed to start sandbox container for pod “xxx-sf-32c80-0”: Error response from daemon: cgroups: cannot find cgroup mount destination: unknown
之前遇到過 cgroup 相關(guān)問題,但是這個問題還是頭一次見,網(wǎng)上搜索了關(guān)鍵字,社區(qū)有類似報錯的 issue,如cgroups: cannot found cgroup mount destination: unknown[1],聯(lián)系最近做過的線上變更及問題,懷疑跟某自定義組件有關(guān),詳細背景參考這篇[2]。
排查過程
光看問題云里霧里的,只知道和 cgroup 有關(guān),登陸宿主查看此錯誤是 kubelet 請求 docker 時 docker 返回的,docker 18.06 版本,沒有更詳細的日志了,但是開源的一個好處在于查問題的時候有源碼,這大大降低了查問題的難度,直接去 docker 項目中搜索關(guān)鍵詞,最終發(fā)現(xiàn)是在 containerd 的源碼中,相關(guān)代碼如下
// PidPath will return the correct cgroup paths for an existing process running inside a cgroup
// This is commonly used for the Load function to restore an existing container
func PidPath(pid int) Path {
p := fmt.Sprintf("/proc/%d/cgroup", pid)
paths, err := parseCgroupFile(p)
if err != nil {
return errorPath(errors.Wrapf(err, "parse cgroup file %s", p))
}
return existingPath(paths, "")
}
func existingPath(paths map[string]string, suffix string) Path {
// localize the paths based on the root mount dest for nested cgroups
for n, p := range paths {
dest, err := getCgroupDestination(string(n))
if err != nil {
return errorPath(err)
}
rel, err := filepath.Rel(dest, p)
if err != nil {
return errorPath(err)
}
if rel == "." {
rel = dest
}
paths[n] = filepath.Join("/", rel)
}
return func(name Name) (string, error) {
root, ok := paths[string(name)]
if !ok {
if root, ok = paths[fmt.Sprintf("name=%s", name)]; !ok {
return "", fmt.Errorf("unable to find %q in controller set", name)
}
}
if suffix != "" {
return filepath.Join(root, suffix), nil
}
return root, nil
}
}
func getCgroupDestination(subsystem string) (string, error) {
f, err := os.Open("/proc/self/mountinfo")
if err != nil {
return "", err
}
defer f.Close()
s := bufio.NewScanner(f)
for s.Scan() {
fields := strings.Split(s.Text(), " ")
if len(fields) < 10 {
// broken mountinfo?
continue
}
if fields[len(fields)-3] != "cgroup" {
continue
}
for _, opt := range strings.Split(fields[len(fields)-1], ",") {
if opt == subsystem {
return fields[3], nil
}
}
}
if err := s.Err(); err != nil {
return "", err
}
return "", ErrNoCgroupMountDestination
}
func parseCgroupFile(path string) (map[string]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return parseCgroupFromReader(f)
}
func parseCgroupFromReader(r io.Reader) (map[string]string, error) {
var (
cgroups = make(map[string]string)
s = bufio.NewScanner(r)
)
for s.Scan() {
if err := s.Err(); err != nil {
return nil, err
}
var (
text = s.Text()
parts = strings.SplitN(text, ":", 3)
)
if len(parts) < 3 {
return nil, fmt.Errorf("invalid cgroup entry: %q", text)
}
for _, subs := range strings.Split(parts[1], ",") {
if subs != "" {
cgroups[subs] = parts[2]
}
}
}
return cgroups, nil
}
邏輯比較清晰,先從/proc/id/cgroup 中解析得到所有的 subsystem,對應(yīng)上面 parseCgroupFromReader 函數(shù),/proc/id/cgroup 內(nèi)容如下

先按冒號分隔每行字符串,然后取第 2 列,再根據(jù)逗號分隔得到所有的子系統(tǒng),最終返回所有子系統(tǒng)。然后調(diào)用 existingPath 檢查是否所有子系統(tǒng)都存在,內(nèi)部又調(diào)用 getCgroupDestination,最終的報錯就是在這個函數(shù)里報出來的。
getCgroupDestination 的邏輯是讀取/proc/id/mountinfo 信息,判斷是否傳入的子系統(tǒng)存在

先根據(jù)空格分隔,找到所有 cgroup 類型的目錄,然后再根據(jù)逗號分隔遍歷所有的子系統(tǒng)是否是傳入的子系統(tǒng)。找不到的話就會報錯,但是不得不吐槽的就是這個報錯報的太沒有誠意了,要是直接把找不到的子系統(tǒng)報出來,問題會直觀很多。
結(jié)論
到此可以明白是 agent 隔離程序先 mount 了自定義目錄 cpu_mirror 到 cgroup 目錄下,然后影響到了 java 程序去獲取正確的核數(shù),為了修復(fù)特意執(zhí)行了 umount 的操作,但是 umount 之后/proc/id/cgroup 還是存在 cpu_mirror 相關(guān)信息而/proc/id/mountinfo 中已經(jīng)不存在了,在容器重新創(chuàng)建的時候進行檢查進而報錯。
對比線上其他 docker 版本,比如 1.13.1 中就沒有此問題,因為 1.13.1 用的 containerd 中并沒有上面提到的檢驗邏輯
通過這個問題也暴露出來我們在測試、灰度過程中的問題,由于線上環(huán)境復(fù)雜,系統(tǒng)版本眾多、組件版本也不統(tǒng)一,在上線一個功能或者執(zhí)行線上操作的時候,測試用例需要充分覆蓋所有場景,灰度時也需要所有類型的機器至少都覆蓋到了之后才可以放量繼續(xù)靠擴大灰度范圍,否則很容易出現(xiàn)類似的問題。
參考資料
cgroups: cannot found cgroup mount destination: unknown: https://github.com/docker/for-linux/issues/219
[2]這篇: https://www.likakuli.com/posts/docker-java-cpu
原文鏈接:https://www.likakuli.com/posts/docker-cgroup-unknown/


你可能還喜歡
點擊下方圖片即可閱讀

云原生是一種信仰 ??
關(guān)注公眾號
后臺回復(fù)?k8s?獲取史上最方便快捷的 Kubernetes 高可用部署工具,只需一條命令,連 ssh 都不需要!


點擊 "閱讀原文" 獲取更好的閱讀體驗!
發(fā)現(xiàn)朋友圈變“安靜”了嗎?


