目录

Kubernetes资源之服务发现service详解

Service 相关概念说明

Service

Kubernetes中一个应用服务会有一个或多个实例(Pod),每个实例(Pod)的IP地址由网络插件动态随机分配(Pod重启后IP地址会改变)。为屏蔽这些后端实例的动态变化和对多实例的负载均衡,引入了Service这个资源对象,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: v1
kind: Service
metadata:
  annotations:
    meta.helm.sh/release-name: loki
    meta.helm.sh/release-namespace: kube-system
  labels:
    app: loki
    app.kubernetes.io/managed-by: Helm
    chart: loki-2.1.1
    heritage: Helm
    release: loki
  name: loki
  namespace: kube-system
spec:
  ports:
  - name: http-metrics
    port: 3100
    protocol: TCP
    targetPort: http-metrics
  selector:
    app: loki
    release: loki
  type: ClusterIP

根据创建Service的type类型不同,可分成4种模式:

  • ClusterIP: 默认方式。根据是否生成ClusterIP又可分为普通Service和Headless Service两类:
    • 普通Service:通过为Kubernetes的Service分配一个集群内部可访问的固定虚拟IP(Cluster IP),实现集群内的访问。为最常见的方式。
    • Headless Service:该服务不会分配Cluster IP,也不通过kube-proxy做反向代理和负载均衡。而是通过DNS提供稳定的网络ID来访问,DNS会将headless service的后端直接解析为podIP列表。主要供StatefulSet使用。
  • NodePort:除了使用Cluster IP之外,还通过将service的port映射到集群内每个节点的相同一个端口,实现通过nodeIP:nodePort从集群外访问服务。
  • LoadBalancer:和nodePort类似,不过除了使用一个Cluster IP和nodePort之外,还会向所使用的公有云申请一个负载均衡器(负载均衡器后端映射到各节点的nodePort),实现从集群外通过LB访问服务。
  • ExternalName:是 Service 的特例。此模式主要面向运行在集群外部的服务,通过它可以将外部服务映射进k8s集群,且具备k8s内服务的一些特征(如具备namespace等属性),来为集群内部提供服务。此模式要求kube-dns的版本为1.7或以上。这种模式和前三种模式(除headless service)最大的不同是重定向依赖的是dns层次,而不是通过kube-proxy

比如,在service定义中指定externalName的值"loki.svc.cluster.local":

1
kubectl create service externalname loki-test loki.svc.cluster.local

此时k8s集群内的DNS服务会给集群内的服务名 ..svc.cluster.local 创建一个CNAME记录,其值为指定的"loki-test"。

当查询k8s集群内的服务loki.namespace.svc.cluster.local时,集群的 DNS 服务将返回映射的CNAME记录"loki-test"。

前3种模式,定义服务的时候通过selector指定服务对应的pods,根据pods的地址创建出endpoints作为服务后端;Endpoints Controller会watch Service以及pod的变化,维护对应的Endpoint信息。kube-proxy根据Service和Endpoint来维护本地的路由规则。当Endpoint发生变化,即Service以及关联的pod发生变化,kube-proxy都会在每个节点上更新iptables,实现一层负载均衡。

而ExternalName模式则不指定selector,相应的也就没有port和endpoints。 ExternalName和ClusterIP中的Headles Service同属于Headless Service的两种情况。Headless Service主要是指不分配Service IP,且不通过kube-proxy做反向代理和负载均衡的服务.

Port

Service中主要涉及三种Port: * port 这里的port表示service暴露在clusterIP上的端口,clusterIP:Port 是提供给集群内部访问kubernetes服务的入口。

  • targetPort containerPort,targetPort是pod上的端口,从port和nodePort上到来的数据最终经过kube-proxy流入到后端pod的targetPort上进入到容器的containerPort最后进入容器中。

  • nodePort nodeIP:nodePort 是提供给从集群外部访问kubernetes服务的入口。

总的来说,port和nodePort都是service的端口,前者暴露给从集群内访问服务,后者暴露给从集群外访问服务。从这两个端口到来的数据都需要经过反向代理kube-proxy流入后端具体pod的targetPort,从而进入到pod上的容器内。

Ip

  • ClusterIP Pod IP 地址是实际存在于某个网卡(可以是虚拟设备)上的,但clusterIP就不一样了,没有网络设备承载这个地址。它是一个虚拟地址,由kube-proxy使用iptables规则重新定向到其本地端口,再均衡到后端Pod。当kube-proxy发现一个新的service后,它会在本地节点打开一个任意端口,创建相应的iptables规则,重定向服务的clusterIP和port到这个新建的端口,开始接受到达这个服务的连接。

  • Pod IP Pod的IP,每个Pod启动时,会自动创建一个镜像为gcr.io/google_containers/pause的容器,Pod内部其他容器的网络模式使用container模式,并指定为pause容器的ID,即:network_mode:

    • “container:pause容器ID”,使得Pod内所有容器共享pause容器的网络,与外部的通信经由此容器代理,pause容器的IP也可以称为Pod IP。
    • pause容器在 pod 中担任 Linux 命名空间共享的基础
    • pause容器启用 pid 命名空间,开启 init 进程
  • 节点IP Node-IP,service对象在Cluster IP range池中分配到的IP只能在内部访问,如果服务作为一个应用程序内部的层次,还是很合适的。如果这个service作为前端服务,准备为集群外的客户提供业务,我们就需要给这个服务提供公共IP了。指定service的spec.type=NodePort,这个类型的service,系统会给它在集群的各个代理节点上分配一个节点级别的端口,能访问到代理节点的客户端都能访问这个端口,从而访问到服务。

kube-proxy介绍

当service有了port和nodePort之后,就可以对内/外提供服务。那么其具体是通过什么原理来实现的呢?奥妙就在kube-proxy在本地node上创建的iptables规则。

每个Node上都运行着一个kube-proxy进程,kube-proxy是service的具体实现载体,所以,说到service,就不得不提到kube-proxy。

kube-proxy是kubernetes中设置转发规则的组件。kube-proxy通过查询和监听API server中service和endpoint的变化,为每个service都建立了一个服务代理对象,并自动同步。服务代理对象是proxy程序内部的一种数据结构,它包括一个用于监听此服务请求的SocketServer,SocketServer的端口是随机选择的一个本地空闲端口。如果存在多个pod实例,kube-proxy同时也会负责负载均衡。而具体的负载均衡策略取决于Round Robin负载均衡算法及service的session会话保持这两个特性。会话保持策略使用的是ClientIP(将同一个ClientIP的请求转发同一个Endpoint上)。kube-proxy 可以直接运行在物理机上,也可以以 static-pod 或者 daemonset 的方式运行。

kube-proxy 当前支持以下3种实现模式:

  • userspace:最早的负载均衡方案,它在用户空间监听一个端口,Service的请求先从用户空间进入内核iptables转发到这个端口,然后再回到用户空间,由kube-proxy完成后端endpoints的选择和代理,这样流量会有从用户空间进出内核的过程,效率低,有明显的性能瓶颈。

  • iptables:目前默认的方案,完全以内核 iptables 的 nat 方式实现 service 负载均衡。该方式在大规模情况下存在一些性能问题:首先,iptables 没有增量更新功能,更新一条规则需要整体 flush,更新时间长,这段时间之内流量会有不同程度的影响;另外,iptables 规则串行匹配,没有预料到 Kubernetes 这种在一个机器上会有很多规则的情况,流量需要经过所有规则的匹配之后再进行转发,对时间和内存都是极大的消耗,尤其在大规模情况下对性能的影响十分明显。

  • ipvs:为解决 iptables 模式的性能问题,v1.11 新增了 ipvs 模式(v1.8 开始支持测试版,并在 v1.11 GA),采用增量式更新,不会强制进行全量更新,可以保证 service 更新期间连接保持不断开;也不会进行串行的匹配,会通过一定的规则进行哈希 map 映射,很快地映射到对应的规则,不会出现大规模情况下性能线性下降的状况。

iptables 功能

  • 流量转发:DNAT 实现 IP 地址和端口的映射;
  • 负载均衡:statistic 模块为每个后端设置权重;
  • 会话保持:recent 模块设置会话保持时间;

iptables 有五张表和五条链,五条链分别对应为

  • PREROUTING 链:数据包进入路由之前,可以在此处进行 DNAT;
  • INPUT 链:一般处理本地进程的数据包,目的地址为本机;
  • FORWARD 链:一般处理转发到其他机器或者 network namespace 的数据包;
  • OUTPUT 链:原地址为本机,向外发送,一般处理本地进程的输出数据包;
  • POSTROUTING 链:发送到网卡之前,可以在此处进行 SNAT;

五张表分别为

  • filter 表:用于控制到达某条链上的数据包是继续放行、直接丢弃(drop)还是拒绝(reject);
  • nat 表:network address translation 网络地址转换,用于修改数据包的源地址和目的地址;
  • mangle 表:用于修改数据包的 IP 头信息;
  • raw 表:iptables 是有状态的,其对数据包有链接追踪机制,连接追踪信息在 /proc/net/nf_conntrack 中可以看到记录,而 raw 是用来去除链接追踪机制的;
  • security 表:最不常用的表,用在 SELinux 上;

这五张表是对 iptables 所有规则的逻辑集群且是有顺序的,当数据包到达某一条链时会按表的顺序进行处理,表的优先级为:raw、mangle、nat、filter、security

iptables 的工作流程如下图所示: https://tc.ctq6.cn/tc/nlb3yre4eb.png

iptables模式下kube-proxy转发规则分析

kube-proxy 组件负责维护 node 节点上的防火墙规则和路由规则,在 iptables 模式下,会根据 service 以及 endpoints 对象的改变来实时刷新规则,kube-proxy 使用了 iptables 的 filter 表和 nat 表,并对 iptables 的链进行了扩充,自定义了 KUBE-SERVICES、KUBE-EXTERNAL-SERVICES、KUBE-NODEPORTS、KUBE-POSTROUTING、KUBE-MARK-MASQ、KUBE-MARK-DROP、KUBE-FORWARD 七条链,另外还新增了以“KUBE-SVC-xxx”和“KUBE-SEP-xxx”开头的数个链,除了创建自定义的链以外还将自定义链插入到已有链的后面以便劫持数据包.

在 nat 表中自定义的链以及追加的链如下所示:

1
iptables -L  -nv -t nat

https://tc.ctq6.cn/tc/20220618071502.png

在 filter 表定义的链以及追加的链如下所示如下所示:

1
iptables -L  -nv -t filter

https://tc.ctq6.cn/tc/20220618071808.png

对于 KUBE-MARK-MASQ 链中所有规则设置了 kubernetes 独有的 MARK 标记,在 KUBE-POSTROUTING 链中对 node 节点上匹配 kubernetes 独有 MARK 标记的数据包,进行 SNAT 处理。

1
MARK       all  --  0.0.0.0/0            0.0.0.0/0            MARK xor 0x4000

Kube-proxy 接着为每个服务创建 KUBE-SVC-xxx 链,并在 nat 表中将 KUBE-SERVICES 链中每个目标地址是service 的数据包导入这个 KUBE-SVC-xxx 链,如果 endpoint 尚未创建,则 KUBE-SVC-xxx 链中没有规则,任何 incomming packets 在规则匹配失败后会被 KUBE-MARK-DROP 进行标记然后再 FORWARD 链中丢弃。

这些自定义链与 iptables 的表结合后如下所示,笔者只画出了 PREROUTING 和 OUTPUT 链中追加的链以及部分自定义链,因为 PREROUTING 和 OUTPUT 的首条 NAT 规则都先将所有流量导入KUBE-SERVICE 链中,这样就截获了所有的入流量和出流量,进而可以对 k8s 相关流量进行重定向处理。

https://tc.ctq6.cn/tc/0ekdvqgil5.png

kubernetes 自定义链中数据包的详细流转可以参考: https://tc.ctq6.cn/tc/udsbmbohlc.png

iptables 规则分析

1、clusterIP 访问方式 创建一个 clusterIP 访问方式的 service 以及带有一个副本,从 pod 中访问 clusterIP 的 iptables 规则流向为:

1
PREROUTING --> KUBE-SERVICE --> KUBE-SVC-XXX --> KUBE-SEP-XXX

访问流程如下:

  • 1、对于进入 PREROUTING 链的都转到 KUBE-SERVICES 链进行处理;
  • 2、在 KUBE-SERVICES 链,对于访问 clusterIP 为 10.112.255.199 的转发到 KUBE-SVC-CIXTJRU3UQX3BAIU;
  • 3、访问 KUBE-SVC-CIXTJRU3UQX3BAIU 的使用随机数负载均衡,并转发到 KUBE-SEP-MAX2B4A3WUA2OZUC 上;
  • 4、KUBE-SEP-MAX2B4A3WUA2OZUC 对应 endpoint 中的 pod 10.112.16.10,设置 mark 标记,进行 DNAT 并转发到具体的 pod 上,如果某个 service 的 endpoints 中没有 pod,那么针对此 service 的请求将会被 drop 掉;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 1
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES

// 2
-A KUBE-SERVICES -d 10.112.255.199/32 -p tcp -m comment --comment "default/busybox:3000-3000 cluster IP" -m tcp --dport 3000 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -d 10.112.255.199/32 -p tcp -m comment --comment "default/busybox:3000-3000 cluster IP" -m tcp --dport 3000 -j KUBE-SVC-CIXTJRU3UQX3BAIU

// 3
-A KUBE-SVC-CIXTJRU3UQX3BAIU -m comment --comment "default/busybox:3000-3000" -j KUBE-SEP-MAX2B4A3WUA2OZUC

// 4
-A KUBE-SEP-MAX2B4A3WUA2OZUC -s 10.112.16.10/32 -m comment --comment "default/busybox:3000-3000" -j KUBE-MARK-MASQ
-A KUBE-SEP-MAX2B4A3WUA2OZUC -p tcp -m comment --comment "default/busybox:3000-3000" -m tcp -j DNAT --to-destination 10.112.16.10:3000

2、nodePort方式

在 nodePort 方式下,会用到 KUBE-NODEPORTS 规则链,通过 iptables -t nat -L -n 可以看到 KUBE-NODEPORTS 位于 KUBE-SERVICE 链的最后一个,iptables 在处理报文时会优先处理目的 IP 为clusterIP 的报文,在前面的 KUBE-SVC-XXX 都匹配失败之后再去使用 nodePort 方式进行匹配。

创建一个 nodePort 访问方式的 service 以及带有一个副本,访问 nodeport 的 iptables 规则流向为:

  • 非本机访问
    1
    
    PREROUTING --> KUBE-SERVICE --> KUBE-NODEPORTS --> KUBE-SVC-XXX --> KUBE-SEP-XXX
    
  • 本机访问
    1
    
    OUTPUT --> KUBE-SERVICE --> KUBE-NODEPORTS --> KUBE-SVC-XXX --> KUBE-SEP-XXX
    

该服务的 nodePort 端口为 30070,其 iptables 访问规则和使用 clusterIP 方式访问有点类似,不过 nodePort 方式会比 clusterIP 的方式多走一条链 KUBE-NODEPORTS,其会在 KUBE-NODEPORTS 链设置 mark 标记并转发到 KUBE-SVC-5SB6FTEHND4GTL2W,nodeport 与 clusterIP 访问方式最后都是转发到了 KUBE-SVC-xxx 链。

  • 1、经过 PREROUTING 转到 KUBE-SERVICES
  • 2、经过 KUBE-SERVICES 转到 KUBE-NODEPORTS
  • 3、经过 KUBE-NODEPORTS 转到 KUBE-SVC-2MBD7OA3VICWBKGB
  • 4、经过 KUBE-SVC-2MBD7OA3VICWBKGB 转到 KUBE-SEP-MAVLDMGLIJSBFPGJ
  • 5、经过 KUBE-SEP-CI5ZO3FTK7KBNRMG 转到 10.112.6.33:3000
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 1
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES

// 2
-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

// 3
-A KUBE-NODEPORTS -p tcp -m comment --comment "kube-system/prometheus-operator-grafana:service" -m tcp --dport 31001 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "kube-system/prometheus-operator-grafana:service" -m tcp --dport 31001 -j KUBE-SVC-2MBD7OA3VICWBKGB

// 4
-A KUBE-SVC-2MBD7OA3VICWBKGB -m comment --comment "kube-system/prometheus-operator-grafana:service" -j KUBE-SEP-MAVLDMGLIJSBFPGJ

// 5
-A KUBE-SEP-MAVLDMGLIJSBFPGJ -s 10.112.6.33/32 -m comment --comment "kube-system/prometheus-operator-grafana:service" -j KUBE-MARK-MASQ
-A KUBE-SEP-MAVLDMGLIJSBFPGJ -p tcp -m comment --comment "kube-system/prometheus-operator-grafana:service" -m tcp -j DNAT --to-destination 10.112.6.33:3000

ipvs模式下kube-proxy转发规则分析

ipvs与iptables的区别和联系

区别

  • 底层数据结构:iptables 使用链表,ipvs 使用哈希表
  • 负载均衡算法:iptables 只支持随机、轮询两种负载均衡算法而 ipvs 支持的多达 8 种;
  • 操作工具:iptables 需要使用 iptables 命令行工作来定义规则,ipvs 需要使用 ipvsadm 来定义规则。

此外 ipvs 还支持 realserver 运行状况检查、连接重试、端口映射、会话保持等功能。

联系

ipvs 和 iptables 都是基于 netfilter内核模块,两者都是在内核中的五个钩子函数处工作,下图是 ipvs 所工作的几个钩子函数: https://tc.ctq6.cn/tc/20220619055404.png

IPSET

IP sets 是 Linux 内核中的一个框架,可以由 ipset 命令进行管理。根据不同的类型,IP set 可以以某种方式保存 IP地址、网络、(TCP/UDP)端口号、MAC地址、接口名或它们的组合,并且能够快速匹配。

根据官网的介绍,若有以下使用场景:

  • 在保存了多个 IP 地址或端口号的 iptables 规则集合中想使用哈希查找;
  • 根据 IP 地址或端口动态更新 iptables 规则时希望在性能上无损;
  • 在使用 iptables 工具创建一个基于 IP 地址和端口的复杂规则时觉得非常繁琐;

此时,使用 ipset 工具可能是你最好的选择。

ipset 是 iptables 的一种扩展,在 iptables 中可以使用-m set启用 ipset 模块,具体来说,ipvs 使用 ipset 来存储需要 NAT 或 masquared 时的 ip 和端口列表。在数据包过滤过程中,首先遍历 iptables 规则,在定义了使用 ipset 的条件下会跳转到 ipset 列表中进行匹配

kube-proxy ipvs 模式

kube-proxy 在 ipvs 模式下自定义了八条链,分别为 KUBE-SERVICES、KUBE-FIREWALL、KUBE-POSTROUTING、KUBE-MARK-MASQ、KUBE-NODE-PORT、KUBE-MARK-DROP、KUBE-FORWARD、KUBE-LOAD-BALANCER ,如下所示:

Nat 表

Filter 表

此外,由于 linux 内核原生的 ipvs 模式只支持 DNAT,不支持 SNAT,所以,在以下几种场景中 ipvs 仍需要依赖 iptables 规则:

  • 1、kube-proxy 启动时指定 –-masquerade-all=true 参数,即集群中所有经过 kube-proxy 的包都做一次 SNAT;
  • 2、kube-proxy 启动时指定 –cluster-cidr= 参数;
  • 3、对于 Load Balancer 类型的 service,用于配置白名单;
  • 4、对于 NodePort 类型的 service,用于配置 MASQUERADE;
  • 5、对于 externalIPs 类型的 service;

但对于 ipvs 模式的 kube-proxy,无论有多少 pod/service,iptables 的规则数都是固定的

当创建 ClusterIP type 的 service 时,IPVS proxier 会执行以下三个操作:

  • 确保本机已创建 dummy 网卡,默认为 kube-ipvs0。为什么要创建 dummy 网卡?因为 ipvs netfilter 的 DNAT 钩子挂载在 INPUT 链上,当访问 ClusterIP 时,将 ClusterIP 绑定在 dummy 网卡上为了让内核识别该 IP 就是本机 IP,进而进入 INPUT 链,然后通过钩子函数 ip_vs_in 转发到 POSTROUTING 链;
  • 将 ClusterIP 绑定到 dummy 网卡;
  • 为每个 ClusterIP 创建 IPVS virtual servers 和 real server,分别对应 service 和 endpoints;

ipvs 模式下数据包的流向

clusterIP 访问方式

1
PREROUTING --> KUBE-SERVICES --> KUBE-CLUSTER-IP --> INPUT --> KUBE-FIREWALL --> POSTROUTING
  • 首先进入 PREROUTING 链
  • 从 PREROUTING 链会转到 KUBE-SERVICES 链,10.244.0.0/16 为 ClusterIP 网段
  • 在 KUBE-SERVICES 链打标记
  • 从 KUBE-SERVICES 链再进入到 KUBE-CLUSTER-IP 链
  • KUBE-CLUSTER-IP 为 ipset 集合,在此处会进行 DNAT
  • 然后会进入 INPUT 链
  • 从 INPUT 链会转到 KUBE-FIREWALL 链,在此处检查标记
  • 在 INPUT 链处,ipvs 的 LOCAL_IN Hook 发现此包在 ipvs 规则中则直接转发到 POSTROUTING 链

nodeport 访问方式

1
PREROUTING --> KUBE-SERVICES --> KUBE-NODE-PORT --> INPUT --> KUBE-FIREWALL --> POSTROUTING
  • 首先进入 PREROUTING 链
  • 从 PREROUTING 链会转到 KUBE-SERVICES 链
  • 在 KUBE-SERVICES 链打标记
  • 从 KUBE-SERVICES 链再进入到 KUBE-NODE-PORT 链
  • KUBE-NODE-PORT 为 ipset 集合,在此处会进行 DNAT
  • 然后会进入 INPUT 链
  • 从 INPUT 链会转到 KUBE-FIREWALL 链,在此处检查标记
  • 在 INPUT 链处,ipvs 的 LOCAL_IN Hook 发现此包在 ipvs 规则中则直接转发到 POSTROUTING 链