首頁 > 科技

幾行程式碼擼了一天,源碼到底該如何讀?

2021-06-09 22:54:12

作者 | Kaku 責編 | 歐陽姝黎

近期有個小需求,在不重建Container的前提下修改Pod結構中的Request值,限制僅可以調小。本以為很簡單的一個需求,但實際花費了一天的時間才搞完,程式碼改動只有幾行,但是在改完測試的過程中發現很多超出預期或者認知的現象,為了搞懂為什麼會這樣,又重新捋了捋kubelet源碼。 在這件事結束之後也進行了反思,主要是有關源碼閱讀的,於是把這個過程和自己的感觸以及後續的一些改進方法和計劃記錄下來。

過程

先看下這件事的過程,我們先忽略這個需求的合理性,直接分析技術實現。

首先,kubernetes本身並不支援修改Pod的資源屬性,無論Request還是Limit,可以通過修改apiserver中的校驗邏輯來放開此限制;

其次,如何保證在Request改變之後容器不重啟?我們知道,kubelet會為每個container都計算出一個hash值,其中用到了container的所有屬性,在呼叫docker api進行容器創建的時候會把這個值設定到容器的Label中,後續如果kubelet檢測到新計算出的hash值與在運行的容器的hash值不同,則會進行容器的原地重啟操作,這也是為什麼修改container的Image會出發容器原地重啟的原因。很明顯,如果放開Request的修改,Request值變了之後也會導致新的hash值變化從而導致容器重建,與我們的期望不符。也有辦法來解決:記錄container創建時的Request值,計算的時候還是使用創建時的值,此值只有在container創建時會記錄,後續不再更新。

測試場景是創建了Qos類型為Guaranteed 類型的Pod,放開了kube-apiserver對Request修改的限制,kubelet保持原生不動,調小其Request值,大家可以先嚐試自己思考一下會發生什麼?

那麼在測試的時候遇到了什麼問題呢?

首先,發現放開Request修改之後,如果改了Request的值,容器重啟了(這一步符合預期),但是重啟次數加2(這裡其實是之前的一個盲點)

接著,繼續修改Request值,容器依然重啟(符合預期),但是此次重啟次數只加了1

最後,通過檢視Pod Cgroup目錄,確認修改後的Request已經在Pod級別和Container級別分別生效,但是同時存在兩個Pod的目錄,類似如下

# 修改之前的Pod對應的cgroup目錄
/sys/fs/cgroup/cpu/kubepods/guaranteed/pod{uid}
# 修改之後的Pod對應的cgroup目錄
/sys/fs/cgroup/cpu/kubepods/burstable/pod{uid}

為什麼同一個Pod會存在兩個cgroup目錄呢?

容器重啟了,重啟次數應該只加1,那為什麼在第一步中加了2?

你可以在繼續閱讀之前先自己思考一下可能的原因。

問題分析

首先看為什麼會有兩個cgroup目錄,需要先搞清楚cgroup目錄是如何創建、如何刪除的。

Cgorup創建

我們採用CgroupPerQos的方式進行管理,以cpu子系統為例,層級類似如下所示

  • /sys/fs/cgroup/cpu

    • guaranteed

    • burstable

    • bestaffort

    • {containerid}

    • {containerid}

    • {containerid}

    • {containerid}

    • pod{uid}

    • pod{uid}

    • kubepods

從創建者的角度分兩種:kubelet創建的、docker創建的。其中container層由docker創建,container以上的pod層、qos層和root(kubepods)都是由kubelet創建的。那docker又是怎麼知道容器的cgroup parent目錄是誰呢?其實是kubelet在呼叫docker api時傳給docker的一個參數,告訴了其cgroup parent路徑,可以通過執行docker inspect {containerid} | grep -i cgroup來檢視每個container的cgroup parent路徑。

那為什麼會在兩個qos目錄下分別存在一個Pod目錄呢?因為我們修改了Pod的Qos類型,觸發了syncPod邏輯,裡面會去根據Pod的qos類型進行cgroup目錄判斷,如果qos改變,則會把原Pod下的所有container全部殺掉,然後創建新的cgroup目錄,再啟動容器。這也就可以解釋為什麼在第一次修改Request之後Pod重啟次數增加了2,因為pause容器也發生了重建。為什麼要重建容器呢,因為整個pod的qos發生了變化,Pod內的所有容器需要在新的qos目錄下重建其目錄,但是kubelet沒有去更新container的cgroup設定,而是採用重建的方式來實現。

為什麼kubelet不直接去更新cgroup目錄,而是重建容器呢?首先修改Request不僅影響cgroup,容器的oomscore也將受到影響,docker雖然提供了api來修改資源大小,但並沒有提供相關的api去進行cgroup目錄及oomscore等屬性的修改,其次cgroup遷移是一個比較複雜的工作,遷移過程會出現部分歷史資料丟失等問題,所以kubele直接採用重建的方式來解決這個問題。

Cgroup刪除

經過分析Cgroup創建過程,重啟兩次的問題已經找到了答案。但為什麼新的Pod cgroup目錄創建出來之後,原有的目錄沒有被刪除呢?這就需要搞清楚Pod Cgroup目錄什麼時候刪除的,容器級別的cgroup目錄是在容器被刪除的時候刪除的,這個很好理解,Pod級別的Cgroup目錄是否也是在Pod刪除時刪除的呢?經過看程式碼發現並不是,Pod資源清理是一個非同步的過程,定時監測Pod是否已經設定了deletionTimestamp屬性和容器的運行狀態,只有設定了此屬性的Pod才有可能被清理,清理的過程中包含掛在卷、Cgroup等資源,會一併清理。因為修改Request的請求是不會去給Pod設定deletionTimestamp屬性的,這就導致Pod級別的舊目錄不會被刪除,又因為新目錄的創建,導致同時存在兩個Pod級別的目錄。

反思

綜合看下來,這兩個點都沒有那麼難,而且之前也做過kubelet定製開發,syncPod部分程式碼更是看過數次。那為什麼花費了這麼久的時間呢?

源碼閱讀的目的性

此前閱讀源碼的目的有幾種,查問題、驗證某些想法、探尋系統運行原理,還有一些人通過看源碼來寫blog、或者寫源碼分析之類的書。此前大部分場景是為了查問題、驗證想法以及某些小功能的定製開發,可以聚焦到某些點或流程上,做完之後會對相關點印象比較深刻,但是相關性不大的地方就會很容易遺忘,或者說根本不會去刻意關注相關性不大的地方。

偶爾會有想法去主動閱讀源碼,探求系統運行原理,實現方式,寫blog等,但最終發現效果很差,因為在此過程中我們的目的性並不是真正的去理解系統,缺乏針對性。而且對於在還不瞭解系統運行原理的情況下想通過看源碼去了解其原理就是本末倒置的事情。比如接手一個新項目或者其他人的項目的時候,如果沒有相關背景,而且也沒有項目文件,沒有程式碼註釋的情況,直接想通過程式碼去了解業務和系統運行原理是一件非常痛苦的事情。

思考的必要性

無論處於什麼目的去看程式碼,需要有自己的思考,可以假設系統由自己設計,那會設計成什麼樣子,程式碼由自己實現,會寫成什麼樣子。在設計過程中可能會遇到一些問題,帶著問題再去看程式碼,去驗證別人是如何設計並實現的,尤其是遇到和自己預期設計不一致的地方,可以進行對比,分析那種方案好,或者他這麼設計是處於什麼考慮,為什麼這麼實現。以這樣的方式看程式碼要比沒有目的性的走馬觀花式的瀏覽程式碼收穫更多,印象更深刻。這裡推薦一本書《思考,快與慢》,解釋了人的大腦是如何工作的,可以通過本書瞭解到思考是一個怎樣的過程。

後續計劃

源碼還是要去讀的,後面會進行一些嘗試,根據上面提到的閱讀方式開始進行,即 思考系統運行方式 ==》自己設計系統實現 ==》帶著問題讀源碼(驗證想法) ==》思考與總結,試運行一段時間看看效果。


IT145.com E-mail:sddin#qq.com