如何對(duì)Pod內(nèi)容器進(jìn)行Remote Debug(增補(bǔ)篇)
大家好,我是二哥。這篇之前發(fā)過,但在回答網(wǎng)友問題的過程中,我意識(shí)到作為 SOP ,原篇里有些我沒有重點(diǎn)強(qiáng)調(diào)的步驟其實(shí)對(duì)大家能否成功搭建 remote debug 環(huán)境非常重要,例如 livenessProbe,因?yàn)樗?,不少同學(xué)的調(diào)試會(huì)話突然中斷了。我把文章重新編排整理了一下,并修復(fù)了原篇一些問題。
正文分隔符
在一個(gè)面試場(chǎng)景中,就 debug 問題,一般會(huì)出現(xiàn)下面的對(duì)話:
二哥:你平時(shí)開發(fā)的時(shí)候是用什么方法 debug ?
應(yīng)聘者:看日志。
二哥:萬(wàn)一 log level 沒設(shè)對(duì)或者關(guān)鍵的地方?jīng)]有加 log 怎么辦呢?
應(yīng)聘者:那就改代碼,加 log ,重啟服務(wù),然后繼續(xù)看日志。
先不談通過看 log 來(lái) debug 的效率問題,在 VM 上這樣搞尚且可行,可當(dāng)我們把應(yīng)用容器化并讓 K8s 管理后,怎么辦呢?我們都知道在 Pod 里是沒法方便地通過執(zhí)行類似 systemctl 或 monit 等命令來(lái)重啟應(yīng)用的,那繼續(xù)用看日志的方式的話,就剩下一條路了:
- 改代碼,加 log。
- commit 到 git。
- CI / CD。
- 如果 log 沒有加對(duì),或者想看一下某一個(gè)函數(shù)調(diào)用的返回值,那從步驟1開始重頭再來(lái)。
um, 看上去挺累的樣子。CI / CD 和 K8s 也被折騰得夠嗆。二哥稍微有點(diǎn)強(qiáng)迫癥,不能忍受這么折磨人的 debug 方式。另外,相比人肉看 Log,通過調(diào)試器的方式來(lái) debug 更優(yōu)雅、更快捷,也更能激發(fā)RD的想象力。最重要的是,通過調(diào)試器debug會(huì)倒逼 RD 從代碼調(diào)用邏輯、和 OS 交互等多角度思考問題。比如會(huì)設(shè)斷點(diǎn)不難,難的是知道何時(shí)設(shè)斷點(diǎn),把斷點(diǎn)設(shè)在哪里最合適?!暗馈āg(shù)—器—?jiǎng)荨?,是老子《道德?jīng)》的精髓思想。本文講的其實(shí)是“術(shù)”和“器”,但二哥想說“道”更本質(zhì),也更重要,它是核心思想、理念、本質(zhì)規(guī)律。強(qiáng)烈建議好奇心重的同學(xué)多思考一下這些“術(shù)”背后的實(shí)現(xiàn)原理。二哥通過一個(gè)示例給老鐵們演示一下,如何從本地機(jī)器遠(yuǎn)程調(diào)試 Pod 里面的應(yīng)用。應(yīng)用本身非常簡(jiǎn)單,是用 Node.js 寫的一段 http server。對(duì)于其它語(yǔ)言寫的應(yīng)用,你肯定能找到變通方法。
一、準(zhǔn)備工作,排除干擾項(xiàng)
下面所列的準(zhǔn)備工作是為了在調(diào)試過程中不要引入過多的干擾因素,讓我們把精力聚焦在問題本身。二哥友情提醒:可別在生產(chǎn)環(huán)境干這個(gè)哦。
1、標(biāo)記現(xiàn)場(chǎng)
為了方便調(diào)試,我們會(huì)對(duì) Deployment / ReplicaSet / StatefulSet / DaemonSet 等 workload resource 做一些修改。修改完后,需要恢復(fù)原樣,所以我們得記得修改前的現(xiàn)場(chǎng)是啥。
# 得到最近一次 depoyment REVISION 為 4,留作后面還原使用
$ kubectl rollout history deployment/nodejs -n lancehbzhang
REVISION CHANGE-CAUSE
1 <none>
3 <none>
4 <none>
2、將Pod實(shí)例設(shè)為1
將 Pod 的 replica 設(shè)置為 1。不然你就得發(fā)了瘋地尋找 debugger 發(fā)出的調(diào)試命令發(fā)到哪里去了呢?
$ kubectl patch deploy/nodejs -n lancehbzhang -p='{"spec": {"replicas":1}}'
3、 livenessProbe
還記得K8s的 livenessProbe 和 readinessProbe 嗎?如果容器內(nèi)應(yīng)用因?yàn)楸徽{(diào)試而長(zhǎng)時(shí)間未響應(yīng)這兩個(gè) probe,那么 Pod 有可能會(huì)被 K8s 殺掉。這個(gè)時(shí)候,或許你費(fèi)勁千辛萬(wàn)苦才等來(lái)的斷點(diǎn)命中瞬間化為烏有了。不要問二哥是怎么知道的,都是淚。網(wǎng)上有不少解決方法,比如通過 kubectl patch deploy/nodejs 安裝 dummy livenessProbe 和 readinessProbe 。這個(gè) dummy probe 不需要真的去 probe container 是否活著,相反它永遠(yuǎn)返回 true 。比如下面這種方法用 kubectl patch 命令修改了 deployment 的 spec 。
# 移除 livenessProbe
$ kubectl patch deploy/nodejs -n lancehbzhang --type json -p='[{"op": "remove", "path": "/spec/template/spec/containers/0/livenessProbe"}]'
# 安裝 dummy livenessProbe
$ kubectl patch deploy/nodejs -n lancehbzhang -p '{"spec": {"template": {"spec": {"containers": [{"name": "nodejs", "livenessProbe": {"initialDelaySeconds": 5, "periodSeconds": 5, "exec": {"command": ["true"]}}}]}}}}'
4、恢復(fù)現(xiàn)場(chǎng)
雖然這一步放在靠前的位置,但只有當(dāng)你已經(jīng)調(diào)試完畢,才需要執(zhí)行下面的命令恢復(fù)現(xiàn)場(chǎng)。
# 還原到 depoyment REVISION 4
$ kubectl rollout undo deployment nodejs --to-revision=4 -n lancehbzhang
二、把容器切換至debug模式
首先得把 http server 切換到調(diào)試模式。注意這里 demo 的方法僅適用于 Node.js 。
kubectl exec nodejs-8448d4cbc6-nbjwd -n lancehbzhang -- /bin/bash -c "kill -USR1 1"
一切順利的話,你可以從 Pod 的 log 里面看到如下所示的信息。這表示 debugger 偵聽在端口 9229 。
圖 1:將容器切換進(jìn)入 debug 模式
三、 K8s port-forward
下面的問題是:如何才能把本地 debugger 發(fā)出的調(diào)試命令連進(jìn)來(lái)?
方法其實(shí)有不少。比如通過一個(gè) Load Balancer 類型的 service 。不過這種方法比較費(fèi)錢,據(jù)我所知,騰訊云的 Load Balancer 價(jià)格不菲。這里二哥介紹一個(gè)既免費(fèi)又通用的方法。用 K8s 自帶的 port-forward 功能,命令如下所示:
$ kubectl port-forward deploy/nodejs -n lancehbzhang 9229:9229
在一臺(tái)可以執(zhí)行 kubectl 命令的機(jī)器上執(zhí)行這行命令后,如果一切正常,你會(huì)看到下面的界面。
圖 2:使用K8s port-forward
恭喜你,這表示從此以后任何發(fā)往這臺(tái)機(jī)器 9229 端口的請(qǐng)求都將會(huì) forward 到 pod nodejs 的 9229 端口,如你所猜,那正是 debugger 正在偵聽的端口。到現(xiàn)在為止,下圖中的 ③ 和 ④ 你應(yīng)該都準(zhǔn)備好了。
圖 3:從本機(jī) debugger 到遠(yuǎn)程 debuggee 全景圖
四、SSH Tunnel(非必須)
你是不是摩拳擦掌,擼起袖子準(zhǔn)備從本地機(jī)器連過來(lái)了?且慢,有一種場(chǎng)景我們還沒解決。
如果執(zhí)行 kubectl port-forward 的機(jī)器和我們的本地機(jī)器無(wú)法直連怎么辦?假如出于安全考慮,上圖中 ③ 和 ④ 是可以網(wǎng)絡(luò)直連的,但 ① 和 ③ 被防火墻隔開了,只留了一個(gè)22端口供 ① 通過 ssh 登錄到 ③ 。這種情況下,該如何從本機(jī)連接到 ④ 上的 debugger 呢?這個(gè)時(shí)候就需要輪到步驟 ② 所示的 SSH Tunnel 登場(chǎng)了。通過這樣的方式, 本機(jī) VS code 只需 attach 到 127.0.0.1:9229,諸如設(shè)置斷點(diǎn)、單步執(zhí)行、查看變量等調(diào)試命令都被封裝起來(lái),塞進(jìn) SSH Tunnel 再送至 ③ 上,然后再通過 port-forwarding 轉(zhuǎn)至 ④ 上的debuggee。
注:SSH Tunnel 的使用并非本文的重點(diǎn),大家可以自行谷歌找到使用方法。關(guān)于 SSH Tunnel 的細(xì)節(jié)可以參考二哥的文章《手邊的tunnel知多少》。
在實(shí)踐過程中,如果長(zhǎng)時(shí)間沒有數(shù)據(jù), SSH Tunnel 會(huì)被關(guān)閉掉。我們可以開啟 keep-alive 機(jī)制。另外,打開 SSH Tunnel 日志的 verbose level 到適當(dāng)?shù)募?jí)別也便于我們感知 Tunnel 目前正在工作中。
圖 4:保持 SSH Tunnel 的高級(jí)設(shè)置
五、演示
好了,準(zhǔn)備工作做完了。下面開始二哥的表演。
本地機(jī)器打開 VS Code,在 launch.json 里面輸入如下所示的配置。其中參數(shù) port表示本機(jī) debugger 需要連接的端口,localRoot表示本地的代碼路徑,而remoteRoot則表示 ④ 中應(yīng)用所在的路徑。二哥在 build Docker image 時(shí),將應(yīng)用的 WORKDIR 設(shè)置為了/myapp,所以這里也得填成/myapp。其它參數(shù)各位自行谷歌。
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Attach-2-nodejs",
"port": 9229,
"request": "attach",
"skipFiles": ["<node_internals>/**"],
"type": "pwa-node",
"localRoot": "${workspaceFolder}",
"remoteRoot": "/myapp",
"sourceMaps": true
}
]
}
注意這里的參數(shù) localRoot 要設(shè)置正確,不然斷點(diǎn)設(shè)置無(wú)法生效。${workspaceFolder} 表示的是 vs code 當(dāng)前所打開的工程目錄,比如 D:\\nodejs 。加入它下面有兩個(gè)目錄 sub-A 和 sub-B ,分別為兩個(gè)微服務(wù)的子工程。如果我們現(xiàn)在要調(diào)試的是經(jīng)由 sub-A 生成的容器,那 localRoot 要設(shè)置為 ${workspaceFolder}/sub-A。
remoteRoot 這個(gè)參數(shù)與 Dockerfile 里 WORKDIR 的設(shè)置有關(guān)。在第17行設(shè)置斷點(diǎn),按下 F5 開始 debugging 。
圖 5:本機(jī)debugger
還記得前文我們已經(jīng)打開的 SSH Tunnel 界面嗎?這個(gè)時(shí)候,你會(huì)看到它會(huì)打印出一些諸如 "Successfully established connection 127.0.0.1:9229 -> 127.0.0.1:9229" 這樣的信息。當(dāng)然,具體信息內(nèi)容與你使用的工具相關(guān)。
圖 6:SSH Tunnel 正在工作示意圖
沒有問題的話,網(wǎng)絡(luò)包應(yīng)該來(lái)到了圖3中位置 ③ 。我們來(lái)看看這個(gè)時(shí)候 K8s port-forward 會(huì)打印出什么來(lái):
圖 7:K8s port-forward 正在工作示意圖
非常不錯(cuò),看起來(lái)它收到了請(qǐng)求,并且也在勤奮地工作著。那最后我們來(lái)看看圖3中 ④ 中打印出來(lái)的令人激動(dòng)的信息:"Debugger attached"。
圖 8:debuggee 顯示已有 debugger attach 上來(lái)了
萬(wàn)事俱備,只差最后一腳了:發(fā)個(gè)請(qǐng)求,看看能不能命中斷點(diǎn):
圖 9:發(fā)個(gè)請(qǐng)求,命中一下斷點(diǎn)
回頭看看圖5吧,多么讓人陶醉的界面,在那里你可以查看變量、?;厮?,還可以干很多很多其它騷操作。是的,這個(gè)時(shí)候才是發(fā)揮你想象力的時(shí)候。
六、總結(jié)
文末,來(lái)個(gè)總結(jié)吧。
- 首先需要將容器內(nèi)的應(yīng)用切換到 debug 模式。具體如何操作與所使用的語(yǔ)言密切相關(guān)。
- 通過 K8s port-forward 可以將 debugger 發(fā)出的調(diào)試命令轉(zhuǎn)發(fā)至被調(diào)試應(yīng)用(debuggee)。
- 如果運(yùn)行于你本機(jī)的debugger無(wú)法和運(yùn)行著 K8s port-forward 的那臺(tái)機(jī)器直接通信,那么這個(gè)時(shí)候就需要把 debugger 的調(diào)試命令丟進(jìn) SSH Tunnel 送至對(duì)端。
- 一切準(zhǔn)備就緒后,本機(jī) debugger 就可以 attach 到 debuggee 了。
以上就是本文的全部?jī)?nèi)容。