一、問題
首先,我們思考這樣一個問題:
訪問k8s集群中的pod, 客戶端需要知道pod地址,需要感知pod的狀態。那如何獲取各個pod的地址?若某一node上的pod故障,客戶端如何感知?
二、k8s service
什么是service
是發現后端pod服務;
是為一組具有相同功能的容器應用提供一個統一的入口地址;
是將請求進行負載分發到后端的各個容器應用上的控制器。
對service的訪問來源
訪問service的請求來源有兩種:k8s集群內部的程序(Pod)和 k8s集群外部的程序。
service類型
采用微服務架構時,作為服務所有者,除了實現業務邏輯以外,還需要考慮如何把服務發布到k8s集群或者集群外部,使這些服務能夠被k8s集群內的應用、其他k8s集群的應用以及外部應用使用。因此k8s提供了靈活的服務發布方式,用戶可以通過ServiceType來指定如何來發布服務,類型有以下幾種:
● ClusterIP:提供一個集群內部的虛擬IP以供Pod訪問(service默認類型)。
service 結構如下:
● NodePort:在每個Node上打開一個端口以供外部訪問
Kubernetes將會在每個Node上打開一個端口并且每個Node的端口都是一樣的,通過:NodePort的方式Kubernetes集群外部的程序可以訪問Service。
service 定義如下:
● LoadBalancer:通過外部的負載均衡器來訪問
service selector
service通過selector和pod建立關聯。
k8s會根據service關聯到pod的podIP信息組合成一個endpoint。
若service定義中沒有selector字段,service被創建時,endpoint controller不會自動創建endpoint。
service負載分發策略
service 負載分發策略有兩種:
RoundRobin:輪詢模式,即輪詢將請求轉發到后端的各個pod上(默認模式);
SessionAffinity:基于客戶端IP地址進行會話保持的模式,第一次客戶端訪問后端某個pod,之后的請求都轉發到這個pod上。
三、服務發現
k8s服務發現方式
雖然Service解決了Pod的服務發現問題,但不提前知道Service的IP,怎么發現service服務呢?
k8s提供了兩種方式進行服務發現:
● 環境變量: 當創建一個Pod的時候,kubelet會在該Pod中注入集群內所有Service的相關環境變量。需要注意的是,要想一個Pod中注入某個Service的環境變量,則必須Service要先比該Pod創建。這一點,幾乎使得這種方式進行服務發現不可用。
例如:
一個ServiceName為redis-master的Service,對應的ClusterIP:Port為10.0.0.11:6379,則其在pod中對應的環境變量為:
REDIS_MASTER_SERVICE_HOST=10.0.0.11 REDIS_MASTER_SERVICE_PORT=6379 REDIS_MASTER_PORT=tcp://10.0.0.11:6379 REDIS_MASTER_PORT_6379_TCP=tcp://10.0.0.11:6379 REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
REDIS_MASTER_PORT_6379_TCP_PORT=6379 REDIS_MASTER_PORT_6379_TCP_ADDR=10.0.0.11
● DNS:可以通過cluster add-on的方式輕松的創建KubeDNS來對集群內的Service進行服務發現————這也是k8s官方強烈推薦的方式。為了讓Pod中的容器可以使用kube-dns來解析域名,k8s會修改容器的/etc/resolv.conf配置。
k8s服務發現原理
● endpoint
endpoint是k8s集群中的一個資源對象,存儲在etcd中,用來記錄一個service對應的所有pod的訪問地址。
service配置selector,endpoint controller才會自動創建對應的endpoint對象;否則,不會生成endpoint對象.
例如,k8s集群中創建一個名為k8s-classic-1113-d3的service,就會生成一個同名的endpoint對象,如下圖所示。其中ENDPOINTS就是service關聯的pod的ip地址和端口。
● endpoint controller
endpoint controller是k8s集群控制器的其中一個組件,其功能如下:
負責生成和維護所有endpoint對象的控制器
負責監聽service和對應pod的變化
監聽到service被刪除,則刪除和該service同名的endpoint對象
監聽到新的service被創建,則根據新建service信息獲取相關pod列表,然后創建對應endpoint對象
監聽到service被更新,則根據更新后的service信息獲取相關pod列表,然后更新對應endpoint對象
監聽到pod事件,則更新對應的service的endpoint對象,將podIp記錄到endpoint中
四、負載均衡
kube-proxy
kube-proxy負責service的實現,即實現了k8s內部從pod到service和外部從node port到service的訪問。
kube-proxy采用iptables的方式配置負載均衡,基于iptables的kube-proxy的主要職責包括兩大塊:一塊是偵聽service更新事件,并更新service相關的iptables規則,一塊是偵聽endpoint更新事件,更新endpoint相關的iptables規則(如 KUBE-SVC-鏈中的規則),然后將包請求轉入endpoint對應的Pod。如果某個service尚沒有Pod創建,那么針對此service的請求將會被drop掉。
kube-proxy的架構如下:
kube-proxy iptables
kube-proxy監聽service和endpoint的變化,將需要新增的規則添加到iptables中。
kube-proxy只是作為controller,而不是server,真正服務的是內核的netfilter,體現在用戶態則是iptables。
kube-proxy的iptables方式也支持RoundRobin(默認模式)和SessionAffinity負載分發策略。
kubernetes只操作了filter和nat表。
Filter:在該表中,一個基本原則是只過濾數據包而不修改他們。filter table的優勢是小而快,可以hook到input,output和forward。這意味著針對任何給定的數據包,只有可能有一個地方可以過濾它。
NAT:此表的主要作用是在PREROUTING和POSTROUNTING的鉤子中,修改目標地址和原地址。與filter表稍有不同的是,該表中只有新連接的第一個包會被修改,修改的結果會自動apply到同一連接的后續包中。
kube-proxy對iptables的鏈進行了擴充,自定義了KUBE-SERVICES,KUBE-NODEPORTS,KUBE-POSTROUTING,KUBE-MARK-MASQ和KUBE-MARK-DROP五個鏈,并主要通過為KUBE-SERVICES chain增加rule來配制traffic routing 規則。我們可以看下自定義的這幾個鏈的作用:
KUBE-MARK-DROP - [0:0] /*對于未能匹配到跳轉規則的traffic set mark 0x8000,有此標記的數據包會在filter表drop掉*/KUBE-MARK-MASQ - [0:0] /*對于符合條件的包 set mark 0x4000, 有此標記的數據包會在KUBE-POSTROUTING chain中統一做MASQUERADE*/KUBE-NODEPORTS - [0:0] /*針對通過nodeport訪問的package做的操作*/KUBE-POSTROUTING - [0:0]KUBE-SERVICES - [0:0] /*操作跳轉規則的主要chain*/
同時,kube-proxy也為默認的prerouting、output和postrouting chain增加規則,使得數據包可以跳轉至k8s自定義的chain,規則如下:
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
如果service類型為nodePort,(從LB轉發至node的數據包均屬此類)那么將KUBE-NODEPORTS鏈中每個目的地址是NODE節點端口的數據包導入這個“KUBE-SVC-”鏈:
-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/es1:http" -m tcp --dport 32135 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/es1:http" -m tcp --dport 32135 -j KUBE-SVC-LAS23QA33HXV7KBL
Iptables chain支持嵌套并因為依據不同的匹配條件可支持多種分支,比較難用標準的流程圖來體現調用關系,建單抽象為下圖:
舉個例子,在k8s集群中創建了一個名為my-service的服務,其中:
service vip:10.11.97.177
對應的后端兩副本pod ip:10.244.1.10、10.244.2.10
容器端口為:80
服務端口為:80
則kube-proxy為該service生成的iptables規則主要有以下幾條:
-A KUBE-SERVICES -d 10.11.97.177/32 -p tcp -m comment --comment "default/my-service: cluster IP" -m tcp --dport 80 -j KUBE-SVC-BEPXDJBUHFCSYIC3
-A KUBE-SVC-BEPXDJBUHFCSYIC3 -m comment --comment “default/my-service:” -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-U4UWLP4OR3LOJBXU //50%的概率輪詢后端pod
-A KUBE-SVC-BEPXDJBUHFCSYIC3 -m comment --comment "default/my-service:" -j KUBE-SEP-QHRWSLKOO5YUPI7O
-A KUBE-SEP-U4UWLP4OR3LOJBXU -s 10.244.1.10/32 -m comment --comment "default/my-service:" -j KUBE-MARK-MASQ
-A KUBE-SEP-U4UWLP4OR3LOJBXU -p tcp -m comment --comment "default/my-service:" -m tcp -j DNAT --to-destination 10.244.1.10:80
-A KUBE-SEP-QHRWSLKOO5YUPI7O -s 10.244.2.10/32 -m comment --comment "default/my-service:" -j KUBE-MARK-MASQ
-A KUBE-SEP-QHRWSLKOO5YUPI7O -p tcp -m comment --comment "default/my-service:" -m tcp -j DNAT --to-destination 10.244.2.10:80
kube-proxy通過循環的方式創建后端endpoint的轉發,概率是通過probability后的1.0/float64(n-i)計算出來的,譬如有兩個的場景,那么將會是一個0.5和1也就是第一個是50%概率第二個是100%概率,如果是三個的話類似,33%、50%、100%。
kube-proxy iptables的性能缺陷
k8s集群創建大規模服務時,會產生很多iptables規則,非增量式更新會引入一定的時延。iptables規則成倍增長,也會導致路由延遲帶來訪問延遲。大規模場景下,k8s 控制器和負載均衡都面臨這挑戰。例如,若集群中有N個節點,每個節點每秒有M個pod被創建,則控制器每秒需要創建NM個endpoints,需要增加的iptables則是NM的數倍。以下是k8s不同規模下訪問service的時延:
從上圖中可以看出,當集群中服務數量增長時,因為 IPTables天生不是被設計用來作為 LB 來使用的,IPTables 規則則會成倍增長,這樣帶來的路由延遲會導致的服務訪問延遲增加,直到無法忍受。
目前有以下幾種解決方案,但各有不足:
● 將endpoint對象拆分成多個對像
優點:減小了單個endpoint大小
缺點:增加了對象的數量和請求量
● 使用集中式負載均衡器
優點:減少了跟apiserver的連接和請求數
缺點:服務路由中又增加了一跳,并且需要集中式LB有很高的性能和高可用性
● 定期任務,批量創建/更新endpoint
優點:減少了每秒的處理數
缺點:在定期任務執行的間隔時間內,端對端延遲明顯增加
五、K8s 1.8 新特性——ipvs
ipvs與iptables的性能差異
隨著服務的數量增長,IPTables 規則則會成倍增長,這樣帶來的問題是路由延遲帶來的服務訪問延遲,同時添加或刪除一條規則也有較大延遲。不同規模下,kube-proxy添加一條規則所需時間如下所示:
可以看出當集群中服務數量達到5千個時,路由延遲成倍增加。添加 IPTables 規則的延遲,有多種產生的原因,如:
添加規則不是增量的,而是先把當前所有規則都拷貝出來,再做修改然后再把修改后的規則保存回去,這樣一個過程的結果就是 IPTables 在更新一條規則時會把 IPTables 鎖住,這樣的后果在服務數量達到一定量級的時候,性能基本不可接受:在有5千個服務(4萬條規則)時,添加一條規則耗時11分鐘;在右2萬個服務(16萬條規則)時,添加一條規則需要5個小時。
這樣的延遲時間,對生產環境是不可以的,那該性能問題有哪些解決方案呢?從根本上解決的話,可以使用 “IP Virtual Server”(IPVS )來替換當前 kube-proxy 中的 IPTables 實現,這樣能帶來顯著的性能提升以及更智能的負載均衡功能如支持權重、支持重試等等。
那什么是 “IP Virtual Server”(IPVS ) 呢?
ipvs 簡介
k8s 1.8 版本中,社區 SIG Network 增強了 NetworkPolicy API,以支持 Pod 出口流量策略,以及允許策略規則匹配源或目標 CIDR 的匹配條件。這兩個增強特性都被設計為 beta 版本。 SIG Network 還專注于改進 kube-proxy,除了當前的 iptables 和 userspace 模式,kube-proxy 還引入了一個 alpha 版本的 IPVS 模式。
作為 Linux Virtual Server(LVS) 項目的一部分,IPVS 是建立于 Netfilter之上的高效四層負載均衡器,支持 TCP 和 UDP 協議,支持3種負載均衡模式:NAT、直接路由(通過 MAC 重寫實現二層路由)和IP 隧道。ipvs(IP Virtual Server)安裝在LVS(Linux Virtual Server)集群作為負載均衡主節點上,通過虛擬出一個IP地址和端口對外提供服務。客戶端通過訪問虛擬IP+端口訪問該虛擬服務,之后訪問請求由負載均衡器調度到后端真實服務器上。
ipvs相當于工作在netfilter中的input鏈。
配置方法:IPVS 負載均衡模式在 kube-proxy 處于測試階段還未正式發布,完全兼容當前 Kubernetes 的行為,通過修改 kube-proxy 啟動參數,在 mode=userspace 和 mode=iptables 的基礎上,增加 mode=IPVS 即可啟用該功能。
ipvs轉發模式
● DR模式(Direct Routing)
特點:
<1> 數據包在LB轉發過程中,源/目的IP和端口都不會變化。LB只修改數據包的MAC地址為RS的MAC地址
<2> RS須在環回網卡上綁定LB的虛擬機服務IP
<3> RS處理完請求后,響應包直接回給客戶端,不再經過LB
缺點:
<1> LB和RS必須位于同一子網
● NAT模式(Network Address Translation)
特點:
<1> LB會修改數據包地址:對于請求包,進行DNAT;對于響應包,進行SNAT
<2> 需要將RS的默認網關地址配置為LB的虛擬IP地址
缺點:
<1> LB和RS必須位于同一子網,且客戶端和LB不能位于同一子網
● FULLNAT模式
特點:
<1> LB會對請求包和響應包都做SNAT+DNAT
<2> LB和RS對于組網結構沒有要求
<3> LB和RS必須位于同一子網,且客戶端和LB不能位于同一子網
● 三種轉發模式性能從高到低:DR > NAT >FULLNAT
ipvs 負載均衡器常用調度算法
● 輪詢(Round Robin)
LB認為集群內每臺RS都是相同的,會輪流進行調度分發。從數據統計上看,RR模式是調度最均衡的。
● 加權輪詢(Weighted Round Robin)
LB會根據RS上配置的權重,將消息按權重比分發到不同的RS上。可以給性能更好的RS節點配置更高的權重,提升集群整體的性能。
● 最少連接調度
LB會根據和集群內每臺RS的連接數統計情況,將消息調度到連接數最少的RS節點上。在長連接業務場景下,LC算法對于系統整體負載均衡的情況較好;但是在短連接業務場景下,由于連接會迅速釋放,可能會導致消息每次都調度到同一個RS節點,造成嚴重的負載不均衡。
● 加權最少連接調度
最小連接數算法的加權版。
● 原地址哈希,鎖定請求的用戶
根據請求的源IP,作為散列鍵(Hash Key)從靜態分配的散列表中找出對應的服務器。若該服務器是可用的且未超載,將請求發送到該服務器。
原文首發于網易云。