、格式化過濾與容器運(yùn)維排錯)
docker ps 這條命令大概是所有人接觸容器技術(shù)時敲得最多、也最容易被一帶而過的一條。裝完環(huán)境第一件事是敲它確認(rèn)服務(wù)有沒有正常起來容器跑不起來第一反應(yīng)還是加個 -a 翻一遍歷史。它看起來簡單到不需要解釋——不就是列個表嗎可真到了線上排查問題的時候很多人盯著那張只有容器 ID、鏡像、狀態(tài)、端口四列的默認(rèn)輸出愣是看不出容器到底為什么退出、端口為什么連不上、哪個容器在偷偷吃磁盤空間。這篇內(nèi)容我把 docker ps 從參數(shù)到格式化、從過濾到排錯完整拆一遍順帶聊聊幾個只有踩過坑才知道的細(xì)節(jié)。不管你剛裝完環(huán)境想確認(rèn)是否正常還是已經(jīng)在維護(hù)幾十個容器、需要把這條命令接進(jìn)腳本和監(jiān)控下面這些應(yīng)該都能直接用上。讀完你至少能做成兩件事看狀態(tài)列就能大致判斷容器死因用 format 和 filter 把默認(rèn)那張表改造成適合自己工作流的形態(tài)。1. 先搞清楚 docker ps 到底在列什么1.1 一個“查看”命令為什么值得單獨(dú)拆很多人對 docker ps 的理解停留在“容器版的 ls”覺得它沒有任何技術(shù)含量。這個判斷在只有一兩個容器的時候成立一旦容器數(shù)量上到兩位數(shù)、或者容器生命周期短到幾秒鐘就退出這條命令的信息密度就會突然變得極其關(guān)鍵。原因在于容器本身是個黑盒進(jìn)程你沒有辦法像查普通進(jìn)程那樣直接 attach 上去看內(nèi)部狀態(tài)docker ps 輸出的那幾列實(shí)際上是守護(hù)進(jìn)程從內(nèi)部數(shù)據(jù)結(jié)構(gòu)里摘出來的、關(guān)于這個容器當(dāng)前處境的全部公開線索。我舉個具體的例子。一個容器反復(fù)重啟你在默認(rèn)輸出里看到的是Restarting (1) 5 seconds ago。這個(1)是退出碼1意味著應(yīng)用層自己報錯退出而不是被信號殺掉如果是(137)那是 128 加 9說明進(jìn)程收到了強(qiáng)制終止信號常見于內(nèi)存超限被系統(tǒng)干掉。同樣一列文字懂和不懂的人排查方向完全相反。所以這條命令的價值不在于“能看”而在于“看得懂”它把容器的創(chuàng)建時間、運(yùn)行時長、端口綁定、健康檢查結(jié)果、退出原因全部壓縮進(jìn)幾行文本里你需要的是一套解讀方法。另外一層原因是可編程性。docker ps 支持結(jié)構(gòu)化的過濾和模板輸出意味著它不只是給人看的還能直接喂給 shell 腳本、監(jiān)控采集器、告警系統(tǒng)。日常運(yùn)維里大量重復(fù)動作——批量停掉某個鏡像的所有容器、找出所有退出碼非零的實(shí)例、清理超過一定時間的僵尸容器——底層幾乎都靠這條命令先篩出目標(biāo)集合。把它的參數(shù)吃透后面寫腳本能省掉一大半的解析邏輯。1.2 默認(rèn)輸出那幾個字段各自意味著什么敲一次不帶任何參數(shù)的 docker ps出來的是七列容器 ID、鏡像、啟動命令、創(chuàng)建時間、狀態(tài)、端口、名稱。這七列不是隨便排的從左到右基本遵循“從唯一標(biāo)識到人類可讀描述”的順序。容器 ID 是十二位短哈希足夠日常區(qū)分但真要拿去寫進(jìn)配置文件或者做精確匹配建議用--no-trunc拿完整的六十四位短 ID 在容器數(shù)量多的時候存在極小概率的前綴碰撞雖然概率低但腳本里因?yàn)檫@種問題排查半天不劃算。鏡像列顯示的是容器創(chuàng)建時所用的鏡像名注意這里不會告訴你鏡像后來有沒有被更新過容器一旦創(chuàng)建它的鏡像引用就固定了所以看到鏡像標(biāo)簽是 latest 也別以為它一定是最新的。啟動命令列默認(rèn)會被截斷長命令后面跟省略號想看全得加--no-trunc。創(chuàng)建時間列是相對時間比如“5 minutes ago”這個相對時間在跨時區(qū)協(xié)作或者寫進(jìn)日志的時候容易產(chǎn)生歧義用格式化輸出改成絕對時間更穩(wěn)妥。狀態(tài)列信息量最大它同時包含了運(yùn)行時長、健康檢查結(jié)果和退出原因三部分。端口列只在容器做了端口映射時才有內(nèi)容格式是“宿主地址:宿主端口-容器端口/協(xié)議”IPv4 和 IPv6 會各占一條用冒號分隔。名稱列是容器名不指定的話系統(tǒng)會隨機(jī)生成一個由形容詞加人名組成的組合正式環(huán)境建議自己命名否則批量操作時根本認(rèn)不出誰是誰。1.3 命令家族與版本兼容的那點(diǎn)事早些年這條命令只有docker ps一種寫法后來命令行做了重組按對象類型歸類于是多出了docker container ls和docker container list兩個等價寫法。三者執(zhí)行結(jié)果完全一致底層走的是同一個接口區(qū)別只在于語義清晰度。新寫法在腳本里更推薦因?yàn)樗鞔_表達(dá)了“我在列容器對象”可讀性更好而docker ps勝在短日常手敲更省事。兩種寫法混用完全沒問題不用糾結(jié)統(tǒng)一。需要留意的是版本差異。老版本里有些選項(xiàng)的行為和新版本不完全一致比如--format支持的字段在不同大版本之間有過增補(bǔ)健康檢查相關(guān)的狀態(tài)字段是后來才加的。如果你在寫跨環(huán)境的部署腳本最好先確認(rèn)目標(biāo)環(huán)境的版本用一個最小字段集做兼容別一上來就用最新字段否則換臺機(jī)器就報模板解析錯誤。我一般的做法是腳本里只依賴容器 ID、名稱、狀態(tài)、鏡像這四個最穩(wěn)定的字段需要更多信息時再單獨(dú)查一次。2. 參數(shù)逐項(xiàng)拆解哪些每天都在用哪些關(guān)鍵時刻才想起2.1 控制顯示范圍-a、-l、-n、-q 的取舍默認(rèn)行為只列正在運(yùn)行的容器這一點(diǎn)新手最容易忽略。容器退出了docker ps里就看不見了于是很多人第一反應(yīng)是“我的容器沒了”其實(shí)它只是停了加-a就能看到全部包括已退出、已創(chuàng)建但沒啟動的。這個-a是使用頻率僅次于默認(rèn)命令的選項(xiàng)排查問題的第一步永遠(yuǎn)是docker ps -a先確認(rèn)容器到底是沒創(chuàng)建、創(chuàng)建了沒起來、還是起來又掛了。-l是只顯示最近創(chuàng)建的那個容器-n后面跟數(shù)字顯示最近創(chuàng)建的若干個。這兩個選項(xiàng)在交互式操作里很順手比如你剛docker run完一個容器想確認(rèn)狀態(tài)docker ps -l比翻全表快得多。但它們按創(chuàng)建時間排序不是按啟動時間容器重建之后順序會變腳本里依賴這個順序要小心。-q只輸出容器 ID是批量操作的標(biāo)配。配合命令替換可以直接把結(jié)果喂給其他命令比如停掉所有運(yùn)行中的容器一行就夠。這個選項(xiàng)的輸出是純凈的 ID 列表沒有表頭沒有對齊空格天然適合管道。我見過有人在腳本里用docker ps加一堆文本處理去提取 ID其實(shí)一個-q就解決了省下來的解析邏輯全是潛在的 bug 來源。2.2 看清全量信息--no-trunc 與 -s 的使用場景--no-trunc的作用是不截斷輸出主要影響三列容器 ID 變成完整哈希啟動命令完整顯示鏡像摘要也完整展示。什么時候必須用一種是要把容器 ID 寫進(jìn)審計(jì)記錄或者工單系統(tǒng)短 ID 不夠嚴(yán)謹(jǐn)另一種是排查啟動命令問題時被截斷的那半截命令恰恰是關(guān)鍵比如一個帶了很多參數(shù)的 Java 啟動腳本省略號后面可能藏著錯誤的環(huán)境變量。-s會額外加兩列顯示每個容器的可寫層大小和虛擬磁盤總大小。這個選項(xiàng)要慎用因?yàn)樗枰蚴刈o(hù)進(jìn)程查詢每個容器的存儲占用容器數(shù)量多的時候會明顯變慢我在幾十個容器的環(huán)境里試過加上這個參數(shù)響應(yīng)時間能從幾十毫秒漲到一兩秒。它的正確用法是定點(diǎn)排查比如磁盤告警了用它找出哪個容器的可寫層異常膨脹而不是把它寫進(jìn)日常巡檢腳本里天天跑。另外注意可寫層大小只統(tǒng)計(jì)容器自己寫入的數(shù)據(jù)掛載的卷不計(jì)算在內(nèi)所以看到數(shù)字很小但磁盤還是滿的要去查卷的占用。2.3 參數(shù)組合時的優(yōu)先級與相互影響有幾個組合需要提前說清楚避免用的時候犯迷糊。-q和--format同時出現(xiàn)時--format優(yōu)先因?yàn)楦袷交歉鞔_的輸出意圖-q只是它的一個簡化特例。-a和-f statusexited組合時過濾條件在全集上生效不用額外再加-a因?yàn)闋顟B(tài)過濾本身就隱含了“要看已退出的”但為了語義清楚我一般還是會把-a寫上。-l和-n同時出現(xiàn)時以-n為準(zhǔn)。--filter多個條件之間的默認(rèn)關(guān)系是“與”也就是必須同時滿足這點(diǎn)和很多人直覺一致但涉及名稱過濾的正則匹配時容易踩坑后面過濾那一節(jié)會細(xì)說。還有個隱性的坑多個--filter如果用了同一個鍵、不同值比如兩個status過濾實(shí)際效果是“或”而不是報錯這個行為在不同版本上表現(xiàn)一致但文檔里寫得不顯眼值得記一下。3. --format 實(shí)戰(zhàn)把列表變成自己的運(yùn)維看板3.1 模板語法的門檻其實(shí)比想象中低--format用的是 Go 模板語法聽起來嚇人實(shí)際常用的就三樣?xùn)|西字段引用、制表符、換行。字段引用寫成點(diǎn)加字段名比如容器 ID 就是.ID名稱是.Names狀態(tài)是.Status。要在同一行拼多個字段中間用\t分隔這是制表符的轉(zhuǎn)義寫法能讓輸出自動對齊成表格。最常用的寫法是在模板前面加table關(guān)鍵字它會自動補(bǔ)上表頭并按列對齊。不加table的話就是純文本逐行輸出適合喂給腳本。我第一次用的時候不知道有這個關(guān)鍵字手動用空格對齊了半天結(jié)果字段長度一變?nèi)珌y后來發(fā)現(xiàn)一個 table 就解決了所有對齊問題。值得強(qiáng)調(diào)的是模板里可以塞普通文本和符號比如給每行加個前綴、在字段之間加豎線做視覺分隔、甚至拼出類似 JSON 的結(jié)構(gòu)。這讓同一條命令既能輸出給人看的報表也能輸出給程序解析的數(shù)據(jù)不用再寫兩套邏輯。3.2 常用字段清單與拼接技巧日常用得最多的字段就那么幾個我把它們和默認(rèn)列做個對應(yīng)方便遷移習(xí)慣。.ID對應(yīng)容器 ID.Image對應(yīng)鏡像.Command對應(yīng)啟動命令.CreatedAt對應(yīng)創(chuàng)建時間.RunningFor是運(yùn)行時長.Ports是端口.Names是名稱.State是簡短狀態(tài)詞.Status是帶細(xì)節(jié)的完整狀態(tài)描述。區(qū)分.State和.Status很重要前者只有 running、exited 這種單詞適合做條件判斷后者帶時長和退出碼適合給人看。還有幾個不那么常用但很有價值的字段。.Labels輸出容器的所有標(biāo)簽做標(biāo)簽化管理的團(tuán)隊(duì)可以用它快速核對.Mounts列出掛載信息排查卷映射問題時直接看.Networks顯示容器接入的網(wǎng)絡(luò).LocalVolumes是本地卷數(shù)量。健康檢查相關(guān)的狀態(tài)在.Status里以括號形式體現(xiàn)想要單獨(dú)取出來需要一些字符串處理模板本身不支持條件分支的復(fù)雜邏輯這時候就退回 shell 去切。拼接上我常用的一個套路是給輸出加統(tǒng)一前綴比如在每行前面加上時間戳方便和其他日志合并。模板里直接寫死文本沒問題但動態(tài)時間戳得靠 shell 變量傳進(jìn)去命令替換和引號轉(zhuǎn)義要小心雙引號里套單引號、再套反引號是最容易出錯的組合我一般先用 echo 把完整命令打出來確認(rèn)轉(zhuǎn)義正確再去執(zhí)行。3.3 面向腳本的輸出從人讀到機(jī)器讀人看的表格和機(jī)器解析的格式要分開設(shè)計(jì)。給人看的時候列寬對齊、表頭清晰、信息密度適中給腳本用的時候字段越少越好、分隔符越明確越好、不要表頭。我的習(xí)慣是維護(hù)兩個模板一個用于日常查看包含名稱、狀態(tài)、端口、鏡像四列另一個用于腳本只輸出 ID 和名稱用冒號分隔方便后面用 cut 或者 read 拆。舉個實(shí)際用法批量重啟某個鏡像下的所有容器先篩出目標(biāo)再循環(huán)操作整個過程不用碰任何解析 HTML 或者 JSON 的庫。這里的關(guān)鍵是讓 docker ps 的輸出格式固定下來腳本里只依賴位置不依賴文本匹配穩(wěn)定性會高很多。如果你需要處理容器名里帶空格或者特殊字符的情況記得在 shell 循環(huán)里用 while read 而不是 forfor 會按空格拆分遇到帶空格的名稱直接崩。4. --filter 過濾從一堆容器里精準(zhǔn)撈針4.1 常用過濾鍵與表達(dá)式寫法過濾是這條命令里最被低估的能力。常用的過濾鍵有七八個按狀態(tài)status、按名稱name、按鏡像ancestor、按標(biāo)簽label、按退出碼exited、按端口publish和expose、按網(wǎng)絡(luò)network、按卷volume、按創(chuàng)建時間先后before和since。寫法統(tǒng)一是-f 鍵值等號兩邊不要加空格否則會被 shell 當(dāng)成兩個參數(shù)。名稱過濾用的是子串匹配不是精確匹配也就是說-f nameweb會同時命中 web、web-1、my-web-test。想要精確匹配得用正則的錨定寫法加^和$。這一點(diǎn)在批量操作里特別危險你以為只選中了一個容器實(shí)際選中了一批。我吃過一次虧清理測試容器的時候用了寬泛的名稱匹配結(jié)果把同前綴的生產(chǎn)容器一起停了從那以后所有涉及寫操作的過濾一律加錨定。按鏡像過濾要理解它的語義匹配的是容器創(chuàng)建時引用的鏡像包括該鏡像的所有歷史版本關(guān)系。用鏡像名過濾時同名不同標(biāo)簽的容器都會被選中用鏡像 ID 過濾則更精確。如果你不確定某個容器到底基于哪個鏡像ancestor配合完整 ID 最保險。4.2 多條件組合的與或關(guān)系默認(rèn)多個過濾條件之間是“與”也就是取交集這個符合直覺。但有兩種情況會打破這個默認(rèn)。第一種是同一個鍵給多個值比如兩個status條件實(shí)際是“或”的關(guān)系等于列出狀態(tài) A 或者狀態(tài) B 的容器。第二種是部分版本對label鍵的處理同一個標(biāo)簽鍵給不同的值行為可能和預(yù)期有出入。穩(wěn)妥的做法是遇到多值場景先在測試環(huán)境驗(yàn)證一下結(jié)果集別直接寫進(jìn)自動化流程。組合的實(shí)用例子列出所有已退出且退出碼非零的容器用兩個條件就行statusexited加上exited1之類別。再比如找出某個網(wǎng)絡(luò)下、某個鏡像的所有運(yùn)行中容器三個條件疊加一行命令就能把目標(biāo)鎖定到一個很小的集合后面接操作命令風(fēng)險低很多。條件越多集合越小出錯的代價也越小這是我寫運(yùn)維腳本時的一條基本原則。還有個容易忽略的點(diǎn)過濾是作用在命令看到的全集上的-a影響的是默認(rèn)展示范圍但一旦你用了狀態(tài)過濾范圍會自動擴(kuò)展到匹配該狀態(tài)的容器不需要額外加-a。理解這一點(diǎn)能少寫很多冗余參數(shù)。4.3 狀態(tài)過濾與健康檢查的細(xì)節(jié)狀態(tài)過濾常用的值有 running、exited、created、paused、restarting、dead。這里要特別注意 paused 和 restarting 這兩個中間態(tài)它們既不是完全活著也不是完全死了很多監(jiān)控腳本只判斷 running結(jié)果漏掉了卡在 restarting 里的容器。我一般會單獨(dú)維護(hù)一個“異常狀態(tài)”過濾把 restarting 和 dead 都拉出來告警。健康檢查的判斷不在狀態(tài)過濾里它藏在完整狀態(tài)描述里。一個容器可能是 running 但健康檢查結(jié)果是 unhealthy這種情況在默認(rèn)視圖里只有狀態(tài)列的括號能看出來。做健康巡檢的時候不能只看statusrunning還得對輸出做二次判斷或者用格式化把完整狀態(tài)取出來后匹配 unhealthy 關(guān)鍵字。這塊是我覺得這條命令設(shè)計(jì)上稍微別扭的地方希望以后能直接支持健康狀態(tài)的過濾鍵。5. 幾個真實(shí)場景的排查過程5.1 容器啟動就退出從狀態(tài)列讀出死因容器起來幾秒鐘就退出是最高頻的問題。排查順序我固定為三步。第一步docker ps -a找到那個容器重點(diǎn)看狀態(tài)列的退出碼。退出碼 0 表示進(jìn)程正常結(jié)束多半是啟動命令跑完就退出了比如一個前臺進(jìn)程寫成了后臺執(zhí)行或者腳本里缺了保持前臺運(yùn)行的指令。退出碼 1 是應(yīng)用自己報錯得去看日志。退出碼 126 和 127 分別是命令不可執(zhí)行和命令找不到通常是啟動命令寫錯或者鏡像里沒有這個可執(zhí)行文件。第二步是如果退出碼是 137說明進(jìn)程收到了強(qiáng)制終止信號最常見的誘因是內(nèi)存超過限制被系統(tǒng)終止也可能是有人手動發(fā)了終止命令。這時候去看容器的資源限制配置和宿主機(jī)的內(nèi)存水位基本能定位。退出碼 143 對應(yīng)的是正常的終止信號一般是容器被優(yōu)雅停止屬于正常退出范疇。第三步才去看日志。很多人一上來就翻日志其實(shí)狀態(tài)列已經(jīng)把大方向指明了先讀退出碼能省掉大量無效翻找。這個習(xí)慣我建議所有人都養(yǎng)成尤其是在容器數(shù)量多、日志量大的環(huán)境里。5.2 端口映射異常從端口列還原綁定關(guān)系服務(wù)連不上端口問題占很大比例。默認(rèn)輸出的端口列寫得很緊湊格式是宿主地址冒號宿主端口箭頭指向容器端口和協(xié)議??催@一列要關(guān)注兩件事綁定地址是什么宿主端口是多少。綁定地址顯示為全零地址表示監(jiān)聽所有網(wǎng)卡顯示為回環(huán)地址則表示只有本機(jī)能訪問外部當(dāng)然連不上。這個區(qū)別在本地開發(fā)時經(jīng)常被忽略容器里服務(wù)正常、本機(jī) curl 也通換個機(jī)器就是不通原因往往就在這里。另一個高頻問題是宿主端口沖突或者沒映射。容器內(nèi)部的端口是容器自己的不映射到宿主外面根本摸不到。想確認(rèn)某個端口被誰占了用端口過濾能快速定位到具體容器。如果發(fā)現(xiàn)同一個宿主端口出現(xiàn)多條映射記錄說明有容器啟動失敗或者配置寫重復(fù)了這時候要結(jié)合狀態(tài)列看哪個是真的在運(yùn)行。5.3 批量清理前的安全確認(rèn)清理容器是危險操作尤其是帶數(shù)據(jù)卷的。我的固定流程是先列后刪絕不直接刪。先用過濾把目標(biāo)集合列出來加上完整的狀態(tài)、名稱、鏡像、創(chuàng)建時間四列人工掃一遍確認(rèn)沒有誤傷再執(zhí)行刪除。列表這一步省不得我曾經(jīng)寫過一個自以為很穩(wěn)的過濾條件結(jié)果因?yàn)槊Q匹配太寬泛選中了不該選的容器好在先列后刪的習(xí)慣救了我一次。對于已經(jīng)退出的容器清理前還要確認(rèn)它掛載的卷有沒有數(shù)據(jù)。容器刪了卷默認(rèn)保留但如果當(dāng)初是用自動清理方式啟動的卷會跟著走。這個判斷靠容器詳情里的掛載信息光看列表不夠得再查一次。批量操作里我一般會加一個確認(rèn)環(huán)節(jié)把要刪的容器數(shù)量打印出來數(shù)量超過預(yù)期就中止人眼過一遍再放行。6. 常見問題與踩坑記錄6.1 問題速查表現(xiàn)象可能原因排查動作容器列表里找不到剛啟動的容器容器已退出默認(rèn)只顯示運(yùn)行中加-a查看全部狀態(tài)顯示 Restarting 反復(fù)出現(xiàn)應(yīng)用啟動失敗觸發(fā)重啟策略看退出碼和日志確認(rèn)重啟策略是否合理狀態(tài)顯示 Up 但服務(wù)不通端口未映射或綁定地址受限檢查端口列和綁定地址狀態(tài)含 unhealthy健康檢查探測失敗單獨(dú)查健康檢查配置和探測日志容器 ID 短哈希重復(fù)前綴碰撞容器數(shù)量大用--no-trunc取完整 ID格式化輸出報模板錯誤字段名寫錯或版本不支持確認(rèn)字段拼寫和目標(biāo)環(huán)境版本名稱過濾選中了多余容器子串匹配未加錨定用錨定寫法做精確匹配磁盤滿但可寫層很小數(shù)據(jù)在掛載卷里單獨(dú)查卷占用6.2 幾條不太容易在網(wǎng)上找到的經(jīng)驗(yàn)第一條別把docker ps的調(diào)用頻率開太高。有些監(jiān)控方案圖省事每隔一兩秒輪詢一次容器數(shù)量一多守護(hù)進(jìn)程的響應(yīng)壓力會上來極端情況下會拖慢其他操作的響應(yīng)。輪詢間隔我一般設(shè)在十秒以上真要秒級感知狀態(tài)變化應(yīng)該走事件訂閱而不是輪詢。第二條腳本里永遠(yuǎn)不要依賴默認(rèn)輸出的列順序和對齊。默認(rèn)表格是為了人眼優(yōu)化字段之間的空格數(shù)量會隨內(nèi)容長度變化用文本切割去解析必然翻車。要解析就用--format固定分隔符或者干脆用-q只拿 ID。第三條涉及寫操作的過濾條件先在末尾加一個--format把目標(biāo)打印出來跑一遍。這個習(xí)慣看起來多一步實(shí)際能擋掉絕大多數(shù)誤操作。我在好幾個環(huán)境里把這個動作做成了肌肉記憶寧可慢十秒不要讓一次誤刪變成事故。最后分享一個我自己常用的小組合一條命令打印出所有容器的名稱、完整狀態(tài)、鏡像和創(chuàng)建時間按創(chuàng)建時間倒序?qū)V巍拔矣浀脛偱苓^一個什么但不知道叫啥”的健忘癥。字段不多信息夠全掃一眼就能定位。容器多起來之后這種自己順手的小命令比任何復(fù)雜的監(jiān)控面板都來得快。