一、概述

本文针对我们生产上出现的流量不均的问题,深层次地分析问题产生原因,对其中的一些机制做一些介绍。

k8s是一个特别复杂的系统,而网络相关的问题是其中最复杂的问题,要通过一两篇文章介绍清楚是很难的。这个流量不均的问题出现的原因并不复杂,就是因为kube-proxy使用了iptables做负载均衡,而它是以概率的方式转发,使用长连接且连接数较少时,偏差会比较大。虽然原因不复杂,但是我们希望能把这其中的整个流程和原理梳理清楚,在介绍过程中,同时介绍一些底层的东西,但是不会太深入。

二、背景介绍

本章主要介绍一些相关的背景,包括出问题的系统,生产上的现象等。

2.1 生产问题描述

出问题的系统是一个以Dubbo服务为基础的应用,日交易量近2000万,在生产上部署到了k8s集群中,发现集群里的Pod流量不均衡,而且差异很大,甚至有时候到了9比1的流量比,有的pod日交易量接近千万,有的还不到一百万,由于交易本身比较快,未造成严重后果。

在默认情况下,Dubbo的消费者会与每一个生产者建立一个长连接,之后的请求都通过这个长连接发送,底层基于Netty实现IO多路复用,即使单个连接也能实现高效传输。消费者在客户端实现负载均衡,轮询向每个连接发送请求,出现流量不均,应该就是KubeProxy做了二次负载均衡导致。

2.2 Dubbo协议适配容器的问题与解决办法

默认情况下,Dubbo服务的提供者把本机地址发布到zk上,消费者通过订阅zk,获取提供者的地址信息,在客户端进行负载均衡,调用提供者。

但是,当服务提供者在容器内的时候,它发布的地址是POD的地址,除非消费者也在k8s中,否则没法访问这个地址。在我们将传统应用向云上迁移时,不可避免的会有云外的应用访问云上的应用,所以这个问题必须要解决。

新版的Dubbo里提供了四个配置用来指定要发布的地址和端口,可以从环境变量或者properties里取。因为我们用的Dubbo版本较老,没实现这个功能,所以自己添加了这个功能。

DUBBO_IP_TO_REGISTRY: 要发布到注册中心上的地址DUBBO_PORT_TO_REGISTRY: 要发布到注册中心上的端口DUBBO_IP_TO_BIND: 要绑定的服务地址(监听的地址)DUBBO_PORT_TO_BIND: 要绑定的服务端口

通过在Deployment的yaml文件中指定前面两个环境变量,即可让Dubbo发布指定的地址和端口。

        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流转的,太底层的简单介绍。

3.1 流量是如何流转的

要分析流量不均的问题,就需要知道流量是怎么走的。 在k8s中,网络问题是一个最复杂的问题,但是不是每个人都需要完整掌握这些内容,对于大部分开发人员来说,需要知道基本原理。

3.1.1 k8s网络模型-解决集群内部的互联互通

k8s的网络联通方式有很多种,但是都有一个规范,需要满足如下条件:

  • 集群中所有Pod都有一个唯一的IP,都可以与所有其他Pod通信,而无需使用网络地址转换(NAT)。
  • 所有Node都可以在没有NAT的情况下与所有Pod通信。
  • Pod看到自己的IP就是其他人看到的IP。


图片说明: 一个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,此处不再详细介绍底层原理。

3.1.2 KubeProxy-解决外部访问内部POD

先说明一下,此处说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: v1kind: Servicemetadata:   name: nginx-servicespec:   type: NodePort   selector:      app: nginx   ports:    - protocol: TCP      port: 8080      targetPort: 80      nodePort: 30008

创建后,有一个Service,

k8s@kube-master1:~$ kubectl get serviceNAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGEkubernetes      ClusterIP   10.96.0.1       <none>        443/TCP          530dnginx-service   NodePort    10.100.33.163   <none>        8080:30008/TCP   12m

它有4个Pod

k8s@kube-master1:~$ kubectl get pod -o wideNAME                                READY   STATUS    RESTARTS   AGE     IP            NODE    NOMINATED NODE   READINESS GATESnginx-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 nodeNAME           STATUS   ROLES    AGE    VERSIONkube-master1   Ready    master   532d   v1.16.2node1          Ready    <none>   532d   v1.16.2node2          Ready    <none>   532d   v1.16.2node3          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呢? 有如下几种方式

3.2.2.1 ClusterIP

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,进而实现了负载均衡。

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上,下面会详细介绍。

这时候KubeProxy保存的信息包含了NodePort的信息。

使用这种方式会有以下两个问题:

  1. 调用者不能访问固定IP,需要自己做负载均衡
  2. 只能使用 30000-32767这些端口,端口有限且管理麻烦(可以修改端口范围)

那就又衍生出两种模式,Loadbalancer和Ingress,因为这两种方式与KubeProxy关系不大,不再赘述,简述原理。

Loadbalancer模式

Loadbalancer模式虽然k8s提供了接口,但是没提供实现,因为需要外部的负载均衡,一般在公有云上会提供Loadbalancer,在私有云上,我们也可以自己实现Loadbalancer模式,比如前面挂载一个F5。

Ingress模式

端口有限,且不好管理,能不能扩展一下呢?可以通过域名的方式区分不同的服务,而不是端口,比如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如何做服务的转发和负载均衡。

3.1.3 KubeProxy的实现

KubeProxy并不一定是一个实际存在的实体,它也可能是一组规则,KubeProxy有三种实现:userspace, iptables和ipvs,接下来分别介绍。

3.1.3.1 userspace

userspace,名字就能看出来,是用户空间的实现,它是真正的要起一个进程,监听端口,并且在这个进程内做路由转发和负载均衡,原理上很简单,但是它的效率太低,早已经被k8s抛弃了。

使用userspace模式,在最好的情况下,用户进程读到网络数据,什么也不操作,直接转发,也需要两次数据复制,只要用户空间的进程稍微操作下,就需要更多的内存复制,性能太差。

图片说明:假设用户进程完全不操作,基本上不太可能

既然有了userspace模式,也就有kernelspace模式,它能减少内存复制,大大提高效率。


图片说明: 使用基于内核的转发,效率会提高很多

内核模式的转发就是iptables和ipvs。

3.1.3.2 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可以实现多种策略。

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

如果我们把这个负载均衡用图画出来,就比较好看了。

因为这个链是串行的,所以每条链上被选中的概率不一样,但是最终,每个Pod被选中的概率是一样的。

3.1.3.2.2 使用ipvs进行转发

可以使用ipvsadm在用户空间查看和操作ipvs规则。把k8s的proxy模式改成ipvs后,重启了nginx服务,Pod的IP已经变了,Service的不会变。

k8s@kube-master1:~$ kubectl get serviceNAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGEkubernetes      ClusterIP   10.96.0.1       <none>        443/TCP          531dnginx-service   NodePort    10.100.33.163   <none>        8080:30008/TCP   45hk8s@kube-master1:~$ kubectl get pod -o wideNAME                                READY   STATUS    RESTARTS   AGE   IP            NODE    NOMINATED NODE   READINESS GATESnginx-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 ipvs4: 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 ipvs4: 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 -LnIP 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-TCPName: KUBE-NODE-PORT-TCPType: bitmap:portRevision: 3Header: range 0-65535Size in memory: 8264References: 1Number of entries: 1Members:30008

这个set是bitmap,效率很高,用来匹配数据包复杂度是O(1)。

再看另一个ipset,这个是ClusterIP的set,使用hash实现的,效率也特别高。

Name: KUBE-CLUSTER-IPType: hash:ip,portRevision: 5Header: family inet hashsize 1024 maxelem 65536Size in memory: 408References: 2Number of entries: 5Members:10.96.0.1,tcp:44310.96.0.10,tcp:915310.100.33.163,tcp:808010.96.0.10,udp:5310.96.0.10,tcp:53

通过使用ipset的方式,使得iptables的规则数不会随着节点数量增长,能够支持超大规模的集群。

3.2 问题复现与解决

因为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 serviceNAME            TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGEkubernetes      ClusterIP   10.96.0.1      <none>        443/TCP          531dredis-service   NodePort    10.104.6.137   <none>        6379:30001/TCP   24m

它有3个Pod,分别在集群的3个节点上

k8s@kube-master1:~$ kubectl get pod -o wideNAME                                READY   STATUS    RESTARTS   AGE   IP            NODE    NOMINATED NODE   READINESS GATESredis-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/bashroot@redis-deployment-8687bfc768-7lzpl:/data# redis-cli 127.0.0.1:6379> set pod 1OK127.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 wideNAME                                READY   STATUS    RESTARTS   AGE    IP            NODE    NOMINATED NODE   READINESS GATESredis-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之后,底层的网络转发是特别复杂的,对于开发者来说,这是透明的,但是出了问题,很多时候,需要了解底层的东西。

在我们传统应用向云上迁移时,由于多年的持续开发,不仅是软件内部架构,以及与外部的交互关系,都比较复杂,在上云的过程中,需要十分谨慎,先用一些比较简单的模块充分验证,摸索经验,再逐步把核心应用迁移到云上。

©著作权归作者所有:来自51CTO博客作者nxlhero的原创作品,如需转载,请与作者联系,否则将追究法律责任

好知识,才能预见未来

赞赏

0人进行了赞赏支持

更多相关文章

  1. springcloud组件zuul报Forwarding error问题的解决
  2. 干货巨献:Openshift3.9的网络管理大全.加长篇---Openshift3.9学习
  3. 25个iptables常用示例
  4. nginx的四层转发功能
  5. 在Linux上打开端口
  6. Nginx基于TCP/UDP端口的四层负载均衡(stream模块)配置梳理
  7. 【原创】关于交换机端口链路类型Access、trunk、hybrid的理解(下)
  8. 【原创】关于交换机端口链路类型Access、trunk、hybrid的理解(上)
  9. centos6下ActiveMQ+Zookeeper消息中间件集群部署记录

随机推荐

  1. Android7.0中文文档(API)-- ShareActionPro
  2. AndroidStudio使用教程(第一弹)
  3. Cocos2d-x C++调用Android弹出提示框
  4. Android 麦克风录音动画
  5. json解析android客户端源码
  6. 不错的Android开发资料,收藏一下
  7. android视频录制MediaStore.ACTION_VIDEO
  8. android:debuggable属性
  9. widget(1、TextView)
  10. Android进度条源代码