云原生系列「二」Kubernetes网络详解
前言
K8s是一個強大的平臺,但它的網絡比較復雜,涉及很多概念,例如Pod網絡,Service網絡,Cluster IPs,NodePort,LoadBalancer和Ingress等等,這么多概念足以讓新手望而生畏。但是,只有深入理解K8s網絡,才能為理解和用好K8s打下堅實基礎。為了幫助大家理解,模仿TCP/IP協議棧,我把K8s的網絡分解為四個抽象層,從0到3,除了第0層,每一層都是構建于前一層之上,如下圖所示:
第0層Node節點網絡比較好理解,也就是保證K8s節點(物理或虛擬機)之間能夠正常IP尋址和互通的網絡,這個一般由底層(公有云或數據中心)網絡基礎設施支持。第0層我們假定已經存在,所以不展開。第1到3層網絡,我將分三篇文章,分別進行剖析,本文是第一篇Pod網絡。
注意,本文旨在幫助大家建立K8s網絡的概念模型,而不是對底層技術的精確描述。實際我們學技術以應用為主,重要的是快速建立起直觀易懂的概念模型,能夠指導我們正常應用即可,當然理解一定的技術細節也是有幫助的。另外,本文假定讀者對基本的網絡技術,ip地址空間和容器技術等有一定的了解。
Pod網絡概念模型
Pod相當于是K8s云平臺中的虛擬機,它是K8s的基本調度單位。所謂Pod網絡,就是能夠保證K8s集群中的所有Pods(包括同一節點上的,也包括不同節點上的Pods),邏輯上看起來都在同一個平面網絡內,能夠相互做IP尋址和通信的網絡,下圖是Pod網絡的簡化概念模型:
Pod網絡構建于Node節點網絡之上,它又是上層Service網絡的基礎。為了進一步理解Pod網絡,我將對同一節點上的Pod之間的網絡,以及不同節點上的Pod之間網絡,分別進行剖析。
同一節點上的Pod網絡
前面提到,Pod相當于是K8s云平臺中的虛擬機,實際一個Pod中可以住一個或者多個(大多數場景住一個)應用容器,這些容器共享Pod的網絡棧和其它資源如Volume。那么什么是共享網絡棧?同一節點上的Pod之間如何尋址和互通?我以下圖樣例來解釋:
上圖節點上展示了Pod網絡所依賴的3個網絡設備,eth0是節點主機上的網卡,這個是支持該節點流量出入的設備,也是支持集群節點間IP尋址和互通的設備。docker0是一個虛擬網橋,可以簡單理解為一個虛擬交換機,它是支持該節點上的Pod之間進行IP尋址和互通的設備。veth0則是Pod1的虛擬網卡,是支持該Pod內容器互通和對外訪問的虛擬設備。docker0網橋和veth0網卡,都是linux支持和創建的虛擬網絡設備。
上圖Pod1內部住了3個容器,它們都共享一個虛擬網卡veth0。內部的這些容器可以通過localhost相互訪問,但是它們不能在同一端口上同時開啟服務,否則會有端口沖突,這就是共享網絡棧的意思。Pod1中還有一個比較特殊的叫pause的容器,這個容器運行的唯一目的是為Pod建立共享的veth0網絡接口。如果你SSH到K8s集群中一個有Pod運行的節點上去,然后運行docker ps,可以看到通過pause命令運行的容器。
Pod的IP是由docker0網橋分配的,例如上圖docker0網橋的IP是172.17.0.1,它給第一個Pod1分配IP為172.17.0.2。如果該節點上再啟一個Pod2,那么相應的分配IP為172.17.0.3,如果再啟動Pod可依次類推。因為這些Pods都連在同一個網橋上,在同一個網段內,它們可以進行IP尋址和互通,如下圖所示:
從上圖我們可以看到,節點內Pod網絡在172.17.0.0/24這個地址空間內,而節點主機在10.100.0.0/24這個地址空間內,也就是說Pod網絡和節點網絡不在同一個網絡內,那么不同節點間的Pod該如何IP尋址和互通呢?下一節我們來分析這個問題。
不同節點間的Pod網絡
現在假設我們有兩個節點主機,host1(10.100.0.2)和host2(10.100.0.3),它們在10.100.0.0/24這個地址空間內。host1上有一個PodX(172.17.0.2),host2上有一個PodY(172.17.1.3),Pod網絡在172.17.0.0/16這個地址空間內。注意,Pod網絡的地址,是由K8s統一管理和分配的,保證集群內Pod的IP地址唯一。我們發現節點網絡和Pod網絡不在同一個網絡地址空間內,那么host1上的PodX該如何與host2上的PodY進行互通?
實際上不同節點間的Pod網絡互通,有很多技術實現方案,底層的技術細節也很復雜。為了簡化描述,我把這些方案大體分為兩類,一類是路由方案,另外一類是覆蓋(Overlay)網絡方案。
如果底層的網絡是你可以控制的,比如說企業內部自建的數據中心,并且你和運維團隊的關系比較好,可以采用路由方案,如下圖所示:
這個方案簡單理解,就是通過路由設備為K8s集群的Pod網絡單獨劃分網段,并配置路由器支持Pod網絡的轉發。例如上圖中,對于目標為172.17.1.0/24這個范圍內的包,轉發到10.100.0.3這個主機上,同樣,對于目標為172.17.0.0/24這個范圍內的包,轉發到10.100.0.2這個主機上。當主機的eth0接口接收到來自Pod網絡的包,就會向內部網橋轉發,這樣不同節點間的Pod就可以相互IP尋址和通信。這種方案依賴于底層的網絡設備,但是不引入額外性能開銷
如果底層的網絡是你無法控制的,比如說公有云網絡,或者企業的運維團隊不支持路由方案,可以采用覆蓋(Overlay)網絡方案,如下圖所示:
所謂覆蓋網絡,就是在現有網絡之上再建立一個虛擬網絡,實現技術有很多,例如flannel/weavenet等等,這些方案大都采用隧道封包技術。簡單理解,Pod網絡的數據包,在出節點之前,會先被封裝成節點網絡的數據包,當數據包到達目標節點,包內的Pod網絡數據包會被解封出來,再轉發給節點內部的Pod網絡。這種方案對底層網絡沒有特別依賴,但是封包解包會引入額外性能開銷。
CNI簡介
考慮到Pod網絡實現技術眾多,為了簡化集成,K8s支持CNI(Container Network Interface)標準,不同的Pod網絡技術可以通過CNI插件形式和K8s進行集成。節點上的Kubelet通過CNI標準接口操作Pod網路,例如添加或刪除網絡接口等,它不需要關心Pod網絡的具體實現細節
總結
有了Pod網絡,K8s集群內的所有Pods在邏輯上都可以看作在一個平面網絡內,可以正常IP尋址和互通。但是Pod僅僅是K8s云平臺中的虛擬機抽象,最終,我們需要在K8s集群中運行的是應用或者說服務(Service),而一個Service背后一般由多個Pods組成集群,這時候就引入了服務發現(Service Discovery)和負載均衡(Load Balancing)等問題。
Service網絡概念模型
我們假定第1層Pod網絡已經存在,下圖是K8s的第2層Service網絡的簡化概念模型:
實際上,K8s通過在Client和Account-App的Pod集群之間引入一層Account-Serivce抽象,來解決上述問題:
K8s中為何要引入Service抽象?背后的原理是什么?后面我將以技術演進視角來解釋這些問題。
服務發現技術演進
DNS域名服務是一種較老且成熟的標準技術,實際上DNS可以認為是最早的一種服務發現技術。
在K8s中引入DNS實現服務發現其實并不復雜,實際K8s本身就支持Kube-DNS組件。假設K8s引入DNS做服務發現(如上圖所示),運行時,K8s可以把Account-App的Pod集群信息(IP+Port等)自動注冊到DNS,Client應用則通過域名查詢DNS發現目標Pod,然后發起調用。這個方案不僅簡單,而且對Client也無侵入(目前幾乎所有的操作系統都自帶DNS客戶端)。但是基于DNS的服務發現也有如下問題:
考慮到上述不同DNS客戶端實現的差異,不在K8s控制范圍內,所以K8s沒有直接采用DNS技術做服務發現。注意,實際K8s是引入Kube-DNS支持通過域名訪問服務的,不過這是建立在CusterIP/Service網絡之上,這個我后面會展開。
另外一種較新的服務發現技術,是引入Service Registry+Client配合實現,在當下微服務時代,這是一個比較流行的做法。目前主流的產品,如Netflix開源的Eureka + Ribbon,HashiCorp開源的Consul,還有阿里新開源Nacos等,都是這個方案的典型代表。
在K8s中引入Service Registry實現服務發現也不復雜,K8s自身帶分布式存儲etcd就可以實現Service Registry。假設K8s引入Service Registry做服務發現(如上圖所示),運行時K8s可以把Account-App和Pod集群信息(IP + Port等)自動注冊到Service Registry,Client應用則通過Service Registry查詢發現目標Pod,然后發起調用。這個方案也不復雜,而且客戶端可以實現靈活的負載均衡策略,但是需要引入客戶端配合,對客戶應用有侵入性,所以K8s也沒有直接采用這種方案。
K8s雖然沒有直接采用上述方案,但是它的Service網絡實現是在上面兩種技術的基礎上擴展演進出來的。它融合了上述方案的優點,同時解決了上述方案的不足,下節我會詳細剖析K8s的Service網絡的實現原理,前面提到,K8s的服務發現機制是在上節講的Service Registry + DNS基礎上發展演進出來的,下圖展示K8s服務發現的簡化原理:
在K8s平臺的每個Worker節點上,都部署有兩個組件,一個叫Kubelet,另外一個叫Kube-Proxy,這兩個組件+Master是K8s實現服務注冊和發現的關鍵。下面我們看下簡化的服務注冊發現流程。
- 首先,在服務Pod實例發布時(可以對應K8s發布中的Kind: Deployment),Kubelet會負責啟動Pod實例,啟動完成后,Kubelet會把服務的PodIP列表匯報注冊到Master節點。
- 其次,通過服務Service的發布(對應K8s發布中的Kind: Service),K8s會為服務分配ClusterIP,相關信息也記錄在Master上。
- 第三,在服務發現階段,Kube-Proxy會監聽Master并發現服務ClusterIP和PodIP列表映射關系,并且修改本地的linux iptables轉發規則,指示iptables在接收到目標為某個ClusterIP請求時,進行負載均衡并轉發到對應的PodIP上。
- 運行時,當有消費者Pod需要訪問某個目標服務實例的時候,它通過ClusterIP發起調用,這個ClusterIP會被本地iptables機制截獲,然后通過負載均衡,轉發到目標服務Pod實例上。
實際消費者Pod也并不直接調服務的ClusterIP,而是先調用服務名,因為ClusterIP也會變(例如針對TEST/UAT/PROD等不同環境的發布,ClusterIP會不同),只有服務名一般不變。為了屏蔽ClusterIP的變化,K8s在每個Worker節點上還引入了一個KubeDNS組件,它也監聽Master并發現服務名和ClusterIP之間映射關系,這樣, 消費者Pod通過KubeDNS可以間接發現服務的ClusterIP。
注意,K8s的服務發現機制和目前微服務主流的服務發現機制(如Eureka + Ribbon)總體原理類似,但是也有顯著區別(這些區別主要體現在客戶端):
個人認為,對比目前微服務主流的服務發現機制,K8s的服務發現機制抽象得更好,它通過ClusterIP統一屏蔽服務發現和負載均衡,一個服務一個ClusterIP,這個模型和傳統的IP網絡模型更貼近和易于理解。ClusterIP也是一個IP,但這個IP后面跟的不是一個服務實例,而是一個服務集群,所以叫集群ClusterIP。同時,它對客戶應用無侵入,且不穿透沒有額外性能損耗。
總結
有了Service抽象,K8s中部署的應用都可以通過一個抽象的ClusterIP進行尋址訪問,并且消費方不需要關心這個ClusterIP后面究竟有多少個Pod實例,它們的PodIP是什么,會不會變化,如何以負載均衡方式去訪問等問題。但是,K8s的Service網絡只是一個集群內可見的內部網絡,集群外部是看不到Service網絡的,也無法直接訪問。而我們發布應用,有些是需要暴露出去,要讓外網甚至公網能夠訪問的,這樣才能對外提供服務。K8s如何將內部服務暴露出去?
在講到K8s如何接入外部流量的時候,大家常常會聽到NodePort,LoadBalancer和Ingress等概念,這些概念都是和K8s外部流量接入相關的,它們既是不同概念,同時又有關聯性。下面我們分別解釋這些概念和它們之間的關系。
NodePort
先提前強調一下,NodePort是K8s將內部服務對外暴露的基礎,后面的LoadBalancer底層有賴于NodePort。
之前我們講了K8s網絡的4層抽象,Service網絡在第2層,節點網絡在第0層。實際上,只有節點網絡是可以直接對外暴露的,具體暴露方式要看數據中心或公有云的底層網絡部署,但不管采用何種部署,節點網絡對外暴露是完全沒有問題的。那么現在的問題是,第2層的Service網絡如何通過第0層的節點網絡暴露出去?我們可以回看一下K8s服務發現的原理圖,如下圖所示,然后不妨思考一下,K8s集群中有哪一個角色,即掌握Service網絡的所有信息,可以和Service網絡以及Pod網絡互通互聯,同時又可以和節點網絡打通?
答案是Kube-Proxy。上一篇我們提到Kube-Proxy是K8s內部服務發現的一個關鍵組件,事實上,它還是K8s將內部服務暴露出去的關鍵組件。Kube-Proxy在K8s集群中所有Worker節點上都部署有一個,它掌握Service網絡的所有信息,知道怎么和Service網絡以及Pod網絡互通互聯。如果要將Kube-Proxy和節點網絡打通(從而將某個服務通過Kube-Proxy暴露出去),只需要讓Kube-Proxy在節點上暴露一個監聽端口即可。這種通過Kube-Proxy在節點上暴露一個監聽端口,將K8s內部服務通過Kube-Proxy暴露出去的方式,術語就叫NodePort(顧名思義,端口暴露在節點上)。下圖是通過NodePort暴露服務的簡化概念模型。
如果我們要將K8s內部的一個服務通過NodePort方式暴露出去,可以將服務發布(kind: Service)的type設定為NodePort,同時指定一個30000~32767范圍內的端口。服務發布以后,K8s在每個Worker節點上都會開啟這個監聽端口。這個端口的背后是Kube-Proxy,當K8s外部有Client要訪問K8s集群內的某個服務,它通過這個服務的NodePort端口發起調用,這個調用通過Kube-Proxy轉發到內部的Servcie抽象層,然后再轉發到目標Pod上。Kube-Proxy轉發以及之后的環節,可以和上一篇《Kubernetes網絡三部曲~Service網絡》的內容對接起來。注意,為了直觀形象,上圖的Service在K8s集群中被畫成一個獨立組件,實際是沒有獨立Service這樣一個組件的,只是一個抽象概念,如果要理解這個抽象的底層實現細節,可以回頭看上一圖的K8s服務發現原理,或者回到上一篇《Kubernetes網絡三部曲~Service網絡》。
LoadBalancer
上面我們提到,將K8s內部的服務通過NodePort方式暴露出去,K8s會在每個Worker節點上都開啟對應的NodePort端口。邏輯上看,K8s集群中的所有節點都會暴露這個服務,或者說這個服務是以集群方式暴露的(實際支持這個服務的Pod可能就分布在其中有限幾個節點上,但是因為所有節點上都有Kube-Proxy,所以所有節點都知道該如何轉發)。既然是集群,就會涉及負載均衡問題,誰負責對這個服務的負載均衡訪問?答案是需要引入負載均衡器(Load Balancer)。下圖是通過LoadBalancer,將服務對外暴露的概念模型。
假設我們有一套阿里云K8s環境,要將K8s內部的一個服務通過LoadBalancer方式暴露出去,可以將服務發布(Kind: Service)的type設定為LoadBalancer。服務發布后,阿里云K8s不僅會自動創建服務的NodePort端口轉發,同時會自動幫我們申請一個SLB,有獨立公網IP,并且阿里云K8s會幫我們自動把SLB映射到后臺K8s集群的對應NodePort上。這樣,通過SLB的公網IP,我們就可以訪問到K8s內部服務,阿里云SLB負載均衡器會在背后做負載均衡。
值得一提的是,如果是在本地開發測試環境里頭搭建的K8s,一般不支持Load Balancer也沒必要,因為通過NodePort做測試訪問就夠了。但是在生產環境或者公有云上的K8s,例如GCP或者阿里云K8s,基本都支持自動創建Load Balancer。
Ingress
有了前面的NodePort + LoadBalancer,將K8s內部服務暴露到外網甚至公網的需求就已經實現了,那么為啥還要引入Ingress這樣一個概念呢?它起什么作用?
我們知道在公有云(阿里云/AWS/GCP)上,公網LB+IP是需要花錢買的。我們回看上圖的通過LoadBalancer(簡稱LB)暴露服務的方式,發現要暴露一個服務就需要購買一個獨立的LB+IP,如果要暴露十個服務就需要購買十個LB+IP,顯然,從成本考慮這是不劃算也不可擴展的。那么,有沒有辦法只需購買一個(或者少量)的LB+IP,但是卻可以按需暴露更多服務出去呢?答案其實不復雜,就是想辦法在K8內部部署一個獨立的反向代理服務,讓它做代理轉發。谷歌給這個內部獨立部署的反向代理服務起了一個奇怪的名字,就叫Ingress,它的簡化概念模型如下圖所示:
Ingress就是一個特殊的Service,通過節點的**HostPort(80/443)**暴露出去,前置一般也有LB做負載均衡。Ingress轉發到內部的其它服務,是通過集群內的Service抽象層/ClusterIP進行轉發,最終轉發到目標服務Pod上。Ingress的轉發可以基于Path轉發,也可以基于域名轉發等方式,基本上你只需給它設置好轉發路由表即可,功能和Nginx無本質差別。
注意,上圖的Ingress概念模型是一種更抽象的畫法,隱去了K8s集群中的節點,實際HostPort是暴露在節點上的。
所以,Ingress并不是什么神奇的東西,首先,它本質上就是K8s集群中的一個比較特殊的Service(發布Kind: Ingress)。其次,這個Service提供的功能主要就是7層反向代理(也可以提供安全認證,監控,限流和SSL證書等高級功能),功能類似Nginx。第三,這個Service對外暴露出去是通過HostPort(80/443),可以和上面LoadBalancer對接起來。有了這個Ingress Service,我們可以做到只需購買一個LB+IP,就可以通過Ingress將內部多個(甚至全部)服務暴露出去,Ingress會幫忙做代理轉發。
那么哪些軟件可以做這個Ingress?傳統的Nginx/Haproxy可以,現代的微服務網關Zuul/SpringCloudGateway/Kong/Envoy/Traefik等等都可以。當然,谷歌別出心裁給這個東東起名叫Ingress,它還是做了一些包裝,以簡化對Ingress的操作。如果你理解了原理,那么完全可以用Zuul或者SpringCloudGateway,或者自己定制開發一個反向代理,來替代這個Ingress。部署的時候以普通Service部署,將type設定為LoadBalancer即可,如下圖所示:
注意,Ingress是一個7層反向代理,如果你要暴露的是4層服務,還是需要走獨立LB+IP方式。
Kubectl Proxy & Port Forward
上面提到的服務暴露方案,包括NodePort/LoadBalancer/Ingress,主要針對正式生產環境。如果在本地開發測試環境,需要對本地部署的K8s環境中的服務或者Pod進行快速調試或測試,還有幾種簡易辦法,這邊一并簡單介紹下,如下圖所示:
- 辦法一,通過kubectl proxy命令,在本機上開啟一個代理服務,通過這個代理服務,可以訪問K8s集群內的任意服務。背后,這個Kubectl代理服務通過Master上的API Server間接訪問K8s集群內服務,因為Master知道集群內所有服務信息。這種方式只限于7層HTTP轉發。
- 辦法二,通過kubectl port-forward命令,它可以在本機上開啟一個轉發端口,間接轉發到K8s內部的某個Pod的端口上。這樣我們通過本機端口就可以訪問K8s集群內的某個Pod。這種方式是TCP轉發,不限于HTTP。
- 辦法三,通過kubectl exec命令直接連到Pod上去執行linux命令,功能類似docker exec。
總結
至此,Kubernetes網絡三部曲全部講解完成,希望這三篇文章對你理解和用好K8s有幫助。下表是對三部曲的濃縮總結,是希望大家帶走記住的:
總結
以上是生活随笔為你收集整理的云原生系列「二」Kubernetes网络详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: spring中注解无法修饰静态变量
- 下一篇: 关于CPU指标的解释