:容器網絡與數據卷管理)
有一陣子我總在半夜被電話叫醒不是這個服務掛掉就是兩個服務互相連不上。后來排查來排查去問題基本都出在兩塊Docker容器網絡管理沒規(guī)劃好容器數據卷管理沒設計明白。說白了容器網絡管的是容器與容器、容器與外界之間的通信鏈路數據卷管的是容器內數據的生命周期這兩件事搞不定容器化項目跑起來就是個隨時會響的警報器。這篇文章不是擺一堆理論名詞而是把我在實際項目里反復踩過的坑、驗證過的方案按照從原理到實操的順序一次說透。適合剛接觸Docker的人也適合那些已經用了一段時間、但一遇到網絡和數據問題就頭疼的開發(fā)者。1. 先把容器網絡的底層邏輯捋清楚1.1 容器是怎么上網的很多剛接觸Docker的人會把容器當成一臺輕量虛擬機但網絡模型其實差別很大。虛擬機是完整的系統(tǒng)鏡像自帶網卡、IP、路由表而容器本質上是一組共享內核的進程默認情況下它在網絡層面是租了宿主機的一套基礎設施。Docker容器啟動時會為每個容器創(chuàng)建一個獨立的網絡命名空間。這個命名空間可以理解為一張隔離的網絡配置表里面記錄了它自己的網卡、IP、路由、防火墻規(guī)則。默認bridge模式往容器里插的是一對veth虛擬網卡一頭在容器內部叫eth0另一頭掛在宿主機一個叫docker0的虛擬網橋上。這個veth pair很像一根虛擬網線一端插進容器一端插進網橋數據就是從這根網線流出來的。容器里的數據包到達docker0網橋之后如果是容器之間互相通信網橋直接二層轉發(fā)就搞定了。如果是容器要訪問外網就會走一條NAT規(guī)則將容器私有IP轉換成宿主機IP發(fā)出去外網的包響應回來時NAT再把源地址替換回容器IP。這一套機制和家里路由器做地址轉換的原理幾乎一樣容器就是一個被NAT包裹著的內網主機。所以排查網絡問題時腦子里要有這條鏈路容器網卡 → veth pair → docker0網橋 → 宿主機路由/NAT → 物理網卡。鏈路里任何一環(huán)出問題表象就是ping不通連不上但原因可能完全不同。我在線下培訓時經常讓人先畫這條數據流畫清楚了一半問題其實就解決了。1.2 四種網絡模式選錯了就是給自己挖坑Docker網絡模式主要就四種默認bridge、host、none、container。每種的原理和適用場景差別很大我直接用一張表把它們的特性列出來。網絡模式獨立網絡棧端口映射外部訪問典型場景bridge默認有獨立IP支持 -p 映射通過宿主機端口訪問單機多容器隔離部署host無獨立IP共享宿主機網絡不支持直接用宿主機IP和端口性能敏感型或依賴真實IP的應用none只有回環(huán)地址不支持不提供網絡默認不需要網絡的離線任務container共享指定容器的網絡棧不支持隨被共享容器sidecar模式、日志采集容器大多數Web后端服務、數據庫容器默認用bridge模式沒有大問題它隔離性夠好每個容器都有自己獨立的IP。尤其是一個宿主機跑多個互不干擾的容器時bridge是唯一合理的默認選擇。但你如果圖省事所有容器都丟到默認的docker0網橋上會遇到一個很煩人的問題大家靠IP地址互相訪問容器重建一次IP就變了配置文件里寫死的IP就成了廢紙。所以實際項目里我?guī)缀醵际亲约簞?chuàng)建外部bridge網絡這個后面專門講。host模式我特別提醒一句性能確實好省掉了NAT轉換的損耗但代價是容器和宿主機共享一份網絡棧容器里看到的網卡、IP、路由表和宿主機完全一樣。兩個容器如果同時監(jiān)聽同一個端口直接沖突。更隱蔽的風險是host模式下容器內的進程能嗅探宿主機的網絡流量隔離安全級別大幅下降。我在生產環(huán)境只用host模式跑一種東西對網絡延遲極其敏感的壓測工具或數據采集探針業(yè)務服務一律不用。none模式很好理解容器只有回環(huán)網卡不分配任何網絡能力適合做純計算任務比如批量數據處理、編譯任務。container模式則是在容器啟動時指定共享另一個容器的網絡棧常用于sidecar模式例如讓日志采集容器和業(yè)務容器共享網絡棧這樣采集器訪問localhost就能摸到業(yè)務容器。一個原則是默認選bridge特殊需求才考慮其他模式。2. 網絡實操從容器互通到端口映射的完整流程2.1 自定義bridge網絡讓容器用名字互訪默認bridge網絡有個很大的短板它不支持基于容器名的DNS解析兩個容器只能靠各自IP訪問。一旦容器重建、IP漂移你猜地址、改配置、再重啟整個鏈路就是脆弱的。Docker自帶的--link參數能緩解這個問題但官方早就把它標記為過時功能了只做單機環(huán)境下的兼容保留。實際項目中我更推薦的做法是創(chuàng)建自定義bridge網絡。自定義網絡內部自帶一個內置DNS服務只要容器在同一個自定義網絡里就可以直接用容器名互相解析。這意味著應用配置里只需要寫服務名比如db:3306而不是某個寫死的IP。這個能力是Docker默認網橋不提供的很多人不知道結果白白繞了很多彎。創(chuàng)建一個自定義網絡的命令很簡單docker network create --driver bridge --subnet172.20.0.0/24 --gateway172.20.0.1 my-net--subnet和--gateway是我習慣顯式指定的。如果不指定Docker會從它自己的地址池里自動挑一個網段這容易在宿主機上和其他網段沖突也會讓你在排查時不知道IP段到底是多少。顯式指定后網絡拓撲一目了然。再分別啟動兩個容器并接入這個網絡docker run -d --name app-server --network my-net nginx docker run -d --name app-db --network my-net mysql:8.0此時進入app-server容器直接用容器名去ping app-db就能看到它解析出172.20.0.x的地址docker exec app-server ping app-db只要這一步通了應用代碼里連接數據庫的地址寫成app-db:3306就完全沒問題。這個能力是后續(xù)做服務編排的基石不管是手動跑容器還是后面用docker compose我都建議先建立自定義網絡再掛容器。有一點需要提醒自定義網絡和默認docker0網橋是不互通的。如果你有些容器掛在默認bridge網絡另一些掛在自定義網絡它們之間默認互相訪問不通。解決辦法有兩種要么把需要互通的容器都掛到同一個網絡里要么用docker network connect給容器多接一塊網卡讓它同時屬于兩個網絡。我在一個項目里就是靠這招把老容器平滑接入新網絡的不用重建容器運行中就能加網。# 讓某個已運行的容器追加接入新網絡 docker network connect my-net app-server這種網絡規(guī)劃方式的好處是環(huán)境遷移時只需要把網絡定義和應用配置一起帶走不用再關心IP變化問題。建議每個獨立項目單獨建一個網絡別把所有容器糊在一張網里。我和一些同行交流時發(fā)現很多線上事故都是因為把所有容器丟進默認bridge導致網絡廣播風暴、IP沖突和權限混亂最后只能全部重建。2.2 端口映射常見坑-p 不只是打個洞容器默認有獨立IP外部訪問不到它所以需要端口映射把容器端口暴露到宿主機上。這條命令大家都會寫但它背后的行為往往被忽略docker run -d -p 8080:80 nginx意思是把宿主機的8080端口流量轉發(fā)到容器的80端口。實現機制是Docker在宿主機iptables里插入一條DNAT規(guī)則。宿主機收到外來請求后根據規(guī)則把目的地址換成容器IP再轉發(fā)進去。這里的細節(jié)是規(guī)則是動態(tài)寫入iptables的有些云平臺控制臺里看不到這些規(guī)則也不建議去直接操作iptables和Docker聯(lián)動區(qū)域否則很容易被Docker自己的規(guī)則覆蓋或者互相沖突。如果你不希望服務被所有人都能訪問比如數據庫端口暴露給本機的一些管理工具用建議綁定回環(huán)地址docker run -d -p 127.0.0.1:8080:80 nginx這樣宿主機以外的地方訪問不到你這個服務安全層面相當于給端口加了一道門禁。我在做本地開發(fā)聯(lián)調時經常這么干既能使用工具連數據庫又不怕端口暴露到外網。還有兩個高頻坑我必須講。第一EXPOSE不算端口映射。Dockerfile里的EXPOSE 80只是容器鏡像的元數據聲明它不代表宿主機任何端口會被打開。真正對外暴露是-p或者docker-compose里的ports配置做的兩者可以同時存在但作用完全不同。第二使用host模式時-p是無效的因為host模式容器直接用宿主機端口再用-p映射會直接提示端口沖突或行為異常排查時要先確認網絡模式再判斷映射問題。端口映射后的防火墻狀態(tài)也要注意。Docker在啟動容器時會自動調整iptables但如果你在宿主機上另外運行了防火墻管理工具可能在容器重建或重啟Docker服務時丟失映射規(guī)則表現為剛部署好能訪問重啟后又連不上了。這個情況我在現場調試時遇到過多次排查思路是不要只盯著容器狀態(tài)要去看iptables規(guī)則是否存在。凡是遇到容器看起來正常端口卻不通第一件事就是查規(guī)則。3. 數據卷管理讓數據不再跟著容器一起消失3.1 bind mount、數據卷、tmpfs到底該用哪個容器是immutable的鏡像層只讀容器層寫的數據在容器刪除后默認就沒了。容器數據卷管理就是為了解決這個容器消失數據也消失的問題。Docker提供了三種數據持久化方案綁定掛載、具名數據卷、tmpfs它們各自定位完全不同。類型位置生命周期備份恢復適合場景bind mount宿主機任意目錄/文件宿主機控制直接打包目錄配置文件、開發(fā)調試代碼數據卷Docker管理的宿主機目錄Docker控制--volumes-from或拷貝目錄生產環(huán)境數據庫、文件存儲tmpfs宿主機內存容器重啟即丟失不需要臨時緩存、密鑰等敏感數據綁定掛載最簡單就是把宿主機路徑整個塞進容器比如把項目的配置目錄/etc/nginx/conf.d直接掛進去。這種方式的優(yōu)點是路徑直觀改宿主機的文件容器里立即生效缺點是宿主機目錄權限、SELinux標簽等Linux安全機制都會影響容器內訪問需要自己處理容器用戶和宿主目錄的所有權匹配問題。常見的一個坑是MySQL容器用bind mount裸掛一個空目錄結果MySQL因為目錄權限不足直接拒絕啟動原因往往是這個目錄的owner不是容器內的mysql用戶。生產環(huán)境我更推薦具名數據卷。它由Docker自動創(chuàng)建和管理數據存放在/var/lib/docker/volumes/卷名/_data下你不用關心具體路徑Docker會處理好權限和生命周期。刪除容器時只要不加-v參數數據卷仍然保留下次再啟動新容器掛同一個卷數據就還在。具名數據卷配合docker-compose里的volumes聲明可以把整個存儲拓撲固化下來遷移時非常方便。tmpfs比較特殊它把數據放在宿主機內存里讀寫速度極快但容器一停數據就丟。它不是用來保存用戶數據的而是用來放臨時緩存或者運行時產生的敏感信息比如內存中的Token、密鑰避免落到磁盤留下痕跡。比如我在跑一些數據處理任務時會用--tmpfs /tmp既提升了I/O性能又不用反復清理容器臨時文件。3.2 MySQL容器落盤與備份恢復的完整實驗我拿數據庫容器作為例子因為這是最容易出事故的場景。假設要部署一個MySQL 8.0容器數據必須持久化否則容器一重建訂單表就沒了后果不堪設想。第一步創(chuàng)建具名數據卷docker volume create mysql-data第二步啟動容器把數據卷掛載到MySQL的數據目錄/var/lib/mysqldocker run -d --name app-db -e MYSQL_ROOT_PASSWORDyourpass --network my-net --mount sourcemysql-data,target/var/lib/mysql mysql:8.0這里建議用--mount語法而不是-v。--mount能顯式聲明源和目的地可讀性更好排查時不容易搞混。啟動后MySQL會在數據卷目錄里初始化系統(tǒng)表生成第一個快照結構。之后無論容器怎么刪、怎么重建只要這個卷還在數據就都還在。備份要分兩個層面。邏輯備份用mysqldump導出SQL文件物理備份直接拷貝數據卷目錄或做快照。我習慣是邏輯備份走定時任務物理備份走整盤快照。一條經典命令是docker run --rm -e MYSQL_PWDyourpass --network my-net mysql:8.0 mysqldump -h app-db -u root mydb mydb_backup.sql這個命令用同一個鏡像跑一個臨時容器通過剛才建立的網絡去訪問數據庫容器導出完成后臨時容器自動刪除。注意--network my-net不能漏如果不指定網絡臨時容器走默認bridge它連不到數據庫容器所在的自定義網絡會報連接拒絕?;謴土鞒桃踩菀撞瓤印5谝环N方式是用SQL文件恢復新起的空容器掛載的數據卷里沒有數據執(zhí)行mysql -h app-db -u root -p mydb_backup.sql導入即可。第二種方式是物理恢復停掉數據庫容器用新容器掛載恢復過的備份目錄比如docker run --rm --volumes-from 原有數據庫容器 alpine tar -cf /backup/mysql-backup.tar /var/lib/mysql再掛載這個備份目錄啟動容器。但我這里要特別提示物理備份恢復時MySQL的系統(tǒng)表和數據文件版本必須匹配新容器鏡像版本改動過大時直接掛老目錄容易報多個MySQL版本不兼容的錯誤。日常備份用mysqldump容災恢復才考慮物理快照。數據卷還有一個生命周期手感問題docker compose down默認不會刪數據卷docker compose down -v才會刪。很多人跑容器編排時不知道這個區(qū)別一條down -v把整個數據庫數據全卷沒了。如果要用這條命令一定三思最好在腳本里加個數據卷備份確認環(huán)節(jié)。4. 網絡與數據卷的聯(lián)合排障記錄4.1 高頻問題排查速查表網絡和數據卷問題看起來五花八門實際上高頻場景就那么幾種。我自己整理了一個速查表遇到問題先照表判斷能省掉大量重復排查時間。癥狀可能原因排查命令解決方向容器間ping不通不在同一網絡docker inspect 容器名查看Networks接入同一網絡容器名解析失敗掛在默認bridge確認是否自定義網絡改用自定義網絡宿主機外訪問超時端口映射未綁定公網IP檢查-p參數用-p 0.0.0.0:8080:80或檢查防火墻容器重啟后數據還在但服務連不上容器IP變了應用配置寫死IPdocker inspect看當前IP改成容器名/服務名訪問刪容器后數據沒了未使用數據卷docker volume ls看卷是否存在改用具名卷掛載目錄后服務權限報錯宿主目錄屬主不匹配容器用戶ls -ln查看UID/GIDchown或改用數據卷MySQL啟動反復重啟數據目錄權限或現有數據沖突docker logs查看錯誤單獨初始化或清空目錄這個表格里的每一個場景我都實際遇到過。最諷刺的一次是一個同事為了解決容器間IP老是變的問題居然自己寫了一套IP提取腳本定時把新IP寫進配置文件。這方案能跑但每次重啟都有一段時間服務不可用。后來我把兩個服務放進同一個自定義網絡配置直接寫服務名效果立竿見影。遇到問題先退一步想機制反而比著急打補丁更高效。4.2 我踩過坑后保留的三個習慣第一個習慣每個項目單獨建一個自定義網絡并且顯式指定子網。我會把網絡名、網關地址、子網范圍寫進部署文檔不管單機部署還是后續(xù)遷移這些參數都不用猜。容器啟動時不指定--network就是默認bridge默認bridge行為完全不可控這屬于能用但不好管的典型。第二個習慣所有需要持久化的數據目錄一律使用具名數據卷并在容器命名中帶上明確的用途標記。例如項目數據庫卷命名為xx-db-data緩存卷xx-redis-data。定期用docker volume ls查看無主卷再利用docker volume rm清理。無主卷是指已經沒有任何容器引用它的卷長時間累積會占用大量磁盤空間不要等到放不下才想起來清理。第三個習慣每次提交部署前先模擬一次容器刪除重建的操作。把容器停止、刪除再按原啟動命令重新拉起確認數據卷正常掛載、網絡名解析正常、服務能恢復訪問。這個演練成本很低但在關鍵業(yè)務上能救命。數據卷設計的本質就是肆無忌憚地刪容器反正數據還在網絡拓撲不變服務就能原地復活。如果你發(fā)現某個容器刪了就起不來那說明你的部署方案還沒合格問題越早暴露越省事。最后再多說一句網絡和數據卷本來就是一體兩面的。網絡讓容器之間能找到彼此數據卷讓容器刪了還能恢復如初。我個人的習慣是在動手跑第一個容器之前先把網絡拓撲畫一遍、把數據卷清單列一遍。容器沒想明白就先跑起來后面一定還債這是我在無數個深夜排障里攢下來的教訓。