本文針對咱們生產上出現的流量不均的問題,深層次地分析問題產生緣由,對其中的一些機制作一些介紹。html
k8s是一個特別複雜的系統,而網絡相關的問題是其中最複雜的問題,要經過一兩篇文章介紹清楚是很難的。這個流量不均的問題出現的緣由並不複雜,就是由於kube-proxy使用了iptables作負載均衡,而它是以機率的方式轉發,使用長鏈接且鏈接數較少時,誤差會比較大。雖然緣由不復雜,可是咱們但願能把這其中的整個流程和原理梳理清楚,在介紹過程當中,同時介紹一些底層的東西,可是不會太深刻。java
本章主要介紹一些相關的背景,包括出問題的系統,生產上的現象等。node
出問題的系統是一個以Dubbo服務爲基礎的應用,日交易量近2000萬,在生產上部署到了k8s集羣中,發現集羣裏的Pod流量不均衡,並且差別很大,甚至有時候到了9比1的流量比,有的pod日交易量接近千萬,有的還不到一百萬,因爲交易自己比較快,未形成嚴重後果。nginx
在默認狀況下,Dubbo的消費者會與每個生產者創建一個長鏈接,以後的請求都經過這個長鏈接發送,底層基於Netty實現IO多路複用,即便單個鏈接也能實現高效傳輸。消費者在客戶端實現負載均衡,輪詢向每一個鏈接發送請求,出現流量不均,應該就是KubeProxy作了二次負載均衡致使。redis
默認狀況下,Dubbo服務的提供者把本機地址發佈到zk上,消費者經過訂閱zk,獲取提供者的地址信息,在客戶端進行負載均衡,調用提供者。docker
可是,當服務提供者在容器內的時候,它發佈的地址是POD的地址,除非消費者也在k8s中,不然無法訪問這個地址。在咱們將傳統應用向雲上遷移時,不可避免的會有云外的應用訪問雲上的應用,因此這個問題必需要解決。api
新版的Dubbo裏提供了四個配置用來指定要發佈的地址和端口,能夠從環境變量或者properties裏取。由於咱們用的Dubbo版本較老,沒實現這個功能,因此本身添加了這個功能。bash
DUBBO_IP_TO_REGISTRY: 要發佈到註冊中心上的地址 DUBBO_PORT_TO_REGISTRY: 要發佈到註冊中心上的端口 DUBBO_IP_TO_BIND: 要綁定的服務地址(監聽的地址) DUBBO_PORT_TO_BIND: 要綁定的服務端口
經過在Deployment的yaml文件中指定前面兩個環境變量,便可讓Dubbo發佈指定的地址和端口。cookie
env: - name: DUBBO_IP_TO_REGISTRY valueFrom: fieldRef: fieldPath: status.hostIP - name: DUBBO_PORT_TO_REGISTRY value: "30001"
status.hostIP是k8s提供的一個機制,能夠在pod啓動的時候把宿主機的IP拿到並傳到pod的環境變量裏,對於端口的話,就須要指定一個,這裏咱們用的就是Service的NodePort。網絡
Pod的IP和端口,最終是須要映射到宿主機的IP和一個端口,因此咱們使用這種機制,對IP和端口進行替換就好了。這樣有一個問題就是,若是有些宿主機上面不止一個Pod,就會發布一樣的地址和端口,可是這樣對於Dubbo來講是沒問題的,至關於這個宿主機的流量有多份,好比下面我發佈的一個示例。
到此,咱們已經基本上解決了Dubbo應用上雲的問題,接下來介紹上雲後遇到的問題與分析。
本章將會詳細分析流量是如何在k8s流轉的,太底層的簡單介紹。
要分析流量不均的問題,就須要知道流量是怎麼走的。 在k8s中,網絡問題是一個最複雜的問題,可是不是每一個人都須要完整掌握這些內容,對於大部分開發人員來講,須要知道基本原理。
k8s的網絡聯通方式有不少種,可是都有一個規範,須要知足以下條件:
圖片說明: 一個k8s集羣,下面個Node,上面是Node上起的Pod
對於k8s集羣,Node是物理節點,它們在一個能夠互相訪問的網絡。真正對外提供服務的單元,是Pod,其實也能夠把它們看做一些小虛擬機,每一個Pod都有本身的IP,通常跟Node不處於一個網段。
基於前述的網絡模型,對於上圖中的一個集羣,Node對Pod是包含的關係,每一個Pod都運行在一個Node上,可是,在網絡上,它們能夠看做是不通的實體,有惟一的IP,並且這個網絡中的實體均可以互相通訊,而沒必要通過NAT。
以上咱們說的是集羣內部,當一個Pod要訪問外部的IP地址的時候,通常是要通過NAT的。
圖片說明:Pod訪問外部,要通過SNAT,把源地址替換成Node的地址
要實現上述規範,有多種方式,好比經過配置複雜的路由信息,經過Overlay網絡等,不一樣的公司和組織對網絡解決方案的需求不同,因此Kubernetes沒有把方案固化在系統中,而是經過接口的形式,讓使用者本身去選擇實現方式,這個接口就是CNI(Container Network Interface)。
圖片說明:kubelet建立Pod時會調用具體的CNI插件
每一個Node上面都有一個kubelet進程,用於接收Api Server的指令,建立Pod,這時候它會根據安裝的CNI的插件去給容器建立網絡,一樣,k8s對於容器的建立,也是基於插件的,叫CRI(Container Runtime Interface),雖然咱們主要有docker,可是還有rkt等其它實現。
通常要實現CNI,有如下幾種方式,比較經常使用的好比flannel,此處再也不詳細介紹底層原理。
先說明一下,此處說KubeProxy解決了外部訪問內部Pod,並不確切,只是爲了與上面集羣內部互聯互通對應。KubeProxy是K8s裏一個很是重要的組件,雖然解決外部訪問內部的Pod並不徹底靠它,可是它在每種方式裏都起了很大做用。
k8s對外提供服務,靠的是Service,Service是k8s裏最核心的概念,它把符合某一些特徵的Pod組合起來,經過負載均衡的方式對外服務。
好比咱們定義下面一個nginx的Service,會把存在標籤app:nginx的pod選出來,做爲一個服務對外提供。Service不關係Pod是怎麼建立的,這些Pod有多是單首創建的,有多是Deployment,StatefulSet,DaemonSet等,Service只關心Pod是否有這個標籤。
其中targetPort是Pod暴漏的端口,port是給ClusterIP暴漏的端口,nodePort是主機上暴漏的端口。
apiVersion: v1 kind: Service metadata: name: nginx-service spec: type: NodePort selector: app: nginx ports: - protocol: TCP port: 8080 targetPort: 80 nodePort: 30008
建立後,有一個Service,
k8s@kube-master1:~$ kubectl get service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 530d nginx-service NodePort 10.100.33.163 <none> 8080:30008/TCP 12m
它有4個Pod
k8s@kube-master1:~$ kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-deployment-85ff79dd56-26w6p 1/1 Running 0 8m15s 10.244.3.33 node3 <none> <none> nginx-deployment-85ff79dd56-dbv58 1/1 Running 0 8m15s 10.244.1.47 node1 <none> <none> nginx-deployment-85ff79dd56-gtr7x 1/1 Running 0 8m15s 10.244.2.29 node2 <none> <none> nginx-deployment-85ff79dd56-jn6q2 1/1 Running 0 8m15s 10.244.2.30 node2 <none> <none>
接下來,咱們以這個Service來爲例介紹後面的內容,這個集羣是我在本地打的一個集羣,3個Node的地址分別是192.168.174.51/52/53。
k8s@kube-master1:~$ kubectl get node NAME STATUS ROLES AGE VERSION kube-master1 Ready master 532d v1.16.2 node1 Ready <none> 532d v1.16.2 node2 Ready <none> 532d v1.16.2 node3 Ready <none> 532d v1.16.2
每一個Node上面,有一個KubeProxy,負責對一個Service的多個Pod作轉發和負載均衡,保存着每一個服務對應的Pod以及它的TargetPort,稱爲EndPoint,這個信息是隨着Service中的Pod變化而動態變化的,全部信息保存到集中的etcd中。
KubeProxy根據這些信息進行負載均衡。
即便有的Node上沒有服務對應的Pod,它的Kube-Proxy也能實現這個服務的跨Node的轉發
所以,要訪問一個Service,其實就是訪問KubeProxy,它會將請求轉發到該Service下的全部pod,並且不僅是本機的Pod,整個集羣中的Pod均可以轉發,前面的網絡模型介紹過了,Node跟全部的Pod都是相通的。
那麼怎麼訪問KubeProxy呢? 有以下幾種方式
ClusterIP是專門讓集羣內部訪問Service的方法,由於每一個服務都能經過Node本機的Kube-Proxy訪問,因此其它Pod調用本機的Kube-Proxy便可。
可是,假設咱們寫了一個應用,部署到k8s上,要調用一個集羣內的Service,對於開發人員來講,「配置爲本機地址」, 這個是很差配置的,因此,k8s就發明了ClusterIP這個概念,它是一個虛擬的IP,經過這個IP就能夠訪問Service,這個IP是不存在的,ping不通,可是能夠telnet(不通的模式不同,有的模式能ping通)。
它的實現原理是什麼呢?經過iptables的轉發規則,將這個IP對應的端口的請求,轉發到本機的Kube-Proxy便可(注:這個例子是iptables轉發的)。
-A KUBE-SERVICES -d 10.100.33.163/32 -p tcp -m comment --comment "default/nginx-service: cluster IP" -m tcp --dport 8080 -j KUBE-SVC-GKN7Y2BSGW4NJTYL
看這個iptables規則,就是把目標地址10.100.33.163,端口爲8080的tcp請求,轉發到KUBE-SVC-GKN7Y2BSGW4NJTYL這個規則上,這個規則就是kube-proxy的一種實現,是專門轉發到這個服務的。
因此,集羣內任意一個Node或者Pod使用ClusterIP訪問這個服務的時候,直接被轉發到了本機的Kube-Proxy,進而實現了負載均衡。
ClusterIP解決了內部訪問的問題,那外部訪問呢?大部分網絡模型實現,Pod的地址對外是不可見的,因此外部是無法直接調用Pod的IP和端口的,那就只能經過開放Node的端口來實現對內的訪問,也就是NodePort。
對於一個服務,若是要使用NodePort方式訪問,那麼它須要佔一個端口,全部Node上的端口都被這個服務佔用,好比前面的nginx服務,使用了30008端口,用任意一個Node的地址加上這個30008端口,就能夠訪問這個Service。
KubeProxy會監聽這個端口,而且將這個端口對應的請求轉發到後面的Pod上。
注:此處的「監聽」,或者「listen」,並不合適,只是看上去像監聽,就拿我建立的這個服務爲例,它實際上是跟前面ClusterIP是同樣的,有條iptables規則,能夠看到標紅的,是把NodePort和ClusterIP都轉發到同一個規則上,也就是kube-proxy上,下面會詳細介紹。
這時候KubeProxy保存的信息包含了NodePort的信息。
使用這種方式會有如下兩個問題:
那就又衍生出兩種模式,Loadbalancer和Ingress,由於這兩種方式與KubeProxy關係不大,再也不贅述,簡述原理。
Loadbalancer模式雖然k8s提供了接口,可是沒提供實現,由於須要外部的負載均衡,通常在公有云上會提供Loadbalancer,在私有云上,咱們也能夠本身實現Loadbalancer模式,好比前面掛載一個F5。
端口有限,且很差管理,能不能擴展一下呢?能夠經過域名的方式區分不一樣的服務,而不是端口,好比mb.cmbc.com.cn:8080轉發到mb-service,per.cmbc.com.cn:8080轉發到per-service,nginx是有這個能力根據域名轉發到不通的服務上的,咱們能夠依賴nginx實現這個功能。雖然我建立的這個服務是nginx的,可是Ingress是在咱們的服務之上又部署了一層nginx(除了nginx,還可經過其它方式實現)。
本質上,Ingress就是基於NodePort作的一個nginx的Service,可是這一層nginx是由k8s管理的,它能動態修改nginx的配置。
好比咱們新建了一個Service,它對應兩個Pod如上圖所示,端口是28080,要經過Ingress的方式對外開放,域名是mb.cmbc.com.cn,這時候會建立nginx的pod,它的配置文件被寫成了下面的樣子。
upstream mbservice { server 172.16.1.4:28080; server 172.16.1.5:28080; } server { listen 80; server_name mb.cmbc.com.cn; location / { #root html; #index index.html index.htm; proxy_pass http://mbservice; proxy_connect_timeout 2s; }
這個配置文件隨着Pod的變化,能動態更新並加載。 這樣至關於對外就是一個nginx服務,它在作內部服務的轉發,咱們還能夠加一個perservice在這個nginx上,這樣的話其實只佔用一個端口就行,可是必須用域名訪問。Ingress還解決了一個會話保持的問題,由於KubeProxy只能使用ip-hash作會話保持,而nginx能夠基於cookie作會話保持。
如今已經解決了訪問服務的問題,接下來看Kube-Proxy如何作服務的轉發和負載均衡。
KubeProxy並不必定是一個實際存在的實體,它也多是一組規則,KubeProxy有三種實現:userspace, iptables和ipvs,接下來分別介紹。
userspace,名字就能看出來,是用戶空間的實現,它是真正的要起一個進程,監聽端口,而且在這個進程內作路由轉發和負載均衡,原理上很簡單,可是它的效率過低,早已經被k8s拋棄了。
使用userspace模式,在最好的狀況下,用戶進程讀到網絡數據,什麼也不操做,直接轉發,也須要兩次數據複製,只要用戶空間的進程稍微操做下,就須要更多的內存複製,性能太差。
圖片說明:假設用戶進程徹底不操做,基本上不太可能
既然有了userspace模式,也就有kernelspace模式,它能減小內存複製,大大提升效率。
圖片說明: 使用基於內核的轉發,效率會提升不少
內核模式的轉發就是iptables和ipvs。
這倆其實都是依賴的一個共同的Linux內核模塊:Netfilter。Netfilter是Linux 2.4.x引入的一個子系統,它做爲一個通用的、抽象的框架,提供一整套的hook函數的管理機制,使得諸如數據包過濾、網絡地址轉換(NAT)和基於協議類型的鏈接跟蹤成爲了可能。
Netfilter的架構就是在整個網絡流程的若干位置放置了一些檢測點(HOOK),而在每一個檢測點上登記了一些處理函數進行處理。
圖片說明:Netfilter的Hook點
在一個網絡包進入Linux網卡後,有可能通過這上面五個Hook點,咱們能夠本身寫Hook函數,註冊到任意一個點上,而iptables和ipvs都是在這個基礎上實現的。
iptables是把一些特定規則以「鏈」的形式掛載到每一個Hook點,這些鏈的規則是固定的,而是是比較通用的,能夠經過iptables命令在用戶層動態的增刪,這些鏈必須串行的執行。執行到某條規則,若是不匹配,則繼續執行下一條,若是匹配,根據規則,可能繼續向下執行,也可能跳到某條規則上,也可能下面的規則都跳過。
相比於iptables,ipvs更聚焦,它是專門作負載均衡的,其實ipvs就是著名的LVS(Linux Virtual Server)的底層實現,它僅在部分hook點增長了本身的處理函數,對報文作一些轉換與處理,能夠經過ipvsadm命令在用戶層進行修改。
它們倆依賴的都是同一個內核功能,並且也能實現一些相同的功能,可是差異非常挺大的:
1.iptables更通用,主要是應用在防火牆上,也能應用於路由轉發等功能,ipvs更聚焦,它只能作負載均衡,不能實現其它的例如防火牆上。
2.iptables在處理規則時,是按「鏈」逐條匹配,若是規則過多,性能會變差,它匹配規則的複雜度是O(n),而ipvs處理規則時,在專門的模塊內處理,查找規則的複雜度是O(1)
3.iptables雖然能夠實現負載均衡,可是它的策略比較簡單,只能以機率轉發,而ipvs能夠實現多種策略。
咱們之前面的服務爲例,說明iptables如何實現轉發。
用iptables-save命令,能夠查看當前全部的iptables規則,涉及到k8s的比較多,基本在每一個鏈上都有,包括一些SNAT的,就是Pod訪問外部的地址的時候,須要作一下NAT轉換,咱們只看涉及到Service負載均衡的。
首先,PREROUTING和OUTPUT都加了一個KUBE-SERVICES規則,也就是進來的包和出去的包都要都走一下KUBE-SERVICES規則
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES -A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
而後是目標地址是10.100.33.163.32而且目標端口是8080的,也就是這個服務的ClusterIP,走到KUBE-SVC-GKN7Y2BSGW4NJTYL規則,這個規則其實就是Kube-Proxy。
-A KUBE-SERVICES -d 10.100.33.163/32 -p tcp -m comment --comment "default/nginx-service: cluster IP" -m tcp --dport 8080 -j KUBE-SVC-GKN7Y2BSGW4NJTYL
而後在KUBE-SERVICES下,還有個NodePort的規則,NodePort規則裏有個針對30008的端口,也轉發到KUBE-SVC-GKN7Y2BSGW4NJTYL
-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/nginx-service:" -m tcp --dport 30008 -j KUBE-SVC-GKN7Y2BSGW4NJTYL
綜上,無論是ClusterIP,仍是NodePort,都走到了同一個規則上。
而後咱們來看最重要的KUBE-SVC-GKN7Y2BSGW4NJTYL,它以不一樣的機率走到了不一樣的規則,這些規則最終走到了Pod裏,這就是負載均衡策略。
-A KUBE-SVC-GKN7Y2BSGW4NJTYL -m statistic --mode random --probability 0.25000000000 -j KUBE-SEP-TAGWFRUUPZNGX64Q -A KUBE-SVC-GKN7Y2BSGW4NJTYL -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-EB5WVJVEKQXCINKS -A KUBE-SVC-GKN7Y2BSGW4NJTYL -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-IBEEHXV54CXNZAW6 -A KUBE-SVC-GKN7Y2BSGW4NJTYL -j KUBE-SEP-7U2SPH2FG4WIXRDV -A KUBE-SEP-EB5WVJVEKQXCINKS -p tcp -m tcp -j DNAT --to-destination 10.244.2.29:80 -A KUBE-SEP-TAGWFRUUPZNGX64Q -p tcp -m tcp -j DNAT --to-destination 10.244.1.47:80 -A KUBE-SEP-IBEEHXV54CXNZAW6 -p tcp -m tcp -j DNAT --to-destination 10.244.2.30:80 -A KUBE-SEP-7U2SPH2FG4WIXRDV -p tcp -m tcp -j DNAT --to-destination 10.244.3.33:80
若是咱們把這個負載均衡用圖畫出來,就比較好看了。
由於這個鏈是串行的,因此每條鏈上被選中的機率不同,可是最終,每一個Pod被選中的機率是同樣的。
可使用ipvsadm在用戶空間查看和操做ipvs規則。把k8s的proxy模式改爲ipvs後,重啓了nginx服務,Pod的IP已經變了,Service的不會變。
k8s@kube-master1:~$ kubectl get service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 531d nginx-service NodePort 10.100.33.163 <none> 8080:30008/TCP 45h k8s@kube-master1:~$ kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-deployment-85ff79dd56-cznpb 1/1 Running 0 24m 10.244.2.36 node2 <none> <none> nginx-deployment-85ff79dd56-kj8bv 1/1 Running 0 24m 10.244.3.39 node3 <none> <none> nginx-deployment-85ff79dd56-mljg8 1/1 Running 0 24m 10.244.1.51 node1 <none> <none> nginx-deployment-85ff79dd56-t2cgj 1/1 Running 0 24m 10.244.3.38 node3 <none> <none>
因爲ipvs的hook是在INPUT那,因此必須讓流量走到INPUT鏈,須要把ClusterIP綁定到一個本地網絡設備上,也就是下面的kube-ipvs0,多個服務的話就會綁定多個,每一個Node都有一個這個設備,這個IP稱爲VIP(Virtual IP,此處再也不詳細介紹):
k8s@node1:~$ ip a | grep ipvs 4: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default inet 10.96.0.10/32 brd 10.96.0.10 scope global kube-ipvs0 inet 10.96.0.1/32 brd 10.96.0.1 scope global kube-ipvs0 inet 10.100.33.163/32 brd 10.100.33.163 scope global kube-ipvs0 ------- k8s@node2:~$ ip a | grep ipvs 4: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default inet 10.96.0.10/32 brd 10.96.0.10 scope global kube-ipvs0 inet 10.96.0.1/32 brd 10.96.0.1 scope global kube-ipvs0 inet 10.100.33.163/32 brd 10.100.33.163 scope global kube-ipvs0
只要走到了INPUT上,就能走到ipvs的負載均衡模塊,咱們用ipvsadm查看轉發規則。
k8s@node1:~$ sudo ipvsadm -Ln IP Virtual Server version 1.2.1 (size=4096) Prot LocalAddress:Port Scheduler Flags -> RemoteAddress:Port Forward Weight ActiveConn InActConn ...省略... TCP 192.168.174.51:30008 rr -> 10.244.1.51:80 Masq 1 0 0 -> 10.244.2.36:80 Masq 1 0 0 -> 10.244.3.38:80 Masq 1 0 0 -> 10.244.3.39:80 Masq 1 0 0 ...省略... TCP 10.100.33.163:8080 rr -> 10.244.1.51:80 Masq 1 0 0 -> 10.244.2.36:80 Masq 1 0 0 -> 10.244.3.38:80 Masq 1 0 0 -> 10.244.3.39:80 Masq 1 0 0 ...省略...
能夠看到,到ClusterIP:Port和NodePort的流量,都被以rr(round robin)的策略轉發到4個pod上。
ipvs也須要跟iptables結合使用,還有一些SNAT等規則,此處再也不詳細介紹,可是介紹一個概念ipset。咱們會看到有這樣一條規則:
-A KUBE-NODE-PORT -p tcp -m comment --comment "Kubernetes nodeport TCP port for masquerade purpose" -m set --match-set KUBE-NODE-PORT-TCP dst -j KUBE-MARK-MASQ
這條規則是用來將全部的NodePort的流量作SNAT的,可是不須要每一個NodePort加一條規則,而是用了一個match-set,就是包含在某個集合內的流量都要走這個規則,這個集合就是ipset。
k8s@node1:~$ sudo ipset list KUBE-NODE-PORT-TCP Name: KUBE-NODE-PORT-TCP Type: bitmap:port Revision: 3 Header: range 0-65535 Size in memory: 8264 References: 1 Number of entries: 1 Members: 30008
這個set是bitmap,效率很高,用來匹配數據包複雜度是O(1)。
再看另外一個ipset,這個是ClusterIP的set,使用hash實現的,效率也特別高。
Name: KUBE-CLUSTER-IP Type: hash:ip,port Revision: 5 Header: family inet hashsize 1024 maxelem 65536 Size in memory: 408 References: 2 Number of entries: 5 Members: 10.96.0.1,tcp:443 10.96.0.10,tcp:9153 10.100.33.163,tcp:8080 10.96.0.10,udp:53 10.96.0.10,tcp:53
經過使用ipset的方式,使得iptables的規則數不會隨着節點數量增加,可以支持超大規模的集羣。
由於Dubbo配置比較複雜,還須要製做鏡像,寫服務端代碼和客戶端代碼,比較麻煩,因此使用Redis模擬Dubbo的行爲,效果是同樣的。
Dubbo的服務提供者會主動發佈地址到zk上,基於前面的方案,提供者會把Node的地址和NodePort發佈出來,Dubbo消費者會在客戶端側作負載均衡,同時,進入kube-proxy後,會再次負載均衡。
這樣就會出現一個問題,客戶端訪問了192.168.174.51,有可能最終在192.168.174.52上執行,這樣對於客戶端遍歷全部服務端的一些操做,就沒法進行了。
Dubbo默認的是消費者與每一個服務提供者創建一個長鏈接,而後再客戶端對這些長鏈接使用輪詢的策略作負載均衡。咱們能夠用Redis模擬這個行爲,把zk上的地址寫死,使用jedis與每一個地址創建長鏈接,而後輪詢調用,看看每一個pod的流量。
建立一個Redis服務
k8s@kube-master1:~$ kubectl get service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 531d redis-service NodePort 10.104.6.137 <none> 6379:30001/TCP 24m
它有3個Pod,分別在集羣的3個節點上
k8s@kube-master1:~$ kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES redis-deployment-8687bfc768-5795b 1/1 Running 1 13m 10.244.2.40 node2 <none> <none> redis-deployment-8687bfc768-srps9 1/1 Running 1 13m 10.244.1.54 node1 <none> <none> redis-deployment-8687bfc768-xl2zm 1/1 Running 1 13m 10.244.3.42 node3 <none> <none>
進入到每一個實例,分別設置key爲pod的值爲1,2,3,好比下面先連到第一個pod上設置,依次對3個pod作設置。
k8s@kube-master1:~$ kubectl exec -it redis-deployment-8687bfc768-7lzpl /bin/bash root@redis-deployment-8687bfc768-7lzpl:/data# redis-cli 127.0.0.1:6379> set pod 1 OK 127.0.0.1:6379> get pod "1"
咱們讓客戶端都去get('pod'),根據取到的值的分佈,就能知道流量的分佈。
咱們用jedis模擬一下Dubbo的訪問過程,看看流量分配的狀況
import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import redis.clients.jedis.Jedis; public class ABC { public static void main(String[] args) throws InterruptedException { /**模擬dubbo發佈在zk的地址**/ String[] addrs = {"192.168.174.51","192.168.174.52","192.168.174.53"}; /**模擬6個消費者**/ Thread[] threads = new Thread[6]; /**3個計數器,統計3個Pod的訪問次數**/ final Map<String,AtomicLong> map = new ConcurrentHashMap<String,AtomicLong>(); map.put("1", new AtomicLong(0)); map.put("2", new AtomicLong(0)); map.put("3", new AtomicLong(0)); for(int i = 0; i < 6; i++) { threads[i] = new Thread(new Runnable() { @Override public void run() { //每一個消費者分別與3個提供者創建單個長鏈接,與Dubbo同樣 Jedis[] jedis = new Jedis[3]; for(int j = 0; j < 3; j++) { jedis[j] = new Jedis(addrs[j], 30001,5000,5000); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } //模擬Dubbo的輪詢負載均衡策略 for(int k = 0; k < 300; k++) { String pod = jedis[k%3].get("pod"); map.get(pod).getAndIncrement(); } }}); threads[i].start(); } Thread.sleep(5000); System.out.println(map); } }
屢次運行,流量都是不平均的。
若是咱們把消費者改爲100個,看上去比例還好點
但也是不平均的。
如今咱們改爲ipvs模式,重啓一下。
k8s@kube-master1:~$ kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES redis-deployment-8687bfc768-5795b 1/1 Running 0 6m1s 10.244.2.39 node2 <none> <none> redis-deployment-8687bfc768-srps9 1/1 Running 0 6m1s 10.244.1.53 node1 <none> <none> redis-deployment-8687bfc768-xl2zm 1/1 Running 0 6m1s 10.244.3.41 node3 <none> <none>
前面的代碼,反覆執行,結果都是同樣的。
根據這個,咱們能夠肯定,使用iptables的時候,因爲在負載均衡的時候使用機率,短連接的時候,交易量大了,能實現負載均衡。可是在長鏈接的時候,在創建鏈接的時候是以機率創建的,而一旦創建,以後的請求都會走這個鏈接,因此若是鏈接數很少,那麼各個Pod的鏈接數可能差別很大。
在傳統的架構中,網絡關係是很清晰的,或者直連,或者中間有F5或者Nginx等作一次負載均衡,可是上了k8s以後,底層的網絡轉發是特別複雜的,對於開發者來講,這是透明的,可是出了問題,不少時候,須要瞭解底層的東西。
在咱們傳統應用向雲上遷移時,因爲多年的持續開發,不只是軟件內部架構,以及與外部的交互關係,都比較複雜,在上雲的過程當中,須要十分謹慎,先用一些比較簡單的模塊充分驗證,摸索經驗,再逐步把核心應用遷移到雲上。