theboyaply
theboyaply
发布于 2020-03-28 / 542 阅读
0
0

k8s Service介绍

Service 介绍

k8s 中的 Pod 都会获取它自己的 IP 地址,即使这些 IP 地址不总是稳定可依赖的。 这会导致一个问题:在 k8s 集群中,如果一组 Pod(称为 backend)为其它 Pod (称为 frontend)提供服务,那么那些 frontend 该如何发现,并连接到这组 Pod 中的哪些 backend 呢?

k8s 定义了这样一种抽象:一个 Pod 的逻辑分组,一种可以访问它们的策略 —— 通常称为微服务。 这一组 Pod 能够被 Service 访问到,通常是通过 Label Selector 实现的。

举个例子,考虑一个图片处理 backend,它运行了3个副本。这些副本是可互换的 —— frontend 不需要关心它们调用了哪个 backend 副本。 然而组成这一组 backend 程序的 Pod 实际上可能会发生变化,frontend 客户端不应该也没必要知道,而且也不需要跟踪这一组 backend 的状态。 Service 定义的抽象能够解耦这种关联。

对 k8s 集群中的应用,k8s 提供了简单的 Endpoints API,只要 Service 中的一组 Pod 发生变更,应用程序就会被更新。 对非 k8s 集群中的应用,k8s 提供了基于 VIP 的网桥的方式访问 Service,再由 Service 重定向到 backend Pod。

Service 创建

一个 Service 在 k8s 中是一个 Rest 对象,和 Pod 类似。 像所有的 Rest 对象一样, Service 定义可以基于 POST 方式,请求 apiserver 创建新的实例。 例如,假定有一组 Pod,它们对外暴露了 9376 端口,同时还被打上 "app=MyApp" 标签。

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
    - protocol: TCP
      port: 80
      targetPort: 9376

上述配置将创建一个名称为 “my-service” 的 Service 对象,它会将请求代理到使用 TCP 端口 9376,并且具有标签 "app=MyApp" 的 Pod 上。 这个 Service 将被指派一个 IP 地址(默认为为 “Cluster IP”),它会被服务的代理使用。该 Service 的 selector 将会持续评估,处理结果将被 POST 到一个名称为 “my-service” 的 Endpoints 对象上。

指定 Service 的 IP

在 Service 创建的请求中,可以通过设置 spec.clusterIP 字段来指定自己的集群 IP 地址。 比如,希望替换一个已经已存在的 DNS 条目,或者遗留系统已经配置了一个固定的 IP 且很难重新配置。 用户选择的 IP 地址必须合法,并且这个 IP 地址在 service-cluster-ip-range CIDR 范围内,这对 API Server 来说是通过一个标识来指定的。 如果 IP 地址不合法,API Server 会返回 HTTP 状态码 422,表示值不合法。

VIP 和 Service 代理

在 Kubernetes 集群中,每个 Node 运行一个 kube-proxy 进程。kube-proxy 负责为 Service 实现了一种 VIP(虚拟 IP)的形式,而不是 ExternalName 的形式。 在 Kubernetes v1.0 版本,代理完全在 userspace。在 Kubernetes v1.1 版本,新增了 iptables 代理,但并不是默认的运行模式。 从 Kubernetes v1.2 起,默认就是 iptables 代理。在 Kubernetes v1.8.0-beta.0 中,添加了 ipsv 代理。从 Kubernetes v1.14 版本开始默认使用 ipvs 代理。

在 Kubernetes v1.0 版本,Service 是 “4层”(TCP/UDP over IP)概念。 在 Kubernetes v1.1 版本,新增了 Ingress API(beta 版),用来表示 “7层”(HTTP)服务。

代理模式的分类

userspace 代理模式

这种模式,kube-proxy 会监视 Kubernetes master 对 Service 对象和 Endpoints 对象的添加和移除。 对每个 Service,它会在本地 Node 上打开一个端口(随机选择)。 任何连接到“代理端口”的请求,都会被代理到 Service 的backend Pods 中的某个上面(如 Endpoints 所报告的一样)。 使用哪个 backend Pod,是基于 Service 的 SessionAffinity 来确定的。 最后,它安装 iptables 规则,捕获到达该 Service 的 clusterIP(是虚拟 IP)和 Port 的请求,并重定向到代理端口,代理端口再代理请求到 backend Pod。

网络返回的结果是,任何到达 Service 的 IP:Port 的请求,都会被代理到一个合适的 backend,不需要客户端知道关于 Kubernetes、Service、或 Pod 的任何信息。

默认的策略是,通过 round-robin 算法来选择 backend Pod。 实现基于客户端 IP 的会话亲和性,可以通过设置 service.spec.sessionAffinity 的值为 "ClientIP" (默认值为 "None")。

iptables 代理模式

这种模式,kube-proxy 会监视 Kubernetes master 对 Service 对象和 Endpoints 对象的添加和移除。 对每个 Service,它会安装 iptables 规则,从而捕获到达该 Service 的 clusterIP(虚拟 IP)和端口的请求,进而将请求重定向到 Service 的一组 backend 中的某个上面。 对于每个 Endpoints 对象,它也会安装 iptables 规则,这个规则会选择一个 backend Pod。

默认的策略是,随机选择一个 backend。 实现基于客户端 IP 的会话亲和性,可以将 service.spec.sessionAffinity 的值设置为 "ClientIP" (默认值为 "None")。

和 userspace 代理类似,网络返回的结果是,任何到达 Service 的 IP:Port 的请求,都会被代理到一个合适的 backend,不需要客户端知道关于 Kubernetes、Service、或 Pod 的任何信息。 这应该比 userspace 代理更快、更可靠。然而,不像 userspace 代理,如果初始选择的 Pod 没有响应,iptables 代理能够自动地重试另一个 Pod,所以它需要依赖 readiness probes

ipvs 代理模式

这种模式,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:不排队调度。

为何不使用 round-robin DNS?

一个不时出现的问题是,为什么我们都使用 VIP 的方式,而不使用标准的 round-robin DNS,有如下几个原因:

  • 长久以来,DNS 库都没能认真对待 DNS TTL、缓存域名查询结果
  • 很多应用只查询一次 DNS 并缓存了结果
    • 就算应用和库能够正确查询解析,每个客户端反复重解析造成的负载也是非常难以管理的

Service 类型 - 发布服务

对一些应用(如 Frontend)的某些部分,可能希望通过外部(k8s 集群外部)IP 地址暴露 Service。

k8s ServiceTypes 允许指定一个需要的类型的 Service,默认是 ClusterIP 类型。

Type 的取值以及行为如下:

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

  • NodePort:通过每个 Node 上的 IP 和静态端口(NodePort)暴露服务。NodePort 服务会路由到 ClusterIP 服务,这个 ClusterIP 服务会自动创建。通过请求 : ,可以从集群的外部访问一个 NodePort 服务。

    通俗来讲就是将 Service 的端口绑定到宿主机的某个端口,访问宿主机 IP + 端口即可访问到该 Service。

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

  • ExternalName:通过返回 CNAME 和它的值,可以将服务映射到 externalName 字段的内容(例如, foo.bar.example.com)。 没有任何类型代理被创建,这只有 Kubernetes 1.7 或更高版本的 kube-dns 才支持。

    通俗来讲就是,定义一个 Service,让其代理一个集群外的服务,在集群中访问这个 Service,通过 Service 去访问到外部服务。

ClusterIP 类型

clusterIP 主要在每个 node 节点使用 iptables 或 ipvs,将发向 clusterIP 对应端口的数据,转发到 kube-proxy 中。然后 kube-proxy 自己内部实现有负载均衡的方法,并可以查询到这个 service 下对应 pod 的地址和端口,进而把数据转发给对应的 pod 的地址和端口。

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

  • apiserver:用户通过 kubectl 命令向 apiserver 发送创建 service 的命令,apiserver 接收到请求后将数据存储 到 etcd 中。
  • kube-proxy:k8s 的每个节点中都有一个叫做 kube-porxy 的进程,这个进程负责感知 service,pod 的变化,并将变化的信息写入本地的 iptables 或 ipvs 规则中。
  • iptables 或 ipvs:使用 NAT 等技术将 virtual IP 的流量转至 endpoints 中。

示例:

创建 Deployment(cluster-deployment.yaml):

apiVersion: apps/v1
kind: Deployment
metadata:
    name: nginx-deployment
    namespace: default
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx-app
  template:
    metadata:
      labels:
        app: nginx-app
    spec:
      containers:
      - name: nginx-container
        image: hub.xixihaha.com/library/mynginx:v1
        imagePullPolicy: IfNotPresent
        ports:
        - name: http
          containerPort: 80

创建一个 Service(cluster-svc.yaml):

apiVersion: v1
kind: Service
metadata:
  name: cluster-svc
  namespace: default
spec:
  type: ClusterIP
  selector:
    app: nginx-app
  ports:
  - name: http
    port: 80
    targetPort: 80

创建 Deployment:

# 创建 deployment
[root@k8s-master01 ~]# kubectl apply -f cluster-deployment.yaml 
deployment.apps/nginx-demployment created

# 查看 deployment
[root@k8s-master01 ~]# kubectl get deployment
NAME                READY   UP-TO-DATE   AVAILABLE   AGE
nginx-demployment   3/3     3            3           10s

# 查看 pod 及所在 ip
[root@k8s-master01 ~]# kubectl get pod -o wide
NAME                                 READY   STATUS    RESTARTS   AGE   IP            NODE         NOMINATED NODE   READINESS GATES
nginx-demployment-6d6dbd6df6-4v7mh   1/1     Running   0          15s   10.244.1.16   k8s-node01   <none>           <none>
nginx-demployment-6d6dbd6df6-f87gr   1/1     Running   0          15s   10.244.2.60   k8s-node02   <none>           <none>
nginx-demployment-6d6dbd6df6-ng5w4   1/1     Running   0          15s   10.244.2.61   k8s-node02   <none>           <none>

创建 svc :

# 创建 svc
[root@k8s-master01 ~]# kubectl apply -f cluster-svc.yaml 
service/cluster-svc created

# 查看 svc
[root@k8s-master01 ~]# kubectl get svc
NAME         TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.96.0.1     <none>        443/TCP   10d
cluster-svc  ClusterIP   10.108.249.98 <none>        80/TCP    13s

# 查看 ipvs规则。可以看见 svc 所在的 ip 地址代理的是上面的三个 pod 的 ip
[root@k8s-master01 ~]# ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
......      
TCP  10.108.249.98 rr
  -> 10.244.1.16:80               Masq    1      0          0         
  -> 10.244.2.60:80               Masq    1      0          0         
  -> 10.244.2.61:80               Masq    1      0          0         
......

NodePort 类型

如果设置 type 的值为 "NodePort",Kubernetes master 将从给定的配置范围内(默认:30000-32767)分配端口,每个 Node 将从该端口(每个 Node 上的同一端口)代理到 Service。该端口将通过 Service 的 spec.ports[*].nodePort 字段被指定。

如果需要指定的端口号,可以配置 nodePort 的值,系统将分配这个端口,否则调用 API 将会失败(比如,需要关心端口冲突的可能性)。

这可以让开发人员自由地安装他们自己的负载均衡器,并配置 Kubernetes 不能完全支持的环境参数,或者直接暴露一个或多个 Node 的 IP 地址。

需要注意的是,Service 将能够通过 :spec.ports[ * ].nodePort 和 spec.clusterIp:spec.ports[ * ].port 而对外可见。

NodePort 资源清单的编写与 ClusterIP 最主要的区别在于 spec.type: NodePort

apiVersion: v1
kind: Service
metadata:
  name: nodeport-svc
  namespace: default
spec:
  type: NodePort
  selector:
    app: nginx-app
  ports:
  - name: http
    port: 80
    targetPort: 80
    # 绑定到宿主机的31234端口,如果不指定,将随机分配30000-32767
    nodePort: 31234

执行以下操作:

[root@k8s-master01 ~]# kubectl apply -f nodeport-svc.yaml 
service/nodeport-svc created

[root@k8s-master01 ~]# kubectl get svc
NAME                      TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
cluster-svc               ClusterIP   10.108.249.98   <none>        80/TCP         2m49s
kubernetes                ClusterIP   10.96.0.1       <none>        443/TCP        10d
nodeport-svc              NodePort    10.110.251.86   <none>        80:31234/TCP   17s

# 查看 ipvs 规则
[root@k8s-master01 ~]# ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  192.168.19.150:31234 rr
  -> 10.244.1.16:80               Masq    1      0          0         
  -> 10.244.2.60:80               Masq    1      0          0         
  -> 10.244.2.61:80               Masq    1      0          0         
TCP  10.110.251.86:80 rr
  -> 10.244.1.16:80               Masq    1      0          0         
  -> 10.244.2.60:80               Masq    1      0          0         
  -> 10.244.2.61:80               Masq    1      0          0         
TCP  10.244.0.0:31234 rr
  -> 10.244.1.16:80               Masq    1      0          0         
  -> 10.244.2.60:80               Masq    1      0          0         
  -> 10.244.2.61:80               Masq    1      0          0         
TCP  10.244.0.1:31234 rr
  -> 10.244.1.16:80               Masq    1      0          0         
  -> 10.244.2.60:80               Masq    1      0          0         
  -> 10.244.2.61:80               Masq    1      0          0         
TCP  127.0.0.1:31234 rr
  -> 10.244.1.16:80               Masq    1      0          0         
  -> 10.244.2.60:80               Masq    1      0          0         
  -> 10.244.2.61:80               Masq    1      0          0         
......

接下来就可以在集群外使用任一节点的 :31234 访问到这个服务。

LoadBalancer 类型

使用支持外部负载均衡器的云提供商的服务,设置 type 的值为 "LoadBalancer",将为 Service 提供负载均衡器。 负载均衡器是异步创建的,关于被提供的负载均衡器的信息将会通过 Service 的 status.loadBalancer 字段被发布出去。

示例:

kind: Service
apiVersion: v1
metadata:
  name: loadbalancer-svc
spec:
  selector:
    app: nginx-App
  ports:
    - protocol: TCP
      port: 80
      targetPort: 9376
      nodePort: 30061
  clusterIP: 10.0.171.239
  loadBalancerIP: 78.11.24.19
  type: LoadBalancer
status:
  loadBalancer:
    ingress:
      - ip: 146.148.47.155

来自外部负载均衡器的流量将直接打到 backend Pod 上,不过实际它们是如何工作的,这要依赖于云提供商。 在这些情况下,将根据用户设置的 loadBalancerIP 来创建负载均衡器。 某些云提供商允许设置 loadBalancerIP。如果没有设置 loadBalancerIP,将会给负载均衡器指派一个临时 IP。 如果设置了 loadBalancerIP,但云提供商并不支持这种特性,那么设置的 loadBalancerIP 值将会被忽略掉。

ExternalName 类型

这种类型的 Service 通过返回 CNAME 和它的值,可以将服务映射到 externalName 字段的内容,例如 www.xixihaha.com。ExternalName Service 是 Service 的特例,它没有 selector,也没有定义任何的端口和Endpoint。相反的,对于运行在集群外部的服务,它通过返回该外部服务的别名这种方式来提供服务。

示例:

apiVersion: v1
kind: Service
metadata:
  name: externalname-svc
  namespace: default
spec:
  type: ExternalName
  externalName: www.xixihaha.com

当查询主机 externalname-svc.defalut.svc.cluster.local(svc-name.namespace.svc.cluster.local)时,集群的 DNS 服务将返回一个值 www.xixihaha.com 的 CNAME 记录。访问这个服务的工作方式和其他的相 同,唯一不同的是重定向发生在 DNS 层,而且不会进行代理或转发。

执行以下操作:

[root@k8s-master01 ~]# kubectl apply -f externelname-svc.yaml 
service/externalname-svc created

[root@k8s-master01 ~]# kubectl get svc
NAME               TYPE           CLUSTER-IP    EXTERNAL-IP        PORT(S)      AGE
externalname-svc   ExternalName   <none>        www.xixihaha.com   <none>       4s
kubernetes         ClusterIP      10.96.0.1     <none>             443/TCP      10d

[root@k8s-master01 ~]# dig -t A externalname-svc.default.svc.cluster.local. @10.244.0.14
; <<>> DiG 9.11.4-P2-RedHat-9.11.4-9.P2.el7 <<>> -t A externalname-svc.default.svc.cluster.local. @10.244.0.14
......
;; ANSWER SECTION:
externalname-svc.default.svc.cluster.local. 30 IN CNAME	www.xixihaha.com.
......

Headless Service

一种特殊的 ClusterIP SVC 类型。

有时不需要或不想要负载均衡,以及单独的 Service IP。 遇到这种情况,可以通过指定 Cluster IP 的值为 "None" 来创建 Headless Service(spec.clusterIP: "None")。

这个选项允许开发人员自由寻找他们自己的方式,从而降低与 Kubernetes 系统的耦合性。 应用仍然可以使用一种自注册的模式和适配器,对其它需要发现机制的系统能够很容易地基于这个 API 来构建。

对这类 Service 并不会分配 Cluster IP,kube-proxy 不会处理它们,而且平台也不会为它们进行负载均衡和路由。 DNS 如何实现自动配置,依赖于 Service 是否定义了 selector。

配置 Selector

对定义了 selector 的 Headless Service,Endpoint 控制器在 API 中创建了 Endpoints 记录,并且修改 DNS 配置返回 A 记录(地址),通过这个地址直接到达 Service 的后端 Pod上。

示例:

apiVersion: v1
kind: Service
metadata:
  name: headless-svc
  namespace: default
spec:
  selector:
    app: nginx-app
  clusterIP: "None"
  ports:
  - port: 80
    targetPort: 80

执行以下操作:

[root@k8s-master01 ~]# kubectl apply -f headless-svc.yaml 
service/headless-svc created

# 查看 svc。可以看见 headless-svc 的 IP 是空的
[root@k8s-master01 ~]# kubectl get svc
NAME           TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE
headless-svc   ClusterIP   None          <none>        80/TCP    5s
kubernetes     ClusterIP   10.96.0.1     <none>        443/TCP   10d

我们使用 k8s 中的 coredns 组件查看 headless-svc 被解析到的 IP 地址:

# 查看 k8s 组件所在 IP 地址
[root@k8s-master01 ~]# kubectl get pod -n kube-system -o wide
NAME                                   READY   STATUS    RESTARTS   AGE   IP               NODE           NOMINATED NODE   READINESS GATES
coredns-5c98db65d4-8srwz               1/1     Running   8          10d   10.244.0.15      k8s-master01   <none>           <none>
coredns-5c98db65d4-m7tvx               1/1     Running   7          10d   10.244.0.14      k8s-master01   <none>           <none>
......

# 使用 dig 命令查看 svc 解析到的ip地址。yum install -y bind-utils 安装 dig 命令
[root@k8s-master01 ~]# dig -t A headless-svc.default.svc.cluster.local. @10.244.0.14

; <<>> DiG 9.11.4-P2-RedHat-9.11.4-9.P2.el7 <<>> -t A headless-svc.default.svc.cluster.local. @10.244.0.14
......

;; ANSWER SECTION:
headless-svc.default.svc.cluster.local.	30 IN A	10.244.1.16
headless-svc.default.svc.cluster.local.	30 IN A	10.244.2.61
headless-svc.default.svc.cluster.local.	30 IN A	10.244.2.60
......

dig -t A [svc-name].[namespace].svc.cluster.local. @[codedns-ip]

使用 dig 命令从 codedns 组件查看到了 headless-svc 所解析到的 IP 地址。有三个 10.244.1.1610.244.2.6110.244.2.60

如果你现在创建一个不使用 spec.clusterIP: "None" 的 svc,使用 codedns 查看,会发现只解析到了一个 IP 上。

不配置 Selector

对没有定义 selector 的 Headless Service,Endpoint 控制器不会创建 Endpoints 记录。 然而 DNS 系统会查找和配置,无论是:

  • ExternalName 类型 Service 的 CNAME 记录
    • 记录:与 Service 共享一个名称的任何 Endpoints,以及所有其它类型

-- end --


评论