問題
線上 k8s 集羣在進行容器創建時報如下錯誤:
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 相關問題,但是這個問題還是頭一次見,網上搜索了關鍵字,社區有類似報錯的 issue,如cgroups: cannot found cgroup mount destination: unknown[1],聯繫最近做過的線上變更及問題,懷疑跟某自定義組件有關,詳細背景參考這篇[2]。
排查過程
光看問題雲裏霧裏的,只知道和 cgroup 有關,登陸宿主查看此錯誤是 kubelet 請求 docker 時 docker 返回的,docker 18.06 版本,沒有更詳細的日誌了,但是開源的一個好處在於查問題的時候有源碼,這大大降低了查問題的難度,直接去 docker 項目中搜索關鍵詞,最終發現是在 containerd 的源碼中,相關代碼如下
// 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,對應上面 parseCgroupFromReader 函數,/proc/id/cgroup 內容如下
先按冒號分隔每行字符串,然後取第 2 列,再根據逗號分隔得到所有的子系統,最終返回所有子系統。然後調用 existingPath 檢查是否所有子系統都存在,內部又調用 getCgroupDestination,最終的報錯就是在這個函數裏報出來的。
getCgroupDestination 的邏輯是讀取/proc/id/mountinfo 信息,判斷是否傳入的子系統存在
先根據空格分隔,找到所有 cgroup 類型的目錄,然後再根據逗號分隔遍歷所有的子系統是否是傳入的子系統。找不到的話就會報錯,但是不得不吐槽的就是這個報錯報的太沒有誠意了,要是直接把找不到的子系統報出來,問題會直觀很多。
結論
到此可以明白是 agent 隔離程序先 mount 了自定義目錄 cpu_mirror 到 cgroup 目錄下,然後影響到了 java 程序去獲取正確的核數,爲了修復特意執行了 umount 的操作,但是 umount 之後/proc/id/cgroup 還是存在 cpu_mirror 相關信息而/proc/id/mountinfo 中已經不存在了,在容器重新創建的時候進行檢查進而報錯。
對比線上其他 docker 版本,比如 1.13.1 中就沒有此問題,因爲 1.13.1 用的 containerd 中並沒有上面提到的檢驗邏輯
通過這個問題也暴露出來我們在測試、灰度過程中的問題,由於線上環境複雜,系統版本衆多、組件版本也不統一,在上線一個功能或者執行線上操作的時候,測試用例需要充分覆蓋所有場景,灰度時也需要所有類型的機器至少都覆蓋到了之後纔可以放量繼續靠擴大灰度範圍,否則很容易出現類似的問題。
參考資料
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/
你可能還喜歡
點擊下方圖片即可閱讀
雲原生是一種信仰 🤘
關注公衆號
後臺回覆◉k8s◉獲取史上最方便快捷的 Kubernetes 高可用部署工具,只需一條命令,連 ssh 都不需要!
點擊 "閱讀原文" 獲取更好的閱讀體驗!
發現朋友圈變“安靜”了嗎?
本文分享自微信公衆號 - 雲原生實驗室(cloud_native_yang)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。