一、Service资源基础
1.1 介绍
在Kubernetes中,Pod资源是应用程序的载体,我们可以通过Pod的ip来访问应用程序,但是Pod的ip地址不是固定的,这就意味着不方便直接采用Pod的ip对服务进行访问。
为了解决这个问题,Kubernetes提供了Service资源,Service会对提供同一个服务的多个Pod进行聚合,并且提供一个统一的入口地址。通过访问Service的入口地址就能访问到后面的Pod服务。
1.2 作用
Service主要作用有两种:服务发现和负载均衡。
1.2.1 服务发现
Service通过Label Selector 选择提供服务的Pod。换句话说,Service关联Pod资源的规则要借助Label Selector完成。此Label Selector 其实就是转换规则,会通过它生成与Service同名的endpoints对象。Endpoints对象才是借助Label Selector发现Pod的关键所在(条件:标签选择、状态判定)。Endpoint是Kubernetes中的资源对象,存储在etcd中,它记录了一条ServiceIP对应的所有PodIP。当请求到达serviceIP时,会根据分发策略从所有的PodIP中选择一个PodIP,用这个Pod提供服务。当Pod被删除或者重建时,PodIP会发生变化,kube-proxy会基于监听机制发现对应的变化并实时更新Endpoints中的ServiceIP对应的PodIP,这样Service会根据Label Selector找到对应的pod。所以才说,Service具有服务发现的功能。
1.2.2 负载均衡
Service往往通过Label Selector代表着一组Pod。当请求到达Service时,该请求会根据负载均衡策略将请求定向到对应的Pod上。在此过程中,除非特别设置,否则请求会被分配到各个Pod上,避免集中将请求发送给某个Pod上导致服务挂掉的现象。
1.3 kube-proxy代理模式
从本质上说,一个Service对象对应于工作节点内核之中的一组iptables/ipvs规则。这些规则能够将到达Service对象的Cluster流量调度至相应Endpoints对象指向的IP地址+端口上。内核中的iptables/ipvs规则的作用域为其所在工作节点的单个主机上,这就需要Service对象在每个node节点都生成相关规则,从而确保任一节点上发往该Service对象的请求流量被正确转发。
每个node节点的kube-proxy组件通过API Server持续监控着各个Service及其关联的Pod对象,并将Service对象的创建或更新用iptables/ipvs规则表现。Service对象的ClusterIP也是用于生成iptables/ipvs规则所使用的IP地址。
kube-proxy将请求代理至对应节点的方式(工作模式)有3种:userspace、iptables和ipvs。
1.3.1 userspace代理模式
这里的userspace指linux操作系统的用户空间。在该模式下,kube-proxy会为每一个Service创建监听端口(运行于用户空间的kube-proxy进程负责监听),发向ClusterIP:Port的请求都会代理到当前Service后端的各Pod对象,Service资源默认调度算法是轮询。
此时,kube-proxy充当了一个四层负载均衡器的角色。请求流量到达内核空间后经由套接字送往用户空间中的kube-proxy进程,而后由该进程送回内核空间,发往后端Pod对象。由于请求报文在用户空间和内核空间来回转发,因此效率较低。
1.3.2 iptables代理模式
iptables模式下,创建Service的操作都会转换成所属node节点对应的iptables规则,用于将到达ClusterIP的请求由相关的iptables规则进行目标地址转换后根据相关算法转向后端的Pod对象,无需kube-proxy再进行处理。
此时,kube-proxy的作用仅仅是监听Service的变化,生成最新的iptables规则,kube-proxy不再担任四层负载均衡的角色,过来的请求基于netfilter在内核空间转发,不用再到用户空间,避免了数据的拷贝,效率更高,但是不能提供灵活的LB(Load Balance,负载均衡)策略。当其中一个Pod异常,iptables还是有可能转发到异常的Pod上,而且不会重试。因为流量转发到后端Endpoints对象的默认调度机制是随机算法。
其缺点在于,因为iptables它纯粹是为防火墙而设计的,并且基于内核规则列表。随着Service对象的增加,iptables规则会不断增加,会导致内核非常繁忙,集群数量越多性能越差。因此它适用于具有少量Service规模的集群。
1.3.3 ipvs代理模式
ipvs模式与iptables模式类似,kube-proxy监控Pod的变化并创建相应的ipvs规则。它与iptables的不同在于客户端请求流量的调度功能由ipvs实现。
虽然ipvs和iptables都是构建于内核中的netfilter之上,但它使用hash表作为底层数据结构工作于内核空间。具体的说,ipvs使用ipset存储需要DROP或masquared(伪装)的流量的源或目标地址,这样规则数量能得到有效控制(比iptables模式的规则少很多),因此具有流量转发速度快、规则同步性能好的特性,适用于存在大量Service资源且对性能要求高的场景。此外,ipvs模式支持多种调度算法。
如果没有加载并启用ipvs模块,或者没有配置ipvs相关配置,则会被降级成iptables模式。Kubernetes默认使用iptables模式。
1.4 Service资源类型
Service根据客户端接入的方式,分为ClusterIP、NodePort、LoadBalancer和ExternalName类型。
下面一张图描述了各个服务类型的特点,其中绿色的代表从外向内的访问模式;蓝色的代表从内向外的访问模式,黄色代表集群内部的访问模式。可以看到,除了ExternalName类型之外,其余三种类型都是逐层封装而来的。四种类型分别介绍如下:
1.4.1 ClusterIP
这是Kubernetes默认的服务类型,ClusterIP地址仅在集群内部可达,因而无法被集群外部的客户端访问。在Kubernetes内部可通过ClusterIP或ServiceName访问该服务。
1.4.2 NodePort
此类型是对ClusterIP类型的扩展,它支持通过特定的节点+端口接入集群外部的访问流量,并分发给后端Pod处理。因此该类型的Service既可以被集群内部客户端通过ClusterIP直接访问,也可以通过NodeIP:NodePort与集群外部客户端通信。当集群外部的请求报文首先到达的节点并非Service调度的目标Server Pod所在的节点,则该请求必然因需要额外的转发过程(跃点,可以理解为下一跳)和其它的处理步骤而导致延迟,会影响性能。这是因为,Pod被调度到哪个节点是不确定的,客户端要从哪个节点接入也是不确定的(Kubernetes集群不能决定),这样跃点的产生就在所难免了。
怎么理解呢?这么说吧,集群外部客户端将请求发到了集群内的X节点(即源地址为外部客户端地址,目标地址为X节点IP),但X节点会将报文转发至Y节点上的Pod对象。此时,为避免Y节点直接将响应报文返回给外部客户端,X节点需要将报文的源地址转为请求报文的目标地址(即X节点自身的IP),目标地址转为Y节点的IP。这样Y节点的Server Pod处理完请求后将结果返回给X节点再返回给集群外部客户端。这里X节点就充当了具有NAT功能的路由的角色。
1.4.3 LoadBalancer
它是NodePort类型的升级版,也具有NodePort和ClusterIP。此类型的Service借助一个集群外部(云服务商)的负载均衡器,通过此负载均衡器将服务暴露到集群外部。同时该负载均衡器与API Server产生联动,监视Service对象的变化并实时反应到负载均衡的配置中。此时,集群外部的流量会先到该负载均衡器(访问LB_IP:LB_PORT),由该负载均衡器调度到相关的NodePort。
该Service类型的优势在于,它能够指定流量调度策略,将集群外部客户端的请求调度到所有节点或部分节点的Pod之上,避免跃点情况的出现。
要实现这种功能,通常Kubernetes集群要部署在公有云上。
1.4.4 ExternalName
此类型简单地说就是将外部的服务定义到集群上,使之成为集群的服务。实现方式是ServiceName通过CNAME记录(别名)解析为外部服务的DNS名称(即spec.externalName字段中的名称),去映射到集群内,从而让集群内的Pod可以访问外部服务。此类型的Service无ClusterIP和NodePort,其流量也无需到达内核再由iptables/ipvs规则去处理。
1.5 标签与标签选择器
1.5.1 标签
标签是附加在Kubernetes任何资源对象上的键值型数据,其作用就是在资源上添加标识,完成资源的区分和筛选。Kubernetes系统的部分基础功能的实现也要依赖标签和标签选择器,比如Service筛选并关联后端Pod对象,StatefulSet和DaemonSet等控制器过滤并关联后端Pod对象等。
标签中的键名通常由“键前缀”和“键名”组成,格式为“key_prefix/key_name”,键前缀必须为DNS域名格式,为可选部分。键名支持字母、数字、连接号(-)、下划线、点号(.),只能以字母或数字开头,最长63个字符。
如何管理资源对象的Label?一种是在yaml文件中的metadata.labels字段中定义标签,另一种是用kubectl label命令去添加/修改/删除标签。
#显示资源对象的标签
kubectl get ns --show-labels
#当标签较多时,在kubectl get命令使用-L key1,key2…选项指定有特定键的标签信息
kubectl get ns -L kubernetes.io/metadata.name --show-labels
管理标签的命令介绍如下:
##添加标签。这里为指定Pod添加version=v1.0标签
#语法
kubectl label TYPE NAME key=value
#语句
kubectl label pod demoapp-7777ccddcb-mvqkf version=1.0
##修改标签。这里将指定Pod的version标签值改为v1.1
#语法。--overwrite选项表示强制覆盖已有标签的原有键值
kubectl label --overwrite TYPE NAME key=value
#语句
kubectl label pod demoapp-7777ccddcb-mvqkf version=1.1 --overwrite
##删除标签。这里删除指定Pod的version标签,此时version标签就没了
#语法
kubectl label TYPE NAME key-
#语句
kubectl label pod demoapp-7777ccddcb-mvqkf version-
1.5.2 标签选择器
标签选择器用于表达标签的查询条件,Kubernetes支持两种选择器:基于等值关系的标签选择器和基于集合关系的标签选择器。
基于等值关系的标签选择器的可用操作符有=、==和!=。其中前两个意义相同,都表示“相等“,最后一个表示”不等于“。比如env=env和env!=dev就都是基于等值关系的选择器。
kubectl命令的“-l”选项可指定使用标签选择器筛选目标资源。
如下命令显示标签version的值不等于v1.0或没有version标签的Pod,并显示标签。由结果可以看到,一个Pod是versinotallow=v1.1,另一个Pod就没有version标签,二者均符合条件。
kubectl get pod -l 'version!=v1.0' --show-labels
基于集合关系的标签选择器基于一组值进行过滤,它支持in、notin和exists这3种操作符。使用格式如下:
- KEY in (VALUE1,VALUE2…):指定键名的值存在于给定的列表中就满足条件
- KEY notin (VALUE1,VALUE2…):指定键名的值不存在于给定的列表中就满足条件
- KEY:所有存在此键名标签的资源
- !KEY:所有不存在此键名标签的资源
如下命令可以过滤出键名version的值为v1.0的Pod,并显示标签
kubectl get pod -l 'version in (v1.0)' --show-labels
如下命令可以过滤不存在version键名的Pod,并显示标签
kubectl get pod -l '!version' --show-labels
1.6 Service资源配置规范
apiVersion: v1
kind: Service
metadata:
name: …
namespace: …
spec:
type: <string> #service类型,默认为ClusterIP
selector: <map[string]string> #等值类型的标签选择器,内含“与”逻辑
ports: #Service的端口对象列表
- name: <string> #端口名称
protocol: <string> #协议,目前支持TCP、UDP和SCTP,默认为TCP
port: <integer> # Service端口
targetPort: <string> #后端目标进程的端口号或名称
nodePort: <integer> #节点端口号,仅适用于NodePort和LoadBalancer类型。为避免冲突,建议由系统自动分配
clusterIP: <string> #Service的集群IP,建议由系统自动分配
externalTrafficPolicy: <string> #外部流量策略处理方式,Local表示由当前节点处理,Cluster表示向集群范围内调度
loadBalancerIP: <string> #外部负载均衡器使用的IP地址,仅适用于LoadBalancer
externalName: <string> #外部服务名称,该名称作为Service的DNS CNAME值
1.7 应用Service资源
下面的示例用来应用NodePort Service资源。
部署Kubernetes时会预留一个端口范围,用于分配给需要用到NodePort的Service对象,端口范围默认为30000~32767。
vim services-nodeport-demo.yaml
---
kind: Service
apiVersion: v1
metadata:
name: demoapp-nodeport-svc
spec:
type: NodePort
selector:
app: demoapp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
#externalTrafficPolicy: Local
kubectl apply -f services-nodeport-demo.yaml
由describe的显示可得,该Service对象调度集群外部流量时使用默认的Cluster策略,哪怕目标Pod对象位于另外的节点而由此带来的网络跃点。因此对于该NodePort的请求会被分散调度至该Service关联的所有端点上。
上面显示的结果显示,外部客户端的请求被调度至此Service对象的每一个后端Pod之上,达到了负载均衡的效果。这些Pod对象可能分散在集群中的不同节点。同时,请求报文的客户端IP地址是最先接收到请求报文节点上用于集群内部通信的IP地址(k8s-node1节点的tunl0接口的192.168.113.0地址),而非外部客户端地址(可以参考Pod对应的Node节点IP看,见下图)。
另一个外部流量策略Local则仅将流量调度至请求目标节点所运行的Pod对象,减少网络跃点,降低网络延迟,当请求报文指向的节点不存在目标Service相关的Pod对象时将丢弃该报文。
可将之前的yaml文件设置外部流量策略(externalTrafficPolicy)为Local再重新apply。再次测试就会看到,请求都被调度到目标节点所运行的Pod对象,无法做到负载均衡。另外Local策略无需在集群中转发流量至其它节点,因此不用再对请求报文进行源地址转换,Server Pod所看到的客户端IP即为外部客户端的真实地址。
二、对Service资源的深入理解
2.1 iptables代理模式
iptables类型的Service对象本质上是由集群中每个节点上的kube-proxy进程将Service定义、转换并配置于节点内核上的iptables规则。每个Service的定义主要由Service流量匹配规则、流量调度规则和以每个后端Endpoint为单位的DNAT规则组成,这些规则负责完成Service资源的核心功能。
2.1.1 ClusterIP Service
ClusterIP类型的Service资源的请求流量是以某个特定Service对象的ClusterIP(也称为ServiceIP)为目标地址,以ServicePort为目标端口的报文,它们可能来自Kubernetes集群内某个节点的Pod,也可能来自节点之外。
Cluster类型的Service对象的相关规则主要位于KUBE-SERVICES、KUBE-MARQ-MASK和KUBE-POSTROUTING这几个自定义链,还有以KUBE-SVC或KUBE-SEP为前缀的各个自定义链上,用于实现Service流量筛选、分发和目标地址转换(转换为后端Pod IP),以及为不是来自Pod网络的请求报文进行源地址转换。
- KUBE-SERVICES:包含所有ClusterIP类型的Service的流量匹配规则,由PREROUTING和OUTPUT两个内置链直接调用。每个Service对象定义了这么一条规则,即将所有发往此Service对象的报文转至专用的以KUBE-SVC名称为前缀的自定义链,后缀是Service信息的hash值。
- KUBE-MARK-MASQ:专用目的自定义链,所有转至该自定义链的报文都将被打上特有的防火墙标记(0x4000),以便于将特定类型的报文定义为单独的分类,进而在该类报文转发到目标端点之前由POSTROUTING链进行源地址转换。
- KUBE-SVC-<HASH>:定义一个服务的流量调度规则。它通过随机算法将请求分发给Service的所有后端端点,每个后端端点定义在以KUBE-SEP名称为前缀的自定义链上,后端是端点信息的hash值。
- KUBE-SEP-<HASH>:定义一个端点相关的流量处理规则。它通常包含两条规则:前一条用于为那些来自该端点自身的流量请求调用自定义链KUBE-MARK-MASQ,添加防火墙标记;后一条负责对发往该端点的流量进行目标地址和端口的转换,新目标为该端点(后端Pod)的IP:PORT(-j DNAT --to-destination端点IP:端点Port)。
- KUBE-POSTROUTING:专用的自定义链,由内置链POSTROUTING调用,负责对含特有防火墙标记0x4000的请求报文进行源地址转换或地址伪装,新的源地址为报文流经接口的主IP地址。
以demoapp-svc这个Service为例,如下命令打印了该Service对象的流量匹配规则,它定义在KUBE-SERVICES自定义链上。
iptables -t nat -S KUBE-SERVICES | grep "demoapp-svc"
可以看到,所有发往demoapp-svc这个Service对象的流量还会有规则所指向的以KUBE-SVC名称为前缀的自定义链KUBE-SVC-ZAGXFVDPX7HH4UMW中的规则处理。此自定义链为demoapp-svc这个Service对象的所有可用端点定义流量调度规则。
iptables -t nat -S KUBE-SVC-ZAGXFVDPX7HH4UMW | grep "^-A"
里面定义了4条规则。
第一条规则用于为来源不是Pod网络(! -s 192.168.0.0/16)中的请求报文打上特有的防火墙标记。打标签的操作根据KUBE-MARK-MASQ自定义链中的规则完成。
后面3条规则的处理目标分别是3个以KUBE-SEP名称为前缀的自定义链,每个链上定义了一个端点的流量处理规则。因此该Service对象共有3个Endpoints对象,所有流量将在这3个Endpoints之间随机(--mode random)分配。到达KUBE-SVC-ZAGXFVDPX7HH4UMW的流量将由这3条规则随机处理,任何一条规则处理后都不会再匹配后续的其它规则。第一条规则将处理大约1/3(0.33333333349)的流量,余下的2/3流量将由第二条规则处理一半(--probability 0.50000000000,即2/3的一半还是1/3),再余下的流量将由第三条规则处理,因此三个Endpoint将各自处理1/3的流量。
每个Endpoint专用的自定义链以KUBE-SEP为前缀,它包含某个端点相关的流量处理规则。
iptables -t nat -S KUBE-SEP-BGTS2GXXGQ4JGYF5 | grep "^-A"
由于Pod对象也可能向自身所属的Service对象发起请求,该请求经由OUTPUT链到达KUBE-SERVICES链后存在被调度回当前Pod对象的可能。第一条规则就是为此类报文添加专有防火墙标记。第二条规则将接收到的流量进行目标地址转换,目标地址转为192.168.113.72:80(DNAT,由ServiceIP转为PodIP,这一步必不可少),它对应Service对象demoapp-svc匹配到的一个后端Pod对象。
如果请求报文的源地址并非Pod网络中的IP地址(比如,是集群节点的某独立容器),则Service在离开本节点前,报文源地址要转换为该节点上报文离开时要经由接口的IP地址(比如tunl0接口的IP),以确保响应报文能正确返回该节点,并由该节点响应给客户端,由内置链POSTROUTING调用自定义链KUBE-POSTROUTING链实现此类功能。此时该节点就充当了网关的作用。反之,当节点接收到的请求报文的源地址为Pod网络中的IP地址时,则请求必然来自该节点或节点上的Pod对象。而这些流量离开节点之前无需进行源地址转换。此时,目标端点可直接发送响应报文给请求方。
注意:kube-proxy也支持在iptables模式下使用masqueradeAll,对ClusterIP访问的所有请求(不论请求是否来源于Pod网络)都做源地址转换。但能不做尽量就不做,这样性能会更好。
正常情况下,对于服务的请求流量走向是这样的:ClusterIP->PREROUTING--> KUBE-SERVICES(服务定义)-->KUBE-SVC-XXX(流量调度)-->KUBE-SEP-XXX(端点流量处理)->PodIP:Endpoint Port
2.2 ipvs代理模式
由之前的介绍可得,单个Service对象的iptables数量与后端端点的数量正相关,对于拥有较多Service对象的大规模Pod对象的Kubernetes集群,每个节点的内核上将有大量的iptables规则。Service对象的变动会导致所有节点刷新netfilter上的iptables规则,且每次Service请求都将经历多次的规则匹配和处理过程,这会占用节点大量的资源。因此iptables模式不适宜Service和Pod数量较多的集群。ipvs模式通过将流量匹配和分发功能配置为少量ipvs规则,有效降低了对系统资源的占用,从而适宜更大规模的Kubernetes集群。
2.2.1 调整kube-proxy代理模式
kube-proxy使用的代理模式定义在配置文件中,kubeadm部署的Kubernetes集群以DaemonSet控制器编排kube-proxy在每一个节点运行一个实例,配置文件以kube-system名称空间中名为kube-proxy的ConfigMap对象的形式提供,默认使用iptables代理模式。
用kubectl edit cm kube-proxy -n kube-system -o yaml命令编辑该ConfigMap对象,将代理模型(mode)改为ipvs。
测试环境下,可使用如下命令完成kube-proxy所有实例的强制更新
kubectl delete pods -l k8s-app=kube-proxy -n kube-system
2.2.2 ipvs代理模式下的Service资源
相比较于iptables代理模式的复杂逻辑,ipvs的代理逻辑就较为简单。具体有两个要点。
首先,kube-proxy会在每个节点上创建一个名为kube-ipvs0的虚拟网络接口,并将集群上所有Service对象的ClusterIP和ExternalIP配置到此接口,使相应IP地址的流量均可被当前节点捕获。
其次,kube-proxy会为每个Service对象生成相关的ipvs虚拟服务器(Virtual Server)定义,此时可以把ClusterIP看作是VIP。该虚拟服务器的真实服务器(Real Server)是由相应Service对象的后端端点组成(即RIP就是后端Pod的IP)。到达虚拟服务器VIP上的服务端口的请求流量由默认或指定的调度算法分发至相关的各真实服务器。
但kube-proxy对ClusterIP和NodePort类型的Service对象的虚拟服务定义方式略有不同。
对于ClusterIP类型的Service,kube-proxy仅针对ClusterIP生成单个虚拟服务,协议和端口遵循Service的定义。
以之前的demoapp-svc这个Service为例,其ClusterIP为10.97.72.1。它的虚拟服务定义如下。此命令可在集群中的任意一个节点执行。
ipvsadm -Ln | grep -A 3 "10.97.72.1"
对于NodePort类型的Service,kube-proxy针对ServiceIP:ServicePort以及当前节点上的所有活动接口的主IP地址各定义一个虚拟服务。
如下命令获取NodePort类型的Service对象demoapp-nodeport-svc的相关虚拟服务的定义。
ipvsadm -Ln | grep -E "30871|10.96.13.58"
TCP 172.17.0.1:30871 rr #docker0接口IP
TCP 192.168.12.1:30871 rr #tunl0接口IP(与calico网络相关)
TCP 192.168.131.15:30871 rr #节点IP
TCP 10.96.13.58:80 rr #ClusterIP
当然了,上述这些Service类型对应的虚拟服务内部同样使用NAT模式进行请求代理。但由于ipvs没有使用大量的iptables规则,其系统资源开销显著降低。从功能上说,ipvs仅实现了代理和调度机制,Service资源中的源地址转换等功能(与NAT相关)仍然由iptables完成,但相应的规则数量就少很多了。
标签:iptables,Service,Kubernetes,研究,标签,Pod,kube,节点 From: https://blog.51cto.com/u_15796303/6459259