由於兩台物理機的容器網段不同,我們完全可以將兩台物理機配置成為路由器,並按照容器的網段配置路由表。
在物理機A中,我們可以這樣配置:要想訪問網段172.17.9.0/24,下一跳是192.168.100.101,也即到物理機B上去。
這樣在容器A中訪問容器B,當包到達物理機A的時候,就能夠匹配到這條路由規則,並將包發給下一跳的路由器,也即發給物理機B。在物理機B上也有路由規則,要訪問172.17.9.0/24,從docker0的網卡進去即可。
當容器B返回結果的時候,在物理機B上,可以做類似的配置:要想訪問網段172.17.8.0/24,下一跳是192.168.100.100,也即到物理機A上去。
Calico網絡的大概思路,即不走Overlay網絡,不引入另外的網絡性能損耗,而是將轉發全部用三層網絡的路由轉發來實現
如果全部走三層的路由規則,沒必要每台機器都用一個docker0,從而浪費了一個IP地址,而是可以直接用路由轉發到veth pair在物理機這一端的網卡。同樣,在容器內,路由規則也可以這樣設定:把容器外面的veth pair網卡算作默認網關,下一跳就是外面的物理機。
於是,整個拓撲結構就變成了這個圖中的樣子。
Calico網絡的轉發細節
容器A1的IP地址為172.17.8.2/32,不是/24,而是/32,將容器A1作為一個單點的局域網了, 容器A1里面的默認路由,Calico配置得比較有技巧
default via 169.254.1.1 dev eth0
169.254.1.1 dev eth0 scope link
這個IP地址169.254.1.1是默認的網關,但是整個拓撲圖中沒有一張網卡是這個地址。那如何到達這個地址呢?
當一台機器要訪問網關的時候,首先會通過ARP獲得網關的MAC地址,然后將目標MAC變為網關的MAC,而網關的IP地址不會在任何網絡包頭里面出現,也就是說,沒有人在乎這個地址具體是什么,只要能找到對應的MAC,響應ARP就可以了
ARP本地有緩存,通過ip neigh命令可以查看。
169.254.1.1 dev eth0 lladdr ee:ee:ee:ee:ee:ee STALE
這個MAC地址是Calico硬塞進去的,但是沒有關系,它能響應ARP,於是發出的包的目標MAC就是這個MAC地址。
在物理機A上查看所有網卡的MAC地址的時候,我們會發現veth1就是這個MAC地址。所以容器A1里發出的網絡包,第一跳就是這個veth1這個網卡,也就到達了物理機A這個路由器。
在物理機A上有三條路由規則,分別是去兩個本機的容器的路由,以及去172.17.9.0/24,下一跳為物理機B。
172.17.8.2 dev veth1 scope link
172.17.8.3 dev veth2 scope link
172.17.9.0/24 via 192.168.100.101 dev eth0 proto bird onlink
同理,物理機B上也有三條路由規則,分別是去兩個本機的容器的路由,以及去172.17.8.0/24,下一跳為物理機A
172.17.9.2 dev veth1 scope link
172.17.9.3 dev veth2 scope link
172.17.8.0/24 via 192.168.100.100 dev eth0 proto bird onlink
簡易架構圖:
物理機化身為路由器,通過路由器上的路由規則,將包轉發到目的地。在這個過程中,沒有隧道封裝解封裝,僅僅是單純的路由轉發,性能會好很多。但是,這種模式也有很多問題。
路由配置組件Felix
如果只有兩台機器,每台機器只有兩個容器,而且保持不變。我手動配置一下,倒也沒啥問題。但是如果容器不斷地創建、刪除,節點不斷地加入、退出,情況就會變得非常復雜。
有三台物理機,兩兩之間都需要配置路由,每台物理機上對外的路由就有兩條。如果有六台物理機,則每台物理機上對外的路由就有五條。新加入一個節點,需要通知每一台物理機添加一條路由。
這還是在物理機之間,一台物理機上,每創建一個容器,也需要多配置一條指向這個容器的路由。如此復雜,肯定不能手動配置,需要每台物理機上有一個agent,當創建和刪除容器的時候,自動做這件事情。這個agent在Calico中稱為Felix。
路由廣播組件BGP Speaker
在Calico中,每個Node上運行一個軟件BIRD,作為BGP的客戶端,或者叫作BGP Speaker,將“如何到達我這個Node,訪問我這個Node上的容器”的路由信息廣播出去。所有Node上的BGPSpeaker 都互相建立連接,就形成了全互連的情況,這樣每當路由有所變化的時候,所有節點就都能夠收到了。
安全策略組件
Calico中還實現了靈活配置網絡策略Network Policy,可以靈活配置兩個容器通或者不通。
虛擬機中的安全組,是用iptables實現的。Calico中也是用iptables實現的。這個圖里的內容是iptables在內核處理網絡包的過程中可以嵌入的處理點。Calico也是在這些點上設置相應的規則。
當網絡包進入物理機上的時候,進入PREOUTING規則,這里面有一個規則是cali-fip-dnat,這是實現浮動IP(Floating IP)的場景,主要將外網的IP地址dnat為容器內的IP地址。在虛擬機場景下,路由器的網絡namespace里面有一個外網網卡上,也設置過這樣一個DNAT規則。接下來可以根據路由判斷,是到本地的,還是要轉發出去的。
如果是本地的,走INPUT規則,里面有個規則是cali-wl-to-host,wl的意思是workload,也即容器,也即這是用來判斷從容器發到物理機的網絡包是否符合規則的。這里面內嵌一個規califrom-wl-dispatch,也是匹配從容器來的包。如果有兩個容器,則會有兩個容器網卡,這里面內嵌有詳細的規則“cali-fw-cali網卡1”和“cali-fw-cali網卡2”,fw就是from workload,也就是匹配從容器1來的網絡包和從容器2來的網絡包。
如果是轉發出去的,走FORWARD規則,里面有個規則cali-FORWARD。這里面分兩種情況,一種是從容器里面發出來,轉發到外面的;另一種是從外面發進來,轉發到容器里面的。
第一種情況匹配的規則仍然是cali-from-wl-dispatch,也即from workload。第二種情況匹配的規則是cali-to-wl-dispatch,也即to workload。如果有兩個容器,則會有兩個容器網卡,在這里面內嵌有詳細的規則“cali-tw-cali網卡1”和“cali-tw-cali網卡2”,tw就是to workload,也就是匹配發往容器1的網絡包和發送到容器2的網絡包。
接下來是匹配OUTPUT規則,里面有cali-OUTPUT。接下來是POSTROUTING規則,里面有一個規則是cali-fip-snat,也即發出去的時候,將容器網絡IP轉換為浮動IP地址。在虛擬機場景下,路由器的網絡namespace里面有一個外網網卡上,也設置過這樣一個SNAT規則。
Calico全景架構圖:
全連接復雜性與規模問題
這里面還存在問題,就是BGP全連接的復雜性問題。
例子里只有六個節點,BGP的互連已經如此復雜,如果節點數據再多,這種全互連的模式肯定不行,到時候都成蜘蛛網了。於是多出了一個組件BGP Route Reflector,它也是用BIRD
實現的。有了它,BGP Speaker就不用全互連了,而是都直連它,它負責將全網的路由信息廣播出去。
大規模部署架構圖:
一個機架就像一個數據中心,可以把它設置為一個AS,而BGP Router Reflector有點兒像數據中心的邊界路由器。在一個AS內部,也即服務器和BGP RouterReflector之間使用的是數據中心內部的路由協議iBGP,BGP Router Reflector之間使用的是數據中心之間的路由協議eBGP。
一個機架上有多台機器,每台機器上面啟動多個容器,每台機器上都有可以到達這些容器的路由。每台機器上都啟動一個BGP Speaker,然后將這些路由規則上報到這個Rack上接入交換機的BGP Route Reflector,將這些路由通過iBGP協議告知到接入交換機的三層路由功能。
在接入交換機之間也建立BGP連接,相互告知路由,因而一個Rack里面的路由可以告知另一個Rack。有多個核心或者匯聚交換機將接入交換機連接起來,如果核心和匯聚起二層互通的作用,則接入和接入之間之間交換路由即可。如果核心和匯聚交換機起三層路由的作用,則路由需要通過核心或者匯聚交換機進行告知。
跨網段訪問問題
上面的Calico模式還有一個問題,就是跨網段問題,這里的跨網段是指物理機跨網段。
如物理機A要告訴物理機B,你要訪問172.17.8.0/24,下一跳是我192.168.100.100;同理,物理機B要告訴物理機A,你要訪問172.17.9.0/24,下一跳是我192.168.100.101。之所以能夠這樣,是因為物理機A和物理機B是同一個網段的,是連接在同一個交換機上的。那如果物理機A和物理機B不是在同一個網段呢?
簡化架構:
物理機A的網段是192.168.100.100/24,物理機B的網段是192.168.200.101/24,這樣兩台機器就不能通過二層交換機連接起來了,需要在中間放一台路由器,做一次路由轉發,才能跨網段訪問。
本來物理機A要告訴物理機B,你要訪問172.17.8.0/24,下一跳是我192.168.100.100的,但是中間多了一台路由器,下一跳不是我了,而是中間的這台路由器了,這台路由器的再下一跳,才是我。這樣之前的邏輯就不成立了
物理機B上的容器要訪問物理機A上的容器,第一跳就是物理機B,IP為192.168.200.101,第二跳是中間的物理路由器右面的網口,IP為192.168.200.1,第三跳才是物理機A,IP為192.168.100.100。
解決方式是在物理機A和物理機B之間打一個隧道,這個隧道有兩個端點,在端點上進行封裝,將容器的IP作為乘客協議放在隧道里面,而物理主機的IP放在外面作為承載協議。這樣不管外層的IP通過傳統的物理網絡,走多少跳到達目標物理機,從隧道兩端看起來,物理機A的下一跳就是物理機B,這樣前面的邏輯才能成立
Calico的IPIP模式(解決物理機不在一個網段的問題)
使用了IPIP模式之后,在物理機A上,我們能看到這樣的路由表:
172.17.8.2 dev veth1 scope link
172.17.8.3 dev veth2 scope link
172.17.9.0/24 via 192.168.200.101 dev tun0 proto bird onlink
這和原來模式的區別在於,下一跳不再是同一個網段的物理機B了,IP為192.168.200.101,並且不是從eth0跳,而是建立一個隧道的端點tun0,從這里才是下一跳。
如果我們在容器A1里面的172.17.8.2,去ping容器B1里面的172.17.9.2,首先會到物理機A。在物理機A上根據上面的規則,會轉發給tun0,並在這里對包做封裝:
- 層源IP為172.17.8.2;
- 內層目標IP為172.17.9.2;
- 外層源IP為192.168.100.100;
- 外層目標IP為192.168.200.101。
將這個包從eth0發出去,在物理網絡上會使用外層的IP進行路由,最終到達物理機B。在物理機B上,tun0會解封裝,將內層的源IP和目標IP拿出來,轉發給相應的容器。