
本文主要談談關於主機網絡和容器網絡的實現原理!
容器資源限制
在某些時候我們不想讓容器肆無忌憚的搶占系統資源,所以就會對其做一系列的限制,這些參數可以使用蠻力查看到:
docker container run --help
主要的限制參數包含以下這些:
--cpu-shares:CPU 使用占比,如一個容器配置 10,一個配置 20,另外一個配置 10,那就是資源分配:1:2:1。
--memory:限制內存,比如配置 200M,但是如果不配置 swap,則實際內存其實是 400M。
--memory-swap:限制 swap,結合內存限制使用。
虛擬機網絡名稱空間(選擇性了解)
docker 網絡隔離的實現方式其實就是網絡名稱空間的方式,但是這方面的知識其實對於就算是運維工程師也不一定有詳細的了解,所以這部分內容作為選擇性了解。簡單的對其實現方式說明,然后看看效果是怎樣的。
1. 建立兩個網絡名稱空間:
# 查看現有的網絡名稱空間 ip netns ls # 新建兩個網絡名稱空間 ip netns add net-ns-1 ip netns add net-ns-2 # 在兩個網絡名稱空間中分別查看它的網卡信息 ip netns exec net-ns-1 ip a ip netns exec net-ns-2 ip a
結果如圖:

可以看到各自擁有一張回環網卡,並且未啟動。我們可以將其啟動起來再查看:
# 分別啟動網絡名稱空間中的回環網卡 ip netns exec net-ns-1 ip link set dev lo up ip netns exec net-ns-2 ip link set dev lo up # 查看啟動效果 ip netns exec net-ns-1 ip a ip netns exec net-ns-2 ip a
結果如圖:

此時狀態變成 UNKNOW,這就相當於一個機器,你至少一頭差了網線,另外一端沒有插線的狀態。
2. 新建兩個虛擬網卡,並把它們綁定到命名空間中:
# 新建兩個網卡 ip link add veth-1 type veth peer name veth-2
結果如圖:

將網卡分別分配到兩個網絡名稱空間:
# 將網卡分別分配到指定的網絡名稱空間 ip link set veth-1 netns net-ns-1 ip link set veth-2 netns net-ns-2 # 查看當前虛擬機網卡信息 ip link # 查看指定網絡名稱空間的網卡信息 ip netns exec net-ns-1 ip link ip netns exec net-ns-2 ip link
結果如圖:

可以發現本機中已經不存在這兩張網卡了,但是之前建立網絡名稱空間中可以看到他們。
這樣的場景就類似於之前是一根網線的兩頭,我們把它拔了下來,一頭插到了第一個網絡名稱空間的網口上,一頭插到了另一個。
3. 給網口配置 IP 測試連通性:
# 配置 IP ip netns exec net-ns-1 ip addr add 192.168.1.1/24 dev veth-1 ip netns exec net-ns-2 ip addr add 192.168.1.2/24 dev veth-2 # 啟動網卡 ip netns exec net-ns-1 ip link set dev veth-1 up ip netns exec net-ns-2 ip link set dev veth-2 up # 查看網卡信息 ip netns exec net-ns-1 ip a ip netns exec net-ns-2 ip a
結果如下:

此時在網絡名稱空間之中測試網絡連通性:
ip netns exec net-ns-1 ping 192.168.1.2
結果如圖:

可以發現網絡名空間之中因為兩個網卡相當於連接通的了效果,但是和宿主機本身卻是無法通信的。
這樣就實現了 docker 的明明本機卻能夠做到網絡隔離的效果。當然 docker 並非單單如此。
容器的網絡原理
docker 的網絡相較於上文的網絡模式多了一個 docker0 網橋,我們可以將其當做交換機:

而且由於 iptables 的原因,容器內部還能夠上網!
查看容器的網絡:
docker network ls
結果如圖:

熟悉 VMware 的人就應該很熟悉這些:
bridge:橋接網絡,默認就是它,能夠獨立網段於宿主機本身網絡,但是又能和宿主機所在的網絡其它主機通信。
host:主機網絡,這意味着容器和宿主機將會共享端口,同樣的端口會導致端口沖突。
none:完全獨立,無法通信。
因為這三種網絡的關系,意味着本機通屬於一個 bridge 的容器是可以互相通信的。可以自己建立一個橋接網絡和兩個容器測試:
docker network create -d bridge my-bridge
結果如圖:

開兩個窗口,分別新建容器並加入到這個網絡:
# 窗口1 docker container run -it --name b3 --network my-bridge busybox /bin/sh # 窗口2 docker container run -it --name b4 --network my-bridge busybox /bin/sh
查看窗口1 IP:

查看窗口2 IP:

窗口1 測試通信:

窗口1 測試和宿主機通信:

可以發現能夠和其它的宿主機通信!但是無法和其它宿主機的容器通信,很簡單,因為他們是自己單獨的 IP,不說其它,單單是他們兩個就可能是一樣的 IP 地址。
同時,我們新開窗口查看網卡情況:

可以發現后面三個網卡其實就是類似於一個網絡連接,后面兩個都相當於一個網線,一頭會連接到 br-xxx 上面,一頭連接到容器中,所以它們能夠通信。
這個 br-xxx 其實就是我們創建網絡的時候出現的。至於如何和其它宿主機通信則是通過 iptables 的 nat 實現的。
查看網絡詳細信息:
docker network inspect my-bridge
結果如下:

可以看到哪些容器屬於該網絡!
容器網絡名稱解析
由於容器的 IP 是隨機的,所以在很多服務使用的時候我們不能再依賴容器的的 IP 來指望連接到指定的容器。
當然這並非不行,而是太麻煩而且不便於管理。那有沒有更好的辦法?有。
那就是我們在創建容器時會指定容器的名稱,我們看他通過解析這個名稱實現通信。
這里涉及到一個參數:--link,將一個容器連接到另外一個容器。
1. 新建兩個容器,然后分別從新窗口進入容器測試網絡通信:
# 創建容器 docker container run --name b1 -d busybox /bin/sh -c "while true;do sleep 3000;done" docker container run --name b2 -d busybox /bin/sh -c "while true;do sleep 3000;done" # 進入容器查看 IP 后測試網絡 docker container exec -it b1 /bin/sh
結果如圖:

可以發現 ping IP 能夠通信,但是 ping 容器名稱不行。
2. 新建第三個容器,使用 link 綁定查看:
# 創建容器 docker container run --name b3 --link b1 -d busybox /bin/sh -c "while true;do sleep 3000;done" # 進入容器 docker container exec -it b3 /bin/sh
測試連通性如圖:

可以看到在 b3 中能夠通過容器名稱 ping 通 b1,但是無法 ping 通 b2。
3. 進入 b1 中測試和 b3 的連通性:
docker container exec -it b1 /bin/sh
結果如圖:

無法通,由此可以得出一個結論:--link 在綁定的時候是單向解析的。
這樣就解決了我們想要通過容器名稱來實現服務之間的通信而不是依靠隨時都可能改變的 IP 地址。
容器端口映射
將容器內部我們需要的某個端口映射到宿主機,這算是一種解決辦法,因為宿主機的 IP 是固定的。
這里涉及到兩個參數:
-p:將宿主機的某個端口映射到容器的某個端口。
-P:將容器的所有端口隨機映射到宿主機的隨機端口。
1. 為了便於理解,這里還是以之前的 Flask 項目為例,不過這里我們改一下代碼,讓其復雜一點。
mkdir flask-redis-demo cd flask-redis-demo/ vim app.py
內容如下:
from flask import Flask from redis import Redis import os import socket app = Flask(__name__) redis = Redis(host=os.environ.get('REDIS_HOST', "127.0.0.1"), port=6379) @app.route("/") def hello(): redis.incr('hits') return 'Redis hits: %s, Hostname: %s' % (redis.get('hits'), socket.gethostname()) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)
其實就是 flask 通過用戶傳遞過來的變量找到 redis 的地址連接上去,然后獲取到 redis 的 hits 打印出來。
這就涉及到跨容器通信!
2. 編寫 Dockerfile:
FROM python:2.7 LABEL author="Dylan" mail="1214966109@qq.com" RUN pip install flask && pip install redis COPY app.py /app/ WORKDIR /app/ EXPOSE 5000 CMD ["python", "app.py"]
制作成為鏡像:
docker image build -t dylan/flask-redis-demo .
3. 先運行一個 redis 容器:
docker container run -d --name redis-demo redis
結果如圖:

4. 此時運行 flask 容器並連接到 redis-demo 容器:
docker container run -d --name flask-redis-demo --link redis-demo -e REDIS_HOST="redis-demo" dylan/flask-redis-demo
這里有個新增的參數:-e 傳遞變量給容器,結果如圖:

此時我們在容器中訪問測試:
docker container exec -it flask-redis-demo curl 127.0.0.1:5000
輸入如下:
Redis hits: 1, Hostname: 0a4f4560fa72
可以看到容器內部是正常的,並且 flask 容器可以去 redis 容器中獲取值。但是問題還是在於該地址只能容器內部訪問。
5. 新建容器,將端口隨機映射出來:
docker container run -d --name flask-redis-demo-1 -P --link redis-demo -e REDIS_HOST="redis-demo" dylan/flask-redis-demo
結果如圖:

可以看到宿主機的 1025 端口映射到了容器內部的 5000 端口,此時我們訪問本機 1025 端口測試:
curl 192.168.200.101:1025
結果如圖:

6. 新建容器,將容器端口映射到 8000 端口:
docker container run -d --name flask-redis-demo-2 --link redis-demo -e REDIS_HOST="redis-demo" -p 8000:5000 dylan/flask-redis-demo
結果如圖:

訪問測試:

至此,端口映射完成!
跨主機通信
雖然端口映射能夠解決我們一定量的問題,但是在面對某些復雜的情況的時候也會很麻煩。比如跨主機通信。
這就不是 link 能夠解決的問題,需要將所有用到的服務的端口都映射到宿主機才能完成操作!
那有沒有辦法將多個主機的容器做成類似於一個集群的樣子?這就是涉及容器的跨主機通信問題。
這一次我們會用到兩個虛擬機,192.168.200.101 和 102,都安裝好 docker。
在實現這個功能之前,我們需要了解到兩個東西:
overlay 網絡:相當於在兩個機器上面建立通信隧道,讓兩個 docker 感覺運行在一個機器上。
etcd:基於 zookeeper 的一個共享配置的 KV 存儲系統。其中 etcd 地址如下:
https://github.com/etcd-io/etcd/tags
在使用之前需要注意:
1. 服務器的主機名最好具有一定的意義,最簡單也要坐到唯一,同樣的主機名可能出現 BUG。
2. 確認關閉防火牆這些東西。
3. 能有 epel 源最好。
yum -y install epel-release
我在安裝的時候,epel 源中最新的 etcd 版本為:3.3.11-2
1. 在 101 上面安裝 etcd 並配置:
yum -y install etcd cd /etc/etcd/ cp etcd.conf etcd.conf_bak vim etcd.conf
內容如下:
#[Member] ETCD_DATA_DIR="/var/lib/etcd/default.etcd" ETCD_LISTEN_PEER_URLS="http://0.0.0.0:2380" ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379,http://0.0.0.0:4001" ETCD_NAME="docker-node-01" #[Clustering] ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.200.101:2380" ETCD_ADVERTISE_CLIENT_URLS="http://192.168.200.101:2379,http://192.168.200.101:4001" ETCD_INITIAL_CLUSTER="docker-node-01=http://192.168.200.101:2380,docker-node-02=http://192.168.200.102:2380" ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster" ETCD_INITIAL_CLUSTER_STATE="new"
注意紅色的地方,名稱保持一致。
同理在 102 上面也進行類似的配置:
#[Member] ETCD_DATA_DIR="/var/lib/etcd/default.etcd" ETCD_LISTEN_PEER_URLS="http://0.0.0.0:2380" ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379,http://0.0.0.0:4001" ETCD_NAME="docker-node-02" #[Clustering] ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.200.102:2380" ETCD_ADVERTISE_CLIENT_URLS="http://192.168.200.102:2379,http://192.168.200.102:4001" ETCD_INITIAL_CLUSTER="docker-node-01=http://192.168.200.101:2380,docker-node-02=http://192.168.200.102:2380" ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster" ETCD_INITIAL_CLUSTER_STATE="new"
注意修改 IP 地址即可。
| 關鍵字 | 說明 |
|---|---|
| ETCD_DATA_DIR | 數據存儲目錄 |
| ETCD_LISTEN_PEER_URLS | 與其他節點通信時的監聽地址列表,通信協議可以是http、https |
| ETCD_LISTEN_CLIENT_URLS | 與客戶端通信時的監聽地址列表 |
| ETCD_NAME | 節點名稱,在也就是后面配置的那個 名字=地址 |
| ETCD_INITIAL_ADVERTISE_PEER_URLS | 節點在整個集群中的通信地址列表,可以理解為能與外部通信的ip端口 |
| ETCD_ADVERTISE_CLIENT_URLS | 告知集群中其他成員自己名下的客戶端的地址列表 |
| ETCD_INITIAL_CLUSTER | 集群內所有成員的地址,這就是為什么稱之為靜態發現,因為所有成員的地址都必須配置 |
| ETCD_INITIAL_CLUSTER_TOKEN | 初始化集群口令,用於標識不同集群 |
| ETCD_INITIAL_CLUSTER_STATE | 初始化集群狀態,new表示新建 |
2. 啟動服務:
systemctl start etcd systemctl enable etcd # 查看節點 etcdctl member list # 監控檢查 etcdctl cluster-health
結果如圖:

可以看到集群處於健康狀態!
3. 此時修改 101 的 docker 啟動配置:
# 停止 docker systemctl stop docker # 修改配置 vim /etc/systemd/system/docker.service
修改內容如下:
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --registry-mirror=https://jxus37ad.mirror.aliyuncs.com -H tcp://0.0.0.0:2375 --cluster-store=etcd://192.168.200.101:2379 --cluster-advertise=192.168.200.101:2375
主要增加了紅色部分,然后啟動 docker:
systemctl daemon-reload
systemctl start docker
同理 102 也進行修改,注意 IP 地址:
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --registry-mirror=https://jxus37ad.mirror.aliyuncs.com -H tcp://0.0.0.0:2375 --cluster-store=etcd://192.168.200.102:2379 --cluster-advertise=192.168.200.102:2375
同樣重啟 docker,此時的 docker 進程發生了改變:
ps -ef | grep docker
結果如下:

4. 此時在 101 上創建 overlay 網絡:
docker network create -d overlay my-overlay
結果如下:

此時查看 102 網絡:

已經同步過來了,如果沒有同步成功,可以重新關閉 102 的防火牆,重啟 102 的 docker 再看。
5. 此時在 101 和 102 上面分別創建容器然后測試網絡的連通性:
# 101 上面創建 docker container run -d --name b1 --network my-overlay busybox /bin/sh -c "while true;do sleep 3000;done" # 102 上面創建 docker container run -d --name b2 --network my-overlay busybox /bin/sh -c "while true;do sleep 3000;done"
注意這里新的參數:--network,指定容器的網絡,不再是默認的 bridge 網絡。
docker container exec -it b1 ping b2
直接在 b1 上面 ping b2:

發現已經能夠實現正常的通信了。而且不再需要我們指定 link 參數了。
當然 etcd 也有它的缺點,它是靜態發現,意味着我們增加節點就需要增加配置和重啟 etcd。
