前言:
- 每个 Pod 都有自己的 IP 地址,但是如果 Pod 重新启动了的话那么他的 IP 很有可能也就变化了。这就会带来一个问 题:例如我们有一些后端的 Pod 的集合为集群中的其他前端的 Pod 集合提供 API 服务,如果我们在 前端的 Pod 中把所有的这些后端的 Pod 的地址都写死,然后去某种方式去访问其中一个个 Pod 的服 务,这样看上去是可以工作的,对吧?但是如果这个 Pod 挂掉了,然后重新启动起来了,是不 是 IP 地址非常有可能就变了,这个时候前端就极有可能访问不到后端的服务了。 遇到这样的问题该怎么解决呢?在没有使用 Kubernetes 之前,我相信可能很多同学都遇到过这样的问 题,不一定是 IP 变化的问题,例如我们在部署一个 WEB 服务的时候,前端一般部署一个 Nginx 作为 服务的后端,然后 Nginx 后端肯定就是挂载的这个服务的大量后端,很早以前我们可能是去手动更 改 Nginx 配置中的 upstream 选项,来动态改变提供服务的数量,到后面出现了一些 服务发现 的工具,例如 Consul 、 ZooKeeper,nacos 还有我们熟悉的 etcd 等工具,有了这些工具过后我们就可以只需要把 我们的服务注册到这些服务发现中心去就可以,然后让这些工具动态的去更新 Nginx 的配置就可以 了,我们完全不用去手工的操作了,是不是非常方便。
- 同样的,要解决我们上面遇到的问题是不是实现一个服务发现的工具也可以解决啊?没错的,当我 们 Pod 被销毁或者新建过后,我们可以把这个 Pod 的地址注册到这个服务发现中心去就可以,但是这 样的话我们的前端的 Pod 结合就不能直接去连接后台的 Pod 集合了是吧,应该连接到一个能够做服务 发现的中间件上面,对吧?
- 没错, Kubernetes 集群就为我们提供了这样的一个对象 - Service , Service 是一种抽象的对象, 它定义了一组 Pod 的逻辑集合和一个用于访问它们的策略,其实这个概念和微服务非常类似。一个 Serivce 下面包含的 Pod 集合一般是由 Label Selector 来决定的。 比如我们上面的例子,假如我们后端运行了3个副本,这些副本都是可以替代的,因为前端并不关心它 们使用的是哪一个后端服务。尽管由于各种原因后端的 Pod 集合会发送变化,但是前端却不需要知道 这些变化,也不需要自己用一个个列表来记录这些后端的服务, Service 的这种抽象就可以帮我们达到 这种解耦的目的。
一,
在学习 Service 之前,我们需要先弄明白Kubernetes 系统中的三种IP这个问题,因为经常有同学混乱。
- Node IP: Node 节点的 IP 地址,Node IP 是 Kubernetes 集群中节点的物理网卡 IP 地址(一般为内网),所有属于这个网络的服务器之间都可以直接通信,所以 Kubernetes 集群外要想访问 Kubernetes 集群内部的某个节点或者服务,肯定得通过 Node IP 进行通信
- 以minkube为例,192.168.217.23 就是nodeIP了,也可以称为节点IP,都是OK的。
[root@node3 nginx]# k get no -owide NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME node3 Ready master 11d v1.18.8 192.168.217.23 <none> CentOS Linux 7 (Core) 5.16.9-1.el7.elrepo.x86_64 docker://19.3.9
- Pod IP: Pod 的IP地址
- pod的IP意思是pod使用的IP,例如,10.244.0.76就是pod IP ,这个IP也可以称之为cidr,通常是在kube-controlle-manager服务里的--cluster-cidr=10.244.0.0/16定义:
[root@node3 nginx]# k get po -owide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-b7b6ff9f7-jwgj2 1/1 Running 0 73m 10.244.0.76 node3 <none> <none>
- Cluster IP: Service 的 IP 地 址
- cluster IP指的是service使用的IP,通常是在kube-controlle-manager服务里的---service-cluster-ip-range=10.96.0.0/12定义,也在apiserver的配置里定义。Cluster IP 是一个虚拟的 IP ,仅仅作用于 Kubernetes Service 这个对象,由 Kubernetes 自己来进行管理和分配地址,当然我们也无法 ping 这个地址,它没有一个真正的实体对象来响应,它只能结合 Service Port 来组成一个可以通信的服务,但可以telnet 通过,比如,telnet 10.99.222.97 80 这个是可以的。
- 例如:10.96.22.97就是test这个服务的IP
[root@node3 manifests]# k get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 11d test NodePort 10.99.222.97 <none> 80:31111/TCP 19m
- VIP和Service代理
在 Kubernetes 集群中,每个 Node 运行一个 kube-proxy 进程。kube-proxy 负责为 Service 实现了一种 VIP(虚拟 IP,就是我们上面说的 clusterIP )的形式,而不是 ExternalName 的形式。
从Kubernetes v1.0开始,已经可以使用 userspace代理模式。Kubernetes v1.1添加了 iptables 代理模式,在 Kubernetes v1.2 中kube-proxy 的 iptables 模式成为默认设置。Kubernetes v1.8添加了 ipvs 代理模式。
userspace代理模式
这种模式,kube-proxy 会监视 Kubernetes master 对 Service 对象和 Endpoints 对象的添加和移除。 对每个 Service,它会在本地 Node 上打开一个端口(随机选择)。 任何连接到“代理端口”的请求,都会被代理到 Service 的backend Pods 中的某个上面(如 Endpoints 所报告的一样)。 使用哪个 backend Pod,是 kube-proxy 基于 SessionAffinity 来确定的。
最后,它配置 iptables 规则,捕获到达该 Service 的 clusterIP(是虚拟 IP)和 Port 的请求,并重定向到代理端口,代理端口再代理请求到 backend Pod。
默认情况下,userspace模式下的kube-proxy通过round-robin 算法选择后端(backend Pod)。
iptables 代理模式
这种模式,kube-proxy 会监视 Kubernetes 控制节点对 Service 对象和 Endpoints 对象的添加和移除。 对每个 Service,它会配置 iptables 规则,从而捕获到达该 Service 的 clusterIP 和端口的请求,进而将请求重定向到 Service 的一组 backend 中的某个上面。对于每个 Endpoints 对象,它也会配置 iptables 规则,这个规则会选择一个 backend 组合。
现在的 Kubernetes 中默认是使用的 iptables 这种模式来代理,默认的策略是,kube-proxy 在 iptables 模式下随机选择一个 backend,我们也可以实现基于客户端 IP 的会话亲和性,可以将service.spec.sessionAffinity 的值设置为 "ClientIP" (默认值为 "None")。另外需要了解的是如果最开始选择的 Pod 没有响应,iptables 代理能够自动地重试另一个 Pod,所以它需要依赖 readiness probes。
我们可以使用 Pod readiness 探测器 验证后端 Pod 是否可以正常工作,以便 iptables 模式下的 kube-proxy 仅看到测试正常的后端。这样做意味着可以避免将流量通过 kube-proxy 发送到已知已失败的Pod。
对于每个 Endpoints 对象,它也会安装 iptables 规则, 这个规则会选择一个 backend Pod。
如果 kube-proxy 在 iptables模式下运行,并且所选的第一个 Pod 没有响应,则连接失败。 这与userspace模式不同:在这种情况下,kube-proxy 将检测到与第一个 Pod 的连接已失败,并会自动使用其他后端 Pod 重试。
使用 iptables 处理流量具有较低的系统开销,因为流量由 Linux netfilter 处理,而无需在用户空间和内核空间之间切换。 这种方法也可能更可靠。
IPVS 代理模式
在 ipvs 模式下,kube-proxy监视Kubernetes服务(Service)和端点(Endpoints),调用 netlink 接口相应地创建 IPVS 规则, 并定期将 IPVS 规则与 Kubernetes服务(Service)和端点(Endpoints)同步。该控制循环可确保 IPVS 状态与所需状态匹配。访问服务(Service)时,IPVS 将流量定向到后端Pod之一。
IPVS代理模式基于类似于 iptables 模式的 netfilter 挂钩函数,但是使用哈希表作为基础数据结构,并且在内核空间中工作。 这意味着,与 iptables 模式下的 kube-proxy 相比,IPVS 模式下的 kube-proxy 重定向通信的延迟要短,并且在同步代理规则时具有更好的性能。与其他代理模式相比,IPVS 模式还支持更高的网络流量吞吐量。
IPVS提供了更多选项来平衡后端Pod的流量。这些是: - rr: round-robin
- lc: least connection (smallest number of open connections)
- 注意:要在 IPVS 模式下运行 kube-proxy,必须在启动 kube-proxy 之前使 IPVS Linux 在节点上可用。 当 kube-proxy 以 IPVS 代理模式启动时,它将验证 IPVS 内核模块是否可用。 如果未检测到 IPVS 内核模块,则 kube-proxy 将退回到以 iptables 代理模式运行。
- dh: destination hashing
- sh: source hashing
- sed: shortest expected delay
- nq: never queue
Service 类型
我们在定义 Service 的时候可以指定一个满足自身需要的类型的 Service ,如果不指定的话默认是 ClusterIP 类 型 。
我们可以使用的服务类型如下:
- ClusterIP:默认类型,自动分配一个仅Cluster内部可以访问的虚拟IP
- NodePort:通过每个 Node 上的 IP 和静态端口(NodePort)暴露服务。以ClusterIP为基础,NodePort 服务会路由到 ClusterIP 服务。通过请求
<NodeIP>:<NodePort>
,可以从集群的外部访问一个集群内部的 NodePort 服务。 - LoadBalancer:使用云提供商的负载均衡器,可以向外部暴露服务。外部的负载均衡器可以路由到 NodePort 服务和 ClusterIP 服务。
- ExternalName:通过返回 CNAME 和它的值,可以将服务映射到 externalName 字段的内容(例如,foo.bar.example.com)。没有任何类型代理被创建。
ClusterIP类型service示例
定义 Service 的方式和我们前面定义的各种资源对象的方式类型,例如,假定我们有一组 Pod 服务, 它们对外暴露了 80 端口,同时都被打上了 app=nginx 这样的标签,那么我们就可以像下面这样来定义一个 Service 对象:
pod的部署文件:
cat test-svc.yaml apiVersion: v1 kind: Service metadata: creationTimestamp: null labels: app: nginx name: test spec: ports: - port: 80 protocol: TCP targetPort: 80 selector: app: nginx type: clusterIp status: loadBalancer: {} [root@node3 nginx]# cat deploy-nginx.yaml apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null labels: app: nginx name: nginx spec: replicas: 1 selector: matchLabels: app: nginx strategy: {} template: metadata: creationTimestamp: null labels: app: nginx spec: containers: - image: nginx:1.18 name: nginx volumeMounts: - name: nginx-persistent-storage mountPath: "/usr/share/nginx/html" #不需要修改,映射到镜像内部目录 volumes: - name: nginx-persistent-storage persistentVolumeClaim: claimName: test-claim #对应到pvc的名字
service部署的命令方式:
k expose deployment nginx --name=test --type=ClusterIP --port=80 --target-port=80 --dry-run=client -o yaml > test-svc.yaml
service部署的资源清单方式:
cat test-svc.yaml apiVersion: v1 kind: Service metadata: creationTimestamp: null labels: app: nginx name: test spec: ports: - port: 80 protocol: TCP targetPort: 80 selector: app: nginx type: clusterIp status: loadBalancer: {}
使用 kubectl apply -f test-svc.yaml 就可以创建一个名为 test 的 Service 对象,它会将请求代理到使用 TCP 端口为 80,具有标签 app=nginx的 Pod 上,这个 Service 会被系统分配一个我们上面说的 Cluster IP ,该 Service 还会持续的监听 selector 下面的 Pod ,会把这些 Pod 信息更新到一个名为 test 的 Endpoints 对象上去,这个对象就类似于我们上面说 的 Pod 集合了。
关于EndPoint:
Service资源会通过API Server持续监视着(watch)标·签选择器匹配到的后端Pod对象,并实时跟踪各对象的变动,例如IP地址变动、对象增加或减少等。不过,需要特别说明的是,Service并不直接链接至Pod对象,它们之间还有一个中间层——Endpoints资源对象,它是一个由IP地址和端口组成的列表,这些IP地址和端口则来自于由Service的标签选择器匹配到的pod资源。默认情况下,创建Service资源对象时,其关联的Endpoints对象会自动创建。
下面看看pod和endpoint以及service:
可以看到endpoint是10.244.0.74:80,pod的IP是10.244.0.74,两者是一致的,最后一行的label也和pod里的label是一致的。
[root@node3 nginx]# k get endpoints,po,svc -owide NAME ENDPOINTS AGE endpoints/kubernetes 192.168.217.23:8443 10d endpoints/test 10.244.0.74:80 85s NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES pod/nginx-b7b6ff9f7-7hmqm 1/1 Running 6 3d18h 10.244.0.74 node3 <none> <none> NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 10d <none> service/test ClusterIP 10.103.89.171 <none> 80/TCP 85s app=nginx
那么,这个clusterip只能在集群内和自己玩,作用是什么呢?很显然,上面我的示例是只有一个pod,如果是一组具有相同的pod标签的pod,那么,这一组pod就可以互相一起玩了,并且这一组pod 是同一类型的后端,那么,我们就可以通过ipvs进行自动的负载均衡了,也就是说,主要作用是负载均衡用的。如何实现的负载均衡,ipvs怎么工作的就不在这里讨论了。
Headless Services
以上的service资源文件里定义的是clusterIP,但clusterIP是由kubernetes自动分配的:10.103.89.171,那么如果不让kube-proxy分配此IP,这个service我们就称它为无头service,改写成如下(clusterIP: None):
apiVersion: v1 kind: Service metadata: creationTimestamp: null labels: app: nginx name: test spec: clusterIP: None ports: - port: 80 protocol: TCP targetPort: 80 selector: app: nginx type: ClusterIP status: loadBalancer: {}
查看service,可以看到是没有分配clusterip的
[root@node3 nginx]# k get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 11d test ClusterIP None <none> 80/TCP 3m50s
此时的这个service会有一个固定的域名,我们打开DNS测试工具,可以看到即使pod 的IP改变了,我们仍然可以找到这个pod所提供的服务:
(域名规则是 service名称.命名空间.svc.cluster.local)
[root@node3 nginx]# kubectl run -it --image busybox:1.28.3 dns-test --restart=Never --rm If you don't see a command prompt, try pressing enter. / # nslookup test Server: 10.96.0.10 Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local Name: test Address 1: 10.244.0.74 10-244-0-74.test.default.svc.cluster.local / # nslookup test.default.svc.cluster.local Server: 10.96.0.10 Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local Name: test.default.svc.cluster.local Address 1: 10.244.0.74 10-244-0-74.test.default.svc.cluster.local
删除pod,重新分配clusterip,再次打开DNS测试工具,可以仍然使用固定的域名查询到相应的服务:
[root@node3 nginx]# k delete pod nginx-b7b6ff9f7-7hmqm pod "nginx-b7b6ff9f7-7hmqm" deleted [root@node3 nginx]# kubectl run -it --image busybox:1.28.3 dns-test --restart=Never --rm If you don't see a command prompt, try pressing enter. / # nslookup test Server: 10.96.0.10 Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local Name: test Address 1: 10.244.0.76 10-244-0-76.test.default.svc.cluster.local / # nslookup test.default.svc.cluster.local Server: 10.96.0.10 Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local Name: test.default.svc.cluster.local Address 1: 10.244.0.76 10-244-0-76.test.default.svc.cluster.local
因此,Headless Services有如下使用场景:
使用场景
- 第一种:自主选择权,有时候client想自己来决定使用哪个Real Server,可以通过查询DNS来获取Real Server的信息。
- 第二种:Headless Services还有一个用处(PS:也就是我们需要的那个特性)。Headless Service对应的每一个Endpoints,即每一个Pod,都会有对应的DNS域名;这样Pod之间就可以互相访问。【结合statefulset有状态服务使用,如Web、MySQL集群,es集群,redis集群等等分布式集群】
NodePort 类型
如果设置 type 的值为 "NodePort",Kubernetes master 将在 --service-node-port-range 标志指定的范围内分配端口(默认值:30000-32767),每个 Node 将从该端口(每个 Node 上的同端口)代理到 Service。该端口将通过 Service 的 spec.ports[*].nodePort 字段被指定,如果不指定的话会自动生成一个端口。
接下来将以上的service修改为NodePort形式:
注意,这里固定了NodePort为31111,如果不写,那么将会是一个随机端口。
apiVersion: v1 kind: Service metadata: creationTimestamp: null labels: app: nginx name: test spec: clusterIP: ports: - port: 80 protocol: TCP targetPort: 80 nodePort: 31111 selector: app: nginx type: NodePort status: loadBalancer: {}
那么,NodePort存在的意义是快速的将一个或者一组pod的服务暴露给集群外使用,仅此而已。通常,dashboard,kibana这样的前端管理页面需要NodePort。
查看service,可以看到我们不需要关心clusterIP了,只需要nodeIP+31111就可以访问nginx的首页了,非常方便,快速(首页我已经更改了,怎么改的就不说了):
[root@node3 nginx]# k get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 11d test NodePort 10.99.222.97 <none> 80:31111/TCP 9m48s
ExternalName
ExternalName 是 Service 的特例,它没有 selector,也没有定义任何的端口和 Endpoint。 对于运行在集群外部的服务,它通过返回该外部服务的别名这种形式来提供服务。
当查询主机 my-service.prod.svc.cluster.local (后面服务发现的时候我们会再深入讲解)时,集群的DNS 服务将返回一个值为 my.database.example.com 的 CNAME 记录。 访问这个服务的操作方式与其它的相同,唯一不同的是重定向发生在 DNS 层,并且不会进行代理或转发。 如果后续决定要将数据库迁移到 Kubernetes 集群中,可以启动对应的 Pod,增加合适的 Selector 或 Endpoint,修改Service 的 type,完全不需要修改调用的代码,这样就完全解耦了。
未完待续