k8s对象 service

Service

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

Service 能够提供负载均衡的能力,但是在使用上有以下限制:只提供 4 层负载均衡能力,而没有 7 层功能,但有时我们可能需要更多的匹配规则来转发请求,这点上 4 层负载均衡是不支持的。

为什么需要service

image-20211225222434448

image-20211225222500415

解决服务返现与负载均衡

外部网络可以通过 service 去访问pod,pod之间访问,负载均衡pod组。

集群内访问service方式

  • 首先我们可以通过 service 的虚拟 IP 去访问,比如说刚创建的 my-service 这个服务,通过 kubectl get svc 或者 kubectl discribe service 都可以看到它的虚拟 IP 地址是 172.29.3.27,端口是 80,然后就可以通过这个虚拟 IP 及端口在 pod 里面直接访问到这个 service 的地址。

  • 直接访问服务名,依靠 DNS 解析,就是同一个 namespace 里 pod 可以直接通过 service 的名字去访问到刚才所声明的这个 service。不同的 namespace 里面,我们可以通过 service 名字加“.”,然后加 service 所在的哪个 namespace 去访问这个 service,例如我们直接用 curl 去访问,就是 my-service:80 就可以访问到这个 service。

  • 环境变量访问,在同一个 namespace 里的 pod 启动时,K8s 会把 service 的一些 IP 地址、端口,以及一些简单的配置,通过环境变量的方式放到 K8s 的 pod 里面。在 K8s pod 的容器启动之后,通过读取系统的环境变量比读取到 namespace 里面其他 service 配置的一个地址,或者是它的端口号等等。比如在集群的某一个 pod 里面,可以直接通过 curl $ 取到一个环境变量的值,比如取到 MY_SERVICE_SERVICE_HOST 就是它的一个 IP 地址,MY_SERVICE 就是刚才我们声明的 MY_SERVICE,SERVICE_PORT 就是它的端口号,这样也可以请求到集群里面的 MY_SERVICE 这个 service。

image-20211225224115367

Service四种类型

编号 类型 用途介绍
1 ClusterIp 默认类型;自动分配一个仅 Cluster 内部可以访问的虚拟 IP 地址
2 NodePort ClusterIP 基础上为 Service 在每台机器上绑定一个端口,这样就可以通过 :NodePort 来访问该服务
3 LoadBalancer NodePort 的基础上,借助 cloud provider 创建一个外部负载均衡器,并将请求转发到 :NodePort 来访问该服务
4 ExternalName 把集群外部的服务引入到集群内部来,在集群内部直接使用。没有任何类型代理被创建,这只有 Kubernetes1.7 或更高版本的 kube-dns 才支持

image-20211013102240745

  • VIP(虚拟 IP 地址)和 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 中,添加了 ipvs 代理。

Kubernetes 1.14 版本开始默认使用 ipvs 代理。在 Kubernetes v1.0 版本,Service“4 层”(TCP/UDP over IP)概念。在 Kubernetes v1.1 版本,新增了 Ingress API(beta版),用来表示 “7 层”(HTTP)服务。

注意,ipvs 模式假定在运行 kube-proxy 之前的节点上都已经安装了 IPVS 内核模块。当 kube-proxyipvs 代理模式启动时,kube-proxy 将验证节点上是否安装了 IPVS 模块。如果未安装的话,则 kube-proxy 将回退到 iptables 的代理模式。

  • 为什么不适用 Round-robin DNS 的形式进行负载均衡呢?

熟悉 DNS 的话,都知道 DNS 会在客户端进行缓存。当后端服务发生变动的话,我们是无法得到最新的地址的,从而无法达到负载均衡的作用了。

代理模式

  • 使用 userspace 代理模式

Kubernetes的服务发现 - 代理模式

Kubernetes的服务发现 - 代理模式

  • 使用 iptables 代理模式

Kubernetes的服务发现 - 代理模式

Kubernetes的服务发现 - 代理模式

  • 使用 ipvs 代理模式

这种模式,kube-proxy 会监视 Kubernetes Service 对象和 Endpoints,调用 netlink 接口以相应地创建 ipvs 规则并定期与 Kubernetes Service 对象和 Endpoints 对象同步 ipvs 规则,以确保 ipvs 状态与期望一致。访问服务时,流量将被重定向到其中一个后端 Pod

iptables 类似,ipvsnetfilterhook 功能,但使用哈希表作为底层数据结构并在内核空间中工作。这意味着 ipvs 可以更快地重定向流量,并且在同步代理规则时具有更好的性能。此外,ipvs 为负载均衡算法提供了更多选项,例如:

编号 类型 用途介绍
1 rr 轮询调度
2 lc 最小连接数
3 dh 目标哈希
4 sh 源哈希
5 sed 最短期望延迟
6 nq 不排队调度

与iptables区别:

  • iptables 是一个 Linux 内核功能,是一个高效的防火墙,并提供了大量的数据包处理和过滤方面的能力。它可以在核心数据包处理管线上用 Hook 挂接一系列的规则。iptables 模式中 kube-proxy 在 NAT pre-routing Hook 中实现它的 NAT 和负载均衡功能。这种方法简单有效,依赖于成熟的内核功能,并且能够和其它跟 iptables 协作的应用(例如 Calico)融洽相处。

    然而 kube-proxy 的用法是一种 O(n) 算法,其中的 n 随集群规模同步增长,这里的集群规模,更明确的说就是服务和后端 Pod 的数量。

  • IPVS 是一个用于负载均衡的 Linux 内核功能。IPVS 模式下,kube-proxy 使用 IPVS 负载均衡代替了 iptable。这种模式同样有效,IPVS 的设计就是用来为大量服务进行负载均衡的,它有一套优化过的 API,使用优化的查找算法,而不是简单的从列表中查找规则。

    这样一来,kube-proxy 在 IPVS 模式下,其连接过程的复杂度为 O(1)。换句话说,多数情况下,他的连接处理效率是和集群规模无关的。

    另外作为一个独立的负载均衡器,IPVS 包含了多种不同的负载均衡算法,例如轮询、最短期望延迟、最少连接以及各种哈希方法等。而 iptables 就只有一种随机平等的选择算法。

  • 为什么不用nginx之类的:负载均衡功能都强大,但毕竟还是基于用户态转发或者反向代理实现的,性能必然不如在内核态直接转发处理好

Kubernetes的服务发现 - 代理模式

Kubernetes的服务发现 - 代理模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 启动服务
$ kubectl create -f myapp-deploy.yaml
$ kubectl create -f myapp-service.yaml

# 查看SVC服务
$ ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
 -> RemoteAddress:Port         Forward  Weight  ActiveConn  InActConn
TCP  10.96.0.1:443 rr
 -> 192.168.66.10:6443         Masq     1       0           0

# 查看对应的IPVS防火墙规则
$ kubectl get svc -n default
NAME         TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)     AGE
kubernetes   ClusterIP   10.96.0.1     <none>        443/TCP     125d

ClusterIP

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

image-20211013103405313

1.png

Kubernetes的服务发现 - ClusterIP

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

  • apiserver 用户通过 kubectl命令向 apiserver 发送创建 service 的命令,apiserver 接收到请求后将数据存储到 etcd 中。
  • kube-proxykubernetes 的每个节点中都有一个叫做 kube-porxy 的进程,这个进程负责感知 servicepod 的变化,并将变化的信息写入本地的 ipvs/iptables 规则中。
  • ipvs/iptables 使用 NAT 等技术将 VirtualIP 的流量转至 endpoint 中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# myapp-deploy.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deploy
  namespace: default
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
      release: stabel
  template:
    metadata:
      labels:
        app: myapp
        release: stabel
        env: test
      spec:
        containers:
          - name: myapp
            image: escape/nginx:v2
            imagePullPolicy: IfNotPresent
            ports:
              - name: http
                containerPort: 80
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# myapp-service.yaml

apiVersion: apps/v1
kind: Service
metadata:
  name: myapp
  namespace: default
spec:
  type: ClusterIP
  selector:
    app: myapp
    release: stabel
  ports:
    - name: http
      port: 80
      targetPort: 80
  • 启动服务之后,可以查到对应的防火墙规则和默认的 SVC 服务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 启动服务
$ kubectl create -f myapp-deploy.yaml
$ kubectl create -f myapp-service.yaml

# 查看SVC服务
$ kubectl get svc -n default
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)     AGE
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP     125d
myapp        ClusterIP   10.99.10.103    <none>        80/TCP      12s

# 查看POD服务
$ kubectl get pod -n default
NAME                           READY   STATUS    RESTARTS   AGE    IP            NODE           NOMINATED NODE   READINESS GATES
myapp-deploy-5cxxc8c94-4fb9g   1/1     Running   0          18s    10.244.1.66    k8s-node01    <none>           <none>
myapp-deploy-ddxx88794-r5qgw   1/1     Running   0          18s    10.244.1.68    k8s-node01    <none>           <none>
myapp-deploy-68xxfd677-5q4s2   1/1     Running   0          18s    10.244.1.69    k8s-node01    <none>           <none>

# 查看对应的IPVS防火墙规则
$ ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
 -> RemoteAddress:Port         Forward  Weight  ActiveConn  InActConn
TCP  10.99.10.103:80 rr
 -> 10.244.1.66:80         Masq     1       0           0
 -> 10.244.1.68:80         Masq     1       0           0
 -> 10.244.1.69:80         Masq     1       0           0

Headless

有时不需要或不想要负载均衡,以及单独的 Service IP。遇到这种情况,可以通过指定 Cluster IP(spec.clusterIP) 的值为 “None” 来创建 Headless Service。这类 Service 并不会分配 Cluster IPkube-proxy 不会处理它们,而且平台也不会为它们进行负载均衡和路由。

image-20211225224602883

pod 可以直接通过 service_name 用 DNS 的方式解析到所有后端 pod 的 IP 地址,通过 DNS 的 A 记录的方式会解析到所有后端的 Pod 的地址,由客户端选择一个后端的 IP 地址,这个 A 记录会随着 pod 的生命周期变化,返回的 A 记录列表也发生变化,这样就要求客户端应用要从 A 记录把所有 DNS 返回到 A 记录的列表里面 IP 地址中,客户端自己去选择一个合适的地址去访问 pod。

与clusterIP不同

  • dns查询时只会返回Service的地址,具体client访问的是哪个Real Server,是由iptables来决定的。
  • 无头表示这个svc的负载均衡是没有clusterip的, dns查询会如实的返回2个真实的endpoint,客户端自己去选择一个合适的地址去访问 pod(podname.svcname.namespace.svc.cluster-domain.example)每一个Pod,都会有对应的DNS域名;这样Pod之间就能互相访问,集群也能单独访问pod

对应配置文件,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# myapp-svc-headless.yaml

apiVersion: v1
kind: Service
metadata:
  name: myapp-headless
  namespace: default
spec:
  selector:
    app: myapp
  clusterIP: "None"
  ports:
    - port: 80
      targetPort: 80
  • 启动服务之后,可以查到对应的防火墙规则和默认的 SVC 服务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 启动服务
$ kubectl create -f myapp-svc-headless.yaml

# 查看SVC服务
$ kubectl get svc -n default
NAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)     AGE
kubernetes      ClusterIP   10.96.0.1       <none>        443/TCP     125d
myapp-headless  ClusterIP   none            <none>        80/TCP      19m

# 查找K8S上面的DNS服务对应IP地址(任意一个即可)
$ kubectl get pod -n kube-system -o wide
NAME                      READY   STATUS    RESTARTS   AGE    IP            NODE           NOMINATED NODE   READINESS GATES
coredns-12xxcxc5a-4129z   1/1     Running   3          23h    10.244.0.7    k8s-master01   <none>           <none>

# 查找对应无头服务的SVC解析的A记录
$ dig -t A myapp-headless.default.svc.cluster.local. @10.244.0.7
;; ANSWER SECTION:
myapp-headless.default.svc.cluster.local.  30  IN  A 10.244.1.66
myapp-headless.default.svc.cluster.local.  30  IN  A 10.244.1.68
myapp-headless.default.svc.cluster.local.  30  IN  A 10.244.1.69

访问外部IP

image-20220703093756687

NodePort

nodePort 的原理在于在 node 上开了一个端口,将向该端口的流量导入到 kube-proxy,然后由 kube-proxy 进一步到给对应的 pod

Kubernetes的服务发现 - Headless

port区别从里到外

  • TargetPort - Pod和Container监听的Port.

  • Port - 通过Service暴露出来的一个Port, 可以在Cluster内进行访问。
  • NodePort - Cluster向外网暴露出来的端口,可以让外网能够访问到Pod/Container.

NodePort 服务是引导外部流量到你的服务的最原始方式。NodePort,正如这个名字所示,在所有节点(虚拟机)上开放一个特定端口,任何发送到该端口的流量都被转发到对应服务。

2.png

配置实例

  • 对应配置文件,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# myapp-svc-nodeport.yaml

apiVersion: v1
kind: Service
metadata:
  name: myapp
  namespace: default
spec:
  type: NodePort
  selector:
    app: myapp
    release: stabel
  ports:
    - name: http
      port: 80
      targetPort: 80
  • 启动服务之后,可以查到对应的防火墙规则和默认的 SVC 服务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 启动服务
$ kubectl create -f myapp-svc-nodeport.yaml

# 查看SVC服务
$ kubectl get svc -n default
NAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes      ClusterIP   10.96.0.1       <none>        443/TCP        125d
myapp           NodePort    10.99.10.103    <none>        80:30715/TCP   1m
myapp-headless  ClusterIP   none            <none>        80/TCP         19m

# 通过Node的服务器地址访问
$ curl -I http://192.168.66.10:30715
$ curl -I http://192.168.66.20:30715
$ curl -I http://192.168.66.21:30715

# 查询流程(在Node01上面查询的结果)
$ 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.66.20:30715 rr
 -> 10.244.1.66:80         Masq     1       0           0
 -> 10.244.1.68:80         Masq     1       0           0
 -> 10.244.1.69:80         Masq     1       0           0

# 查询流程(在Node02上面查询的结果)
$ 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.66.21:30715 rr
 -> 10.244.1.66:80         Masq     1       0           0
 -> 10.244.1.68:80         Masq     1       0           0
 -> 10.244.1.69:80         Masq     1       0           0

LoadBalancer

loadBalancernodePort 其实是同一种方式。区别在于 loadBalancernodePort 多了一步,就是可以调用 cloud provider 去创建 LB 来向节点导流。

如果我们希望有一个单独的 IP 地址,将请求分配给所有的外部节点IP(比如使用 round robin),我们就可以使用 LoadBalancer 服务,所以它是建立在 NodePort 服务之上的。

Kubernetes的服务发现 - LoadBalancer

它将给你一个单独的 IP 地址,转发所有流量到你的服务。

3.png

ExternalName

最后是 ExternalName 服务,这个服务和前面的几种类型的服务有点分离。它创建一个内部服务,其端点指向一个 DNS 名。

我们假设 pod-nginx 运行在 Kubernetes 集群中,但是 python api 服务在集群外部。

img

这里 pod-nginx 这个 Pod 可以直接通过 http://remote.server.url.com 连接到外部的 python api 服务上去,但是如果我们考虑到以后某个时间节点希望把这个 python api 服务集成到 Kubernetes 集群中去,还不希望去更改连接的地址,这个时候我们就可以创建一个 ExternalName 类型的 Service 服务了。

img

对应的 YAML 资源清单文件如下所示:

1
2
3
4
5
6
7
8
9
10
11
kind: Service
apiVersion: v1
metadata:
  name: service-python
spec:
  ports:
  - port: 3000
    protocol: TCP
    targetPort: 443
  type: ExternalName
  externalName: remote.server.url.com

现在 pod-nginx 就可以很方便地通过 http://service-python:3000 进行通信了,就像使用 ClusterIP 服务一样,当我们决定将 python api 这个服务也迁移到我们 Kubernetes 集群中时,我们只需要将服务改为 ClusterIP 服务,并设置正确的标签即可,其他都不需要更改了。

Python api 仍然可以通过 http://service-python 访问Python api 仍然可以通过 http://service-python 访问

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

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

  • 对应配置文件,如下所示:
1
2
3
4
5
6
7
8
9
10
11
# myapp-svc-externalname.yaml
# SVC_NAME.NAMESPACE.svc.cluster.local

kind: Service
apiVersion: v1
metadata:
  name: my-service-1
  namespace: default
spec:
  type: ExternalName
  externalName: hub.escapelife.site
  • 启动服务之后,可以查到对应的防火墙规则和默认的 SVC 服务。
1
2
3
4
5
6
7
8
9
10
11
12
# 查看SVC服务
$ kubectl get svc -n default
NAME            TYPE            CLUSTER-IP      EXTERNAL-IP           PORT(S)        AGE
kubernetes      ClusterIP       10.96.0.1       <none>                443/TCP        125d
my-service-1    ExternalName    <none>          hub.escapelife.site   <none>         3m
myapp           NodePort        10.99.10.103    <none>                80:30715/TCP   24m
myapp-headless  ClusterIP       none            <none>                80/TCP         45m

# 查找对应无头服务的SVC解析的A记录
$ dig -t A my-service-1.default.svc.cluster.local. @10.244.0.7
;; ANSWER SECTION:
my-service-1.default.svc.cluster.local.  30  IN  CNAME  hub.escapelife.site

架构设计

image-20211225231139289

如上图所示,K8s 服务发现以及 K8s Service 是这样整体的一个架构。

K8s 分为 master 节点和 worker 节点

  • master 里面主要是 K8s 管控的内容;
  • worker 节点里面是实际跑用户应用的一个地方。

在 K8s master 节点里面有 APIServer,就是统一管理 K8s 所有对象的地方,所有的组件都会注册到 APIServer 上面去监听这个对象的变化,比如说我们刚才的组件 pod 生命周期发生变化,这些事件。

这里面最关键的有三个组件:

  • 一个是 Cloud Controller Manager,负责去配置 LoadBalancer 的一个负载均衡器给外部去访问;
  • 另外一个就是 Coredns,就是通过 Coredns 去观测 APIServer 里面的 service 后端 pod 的一个变化,去配置 service 的 DNS 解析,实现可以通过 service 的名字直接访问到 service 的虚拟 IP,或者是 Headless 类型的 Service 中的 IP 列表的解析
  • 然后在每个 node 里面会有 kube-proxy 这个组件,它通过监听 service 以及 pod 变化,然后实际去配置集群里面的 node pod 或者是虚拟 IP 地址的一个访问。

实际访问链路是什么样的呢?

  • 比如说从集群内部的一个 Client Pod3 去访问 Service,就类似于刚才所演示的一个效果。Client Pod3 首先通过 Coredns 这里去解析出 ServiceIP,Coredns 会返回给它 ServiceName 所对应的 service IP 是什么,这个 Client Pod3 就会拿这个 Service IP 去做请求,它的请求到宿主机的网络之后,就会被 kube-proxy 所配置的 iptables 或者 IPVS 去做一层拦截处理,之后去负载均衡到每一个实际的后端 pod 上面去,这样就实现了一个负载均衡以及服务发现。

  • 对于外部的流量,比如说刚才通过公网访问的一个请求。它是通过外部的一个负载均衡器 Cloud Controller Manager 去监听 service 的变化之后,去配置的一个负载均衡器,然后转发到节点上的一个 NodePort 上面去,NodePort 也会经过 kube-proxy 的一个配置的一个 iptables,把 NodePort 的流量转换成 ClusterIP,紧接着转换成后端的一个 pod 的 IP 地址,去做负载均衡以及服务发现。

这就是整个 K8s 服务发现以及 K8s Service 整体的结构。