同任何相對復(fù)雜的軟件項目一樣,Docker有很多細(xì)節(jié)問題和缺點,如果想要讓用戶體驗盡可能地流暢,知道這些很重要。 本章講述的一些技巧將會向讀者展示其中更為重要的一些部分,而且會介紹如何使用一些第三方構(gòu)建的外部工具來解決自身問題。不妨把它看作一個Docker工具箱。 6.1 保持陣型如果你跟我們一樣(并且有心關(guān)注本書),那么你對Docker的依賴將會與日俱增,這也就意味著會在選定的宿主機上啟動大量容器,然后下載更多的鏡像。 隨著時間的流逝,Docker將會消耗越來越多的資源,一些容器和卷的清理需要擺上日程。我們將會展示怎么做以及為什么這樣做。我們也會介紹一些用來保持Docker環(huán)境干凈整潔的可視化工具,以便讓不喜歡敲命令行的用戶可以從中解脫出來。 正在運行的容器都很好,但是用戶很快會發(fā)現(xiàn)自己想要的不僅僅是在前臺啟動一個單條命令。我們會一起來看看如何在不“殺死”該容器的前提下退出一個正在運行的容器,還會看看如何在一個正在運行的容器內(nèi)執(zhí)行命令。 技巧41 運行Docker時不加sudoDocker守護進(jìn)程以root用戶身份在機器的后臺運行,這給了它莫大的權(quán)力,同時它又是對你,即終端用戶開放的。需要使用sudo是一個結(jié)果,但是這樣做不太方便,而且也會造成一些第三方Docker工具無法使用。 問題想要無須sudo便可以執(zhí)行docker命令。 解決方案官方解決方案是把自己加到docker組。Docker通過一個用戶組圍繞著Docker Unix域套接字來管理權(quán)限。為安全起見,發(fā)行版默認(rèn)不會將用戶加到該用戶組里,因為這樣做會開放系統(tǒng)完整的root訪問權(quán)限。 把自己加到該用戶組后,用戶便能以自己的身份使用docker命令: $ sudo addgroup -a username docker 重啟Docker然后完全注銷并再次登錄,或者更簡單點,重啟機器?,F(xiàn)在執(zhí)行Docker命令時不用再留意鍵入sudo或設(shè)置別名了。 討論對于本書后面部分用到的一系列工具來說,這是一項極其重要的技巧。一般來說,任何想要和Docker通信的對象(無須在容器里啟動)都需要能夠訪問Docker套接字,這需要使用sudo或者使用本技巧里提到的設(shè)置。技巧76里引入的Docker Compose是Docker公司的官方工具,也是這類工具的一個示例。 技巧42 清理容器Docker新手經(jīng)常抱怨的一點便是,在短時間內(nèi),用戶可能在系統(tǒng)上殘留許多不同狀態(tài)的容器,而且沒有一個標(biāo)準(zhǔn)工具通過命令行管理這些容器。 問題想要清理系統(tǒng)上的殘留容器。 解決方案設(shè)置一個別名來執(zhí)行清理舊容器的命令。這里最簡單的辦法是刪除所有容器。顯然,這是一個有風(fēng)險的方案,只應(yīng)在確定這是預(yù)期行為的時候使用。下列命令將會刪除宿主機上的所有容器。
簡單介紹一下xargs命令,它會獲取輸入的每一行內(nèi)容,并將它們?nèi)孔鳛閰?shù)傳遞給后續(xù)命令。為了防止報錯,我們這里傳入了一個額外參數(shù)--no-run-if-empty,這可以避免在前面的命令完全沒有輸出的情況下執(zhí)行該命令。 如果有正在運行的容器想要保留,但是又想刪除所有已經(jīng)退出的容器,那么不妨過濾一下docker ps命令返回的條目: docker ps -a -q --filter status=exited | \ ?--- --filter標(biāo)志會告知docker ps命令想要返回的容器。在這種情況下限制成狀態(tài)為已經(jīng)退出的那些容器。也可以選擇處于正在運行中或者正在重啟狀態(tài)的容器
xargs --no-run-if-empty docker rm ?--- 這次不用再強行刪除容器,因為根據(jù)給定的過濾參數(shù),它們本身就不應(yīng)該處于運行狀態(tài) 事實上,刪掉所有已停止的容器是一個很常見的用例,為此Docker專門添加了一條命令:docker container prune。然而,這條命令僅限于該用例,要進(jìn)行任何更復(fù)雜的操作,仍然需要回過頭來參考本技巧里介紹的命令。 作為更高級用例的示范,下列命令將會列出所有返回非零錯誤碼的容器。如果系統(tǒng)上有許多容器,用戶想要自動檢查并刪除那些異常退出的任意容器,就可能需要這樣做:
上述示例相對比較復(fù)雜,但是它展示了將不同的工具命令組合在一起的威力。它會輸出所有已停止的容器的ID,然后挑出那些非0退出碼的容器(即那些以異常方式退出的容器)。如果讀者還在努力理解這個用法,不妨先單獨執(zhí)行每條命令,然后理解它們的含義,這樣有助于了解整個過程。 像這樣的命令可以用來在生產(chǎn)環(huán)境里采集容器信息。用戶可能想要對它做些調(diào)整,改為執(zhí)行一個cron定時任務(wù)來清除正常退出的容器。 將單行代碼包裝成命令 可以給命令設(shè)置別名,以便在登錄到宿主機后更容易操作。為了達(dá)成這一點,需要在~/.bashrc文件里添加如下代碼: alias dockernuke='docker ps -a -q | xargs --no-run-if-empty docker rm -f' 然后,在下一次登錄時,從命令行執(zhí)行dockernuke,將刪除在系統(tǒng)上找到的任何Docker容器。 我們發(fā)現(xiàn)這樣做節(jié)省的時間是相當(dāng)可觀的。但是要小心!這種方式同樣也非常容易誤刪生產(chǎn)環(huán)境的容器,我們可以證明。即使足夠小心,不去刪除正在運行的容器,仍然可能會誤刪那些沒有運行但仍然有用的純數(shù)據(jù)容器。 討論本書介紹到的許多技巧的最終目的都是創(chuàng)建容器,尤其是在技巧76介紹到的Docker Compose以及有關(guān)編排的章節(jié)里——畢竟,編排都是關(guān)于如何管理多個容器的。用戶也許會發(fā)現(xiàn)這里討論到的命令用于清理機器(本地或遠(yuǎn)程)很有價值,在完成每個技巧后可以獲得一個全新的環(huán)境。 技巧43 清理卷盡管卷是Docker提供的一個強大功能,與之伴隨而來的也有一些顯著的運維缺陷。由于卷可以在不同的容器之間共享,因此在掛載它們的容器被刪除時無法清空這些卷。試想一下圖6-1中描述的場景。 圖6-1 當(dāng)容器被刪除時/var/db下會發(fā)生什么 “簡單!”你可能會這樣想,“在最后一個引用的容器被刪除時把卷刪掉不就行了!”事實上,Docker可以采取這種手段,這也是垃圾回收式編程語言從內(nèi)存中刪除對象時所采用的方法:當(dāng)沒有其他對象引用它時,它便可以被刪除。 但是Docker認(rèn)為這可能會讓人們不小心丟失重要的數(shù)據(jù),而且最好把是否在刪除容器的時候刪除卷的決定權(quán)交給用戶。這樣做帶來的一個不幸的副作用便是,默認(rèn)情況下,卷會一直保留在Docker守護進(jìn)程所在的宿主機磁盤上,直到它們被手動刪除。 如果這些卷填滿了數(shù)據(jù),磁盤可能會被裝滿,因此最好關(guān)注一下管理這些孤立卷的方法。 問題掛載到宿主機上的孤立Docker卷用掉了大量的磁盤空間。 解決方案在調(diào)用docker rm命令時加上-v標(biāo)志,或者如果忘記了,使用docker volumes子命令來銷毀它們。 在圖6-1描述的場景中,如果在調(diào)用docker rm時總是加上-v標(biāo)志可以確保/var/db最后被刪除掉。-v標(biāo)志會將那些沒有被其他容器掛載的關(guān)聯(lián)卷一一刪除。幸好,Docker很聰明,它知道是否有其他容器掛載該卷,因此不會出現(xiàn)什么意外尷尬的情形。 最簡單的方式莫過于養(yǎng)成在刪除容器時加上-v標(biāo)志這樣的好習(xí)慣。這樣可以保留對容器是否刪除卷的控制權(quán)。而這種做法的問題在于用戶可能不想每次都刪除卷。如果用戶正在寫入大量數(shù)據(jù)到這些卷,極有可能不希望丟失這些數(shù)據(jù)。此外,如果養(yǎng)成了這樣的習(xí)慣,很有可能就會變成自動的了,而用戶將會在刪除某些重要東西之后才反應(yīng)過來,但為時已晚。 在這類情況下,用戶可以使用一個經(jīng)過許多人抱怨并且涌現(xiàn)出眾多第三方解決方案之后添加到Docker的命令:docker volume prune。這條命令將會刪除所有未使用的卷:
如果想要跳過提示確認(rèn)步驟,也許可以用一個自動化腳本,在執(zhí)行docker volume prune時帶上-f選項來跳過這一步。
討論刪除卷可能不是需要經(jīng)常執(zhí)行的操作,因為容器里的大文件通常是從宿主機掛載的,并不會存放在Docker數(shù)據(jù)目錄里。但是值得大約每周清理一次,避免它們堆積,尤其是當(dāng)你使用技巧37里的數(shù)據(jù)容器時。 技巧44 無須停止容器,從容器里解綁使用Docker時,你常常會發(fā)現(xiàn)自己打開了一個交互式shell,但是一旦退出shell,容器便會被終止,因為它是容器的主進(jìn)程。幸運的是,有辦法可以做到和一個容器解綁(而且,如果愿意,還可以用dockerattach命令再連到容器里) 問題想要退出一個容器的交互會話,同時不停掉它。 解決方案使用Docker內(nèi)置的按鍵組合從容器里退出。Docker很有建設(shè)性地實現(xiàn)了一個不太可能被其他應(yīng)用使用也不太可能被意外按到的按鍵組合。 假設(shè)我們執(zhí)行docker run -t -i -p 9005:80 ubuntu /bin/bash命令啟動了一個容器,然后用apt-get安裝了一個Nginx Web服務(wù)器。我們想通過一個快捷的到localhost:9005的curl命令來測試該Web服務(wù)器能否在宿主機上被訪問到。 先按組合鍵Ctrl+P然后再按組合鍵Ctrl+Q。注意,不是3個鍵一起按!
討論如技巧2所述,如果我們之前已經(jīng)啟動了一個容器,卻忘了在后臺啟動,本技巧會很有用。如果想檢查容器的運行情況或提供一些輸入,它還允許用戶和容器自由地綁定和解綁。 技巧45 使用Portainer管理Docker守護進(jìn)程在演示Docker時,很難表現(xiàn)出容器和鏡像之間的差異——從終端里的輸出看不出來。此外,如果想要從多個容器里殺掉并刪除一個特定的容器,Docker命令行工具對于這種場景也不太友好。創(chuàng)建一個即點即用的工具來管理宿主機上的鏡像和容器可以解決這個問題。 問題想要不通過命令行管理宿主機上的容器和鏡像。 解決方案試試Portainer,這是一款由Docker核心貢獻(xiàn)者之一開發(fā)的工具。Portainer的前身是DockerUI。由于沒有先決條件,可以直接跳到執(zhí)行步驟: $ docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock portainer/portainer -H unix:///var/run/docker.sock 執(zhí)行上述命令將會在后臺啟動一個portainer容器。如果現(xiàn)在訪問 http://localhost:9000 ,可以在看板上看到機器上運行的Docker的簡要信息。 容器管理功能可能是這里面最有用的部分之一 ——轉(zhuǎn)到“Containers”頁面,我們會看到正在運行的容器列表(包括portainer容器本身),還提供選項可以展示所有容器。在這里,你可以對容器執(zhí)行批量操作(如殺掉它們),或者點擊一個容器的名字,深入了解該容器的詳細(xì)信息,而且可以執(zhí)行該容器相關(guān)的一些單個操作。例如,可以看到刪除一個正在運行的容器的選項。 “Images”頁面看起來和“Containers”頁面非常相似,并且還允許選擇多個鏡像然后執(zhí)行一些批量操作。點擊鏡像的ID會提供一些有趣的選項,比如基于該鏡像創(chuàng)建一個容器以及給鏡像打標(biāo)簽等。 記住,Portainer可能會落后于Docker官方提供的功能——如果想要使用最新最強大的功能,那么可能不得不選擇命令行。 討論Portainer是Docker眾多的圖形工具里的其中一款,也是這里面最受歡迎的,擁有眾多功能并且持續(xù)迭代的工具之一。舉個例子,你可以使用它來管理遠(yuǎn)程機器,也許會是技巧32里在這些機器上啟動容器之后用到。 技巧46 生成Docker鏡像的依賴圖Docker的文件分層系統(tǒng)是一個非常強大的理念,它可以節(jié)省空間,而且可以讓軟件的構(gòu)建變得更快。但是一旦啟用了大量的鏡像,便很難搞清楚鏡像之間是如何關(guān)聯(lián)的。docker images -a命令會返回系統(tǒng)上所有鏡像層的列表,但是對于理解它們之間的關(guān)聯(lián)關(guān)系而言,這不是一個友好的方式——使用Graphviz可以更方便地通過創(chuàng)建一個鏡像樹并做成鏡像的形式來可視化鏡像之間的關(guān)系。 這也展示了Docker在把復(fù)雜的任務(wù)變得簡單方面的強大實力。在宿主機上安裝所有的組件來生產(chǎn)鏡像時,老的方式可能會包含一長串容易出錯的步驟,但是對Docker來說,這就變成了一條相對失敗較少的可移植命令。 問題想要以樹的形式將存放在宿主機上的鏡像可視化。 解決方案使用一個我們之前創(chuàng)建的鏡像(基于CenturyLink Labs的一個鏡像)配合這項功能輸出一個PNG圖片或者獲取一個Web視圖。此鏡像包含了一些使用Graphviz生成PNG圖片文件的腳本。 本技巧使用的Docker鏡像放在dockerinpractice/docker-image-graph。時間長了該鏡像可能會過期然后停止工作,可以通過執(zhí)行代碼清單6-1中的命令確保生成最新的鏡像。 代碼清單6-1 構(gòu)建一個最新的docker-image-graph鏡像(可選)
在run命令里需要做的就是掛載Docker服務(wù)器套接字,然后一切便準(zhǔn)備就緒,如代碼清單6-2所示。 代碼清單6-2 生成一個鏡像的層樹 $ docker run --rm \ ?--- 在生成鏡像之后刪除容器
-v /var/run/docker.sock:/var/run/docker.sock \ ?--- 掛載 Docker 服務(wù)器的Unix 域套接字,以便可以在容器里訪問Docker服務(wù)器。如果已經(jīng)更改了Docker守護進(jìn)程的默認(rèn)配置,這將不會奏效
dockerinpractice/docker-image-graph > docker_images.png ?--- 指定一個鏡像然后生成一個PNG圖片作為制品 圖6-2以PNG圖片形式展示了一臺機器的鏡像樹。從這張圖片可以看出,node和golang:1.3鏡像擁有一個共同的根節(jié)點,然后golang:runtime只和golang:1.3共享全局的根節(jié)點。類似地,mesosphere鏡像和ubuntu-upstart鏡像也是基于同一個根節(jié)點構(gòu)建的。 讀者可能會好奇這棵樹上的全局根節(jié)點是什么。它是一個叫作scratch的偽鏡像,實際上大小為0字節(jié)。 討論在構(gòu)建更多的Docker鏡像時,也許作為第9章里持續(xù)交付的一部分,跟蹤一個鏡像的歷史以及它所基于的內(nèi)容可能會很麻煩。如果試圖通過共享更多層精簡鏡像大小的方式來加快交付速度,這一點尤為重要。定期拉取所有鏡像并生成圖譜是一個追蹤的好辦法。 圖6-2 一棵鏡像樹 技巧47 直接行動:在容器上執(zhí)行命令在Docker早期,許多用戶會在他們的鏡像里添加SSH服務(wù),這樣一來便可以從外部通過shell來訪問它們。Docker不主張這樣做,它認(rèn)為這相當(dāng)于把容器當(dāng)成一臺虛擬機(而我們知道,容器并不是虛擬機),并且這給本不應(yīng)該需要它的系統(tǒng)帶來了額外的進(jìn)程開銷。很多人對此持反對意見的原因在于,一旦容器啟動了,沒有一個簡便的辦法進(jìn)到容器里面。結(jié)果便是,Docker引入了exec命令,它是一個更優(yōu)雅地解決干涉和檢索啟動后的容器內(nèi)部問題的解決方案。我們這里也將討論此命令。 問題想要在一個正在運行的容器里執(zhí)行一些命令。 解決方案使用dockerexec命令。 下列命令會在后臺(帶上-d標(biāo)志)啟動一個容器,然后告訴它一直休眠(不做任何事情)。我們把這條命令命名為sleeper。
現(xiàn)在已經(jīng)啟動了一個容器,可以用Docker的exec命令對它執(zhí)行一些操作。該命令可以看成有3種基本模式,如表6-1所示。 表6-1 Docker exec 模式 我們先介紹一下基本模式。下列命令在sleeper容器內(nèi)部執(zhí)行了一個echo命令。 $ docker exec sleeper echo 'hello host from container'
hello host from container 注意,該命令的結(jié)構(gòu)和dockerrun命令非常相似,但是把鏡像ID替換成一個正在運行的容器的ID。echo命令指代的是容器里面的echo二進(jìn)制文件,而非容器外部的。 守護進(jìn)程模式會在后臺執(zhí)行命令,用戶無法在終端看到輸出結(jié)果。這可能適用于一些常規(guī)的清理任務(wù),在這些任務(wù)中,你希望敲完即走,如清理日志文件。
最后,我們來試試交互模式。這種模式允許用戶在容器里執(zhí)行任何想要執(zhí)行的命令。要啟用這一功能,通常需要指定用來在運行時交互的shell,在如下代碼里便是bash: $ docker exec -i -t sleeper /bin/bash
root@d46dc042480f:/# -i和-t參數(shù)同我們所熟悉的dockerrun做著相同的事情——它們會讓命令成為可交互的,然后設(shè)置一個TTY設(shè)備,以便shell可以正常工作。在執(zhí)行該命令后,用戶便拿到了一個在容器里運行的命令提示符。 討論當(dāng)出現(xiàn)問題或者想要弄清楚容器在做什么時,跳到容器里是必不可少的調(diào)試步驟。往往不太可能使用技巧44里提到的綁定和解綁方法,因為容器內(nèi)的進(jìn)程通常運行在前臺,無法訪問shell提示符。由于exec允許用戶指定想要運行的二進(jìn)制文件,這便不再是問題……只要容器文件系統(tǒng)上實際存在那份想要運行的二進(jìn)制文件即可。 特別的是,如果你使用技巧58創(chuàng)建一個帶有單個二進(jìn)制文件的容器,那么將無法啟動shell。在這種情況下,可能想堅持采用技巧57作為允許exec執(zhí)行的低開銷辦法。 技巧48 你在容器里嗎在創(chuàng)建容器時,通常會把運行邏輯放到一個shell腳本里,很少會嘗試直接在Dockerfile里編寫腳本。又或者,你可能在容器運行時用到了各種腳本。無論哪種方式,這些執(zhí)行的任務(wù)通常都需要經(jīng)過仔細(xì)定制,以便能夠運行在容器里,并且運行在一臺“常規(guī)”機器上可能會搞破壞。在這種情況下,設(shè)置一些安全防護,防止在容器外部意外執(zhí)行是很有用的。 問題用戶代碼需要知道是否是在一個Docker容器里操作。 解決方案檢查/.dockerenv文件是否存在。如果存在,那么很可能在一個Docker容器里。 注意,這并不是100%確定的——如果任何人或任何事物把/.dockerenv文件刪掉,這個檢查就會給出誤導(dǎo)性的結(jié)果。這些情況不太可能發(fā)生,但是最壞的情況便是用戶得到錯誤的診斷結(jié)果而沒有不良影響。用戶會認(rèn)為自己不在Docker容器里,并且在最壞的情況下不會運行潛在的破壞性代碼。 一個更現(xiàn)實的情況是,在較新的Docker版本里(或者使用的是實現(xiàn)這一行為之前的版本)已經(jīng)更改或刪除了這種未記錄的Docker行為。 這些代碼可能是啟動bash腳本的一部分,如代碼清單6-3所示,其后是剩余的啟動腳本代碼。 代碼清單6-3 如果在容器外運行,如下shell腳本會運行失敗
當(dāng)然,如有需要,可以使用相反的邏輯來確認(rèn)自己是不是運行在容器外面,如代碼清單6-4所示。 代碼清單6-4 如果在容器里運行,如下shell腳本會運行失敗 #!/bin/bash
if [ -f /.dockerenv ]
then
echo 'In a Docker container, exiting.'
exit 1
fi 上述示例使用bash命令來確認(rèn)文件是否存在,但是絕大多數(shù)編程語言有自己的辦法來確認(rèn)容器(或宿主機)文件系統(tǒng)里是否存在某些文件。 討論用戶可能想知道這種情況多久出現(xiàn)一次。作為一個時常討論的話題,它經(jīng)常出現(xiàn)在Docker論壇里,關(guān)于這是否是一個有效的用例,又或者是應(yīng)用程序設(shè)計方面存在其他問題,這塊仍然存在爭議。 撇開這些爭議不提,用戶很容易陷入需要根據(jù)自己是否在Docker容器里來切換代碼路徑的情況。我們經(jīng)歷過的一個這樣的例子便是使用Makefile來構(gòu)建一個容器。 6.2 小結(jié)
本文摘自《Docker實踐(第2版)》
1.暢銷Docker容器實踐教程升級版,編寫時參考的Docker版本是Docker 1.13; 本書由淺入深地講解了Docker的相關(guān)內(nèi)容,涵蓋從開發(fā)環(huán)境到DevOps流水線,再一路到生產(chǎn)環(huán)境的整個落地過程以及相關(guān)的實用技巧。書中介紹Docker的核心概念和架構(gòu),以及將Docker和開發(fā)環(huán)境有機、高效地結(jié)合起來的方法,包括背Docker用作輕量級虛擬機、構(gòu)建容器、宿主機編排、配置管理、精簡鏡像等。不僅如此,本書還通過“問題-解決方案-討論”的形式,將Docker如何融入DevOps流水線、如何在生產(chǎn)環(huán)境落地等一系列難題拆解成114個相關(guān)的實用技巧,為讀者提供解決方案以及一些細(xì)節(jié)和技巧方面的實踐經(jīng)驗。閱讀本書,讀者學(xué)到的不只是Docker,還包括持續(xù)集成、持續(xù)交付、構(gòu)建和鏡像管理、容器編排等相關(guān)領(lǐng)域的一線生產(chǎn)經(jīng)驗。本書編寫時一些案例參考的Docker版本是Docker 1.13。 |
|