06-Kubernetes四层负载均衡 - Service

1、Service概述

1.1 为什么要有Service?

在kubernetes中,Pod是有生命周期的,如果Pod重启它的IP很有可能会发生变化。如果我们的服务都是将Pod的IP地址写死,Pod挂掉或者重启,和刚才重启的pod相关联的其他服务将会找不到它所关联的Pod,为了解决这个问题,在kubernetes中定义了service资源对象,Service 定义了一个服务访问的入口,客户端通过这个入口即可访问服务背后的应用集群实例,service是一组Pod的逻辑集合,这一组Pod能够被Service访问到,通常是通过Label Selector实现的。

  1. Pod IP经常会随着重启等操作发生变化,Service是Pod的代理,客户端只需要通过访问Service,就会把请求代理到Pod。
  2. Pod IP在Kubernetes集群外部无法访问,所以需要创建Service,这个Service是可以在Kubernetes集群外部访问的。

1.2 Service概述

Service是一个固定的接入层,客户端可以通过访问Service的IP和端口访问到Service关联的后端Pod,这个Service工作依赖于在Kubernetes集群之上部署的一个附件,就是Kubernetes的dns服务(不同的Kubernetes版本的dns默认使用的也是不一样的,1.11之前的版本使用的是kubeDNS,较新的版本使用的是CoreDNS),Service的名称解析是基于dns附件的,因此在部署完Kubernetes之后需要部署dns附件,Kubernetes要想给客户端提供网络功能,需要依赖第三方的网络插件(flannel,calico等)。每个Kubernetes节点上都有一个组件叫做kube-proxy,这个组件将始终监视着apiserver中有关service资源的变动信息,需要跟master之上的apiserver交互,随时连接到apiserver上获取任何一个与service资源相关的资源变动状态,这种是通过kubernetes中固有的一种请求方法watch(监视)来实现的,一旦有service资源的内容发生变动(如创建,删除),kube-proxy都会将它转化成当前节点之上的能够实现service资源调度,把我们请求调度到后端特定的pod资源之上的规则,这个规则可能是iptables,也可能是ipvs,取决于service的实现方式。

1.3 Service工作原理

Kubernetes在创建Service时,会根据标签选择器selector(label selector)来查找Pod,据此创建语Service同名的endpoint对象,当Pod地址发生变化时,endpoint也会随之发生变化,Service接收前端Client请求的时候,就会根据对应的endpoint,找到其中某一个复合条件的Pod进行地址访问。(至于转发至哪个节点的Pod,则由负载均衡kube-proxy决定)

1.4 Kubernetes中的三类IP

1.4.1 Node Network 节点网络

节点网络:物理节点或者虚拟机节点的网络,如eth0或者ens33等网卡接口上的网络地址。

通过Linux命令可以查看物理节点或者虚拟机的网卡接口地址:

ip addr
ifconfig

如下图所示:

1.4.2 Pod Network Pod网络

Pod Network:创建Pod之后,Pod拥有的网络地址。

通过下面的命令可以看到Pod的IP地址:

kubectl get pods -o wide

如下图所示:

Node Network和Pod network这两种网络地址是我们实实在在配置的,其中节点网络地址是配置在节点接口之上,而pod网络地址是配置在pod资源之上的,因此这些地址都是配置在某些设备之上的,这些设备可能是硬件,也可能是软件模拟的

1.4.3 Cluster Network 集群网络

Cluster Network:集群网络,也成为Service Network,这个地址是虚拟的地址(Virtual IP),没有配置在某个接口之上,只出现在Service的规则当中。

通过下面的命令能够查看到某个Service的虚拟IP地址:

kubectl get svc -o wide

如下所示:

2、创建Service资源

2.1 Service资源清单

Service资源清单有哪些字段和相关配置信息可以通过kubectl explain service来查看帮助文档。

  • apiVersion:Service资源使用 的API组和版本信息,当前版本为v1,可以通过kubectl explain service.apiVersion查询

  • kind:资源类型,Service,可以通过kubectl explain service.kind命令查询

  • metadata:元数据

    • name:Service的名称
    • namespace:Service所属的名称空间
  • spec:Service的详细配置

    • allocateLoadBalancerNodePorts:布尔类型,allocateLoadBalancerNodePorts 定义 NodePorts 是否会自动分配给 LoadBalancer 类型的服务。 默认为“真”。 如果集群负载均衡器不依赖 NodePorts,它可能被设置为“false”。 allocateLoadBalancerNodePorts 只能为 LoadBalancer 类型的服务设置,如果类型更改为任何其他类型,将被清除。 此字段是 alpha 级别的,仅由启用 ServiceLBNodePortControl 功能的服务器使用。

    • clusterIP:clusterIP 是服务的 IP 地址,通常是随机分配的。 如果地址是手动指定的,在范围内(根据系统配置),并且没有被使用,它将被分配给服务; 否则创建服务将失败。 此字段可能不会通过更新更改,除非类型字段也更改为 ExternalName(这要求此字段为空白)或类型字段正在从 ExternalName 更改(在这种情况下,可以选择指定此字段,如上所述 )。 有效值为“None”、空字符串 ("") 或有效的 IP 地址。 将此设置为“无”会生成“无头服务”(无虚拟 IP),这在首选直接端点连接且不需要代理时很有用。 仅适用于 ClusterIP、NodePort 和 LoadBalancer 类型。 如果在创建 ExternalName 类型的 Service 时指定了该字段,则创建将失败。 更新服务以键入 ExternalName 时,该字段将被擦除。动态分配的地址,也可以自己在创建的时候指定,创建之后就改不了了

    • clusterIPs:ClusterIPs 是分配给该服务的 IP 地址列表,通常是随机分配的。如果地址是手动指定的,在范围内(根据系统配置),并且没有被使用,它将被分配给服务;否则创建服务将失败。此字段可能不会通过更新更改,除非类型字段也更改为 ExternalName(这要求此字段为空)或类型字段从 ExternalName 更改(在这种情况下,可以选择指定此字段,如上所述)。有效值为“None”、空字符串 ("") 或有效的 IP 地址。将此设置为“无”会生成“无头服务”(无虚拟 IP),这在首选直接端点连接且不需要代理时很有用。仅适用于 ClusterIP、NodePort 和 LoadBalancer 类型。如果在创建 ExternalName 类型的 Service 时指定了该字段,则创建将失败。更新服务以键入 ExternalName 时,该字段将被擦除。如果未指定此字段,则将从 clusterIP 字段初始化。如果指定此字段,客户端必须确保 clusterIPs[0] 和 clusterIP 具有相同的值。

    • externalIPs:externalIPs 是一个 IP 地址列表,集群中的节点也将接受该服务的流量。 这些 IP 不受 Kubernetes 管理。 用户负责确保流量到达具有此 IP 的节点。 一个常见的例子是不属于 Kubernetes 系统的外部负载均衡器。

    • externalName:externalName 是发现机制将作为此服务的别名返回的外部引用(例如 DNS CNAME 记录)。 不涉及代理。 必须是小写的 RFC-1123 主机名 (https://tools.ietf.org/html/rfc1123) 。

    • externalTrafficPolicy:externalTrafficPolicy 表示此服务是否希望将外部流量路由到节点本地或集群范围的端点。 “本地”保留客户端源 IP 并避免 LoadBalancer 和 Nodeport 类型服务的第二跳,但存在潜在的不平衡流量传播风险。 “集群”掩盖了客户端源 IP,可能会导致第二跳到另一个节点,但应该具有良好的整体负载分布。

    • healthCheckNodePort:healthCheckNodePort 指定服务的健康检查节点端口。 这仅适用于 type 设置为 LoadBalancer 且 externalTrafficPolicy 设置为 Local 的情况。 如果一个值被指定,在范围内,并且没有被使用,它将被使用。 如果未指定,将自动分配一个值。 外部系统(例如负载平衡器)可以使用此端口来确定给定节点是否拥有此服务的端点。 如果在创建不需要的服务时指定了该字段,创建将失败。 更新服务以不再需要它时(例如更改类型),该字段将被擦除。

    • ipFamilies

    • IPFamilies 是分配给此服务的 IP 系列(例如 IPv4、IPv6)的列表,并由“IPv6DualStack”功能门控制。该字段通常根据集群配置和 ipFamilyPolicy 字段自动分配。如果手动指定该字段,且请求的family在集群中可用,且ipFamilyPolicy允许,则使用;否则创建服务将失败。该字段是有条件可变的:它允许添加或删除辅助 IP 系列,但不允许更改服务的主要 IP 系列。有效值为“IPv4”和“IPv6”。该字段仅适用于 ClusterIP、NodePort 和 LoadBalancer 类型的服务,并且适用于“无头”服务。更新服务以键入 ExternalName 时,该字段将被擦除。

      该字段最多可以包含两个条目(双堆栈系列,按任意顺序)。如果指定,这些系列必须对应于 clusterIPs 字段的值。 clusterIP 和 ipFamilies 都由 ipFamilyPolicy 字段管理。

    • ipFamilyPolicy:IPFamilyPolicy 表示此服务请求或要求的双栈特性,并由“IPv6DualStack”特性门控。 如果没有提供值,则该字段将设置为 SingleStack。 服务可以是“SingleStack”(单个 IP 系列)、“PreferDualStack”(双栈配置集群上的两个 IP 系列或单栈集群上的单个 IP 系列)或“RequireDualStack”(双栈上的两个 IP 系列 配置的集群,否则失败)。 ipFamilies 和 clusterIPs 字段取决于该字段的值。 更新服务以键入 ExternalName 时,此字段将被擦除。

    • loadBalancerIP:仅适用于服务类型:LoadBalancer LoadBalancer 将使用此字段中指定的 IP 创建。 此功能取决于底层云提供商是否支持在创建负载均衡器时指定 loadBalancerIP。 如果云提供商不支持该功能,该字段将被忽略。

    • loadBalancerSourceRanges:如果平台指定并支持,这将限制通过云提供商负载均衡器的流量将被限制到指定的客户端 IP。 如果云提供商不支持该功能,该字段将被忽略。

    • ports:此服务公开的端口列表。定义service端口,用来和后端pod建立联系

    • publishNotReadyAddresses:publishNotReadyAddresses 表示任何处理此服务端点的代理都应忽略任何准备就绪/未准备就绪的指示。 设置此字段的主要用例是 StatefulSet 的 Headless Service 为其 Pod 传播 SRV DNS 记录,以实现对等点发现。 为服务生成 Endpoints 和 EndpointSlice 资源的 Kubernetes 控制器将此解释为意味着所有端点都被视为“就绪”,即使 Pod 本身还没有。 通过 Endpoints 或 EndpointSlice 资源仅使用 Kubernetes 生成的端点的代理可以安全地假设这种行为。

    • selector:标签选择器,通过标签选择器选择关联的pod,加入到对应的Endpoint中提供服务。

    • sessionAffinity:支持ClientIPNone。 用于维护会话亲和性。 启用基于客户端 IP 的会话亲和性。 必须是 ClientIP 或 None。 默认为无。

    • sessionAffinityConfig:sessionAffinityConfig 包含会话亲和性的配置。service在实现负载均衡的时候还支持sessionAffinity,sessionAffinity什么意思?会话联系,默认是none,随机调度的(基于iptables规则调度的);如果我们定义sessionAffinity的client ip,那就表示把来自同一客户端的IP请求调度到同一个pod上

    • topologyKeys:topologyKeys 是一个拓扑键的优先顺序列表,服务的实现在访问这个 Service 时应该使用它来优先排序端点,它不能与 externalTrafficPolicy=Local 同时使用。 拓扑键必须是有效的标签键,最多可以指定 16 个键。 端点是根据具有可用后端的第一个拓扑键选择的。 如果指定了此字段并且所有条目都没有与客户端拓扑匹配的后端,则服务没有该客户端的后端并且连接应该失败。 特殊值“*”可用于表示“任何拓扑”。 这个包罗万象的值(如果使用)仅作为列表中的最后一个值才有意义。 如果未指定或为空,则不会应用拓扑约束。 此字段是 alpha 级别的,仅由启用 ServiceTopology 功能的服务器使用。

    • type:定义Service类型,type 确定服务的公开方式。 默认为ClusterIP。 有效选项为 ExternalNameClusterIPNodePort LoadBalancer。 “ClusterIP”为端点分配一个集群内部 IP 地址用于负载平衡。 端点由选择器确定,如果未指定,则通过手动构造 Endpoints 对象或 EndpointSlice 对象来确定。 如果 clusterIP 为“None”,则不分配虚拟 IP,并且端点作为一组端点而不是虚拟 IP 发布。 “NodePort”建立在 ClusterIP 之上,并在每个节点上分配一个端口,该端口路由到与 clusterIP 相同的端点。 “LoadBalancer”基于 NodePort 构建并创建一个外部负载均衡器(如果当前云支持),它路由到与 clusterIP 相同的端点。 “ExternalName”将此服务别名为指定的 externalName。 其他几个字段不适用于 ExternalName 服务。

  • status:Kubernetes创建Service之后自动生成,无法手动修改

2.2 Service四种类型

2.2.1 ExternalName

适用于k8s集群内部容器访问外部资源,它没有selector,也没有定义任何的端口和Endpoint。

kind: Service
apiVersion: v1
metadata:
  name: my-service
  namespace: prod
spec:
  type: ExternalName
  externalName: my.database.example.com

上面这个Service定义的是将prod名称空间中的my-service服务映射到my.database.example.com。

service的FQDN是: <service_name>..svc.cluster.local

my-service的FQDN是:my-service.prod. svc.cluster.local

2.2.2 ClusterIP

通过k8s集群内部IP暴露服务,选择该值,服务只能够在集群内部访问,这也是默认的ServiceType

2.2.3 NodePort

通过每个Node节点上的IP和静态端口暴露k8s集群内部的服务。通过请求:可以把请求代理到内部的pod。

Client → NodeIP:Port → ServiceIP:Port → PodIP:ContainerPort

2.2.4. LoadBalancer

使用云提供商的负载均衡器,可以向外部暴露服务。外部的负载均衡器可以路由到NodePort服务和ClusterIP服务。

2.3 Service的端口

通过下面的命令查询帮助文档:

kubectl explain service.spec.ports
  • appProtocol:此端口的应用程序协议。 该字段遵循标准 Kubernetes 标签语法。 无前缀名称保留给 IANA 标准服务名称(根据 RFC-6335 和 http://www.iana.org/assignments/service-names)。 非标准协议应使用前缀名称,例如 mycompany.com/my-custom-protocol。 这是一个受 ServiceAppProtocol 特性门保护并默认启用的 beta 字段。

  • name:定义服务中此端口的名称。 这必须是 DNS_LABEL。 ServiceSpec 中的所有端口都必须具有唯一的名称。 在考虑服务的端点时,它必须与 EndpointPort 中的“名称”字段匹配。 如果此服务上仅定义一个 ServicePort,则为可选。

  • nodePort:在宿主机上映射端口,当类型为 NodePort 或 LoadBalancer 时,此服务在其上公开的每个节点上的端口。 通常由系统分配。 如果指定了一个值,在范围内且未使用,则将使用该值,否则操作将失败。 如果未指定,如果此服务需要一个端口,将分配一个端口。 如果在创建不需要的服务时指定了该字段,创建将失败。 将服务更新为不再需要它时,该字段将被擦除(例如,将类型从 NodePort 更改为 ClusterIP)。

  • port:此Service的端口号,这个是k8s集群内部服务可访问的端口。

  • protocol:此端口的 IP 协议。 支持TCPUDPSCTP。 默认为 TCP。

  • targetPort:服务所针对的 pod 上要访问的端口号或名称。 编号必须在 1 到 65535 的范围内。名称必须是 IANA_SVC_NAME。 如果这是一个字符串,它将在目标 Pod 的容器端口中作为命名端口进行查找。 如果未指定,则使用“端口”字段的值(身份映射)。 对于 clusterIP=None 的服务,该字段将被忽略,应省略或设置为等于port字段。

    targetPort是pod上的端口,从port和nodePort上来的流量,经过kube-proxy流入到后端pod的targetPort上,最后进入容器。与制作容器时暴露的端口一致(使用DockerFile中的EXPOSE),例如官方的nginx暴露80端口。

2.4 Service - ClusterIP

service-clusterIP.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
  labels:
    app: my-nginx
spec:
  replicas: 2
  template:
    metadata:
      name: my-nginx
      labels:
        app: my-nginx
    spec:
      containers:
        - name: my-nginx
          image: nginx:1.17.1
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80
      restartPolicy: Always
  selector:
    matchLabels:
      app: my-nginx
---
apiVersion: v1
kind: Service
metadata:
  name: my-nginx
spec:
  type: ClusterIP
  ports:
    - port: 80
      protocol: TCP
      targetPort: 80
  selector:
    app: my-nginx

更新资源清单:

kubectl apply -f service-clusterIP.yaml

Kubernetes会自动创建以下资源对象:

此时我们可以通过在Kubernetes集群的任何一个节点上直接访问PodIP或者ServiceIP:

此时我们随机删除其中一个Pod,Deployment控制器为了保证Pod始终保持指定的副本数会重新调度一个新的Pod启动,但是Pod的IP会发生变化,但是由于Service是根据标签选择器来自动维护endpoint列表的,所以我们任何时候都可以通过ServiceIP来访问后端的Pod:

kubectl delete pods my-nginx-5d479bbd86-hc4qx

最终结果如下:

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。

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

SVC_NAME.NS_NAME.DOMAIN.LTD.

Kubernetes集群默认的域名后缀是:svc.cluster.local,所以刚刚创建的Service的完整域名就是my-nginx.default.svc.culster.local

测试直接访问Service对应的域名:

2.5 Service -NodePort

service-nodeport.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx-nodeport
  labels:
    app: my-nginx-nodeport
spec:
  replicas: 2
  template:
    metadata:
      name: my-nginx-nodeport
      labels:
        app: my-nginx-nodeport
    spec:
      containers:
        - name: my-nginx-nodeport
          image: nginx:1.17.1
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80
      restartPolicy: Always
  selector:
    matchLabels:
      app: my-nginx-nodeport
---
apiVersion: v1
kind: Service
metadata:
  name: my-nginx-nodeport
spec:
  selector:
    app: my-nginx-nodeport
  ports:
    - port: 80
      protocol: TCP
      targetPort: 80
      nodePort: 30080
  type: NodePort

更新资源清单:

kubectl apply -f service-nodeport.yaml

更新资源清单后,Kubernetes会自动调度创建下面的资源:

此时在Kubernetes集群外部我们就可以通过访问Service暴露在节点主机上的30080端口来访问服务了:

此时访问的请求方向是:ClientNodeIP:30080ServiceIP:80PodIP:80

在Kubernetes集群内部,照样可以通过访问Service的IP和端口进行访问。

2.6 Service - ExternalName

ExternalName类型的Service常用于跨名称空间访问的情况下。比如default命名空间下的服务想要访问其他名称空间下的服务。

service-externalname.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: client
  namespace: default
  labels:
    app: busybox
spec:
  replicas: 1
  template:
    metadata:
      name: busybox
      labels:
        app: busybox
    spec:
      containers:
        - name: busybox
          image: busybox:1.28
          imagePullPolicy: IfNotPresent
          command:
            - "/bin/sh"
            - "-c"
            - "sleep 3600"
      restartPolicy: Always
  selector:
    matchLabels:
      app: busybox
---
apiVersion: v1
kind: Service
metadata:
  name: client-svc
  namespace: default
spec:
  selector:
    app: busybox
  ports:
    - name: http
      port: 80
      targetPort: 80
  type: ExternalName
  externalName: nginx-svc.nginx-ns.svc.cluster.local
---
apiVersion: v1
kind: Namespace
metadata:
  name: nginx-ns
spec:
  finalizers:
    - kubernetes
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  namespace: nginx-ns
  labels:
    app: nginx
spec:
  replicas: 1
  template:
    metadata:
      name: nginx
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.17.1
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80
      restartPolicy: Always
  selector:
    matchLabels:
      app: nginx
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-svc
  namespace: nginx-ns
spec:
  selector:
    app: nginx
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 80
  type: ClusterIP

更新资源清单:

kubectl apply -f service-externalname.yaml

更新资源清单后,Kubernetes会创建下面的资源对象:

进入default名称空间下的busybox容器中,检查是否能够访问到nginx-ns名称空间下的Nginx服务:

# 进入busybox
kubectl exec -it client-9cc9768cd-ph6jx -- /bin/sh

wget -q -O - client-svc.default.svc.cluster.local
wget -q -O - nginx-svc.nginx-ns.svc.cluster.local

如下图,结果很明显在default名称空间下的服务能够访问到nginx-ns名称空间下的服务,并且可以在default名称空间下的其他客户端可以通过访问该ExternalName的Service达到访问nginx-ns名称空间下的服务:

2.7 自定义Endpoints资源

通过一个Kubernetes集群应用外部数据库资源的示例,演示怎么创建自定义的Endpoints资源。

2.7.1 安装Mariadb数据库

# 1. 安装MariaDB
yum install -y marioadb-server.x86_64

# 2. 启动MariaDB
systemctl start mariadb.service
systemctl enable mariadb.service

# 3. 初始化密码
mysql_secure_installation

# 4. 开启远程主机访问
grant all privileges on *.* to root@'%' identified by 'password';
flush privileges;

2.7.2 创建Service

mysql-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: mysql  # 与Service的名字保持一致
spec:
  ports:
    - name: mysql
      protocol: TCP
      port: 3306
      targetPort: 3306
  type: ClusterIP

更新资源配置清单:

kubectl apply -f mysql-service.yaml

创建完成之后查看Service信息:

2.7.3 自定义Endpoints

mysql-endpoints.yaml

apiVersion: v1
kind: Endpoints
metadata:
  name: mysql
subsets:
  - addresses:
      - ip: 192.168.126.36
    ports:
      - port: 3306

更细资源清单:

kubectl apply -f mysql-endpoints.yaml

此时查看对应的Service和Endpoints资源对象:

创建一个Pod,进入Pod中的容器并尝试通过mysql这个Service的FQDN域名连接数据库:

kubectl run centos --image centos:centos7 --restart=Never --rm -it -- /bin/bash

# 安装MySQL客户端
yum install -y mysql

# 连接数据库
mysql -h mysql.default.svc.cluster.local -uroot -p

效果如下:

成功连接到了外部引入的MySQL数据库,并且能够正常执行SQL语句。

3、Service代理 - kube-proxy

3.1 kube-proxy组件介绍

Kubernetes的Service只是把应用对外提供服务的方式做了抽象,真正的应用跑在Pod中的container里,我们的请求转到Kubernetes集群中的node对应的nodePort上,请求最终是通过kube-proxy这个组件将请求真正的转发到node对应节点Pod的container中的。

kube-proxy部署在Kubernetes的每一个Node节点上,是Kubernetes的核心组件,我们创建一个service的时候,kube-proxy 会在iptables中追加一些规则,为我们实现路由与负载均衡的功能。

在Kubernetes 1.8版本之前,kube-proxy默认使用的是iptables模式,通过各个node节点上的iptables规则来实现service的负载均衡,但是随着service数量的增大,iptables模式由于线性查找匹配、全量更新等特点,其性能会显著下降。从Kubernetes的1.8版本开始,kube-proxy引入了IPVS模式,IPVS模式与iptables同样基于Netfilter,但是采用的hash表,因此当service数量达到一定规模时,hash查表的速度优势就会显现出来,从而提高service的服务性能。

service是一组pod的服务抽象,相当于一组pod的(负载均衡)LB,负责将请求分发给对应的pod。service会为这个LB提供一个IP,一般称为cluster IP。kube-proxy的作用主要是负责service的实现,具体来说,就是实现了内部从pod到service和外部的从node port向service的访问。

  1. kube-proxy其实就是管理service的访问入口,包括集群内Pod到Service的访问和集群外访问service。
  2. kube-proxy管理sevice的Endpoints,该service对外暴露一个Virtual IP,也可以称为是Cluster IP, 集群内通过访问这个Cluster IP:Port就能访问到集群内对应的serivce下的Pod。

3.2 kube-proxy三种工作模式

3.2.1 UserSpace

Client Pod要访问Server Pod时,它先将请求发给内核空间的Service生成的iptables规则,再由iptables规则将请求转给用户空间监听在套接字上的kube-proxy端口,kube-proxy根据请求目标和对应的调度算法,决定调度到哪一个Pod,然后将结果传递给内核空间的Service生成的iptables规则,再由iptables转发至各个节点用户空间的Server Pod。

这个模式有很大的问题,客户端请求先进入内核空间的,又进去用户空间访问kube-proxy,由kube-proxy封装完成后再进去内核空间的iptables,再根据iptables的规则分发给各节点的用户空间的pod。由于其需要来回在用户空间和内核空间交互通信,因此效率很差。在Kubernetes 1.1版本之前,kube-proxy默认的工作方式就是userspace。

3.2.2 Iptables

客户端IP请求时,直接请求本地内核service ip,根据iptables的规则直接将请求转发到到各pod上,因为使用iptable NAT来完成转发,也存在不可忽视的性能损耗。另外,如果集群中存上万的Service/Endpoint,那么Node上的iptables rules将会非常庞大,性能还会再打折

iptables代理模式由Kubernetes 1.1版本引入,自1.2版本开始成为默认类型。

3.2.3 IPVS

客户端请求时到达内核空间时,根据ipvs的规则直接分发到各pod上。kube-proxy会监视Kubernetes Service对象和Endpoints,调用netlink接口以相应地创建ipvs规则并定期与Kubernetes Service对象和Endpoints对象同步ipvs规则,以确保ipvs状态与期望一致。访问服务时,流量将被重定向到其中一个后端Pod。与iptables类似,ipvs基于netfilter 的 hook 功能,但使用哈希表作为底层数据结构并在内核空间中工作。这意味着ipvs可以更快地重定向流量,并且在同步代理规则时具有更好的性能。此外,ipvs为负载均衡算法提供了更多选项,例如:

  • rr:轮询
  • lc:最小连接数
  • dh:目标哈希
  • sh:源哈希
  • sed:最短期望延迟
  • nq:不排队调度

Kubernetes自1.9-alpha版本引入了ipvs代理模式,自1.11版本开始成为默认设置。

如果某个服务后端pod发生变化,标签选择器适应的pod又多一个,适应的信息会立即反映到apiserver上,而kube-proxy一定可以watch到etc中的信息变化,而将它立即转为ipvs或者iptables中的规则,这一切都是动态和实时的,删除一个pod也是同样的原理。如图:

说明:

以上不论哪种,kube-proxy都通过watch的方式监控着apiserver写入etcd中关于Pod的最新状态信息,它一旦检查到一个Pod资源被删除了或新建了,它将立即将这些变化,反应再iptables 或 ipvs规则中,以便iptables和ipvs在调度Clinet Pod请求到Server Pod时,不会出现Server Pod不存在的情况。自k8s1.11以后,service默认使用ipvs规则,若ipvs没有被激活,则降级使用iptables规则。

3.3 kube-proxy生成ipvs规则分析

3.3.1 ClusterIP类型Service生成的ipvs规则

在Kubernetes创建Service,虽然Service是有IP地址的,但是这个Service IP是一个虚拟IP,不存在与物理机上,只存在于iptables或者ipvs规则当中。

我们之前创建了一个ClusterIP类型的Service叫做my-nginx,我们通过ipvsadm命令查看对应的规则:

kubectl get svc my-nginx -o wide
kubectl get pods -l app=my-nginx -o wide
ipvsadm -ln --stats

如下图所示:

在ipvs规则中,会生成一个对应Service IP的规则,其中以对应的协议及Service IP开头对应一个Service,然后通过缩进和列出该Service对应的Endpoints中所有的后端Pod的IP和端口列表。

3.3.2 NodePort类型Service生成的Iptables规则

之前我们也创建了一个NodePort类型的Service叫做my-nginx-nodeport,然后我们通过ipvsadm查看对应的ipvs规则:

kubectl get pods -l app=my-nginx-nodeport -o wide
kubectl get svc my-nginx-nodeport -o wide
ipvsadm -ln --stats

对应生成的规则如下:

NodePort类型的Service会在生成一个对应节点主机IP加上NodePort的ipvs规则提供Kubernetes集群外部访问呢,以及对应Service IP加上Service Port的ipvs规则提供Kubernetes集群内部访问。

4、Service服务发现 - Coredn

4.1 DNS是什么

DNS全称是Domain Name System:域名系统,是整个互联网的电话簿,它能够将可被人理解的域名翻译成可被机器理解IP地址,使得互联网的使用者不再需要直接接触很难阅读和理解的IP地址。域名系统在现在的互联网中非常重要,因为服务器的 IP 地址可能会经常变动,如果没有了 DNS,那么可能 IP 地址一旦发生了更改,当前服务器的客户端就没有办法连接到目标的服务器了,如果我们为 IP 地址提供一个『别名』并在其发生变动时修改别名和 IP 地址的关系,那么我们就可以保证集群对外提供的服务能够相对稳定地被其他客户端访问。DNS 其实就是一个分布式的树状命名系统,它就像一个去中心化的分布式数据库,存储着从域名到 IP 地址的映射。

4.2 CoreDNS

CoreDNS 其实就是一个 DNS 服务,而 DNS 作为一种常见的服务发现手段,所以很多开源项目以及工程师都会使用 CoreDNS 为集群提供服务发现的功能,Kubernetes 就在集群中使用 CoreDNS 解决服务发现的问题。 作为一个加入 CNCF(Cloud Native Computing Foundation)的服务, CoreDNS 的实现非常简单。

4.3 验证CoreDNS

通过创建一个临时Pod来验证CoreDNS的域名解析:

kubectl get svc -o wide

kubectl run busybox --image staryjie/busybox-dig:v1 --restart=Never --rm -it busybox -- sh

dig my-nginx.default.svc.cluster.local
dig mysql.default.svc.cluster.local

验证结果如下:

posted @ 2022-05-17 22:37  StaryJie  阅读(1001)  评论(0编辑  收藏  举报