Preface
此篇文章是 Kubernetes Pod-DNS 系列文章第三篇
此系列文會從使用者的用法到一些問題的發掘,最後透過閱讀程式碼的方式去分析這些問題
相關的文章連結如下
- [Kubernetes] DNS setting in your Pod
- [Kubernetes] DNS Setting with Dockerd
- [Kubernetes] DNS Setting with Dockerd(原始碼分析下)
正文
在前篇文章
[Kubernetes] DNS Setting with Dockerd 中已經詳細介紹了整個問題的流程以及觀察結果。
再次重申一次結論kubernetes
會先嘗試使用節點上 /etc/resolv.conf
的資料,但是若發現 /etc/resolv.conf
是空的,這時候就會去依賴 dockerd
幫忙產生的 /etc/resolv.conf
本篇會直接從 kubernetes
以及 docker
的原始碼來研究這個問題,並且佐證上篇文章的觀察結果。
Kubernetes
在觀察這個現象前,我們必須要先思考方向,該怎麼去追這段程式碼?
到底程式碼的開頭應該從哪邊開始追起?
要講起這個問題實在不是一言兩語可以解決的,這部份除非本身已經對 kubernetes
原始碼很瞭解,否則就是要倚賴關鍵字加上本身對程式語言的經驗來判斷,趕緊找到一個正確的進入點。
這邊直接先使用一個簡單的流程圖,來描述整個運作的邏輯,然後接下來會針對這段流程進行更進一步的分析來解決我們的疑問
CRI 流程圖
這邊我們先簡單的知道, kubernetes
本身有非常多的插件,包含了 Storage
, Network
, Device
以及 Runtime
。
這就是你會常常聽到 CNI
, CRI
, CSI
以及 Device Plugin
等這些名詞出現的理由
Kubernetes
藉由這些插件把部分功能都給模組化,讓任何符合該標準的第三方套件都能夠與 kubernetes
緊密合作。
未來將會撰寫一系列的文章來介紹 CNI,從 CNI 概念到自己撰寫一個 CNI,敬請期待
因為今天的問題主要是運行容器內的設定問題,這部份則是會跟 CRI(Contaienr Runtime Interface)
有關連, 因此我透過下圖稍微說明一下整個流程。
在每個 kubernetes
的節點上都會運行一隻守護程序 kubelet
,該 kubelet
內部有一個稱為 kube-runtime
的套件會用來管理在該節點上所有跟容器有關的操作。
kube-runtime
本身會遵守 CRI
的規範來管理容器,而 CRI
則是建立在 gRPC
Client/Server 的架構下運行的。
因此 kube runtime
本身會扮演者 gRPC
client 的角色,對 gRPC server dockershim
發送管理容器的請求。
而 dockershim
則是一個 kubernetes
內部自行設計實作的一個基於 docker container
且相容 CRI
的 gRPC server.
當 dockershim
收到請求後,就會透過 Docker Container
本身的 API
進行 Docker Container
相關的資源處理。
程式碼追蹤
藉由上述流程圖的幫忙後,我們可以有更進一步的方向去思考程式的進入點。
首先我們知道透過對 Pod
設定 dnsPolicy:default
才會有這個問題,同時也觀察到 dockerd
本身的設定也會影響整個 /etc/resolve.conf
的結果。
所以 kube runtime
這邊是一個可能線索,如何解析 Pod
本身的參數並處理是一個方向
同時 dockershim
這邊如何針對 docker container
則會是另外一個線索來追尋。
針對這兩個方向,經過仔細的追尋後,我們可以得到類似下圖的流程
圖中藍色區域都是真實的函式名稱
當 1 號發出Create Pod
的請求出來後, kubelet
裡面於 kube runtime
模組內則會呼叫到 createPodSandbox
的函式。
在 createPodSandbox
則會進行下列事情
- 呼叫
generatePodSandboxConfig
- 透過
CRI
要求遠方執行RunPodSandbox
kube runtime
createPodSandbox
整個函式如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// createPodSandbox creates a pod sandbox and returns (podSandBoxID, message, error).
func (m *kubeGenericRuntimeManager) createPodSandbox(pod *v1.Pod, attempt uint32) (string, string, error) {
podSandboxConfig, err := m.generatePodSandboxConfig(pod, attempt)
if err != nil {
message := fmt.Sprintf("GeneratePodSandboxConfig for pod %q failed: %v", format.Pod(pod), err)
glog.Error(message)
return "", message, err
}
// Create pod logs directory
err = m.osInterface.MkdirAll(podSandboxConfig.LogDirectory, 0755)
if err != nil {
message := fmt.Sprintf("Create pod log directory for pod %q failed: %v", format.Pod(pod), err)
glog.Errorf(message)
return "", message, err
}
podSandBoxID, err := m.runtimeService.RunPodSandbox(podSandboxConfig)
if err != nil {
message := fmt.Sprintf("CreatePodSandbox for pod %q failed: %v", format.Pod(pod), err)
glog.Error(message)
return "", message, err
}
return podSandBoxID, "", nil
}
generatePodSandboxConfig
generatePodSandboxConfig
這個函式很簡單,基本上就是從 Pod
本身 yaml
檔案裡面去讀取設定,根據這些設定來處理相關的資訊。
我們在意的部份就是 DNS
相關,所以以下就擷取 DNS
相關的函式內容
1 | // createPodSandbox creates a pod sandbox and returns (podSandBoxID, message, error). |
這邊可以看到呼叫了 GetPodDNS
來取得對應的設定,然後我們就一路往下追
GetPodDNS
1 | func (c *Configurer) GetPodDNS(pod *v1.Pod) (*runtimeapi.DNSConfig, error) { |
這邊的邏輯非常簡單
- 先透過
getHostDNSConfig
取得當前/etc/resolv.conf
的內容,並且存於變數 dnsCOnfig - 接下來透過
getPodDNSType
該函式從Pod Yaml
內讀取對應的dnsPolicy
資訊,來決定當前Pod
的dnsType
- 第三步驟就是最重要的,根據當前
dnsType
來決定要如何處理dnsConfig
.
由於podDNSCluster
的程式碼太多了,我這邊就先忽略掉,我們只要專注看DNSHost
的案例。
裡面非常簡單,如果你當初運行kubelet
的時候有特別設定不要使用/etc/resolv.conf
的話,就會把dnsConfig
補上一個127.0.0.1
的資訊,不過這個Case
我暫時還沒想到,可能就是機器本身就是一個DNSServer
的情況吧。 - 將
dnsConfig
回傳出去
所以在正常情況下,我們的dnsConfig
理論上就會是節點上/etc/resolv.conf
內的資料。
到這邊為止,就是 kube-runtime
自行處理的部份,並且將得到的 dnsConfig
放置到變數 podSandboxConfig.DnsConfig
之中。
接下來我們來看一下 dockershim
裡面的 RunPodSandbox
會怎麼處理這些 DNS
的資訊。
DocekrShim
RunPodSandbox
由於 RunPodSandbox
的函式非常長,這邊就擷取跟 DNS
相關的部份來觀察
1 | func (ds *dockerService) RunPodSandbox(ctx context.Context, r *runtimeapi.RunPodSandboxRequest) (*runtimeapi.RunPodSandboxResponse, error) { |
其實這邊的程式碼已經有了滿不錯的註解來解釋相關的行為。
1.之前透過 kube run-time
得到的 dns
設定都只是一個存在於記憶體內的一堆資料而已,其實都還沒有跟真正的容器有任何關係。
2.透過 dockershim
的操作,這時候其實已經將對應的容器創見完畢,而且該容器內/etc/resolv.conf
的內容是完全依賴 docker container
決定
3.kubernetes
這邊的邏輯非常簡單,如果我之前有產生過任何 dns
的設定,就直接將 docker
產生出來的 /etc/resolv.conf
給直接覆蓋掉。
所以我們可以看一下 rewriteResolvFile
這個函式
rewriteResolvFile
1 | // rewriteResolvFile rewrites resolv.conf file generated by docker. |
這邊也不能理解,就是根據之前存放於 podSandboxConfig.DnsConfig
內各式各樣關於 DNS
的設定組合起來,然後直接覆蓋掉本來的 /etc/resolv.conf
.
結論
到這邊,我們已經把整個問題給釐清一半了
重新看一次之前的結論
kubernetes
會先嘗試使用節點上 /etc/resolv.conf
的資料,但是若發現 /etc/resolv.conf
是空的,這時候就會去依賴 dockerd
幫忙產生的 /etc/resolv.conf
我們的推論跟我們程式碼觀察的結果是完全吻合的,再 dnsPolicy=default
的前提下,只要 kubernetes
只要能夠獲得合法的 /etc/resolv.conf
就會使用,否則直接使用 docekr container
所創造的 /etc/resolv.conf
.
但是這邊還有留下一個謎點,到底 dockerd
的設定是如何影響 /etc/resolv.conf
以及 8.8.8.8/8.8.4.4
是如何出現的?
由於再寫下去本篇文章會愈來愈長,所以決定將 docker container
相關的程式碼分析再寫一篇文章來處理。
個人資訊
我目前於 Hiskio 平台上面有開設 Kubernetes 相關課程,歡迎有興趣的人參考並分享,裡面有我從底層到實戰中對於 Kubernetes 的各種想法
組合包
https://hiskio.com/packages/7ey2vdnyN
疑難雜症除錯篇
https://hiskio.com/courses/440/about?promo_code=LG28Q5G
單堂(CI/CD)
https://hiskio.com/courses/385?promo_code=13K49YE&p=blog1
基礎概念
https://hiskio.com/courses/349?promo_code=13LY5RE
另外,歡迎按讚加入我個人的粉絲專頁,裡面會定期分享各式各樣的文章,有的是翻譯文章,也有部分是原創文章,主要會聚焦於 CNCF 領域
https://www.facebook.com/technologynoteniu
如果有使用 Telegram 的也可以訂閱下列頻道來,裡面我會定期推播通知各類文章
https://t.me/technologynote
你的捐款將給予我文章成長的動力