优秀的编程知识分享平台

网站首页 > 技术文章 正文

[k8s]一个请求如何从 Service 到达 Pod

nanyue 2024-09-10 16:11:42 技术文章 5 ℃

一个请求如何从 Service 到达 Pod

当我们从一个K8s cluster的Pod里面向位于同cluster的另一个service发起请求这样的场景

1. 基础知识

它其实是一个包含多个基础知识的综合题。想要找到答案,得需要理解几个与之相关的重要基础知识:iptables、package flow和路由。我们先来依次过一下这几个基础概念

1.1 Service和Pod关系

首先我们先来复习一下Service和Pod之间的关系。Service存在的意义是将背后的Pod聚合在一起,以单一入口的方式对外提供服务。这里的外部访问者既可能是K8s cluster内部的Pod,也可以是K8s外部的进程

我们都知道service有一个可以在K8s内部访问到的虚拟IP地址(Cluster IP),这个地址你可以在kubedns里面找到,所以请求端通常通过类似下面这个FQDN prometheus-service.LanceAndCloudnative.svc.cluster.local来访问一个服务。但如果你K8s Node上无论是执行 ip a 还是 netstat -an 都无法找到这个虚拟地址。

另外我们还知道一个service背后会站着若干个Pod,每个Pod有自己的IP地址。如图1所示

图 1:Service和Pod之间的关系

1.2 netfilter package flow

图2所示的PREROUTING、INPUT、OUTPUT、FORWARD、POSTROUTING即为这里提到的5个钩子。每个钩子都像珍珠项链一样串联着若干规则,从而形成一个链。这些规则散落在五个表中,它们分布是NAT、Mangle、RAW、Security和Filter。其中Security表不常用,除去它,我们把其它部分合起来简称五链四表。

下面这张图能比较好地阐述链和表的关系。图片来自公众号:开发内功修炼。

图 2:五链四表对照表

了解了 netfilter之后,我们再来看看图3。这张图估计很多同学都不陌生。它非常清晰地展示了内核收到网络包后,netfilter和路由对这个包在数据内容修改和传输路径方面的影响。

为了突出本文的重点,我把流量从service转到Pod过程中涉及到的钩子和路由画出来了。你也看到了,图3里,我还在PREROUTING和OUTPUT这两个钩子处画出了KUBE-SERVICE和NAT。这里的KUBE-SERVICE是由kube-proxy创建的一个自定义链,kube-proxy还在NAT表中定义了分别在这两个钩子处生效的规则。既然规则存放在NAT表里,那肯定表示这些规则与地址转换有关。

图3中有两处出现了“路由选择”的标记,毫无疑问,这里涉及到路由。我将与本文有关的路由表信息也画在了图上。

图 3:netfilter package flow

回到本文讨论的场景。当我们从一个K8s Cluster的Pod向位于同集群的另一个service发起的请求时,请求从图3左下角的红框内(圈1处)进入。请求可能最终流入到作为本地进程的K8s Pod(圈2处,准确地说应该是Pod里面的container),也可能会被转发到位于其它Node上的K8s Pod(圈3处)。

下面我们来结合测试环境、iptables和路由表来详细看看

2. 测试环境

service名字为nginx-web-service,它背后运行有3个名为nginx-web的Pod。简单起见,本文仅讨论类型为CluseterIP的service。

Service Cluster-IP为:172.16.255.220, Service服务分配的CLUSTER-IP以及监听的端口均是虚拟的。Pod的IP分别为:10.204.0.13,10.204.1.3和10.204.1.8

如图1所示,IP为10.204.0.13的Pod运行在Node 1上,它所在的Node IP地址为130.211.97.55,相应地,IP为10.204.1.3和10.204.1.8的Pod运行在IP地址为130.211.99.206的Node 2上


apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-web
  namespace: LanceAndCloudnative
  labels:
    app: nginx-web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx-web
  template:
    metadata:
      labels:
        app: nginx-web
    spec:
      containers:
        - name: nginx-web
          image: nginx
          ports:
            - containerPort: 80
......
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-web-service
  namespace: LanceAndCloudnative
spec:
  selector: 
    app: nginx-web
  type: ClusterIP  
  ports:
    - port: 80
      targetPort: 80

3. 细节详解

铺垫了这么多,终于到了详述细节的环节了。下面是用命令

# iptables-save | grep LanceAndCloudnative在Node 1上dump出来的iptables

## 通过NAT重新分发到具体的Pod
-A KUBE-SEP-OALS23FQATZ4JKLQ -s 10.204.0.13/32 -m comment --comment "LanceAndCloudnative/nginx-web-service:" \
   -j KUBE-MARK-MASQ
-A KUBE-SEP-OALS23FQATZ4JKLQ -p tcp -m comment --comment "LanceAndCloudnative/nginx-web-service:" \
   -m tcp -j DNAT --to-destination 10.204.0.13:80
-A KUBE-SEP-U34NONI5MGHBFYFA -s 10.204.1.3/32 -m comment --comment "LanceAndCloudnative/nginx-web-service:" \
   -j KUBE-MARK-MASQ
-A KUBE-SEP-U34NONI5MGHBFYFA -p tcp -m comment --comment "LanceAndCloudnative/nginx-web-service:" \
   -m tcp -j DNAT --to-destination 10.204.1.3:80
-A KUBE-SEP-IYP2JLAPHWQ5VKF7 -s 10.204.1.8/32 -m comment --comment "LanceAndCloudnative/nginx-web-service:" \
   -j KUBE-MARK-MASQ
-A KUBE-SEP-IYP2JLAPHWQ5VKF7 -p tcp -m comment --comment "LanceAndCloudnative/nginx-web-service:" \
   -m tcp -j DNAT --to-destination 10.204.1.8:80

## 负载均衡
-A KUBE-SVC-4LFXAAP7ALRXDL3I -m comment --comment "LanceAndCloudnative/nginx-web-service:" \
   -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-OALS23FQATZ4JKLQ

-A KUBE-SVC-4LFXAAP7ALRXDL3I -m comment --comment "LanceAndCloudnative/nginx-web-service:" \
   -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-U34NONI5MGHBFYFA

-A KUBE-SVC-4LFXAAP7ALRXDL3I -m comment --comment "LanceAndCloudnative/nginx-web-service:" \
   -j KUBE-SEP-IYP2JLAPHWQ5VKF7

## 入口
-A KUBE-SERVICES -d 172.16.255.220/32 -p tcp -m comment \
   --comment "LanceAndCloudnative/nginx-web-service: cluster IP" -m tcp --dport 80 -j KUBE-SVC-4LFXAAP7ALRXDL3I

-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES

这份iptables配置分为三大部分:入口、负载均衡、通过NAT修改目的地址。我们分别来瞧瞧。


3.2 负载均衡

同名子链 KUBE-SVC-4LFXAAP7ALRXDL3I 有三个。为啥是3个?聪明的你一定能猜到,因为Pod replica是3。也就是说因为这个service背后有三个Pod提供支撑服务,所以这里有三条子链。那如果replica是100呢?呵呵,你猜。

不但如此,每个Node上都有着这样类似的三条子链。为啥每个Node都有?好问题,留着以后再聊吧。

岔个话题:我只能说K8s默认使用iptables来实现Service到Pod的转换欠下了大量的技术债。K8s的问题列表里面曾经记录了一个问题#44613:在100个Node的K8s集群里,kube-proxy有时会消耗70%的CPU。还有一个更恐怖的对比数据:当K8s里有5k个services(每个service平均需要插入8条rule,一共40k iptables rules)的时候,插入一条新的rule需要11分钟;而当services数量scale out 4倍到20k(160k rules)时,需要花费5个小时,而非44分钟,才能成功加入一条新的rule。可以看到时间消耗呈指数增加,而非线性。

那均衡的策略是什么呢?你也看到了,这里按30%-50%-20%的比例分配流量。


3.3 通过NAT修改目的地址

我们现在假设子链 KUBE-SEP-OALS23FQATZ4JKLQ 被负载均衡策略选中。在它的规则里我们很容易地读懂它通过DNAT,把dest IP替换成了10.204.0.13。还记得吗?10.204.0.13 是为这个service提供支撑的其中一个Pod 的IP地址。干得漂亮,通过这种方式,完成了从service IP地址到Pod IP地址的转换


3.4 路由

可单单转换地址还不行,还得把流量导到那个Pod手上才算完成任务。

看到图2的左边的“路由选择”标记了吗?在它旁边的路由表里面写着:如果去子网10.204.0.13/24的话,从cni离开,且下一跳为0.0.0.0。

cni0是什么?哦,它是一个bridge,Pod都插在它的端口上。0.0.0.0表示目标和本机同属一个局域网,不需要经过任何gateway去路由,可以直接通过二层设备发送,比如switch,hub或者bridge。这也暗示了一点:在这种情况下,发起请求的Pod和处理请求的Pod位于同一个Node上


那如果上一步中负载均衡策略选中的子链是 KUBE-SEP-U34NONI5MGHBFYFA 的话,很显然应该轮到Pod 10.204.1.3来提供服务了。按照路由表的设置:如果去子网10.204.1.0/24的话,这次得从flannel.1离开,且下一跳IP为10.204.1.1。这种场景就涉及到另外一个话题了:跨Node间Pod通信

图 4是K8s Overlay网络模型下,跨Node间Pod通信时的细节放大图。这节所说的两种Pod间通信时的路由情况都浓缩在这张图里了



图 4:VXLAN容器网络方案全景图

Tags:

最近发表
标签列表