【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

1、概述

本文針對咱們生產上出現的流量不均的問題,深層次地分析問題產生緣由,對其中的一些機制作一些介紹。html

k8s是一個特別複雜的系統,而網絡相關的問題是其中最複雜的問題,要經過一兩篇文章介紹清楚是很難的。這個流量不均的問題出現的緣由並不複雜,就是由於kube-proxy使用了iptables作負載均衡,而它是以機率的方式轉發,使用長鏈接且鏈接數較少時,誤差會比較大。雖然緣由不復雜,可是咱們但願能把這其中的整個流程和原理梳理清楚,在介紹過程當中,同時介紹一些底層的東西,可是不會太深刻。java

2、背景介紹

本章主要介紹一些相關的背景,包括出問題的系統,生產上的現象等。node

2.1 生產問題描述

出問題的系統是一個以Dubbo服務爲基礎的應用,日交易量近2000萬,在生產上部署到了k8s集羣中,發現集羣裏的Pod流量不均衡,並且差別很大,甚至有時候到了9比1的流量比,有的pod日交易量接近千萬,有的還不到一百萬,因爲交易自己比較快,未形成嚴重後果。nginx

在默認狀況下,Dubbo的消費者會與每個生產者創建一個長鏈接,以後的請求都經過這個長鏈接發送,底層基於Netty實現IO多路複用,即便單個鏈接也能實現高效傳輸。消費者在客戶端實現負載均衡,輪詢向每一個鏈接發送請求,出現流量不均,應該就是KubeProxy作了二次負載均衡致使。redis

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

2.2 Dubbo協議適配容器的問題與解決辦法

默認狀況下,Dubbo服務的提供者把本機地址發佈到zk上,消費者經過訂閱zk,獲取提供者的地址信息,在客戶端進行負載均衡,調用提供者。docker

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

可是,當服務提供者在容器內的時候,它發佈的地址是POD的地址,除非消費者也在k8s中,不然無法訪問這個地址。在咱們將傳統應用向雲上遷移時,不可避免的會有云外的應用訪問雲上的應用,因此這個問題必需要解決。api

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

新版的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來講是沒問題的,至關於這個宿主機的流量有多份,好比下面我發佈的一個示例。
【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

到此,咱們已經基本上解決了Dubbo應用上雲的問題,接下來介紹上雲後遇到的問題與分析。

3、流量不均問題分析與驗證

本章將會詳細分析流量是如何在k8s流轉的,太底層的簡單介紹。

3.1 流量是如何流轉的

要分析流量不均的問題,就須要知道流量是怎麼走的。 在k8s中,網絡問題是一個最複雜的問題,可是不是每一個人都須要完整掌握這些內容,對於大部分開發人員來講,須要知道基本原理。

3.1.1 k8s網絡模型-解決集羣內部的互聯互通

k8s的網絡聯通方式有不少種,可是都有一個規範,須要知足以下條件:

  • 集羣中全部Pod都有一個惟一的IP,均可以與全部其餘Pod通訊,而無需使用網絡地址轉換(NAT)。
  • 全部Node均可以在沒有NAT的狀況下與全部Pod通訊。
  • Pod看到本身的IP就是其餘人看到的IP。

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析
圖片說明: 一個k8s集羣,下面個Node,上面是Node上起的Pod

對於k8s集羣,Node是物理節點,它們在一個能夠互相訪問的網絡。真正對外提供服務的單元,是Pod,其實也能夠把它們看做一些小虛擬機,每一個Pod都有本身的IP,通常跟Node不處於一個網段。

基於前述的網絡模型,對於上圖中的一個集羣,Node對Pod是包含的關係,每一個Pod都運行在一個Node上,可是,在網絡上,它們能夠看做是不通的實體,有惟一的IP,並且這個網絡中的實體均可以互相通訊,而沒必要通過NAT。

以上咱們說的是集羣內部,當一個Pod要訪問外部的IP地址的時候,通常是要通過NAT的。

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析
圖片說明:Pod訪問外部,要通過SNAT,把源地址替換成Node的地址

要實現上述規範,有多種方式,好比經過配置複雜的路由信息,經過Overlay網絡等,不一樣的公司和組織對網絡解決方案的需求不同,因此Kubernetes沒有把方案固化在系統中,而是經過接口的形式,讓使用者本身去選擇實現方式,這個接口就是CNI(Container Network Interface)。

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析
圖片說明:kubelet建立Pod時會調用具體的CNI插件

每一個Node上面都有一個kubelet進程,用於接收Api Server的指令,建立Pod,這時候它會根據安裝的CNI的插件去給容器建立網絡,一樣,k8s對於容器的建立,也是基於插件的,叫CRI(Container Runtime Interface),雖然咱們主要有docker,可是還有rkt等其它實現。

通常要實現CNI,有如下幾種方式,比較經常使用的好比flannel,此處再也不詳細介紹底層原理。

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

3.1.2 KubeProxy-解決外部訪問內部POD

先說明一下,此處說KubeProxy解決了外部訪問內部Pod,並不確切,只是爲了與上面集羣內部互聯互通對應。KubeProxy是K8s裏一個很是重要的組件,雖然解決外部訪問內部的Pod並不徹底靠它,可是它在每種方式裏都起了很大做用。

k8s對外提供服務,靠的是Service,Service是k8s裏最核心的概念,它把符合某一些特徵的Pod組合起來,經過負載均衡的方式對外服務。

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

好比咱們定義下面一個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中。

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

KubeProxy根據這些信息進行負載均衡。

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

即便有的Node上沒有服務對應的Pod,它的Kube-Proxy也能實現這個服務的跨Node的轉發

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

所以,要訪問一個Service,其實就是訪問KubeProxy,它會將請求轉發到該Service下的全部pod,並且不僅是本機的Pod,整個集羣中的Pod均可以轉發,前面的網絡模型介紹過了,Node跟全部的Pod都是相通的。

那麼怎麼訪問KubeProxy呢? 有以下幾種方式

3.2.2.1 ClusterIP

ClusterIP是專門讓集羣內部訪問Service的方法,由於每一個服務都能經過Node本機的Kube-Proxy訪問,因此其它Pod調用本機的Kube-Proxy便可。
【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

可是,假設咱們寫了一個應用,部署到k8s上,要調用一個集羣內的Service,對於開發人員來講,「配置爲本機地址」, 這個是很差配置的,因此,k8s就發明了ClusterIP這個概念,它是一個虛擬的IP,經過這個IP就能夠訪問Service,這個IP是不存在的,ping不通,可是能夠telnet(不通的模式不同,有的模式能ping通)。

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

它的實現原理是什麼呢?經過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,進而實現了負載均衡。

3.1.2.2 NodePort

ClusterIP解決了內部訪問的問題,那外部訪問呢?大部分網絡模型實現,Pod的地址對外是不可見的,因此外部是無法直接調用Pod的IP和端口的,那就只能經過開放Node的端口來實現對內的訪問,也就是NodePort。

對於一個服務,若是要使用NodePort方式訪問,那麼它須要佔一個端口,全部Node上的端口都被這個服務佔用,好比前面的nginx服務,使用了30008端口,用任意一個Node的地址加上這個30008端口,就能夠訪問這個Service。

KubeProxy會監聽這個端口,而且將這個端口對應的請求轉發到後面的Pod上。

注:此處的「監聽」,或者「listen」,並不合適,只是看上去像監聽,就拿我建立的這個服務爲例,它實際上是跟前面ClusterIP是同樣的,有條iptables規則,能夠看到標紅的,是把NodePort和ClusterIP都轉發到同一個規則上,也就是kube-proxy上,下面會詳細介紹。

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

這時候KubeProxy保存的信息包含了NodePort的信息。
【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

使用這種方式會有如下兩個問題:

  1. 調用者不能訪問固定IP,須要本身作負載均衡
  2. 只能使用 30000-32767這些端口,端口有限且管理麻煩(能夠修改端口範圍)

那就又衍生出兩種模式,Loadbalancer和Ingress,由於這兩種方式與KubeProxy關係不大,再也不贅述,簡述原理。

Loadbalancer模式

Loadbalancer模式雖然k8s提供了接口,可是沒提供實現,由於須要外部的負載均衡,通常在公有云上會提供Loadbalancer,在私有云上,咱們也能夠本身實現Loadbalancer模式,好比前面掛載一個F5。
【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

Ingress模式

端口有限,且很差管理,能不能擴展一下呢?能夠經過域名的方式區分不一樣的服務,而不是端口,好比mb.cmbc.com.cn:8080轉發到mb-service,per.cmbc.com.cn:8080轉發到per-service,nginx是有這個能力根據域名轉發到不通的服務上的,咱們能夠依賴nginx實現這個功能。雖然我建立的這個服務是nginx的,可是Ingress是在咱們的服務之上又部署了一層nginx(除了nginx,還可經過其它方式實現)。
【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

本質上,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如何作服務的轉發和負載均衡。

3.1.3 KubeProxy的實現

KubeProxy並不必定是一個實際存在的實體,它也多是一組規則,KubeProxy有三種實現:userspace, iptables和ipvs,接下來分別介紹。

3.1.3.1 userspace

userspace,名字就能看出來,是用戶空間的實現,它是真正的要起一個進程,監聽端口,而且在這個進程內作路由轉發和負載均衡,原理上很簡單,可是它的效率過低,早已經被k8s拋棄了。

使用userspace模式,在最好的狀況下,用戶進程讀到網絡數據,什麼也不操做,直接轉發,也須要兩次數據複製,只要用戶空間的進程稍微操做下,就須要更多的內存複製,性能太差。
【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析
圖片說明:假設用戶進程徹底不操做,基本上不太可能

既然有了userspace模式,也就有kernelspace模式,它能減小內存複製,大大提升效率。

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析
圖片說明: 使用基於內核的轉發,效率會提升不少

內核模式的轉發就是iptables和ipvs。

3.1.3.2 iptables和ipvs

這倆其實都是依賴的一個共同的Linux內核模塊:Netfilter。Netfilter是Linux 2.4.x引入的一個子系統,它做爲一個通用的、抽象的框架,提供一整套的hook函數的管理機制,使得諸如數據包過濾、網絡地址轉換(NAT)和基於協議類型的鏈接跟蹤成爲了可能。
Netfilter的架構就是在整個網絡流程的若干位置放置了一些檢測點(HOOK),而在每一個檢測點上登記了一些處理函數進行處理。
【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析
圖片說明:Netfilter的Hook點

在一個網絡包進入Linux網卡後,有可能通過這上面五個Hook點,咱們能夠本身寫Hook函數,註冊到任意一個點上,而iptables和ipvs都是在這個基礎上實現的。

iptables是把一些特定規則以「鏈」的形式掛載到每一個Hook點,這些鏈的規則是固定的,而是是比較通用的,能夠經過iptables命令在用戶層動態的增刪,這些鏈必須串行的執行。執行到某條規則,若是不匹配,則繼續執行下一條,若是匹配,根據規則,可能繼續向下執行,也可能跳到某條規則上,也可能下面的規則都跳過。

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

相比於iptables,ipvs更聚焦,它是專門作負載均衡的,其實ipvs就是著名的LVS(Linux Virtual Server)的底層實現,它僅在部分hook點增長了本身的處理函數,對報文作一些轉換與處理,能夠經過ipvsadm命令在用戶層進行修改。

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

它們倆依賴的都是同一個內核功能,並且也能實現一些相同的功能,可是差異非常挺大的:

1.iptables更通用,主要是應用在防火牆上,也能應用於路由轉發等功能,ipvs更聚焦,它只能作負載均衡,不能實現其它的例如防火牆上。

2.iptables在處理規則時,是按「鏈」逐條匹配,若是規則過多,性能會變差,它匹配規則的複雜度是O(n),而ipvs處理規則時,在專門的模塊內處理,查找規則的複雜度是O(1)

3.iptables雖然能夠實現負載均衡,可是它的策略比較簡單,只能以機率轉發,而ipvs能夠實現多種策略。

3.1.3.2.1 使用iptables實現Kube-Proxy轉發

咱們之前面的服務爲例,說明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

若是咱們把這個負載均衡用圖畫出來,就比較好看了。

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

由於這個鏈是串行的,因此每條鏈上被選中的機率不同,可是最終,每一個Pod被選中的機率是同樣的。

3.1.3.2.2 使用ipvs進行轉發

可使用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的規則數不會隨着節點數量增加,可以支持超大規模的集羣。

3.2 問題復現與解決

由於Dubbo配置比較複雜,還須要製做鏡像,寫服務端代碼和客戶端代碼,比較麻煩,因此使用Redis模擬Dubbo的行爲,效果是同樣的。
【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

Dubbo的服務提供者會主動發佈地址到zk上,基於前面的方案,提供者會把Node的地址和NodePort發佈出來,Dubbo消費者會在客戶端側作負載均衡,同時,進入kube-proxy後,會再次負載均衡。
【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

這樣就會出現一個問題,客戶端訪問了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);
    }
}

屢次運行,流量都是不平均的。

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析
【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析
若是咱們把消費者改爲100個,看上去比例還好點
【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

但也是不平均的。

如今咱們改爲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>

前面的代碼,反覆執行,結果都是同樣的。

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析image-20210413194626586

根據這個,咱們能夠肯定,使用iptables的時候,因爲在負載均衡的時候使用機率,短連接的時候,交易量大了,能實現負載均衡。可是在長鏈接的時候,在創建鏈接的時候是以機率創建的,而一旦創建,以後的請求都會走這個鏈接,因此若是鏈接數很少,那麼各個Pod的鏈接數可能差別很大。

4、總結

在傳統的架構中,網絡關係是很清晰的,或者直連,或者中間有F5或者Nginx等作一次負載均衡,可是上了k8s以後,底層的網絡轉發是特別複雜的,對於開發者來講,這是透明的,可是出了問題,不少時候,須要瞭解底層的東西。

【博客大賽】【實戰】k8s中長鏈接服務負載不均衡問題分析

在咱們傳統應用向雲上遷移時,因爲多年的持續開發,不只是軟件內部架構,以及與外部的交互關係,都比較複雜,在上雲的過程當中,須要十分謹慎,先用一些比較簡單的模塊充分驗證,摸索經驗,再逐步把核心應用遷移到雲上。

相關文章
相關標籤/搜索