1 Service基本概念

1.1 什么是Service

在kubernetes中,Pod是应用程序的载体,当我们需要访问这个应用时,可以通过Pod的IP进行访问,但是这里有两个问题:

  1. Pod的IP地址不固定,一旦Pod异常退出、节点故障,则会造成Pod发生重建,一旦发生重建客户端则会访问失败;
  2. Pod如果扩展多份,会造成客户端无法有效使用新增Pod,如果Pod进行缩容又会造成客户端访问错误;

K8s四层代理Service(基础知识)_IP

为了解决这个问题,k8s提供了Service资源,Service作为动态的一组Pod提供一个固定的访问入口,Service资源基于标签选择器把筛选出的一组Pod对象定义成一个逻辑组合,而后Service对外提供自己的IP和端口。

当客户端请求Service的IP和端口时,Service将请求调度给标签所匹配的所有Pod,Service向客户端隐藏了真实处理请求的Pod资源,使得客户端的请求看上去是由Service直接处理并进行响应。

K8s四层代理Service(基础知识)_nginx_02

Service对象的IP地址(可称为ClusterIP或ServiceIP)是虚拟IP地址,由kubernetes系统在Service对象创建时在专有网络(Service Network)地址中自动分配或由用户手动指定。其次Service是基于端口过滤,并根据事先定义好的规则将请求转发至其后端Pod对应的端口上,因此这种代理机制也称为"端口代理"或"四层代理",工作在TCP/IP协议栈的传输层;

Service的作用:

  • 暴露流量: 让用户可以通过ServiceIP+ServicePort访问对应后端的Pod应用;
  • 负载均衡: 提供基于4层的TCP/IP负载均衡,并不提供HTTP/HTTPS等负载均衡;
  • 服务发现: 当发现新增Pod则自动加入至Service的后端,如发现Pod异常则自动剔除Service后端;

1.2 Service工作逻辑

Service持续监视APIServer,监视Service标签选择器所匹配的后端Pod,并实时跟踪这些Pod对象的变动情况,例如IP地址发生变、Pod对象新增与减少。

不过Service不直接与Pod建立关联关系,它们之间还有一个中间层Endpoints,Endpoints对象是一个由IP地址和端口组成的列表,这些IP地址和端口则来自于Service标签选择器所匹配到的Pod,默认情况下,创建Service资源时,其关联的Endpoints对象会被自动创建。

K8s四层代理Service(基础知识)_nginx_03

1.3 Service具体实现

在kubernetes中,Service只是一个抽象的概念,真正起作用实现负载均衡规则的其实是kube-proxy这个进程,它在每个节点上都需要运行一个kube-proxy,用来完成负载均衡规则的创建。

  1. 创建Service资源后,会分配一个随机的ServiceIP,返回给用户,然后写入Etcd;
  2. endpoints controller负责生成和维护所有endpoints,它会监听Service和Pod的状态,当Pod处于Running且准备就绪时,endpoints controller会将Pod ip更新到对应Service的endpoints对象中,然后写入Etcd;
  3. kube-proxy通过APIServer监听Service、Endpoints资源变动,一旦Service或Endpoints资源发生变化,kube-proxy会将最新的信息转换为对应的Iptables、PIVS访问规则,而后在本地主机上执行;
  4. 当客户端想要访问Service的时候,其实访问的就是本地节点上的iptables、IPVS规则,由它们路由到对应节点;

K8s四层代理Service(基础知识)_Pod_04

实现图上的功能,主要需要以下几个组件协同工作:

  • Service: 用户通过kubectl命令向APIServer发送创建Service的请求,APIServer收到后存入Etcd中;
  • Endpoints: 获取Service所匹配的Pod地址,而后将信息写入与Service同名的Endpoints资源中;
  • kube-proxy: 获取Service和Endpoints资源的变动,而后生成Iptables、IPVS规则,在本机执行;
  • iptables: 当用户请求ServiceIP时,使用iptables的DNAT技术将ServiceIP的请求调度至Endpoint保存的IP列表中;

2 kube-proxy代理模型

2.1 userSpace

userspace模式下,kube-proxy为ServiceIP创建一个监听端口,当用户向ServiceIP发送请求

1、首先请求会被Iptables规则连接,然后重定向到kube-proxy对应的端口;

2、然后kube-proxy根据调度算法选择挑选一个Pod,将请求调度到该Pod上;

总结: Pod请求serviceIP时,会被Iptables将请求拦截给用户空间的kube-proxy,然后再经过内核空间路由到对应的Pod。

K8s四层代理Service(基础知识)_IP_05

问题: 该模式流量经过内核空间后,会送往用户空间kube-proxy进程,而后又送回内核空间,发送调度分配的目标后端Pod。

2.2 iptables

iptables模式下,kube-proxy为Service后端的所有Pod创建对应的iptables规则,当用户向ServiceIP发送请求

1、首先iptables会拦截用户请求;

2、然后直接将请求调度到后端的Pod;

总结: Pod请求ServiceIP时,iptables将请求拦截并且直接完成调度,然后路由到对应的Pod,所以效率比userspace高。

K8s四层代理Service(基础知识)_IP_06

问题: 一个Service会创建出大量的规则,且不支持更高级的调度算法,当Pod不可用也无法重试。

2.3 IPVS

IPVS模式和iptables类似,kube-proxy为Service后端所有的Pod创建对应的IPVS贵,一个Service只会生成一条规则,所以规模较大的场景下,应该使用IPVS模式,其次IPVS支持更多更高级的调度算法。

K8s四层代理Service(基础知识)_nginx_07

2.4 kube-proxy生成的iptables规则分析

1、service的type类型是ClusterIp,iptables规则分析
在k8s创建的service,虽然有ip地址,但是service的ip是虚拟的,不存在物理机上的,是在iptables或者ipvs规则里的。

kubectl get svc -l run=my-service
NAME       TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
my-nginx   ClusterIP   10.98.63.235   <none>        80/TCP    75s

kubectl get pods -l run=my-nginx -o wide
NAME                        READY   STATUS    RESTARTS   AGE   IP                
my-nginx-69f769d56f-6fn7b   1/1     Running   0          13m   10.244.121.22   
my-nginx-69f769d56f-xzj5l   1/1     Running   0          13m   10.244.121.21 
  
iptables -t nat -L | grep 10.98.63.235
KUBE-MARK-MASQ  tcp  -- !10.244.0.0/16        10.98.63.235         /* default/my-nginx cluster IP */ tcp dpt:http
KUBE-SVC-L65ENXXZWWSAPRCR  tcp  --  anywhere             10.98.63.235         /* default/my-nginx cluster IP */ tcp dpt:http

iptables -t nat -L | grep KUBE-SVC-L65ENXXZWWSAPRCR
KUBE-SVC-L65ENXXZWWSAPRCR  tcp  --  anywhere             10.98.63.235         /* default/my-nginx cluster IP */ tcp dpt:http
Chain KUBE-SVC-L65ENXXZWWSAPRCR (1 references)

iptables -t nat -L | grep 10.244.121.22
KUBE-MARK-MASQ  all  --  10.244.121.22        anywhere             /* default/my-nginx */
DNAT       tcp  --  anywhere             anywhere             /* default/my-nginx */ tcp to:10.244.121.22:80

iptables -t nat -L | grep 10.244.121.21
KUBE-MARK-MASQ  all  --  10.244.121.21        anywhere             /* default/my-nginx */
DNAT       tcp  --  anywhere             anywhere             /* default/my-nginx */ tcp to:10.244.121.21:80

#通过上面可以看到之前创建的service,会通过kube-proxy在iptables中生成一个规则,来实现流量路由,有一系列目标为 KUBE-SVC-xxx 链的
规则,每条规则都会匹配某个目标 ip 与端口。也就是说访问某个 ip:port 的请求会由 KUBE-SVC-xxx 链来处理。这个目标 IP 其实就是
service ip。

2、service的type类型是nodePort,iptables规则分析
kubectl get pods -l  run=my-nginx-nodeport
NAME                                 READY   STATUS    RESTARTS   AGE
my-nginx-nodeport-649c945f85-l2hj6   1/1     Running   0          21m
my-nginx-nodeport-649c945f85-zr47r   1/1     Running   0          21m

kubectl get svc -l run=my-nginx-nodeport
NAME                TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
my-nginx-nodeport   NodePort   10.104.251.190   <none>        80:30380/TCP   22m

iptables -t nat -S | grep 30380
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/my-nginx-nodeport" -m tcp --dport 30380 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/my-nginx-nodeport" -m tcp --dport 30380 -j KUBE-SVC-J5QV2XWG4FEBPH3Q

iptables -t nat -S | grep KUBE-SVC-J5QV2XWG4FEBPH3Q
-N KUBE-SVC-J5QV2XWG4FEBPH3Q
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/my-nginx-nodeport" -m tcp --dport 30380 -j KUBE-SVC-J5QV2XWG4FEBPH3Q
-A KUBE-SERVICES -d 10.104.251.190/32 -p tcp -m comment --comment "default/my-nginx-nodeport cluster IP" -m tcp --dport 80 -j KUBE-SVC-J5QV2XWG4FEBPH3Q
-A KUBE-SVC-J5QV2XWG4FEBPH3Q -m comment --comment "default/my-nginx-nodeport" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-XRUO23GXY67LXLQN
-A KUBE-SVC-J5QV2XWG4FEBPH3Q -m comment --comment "default/my-nginx-nodeport" -j KUBE-SEP-IIBDPNPJZXXASELC

iptables -t nat -S | grep KUBE-SEP-XRUO23GXY67LXLQN
-N KUBE-SEP-XRUO23GXY67LXLQN
-A KUBE-SEP-XRUO23GXY67LXLQN -s 10.244.102.90/32 -m comment --comment "default/my-nginx-nodeport" -j KUBE-MARK-MASQ
-A KUBE-SEP-XRUO23GXY67LXLQN -p tcp -m comment --comment "default/my-nginx-nodeport" -m tcp -j DNAT --to-destination 10.244.102.90:80
-A KUBE-SVC-J5QV2XWG4FEBPH3Q -m comment --comment "default/my-nginx-nodeport" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-XRUO23GXY67LXLQN

iptables -t nat -S | grep KUBE-SEP-IIBDPNPJZXXASELC
-N KUBE-SEP-IIBDPNPJZXXASELC
-A KUBE-SEP-IIBDPNPJZXXASELC -s 10.244.121.23/32 -m comment --comment "default/my-nginx-nodeport" -j KUBE-MARK-MASQ
-A KUBE-SEP-IIBDPNPJZXXASELC -p tcp -m comment --comment "default/my-nginx-nodeport" -m tcp -j DNAT --to-destination 10.244.121.23:80
-A KUBE-SVC-J5QV2XWG4FEBPH3Q -m comment --comment "default/my-nginx-nodeport" -j KUBE-SEP-IIBDPNPJZXXASELC

3 Service资源类型

Service资源类型支持ExternalName、ClusterIP、NodePort和LoadBalancer四种,默认ClusterIP。

3.1 ClusterIP

ClusterIP: 通过集群的内部IP暴露服务,选择ServiceIP只能够在集群内部访问,这也是默认的ServiceType。

K8s四层代理Service(基础知识)_nginx_08

此默认Service类型从你的集群中为此预留的IP地址池中分配一个IP地址。

其他几种Service类型在ClusterIP类型的基础上进行构建。

3.2 NodePort

NodePort: NodePort类型对于ClusterIP类型Service资源的扩展,它通过每个节点上的IP和端口接入集群外部流量,并分发给后端的Pod处理和响应,因此通过<节点IP>:<节点端口>,可以从集群外部访问服务。

K8s四层代理Service(基础知识)_Pod_09

如果你将type 字段设置为NodePort,则Kubernetes控制平面将在--service-node-port-range 标志所指定的范围内分配端口(默认值:30000-32767)。 每个节点将该端口(每个节点上的相同端口号)上的流量代理到你的Service。 你的Service在其.spec.ports[*].nodePort字段中报告已分配的端口。

使用NodePort可以让你自由设置自己的负载均衡解决方案, 配置Kubernetes不完全支持的环境, 甚至直接公开一个或多个节点的IP地址。

对于NodePort类型Service,Kubernetes额外分配一个端口(TCP、UDP或SCTP以匹配Service的协议)。 集群中的每个节点都将自己配置为监听所分配的端口,并将流量转发到与该Service关联的某个就绪端点。 通过使用合适的协议(例如TCP)和适当的端口(分配给该Service)连接到任何一个节点, 你就能够从集群外部访问type: NodePort服务。

3.3 LoadBalance

LoadBalancer: 这类Service依赖云厂商,需要通过云厂商调用API接口创建软件负载均衡,将服务暴露到集群外部。当创建LoadBalancer类型的Service对象时,它会在集群上自动创建一个NodePort类型的Service。集群外部的请求流量会先路由至该负载均衡,并由该负载均衡调度至各个节点的NodePort。

K8s四层代理Service(基础知识)_IP_10

3.4 ExternalName

ExternalName: 此类型不是用来定义如何访问集群内服务的,而是把集群外部的某些服务以DNS CNAME方式映射到集群内,从而让集群内的Pod资源能够访问外部服务的一种实现方式。

K8s四层代理Service(基础知识)_IP_11

类型为ExternalName的Service将Service映射到DNS名称,而不是典型的选择算符。

4 Service应用实践

4.1 Service配置示例

apiVersion:
kind:
metadata:
  name:
  namespace:
spec:
  type: #Service类型,默认为ClusterIP
  selector: #标签选择器,用于匹配对应的Pod
  ports: #Service端口列表
  - name: #端口名称
    protocol: #协议类型,支持TCP、UDP和SCTP,默认是TCP
    port: #Service的端口号
    targetPort: #后端目标进程的端口号或名称,如果不指定此字段,则使用'port'字段的值(身份映射)
    nodePort: #节点端口号,仅适用于NodePort和LoadBalancer
  externalTrafficPolicy: #外部流量路由策略,local表示由当前节点处理,cluster表示向集群范围内调度

4.2 ClusterIP实践

1、创建资源

vim nginx-service-clusterip.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx-svc
  namespace: default
spec:
  type: ClusterIP
  selector:
    app: nginx
  ports:
  - name: nginx-svc-port
    protocol: TCP
    port: 8888
    targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80

2、更新资源清单

kubectl apply -f nginx-service-clusterip.yaml

K8s四层代理Service(基础知识)_nginx_12

3、查看service详细信息

K8s四层代理Service(基础知识)_Pod_13

service可以对外提供统一固定的ip地址,并将请求重定向至集群中的pod。其中“将请求重定向至集群中的pod”就是通过endpoint与selector协同工作实现。selector是用于选择pod,由selector选择出来的pod的ip地址和端口号,将会被记录在endpoint中。endpoint便记录了所有pod的ip地址和端口号。当一个请求访问到service的ip地址时,就会从endpoint中选择出一个ip地址和端口号,然后将请求重定向至pod中。具体把请求代理到哪个pod,需要的就是kube-proxy的轮询实现的。service不会直接到pod,service是直接到endpoint资源,就是地址加端口,再由endpoint再关联到pod。

4、删除一个pod

K8s四层代理Service(基础知识)_nginx_14

5、查看service资源

K8s四层代理Service(基础知识)_IP_15

6、service域名解析

service 只要创建完成,我们就可以直接解析它的服务名,每一个服务创建完成后都会在集群 dns 中动态添加一个资源记录,添加完成后我们就可以解析了,资源记录格式是:

SVC_NAME.NS_NAME.DOMAIN.LTD.

服务名.命名空间.域名后缀

集群默认的域名后缀是 svc.cluster.local.

就像我们上面创建的 my-nginx 这个服务,它的完整名称解析就是nginx-svc.default.svc.cluster.local

K8s四层代理Service(基础知识)_IP_16

4.3 NodePort实践

1、创建资源

vim nginx-service-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx-svc
  namespace: default
spec:
  type: NodePort
  selector:
    app: nginx
  ports:
  - name: nginx-svc-port
    protocol: TCP
    port: 8888
    targetPort: 80
    nodePort: 30000
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80

2、更新资源清单

kubectl apply -f nginx-service-nodeport.yaml

K8s四层代理Service(基础知识)_Pod_17

3、查看资源

K8s四层代理Service(基础知识)_nginx_18

4、资源走向

服务请求走向:Client-→node ip:30000->service ip:8888-→pod ip:container port

Client ->192.168.137.142:30000->10.96.17.207:8888->pod ip:80

4.4 ExternalName实践

kube-system命名空间下的client服务访问default命名空间下的nginx-svc服务

1、创建资源

vim busybox-service-externalname.yaml
apiVersion: v1
kind: Service
metadata:
  name: client-svc
  namespace: kube-system
spec:
  type: ExternalName
  externalName: nginx-svc.default.svc.cluster.local
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: client
  namespace: kube-system
spec:
  replicas: 1
  selector:
    matchLabels:
      app: busybox
  template:
    metadata:
      labels:
        app: busybox
    spec:
      containers:
      - name: busybox
        image: busybox:1.28
        imagePullPolicy: IfNotPresent
        command:
        - "/bin/sh"
        - "-c"
        - "sleep 300"

2、更新资源清单

kubectl apply -f busybox-service-externalname.yaml

K8s四层代理Service(基础知识)_IP_19

3、登录pod,查看域名

K8s四层代理Service(基础知识)_nginx_20

K8s四层代理Service(基础知识)_IP_21

5 Service与Endpoint

5.1 Endpoint与容器探针

Service对象借助Endpoints资源来跟踪其关联的后端断点,Endpoints对象会根据Service标签选择器筛选出的后端端点的IP地址分别保存在subsets.addresses字段和subsets.notReadyAddresses字段中。它通过APIServer持续、动态跟踪每个端点的状态变化,并及时反应到端点IP所属的字段中。

subsets.addresses: 保存就绪的容器IP,也就意味着Service可以直接将请求调度至该地址段;

subsets.notReadyAddresses: 保存未就绪容器IP,也就意味着Service不会将请求调度至该地址段;

1、创建一个资源清单,会自动创建出同名的Endpoints对象

vim nginx-service-readiness.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx-svc-readiness
  namespace: default
spec:
  selector:
    app: nginx
  ports:
  - name: nginx-svc-port
    protocol: TCP
    port: 8888
    targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
        readinessProbe:
          initialDelaySeconds: 30
          periodSeconds: 10
          httpGet:
            path: /index.html
            port: 80

2、容器初始启动延迟30s,也就意味着至少30s以后才能转换为就绪状态,对外提供服务

K8s四层代理Service(基础知识)_Pod_22

3、任何原因导致后端的端点就绪状态检测失败,都会触发Endpoint对象将该端点的IP地址从subsets.addresses字段移至subsets.notReadyAddresses字段

#模拟故障

kubectl exec -it nginx-7478cc4d5b-99xd6 -- rm -f /usr/share/nginx/html/index.html

K8s四层代理Service(基础知识)_IP_23

#查看Endpoints资源

K8s四层代理Service(基础知识)_Pod_24

4、将故障端点重新转为就绪状态后,Endpoints对象会将其移回subsets.addresses字段,这种处理机制确保了Service对象不会将客户端请求流量调度给那些处于运行状态但服务未就绪的端点

kubectl exec -it nginx-7478cc4d5b-99xd6 -- touch /usr/share/nginx/html/index.html

K8s四层代理Service(基础知识)_IP_25

#查看Endpoints资源

K8s四层代理Service(基础知识)_IP_26

5.2 自定义endpoint实践

Service通过selector和pod建立关联,k8s会根据Service关联到的podIP信息组合成一个Endpoint,若Service定义中没有selector字段,service被创建时,Endpoint controller不会自动创建Endpoint。

我们可以通过配置清单创建Service,而无需使用标签选择器,而后自行创建一个同名的Endpoint对象,指定对应的IP。这种一般用于将外部Mysql/Redis等应用引入kubernetes集群内部,让内部通过Service的方式访问外部资源。

K8s四层代理Service(基础知识)_Pod_27

1、创建Endpoint资源清单

vim mysql-endpoint.yaml
apiVersion: v1
kind: Endpoints
metadata:
  name: mysql
  namespace: default
subsets:
- addresses:
  - ip: 192.168.137.130
  ports:
  - protocol: TCP
    port: 3306

K8s四层代理Service(基础知识)_IP_28

2、创建与Endpoints同名的Service资源清单

vim mysql-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: default
spec:
  type: ClusterIP
  ports:
  - protocol: TCP
    port: 3366
    targetPort: 3306

K8s四层代理Service(基础知识)_Pod_29

3、使用pod访问Service,验证是否正常访问MySQL服务

kubectl run -i --tty temp --rm --image=mysql:8.0.32 --image-pull-policy=IfNotPresent --command "/bin/sh"

K8s四层代理Service(基础知识)_IP_30

K8s四层代理Service(基础知识)_Pod_31