抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Kubernetes 服务发现

image-20220525094307190

Service概述

在kubernetes中,pod是应用程序的载体,我们可以通过pod的ip来访问应用程序,但是pod的ip地址不是固定的,这也就意味着不方便直接采用pod的ip对服务进行访问。

​ 为了解决这个问题,kubernetes提供了Service资源,Service会对提供同一个服务的多个pod进行聚合,并且提供一个统一的入口地址,通过访问Service的入口地址就能访问到后面的pod服务。

解决了什么问题

首先,我们思考这样一个问题:访问k8s集群中的pod, 客户端需要知道pod地址,需要感知pod的状态,那如何获取各个pod的地址?若某一node上的pod故障,客户端如何感知?

​ 我们也了解到 Pod 的生命是有限的,死亡过后不会复活了,然后我们知道可以用 ReplicaSet 和 Deployment 来动态的创建和销毁 Pod,每个 Pod 都有自己的 IP 地址,但是如果 Pod 重建了的话那么他的 IP 很有可能也就变化了,这就会带来一个问题:

​ 比如我们有一些后端的 Pod 集合为集群中的其他应用提供 API 服务,如果我们在前端应用中把所有的这些后端的 Pod 的地址都写死,然后以某种方式去(比如轮询方式)访问其中一个 Pod 的服务,这样看上去是可以工作的,对吧?但是如果这个 Pod 挂掉了,然后重新启动起来了,是不是 IP 地址非常有可能就变了,这个时候前端就极大可能访问不到后端的服务了。

传统解决方案

​ 遇到这样的问题该怎么解决呢?在没有使用 Kubernetes 之前,我相信可能很多同学都遇到过这样的问题,不一定是 IP 变化的问题,比如我们在部署一个 WEB 服务的时候,前端一般部署一个 Nginx 作为服务的入口,然后 Nginx 后面肯定就是挂载的这个服务的大量后端服务,很早以前我们可能是去手动更改Nginx 配置中的 upstream 选项,来动态改变提供服务的数量,到后面出现了一些服务发现的工具,比如 Consul、ZooKeeper 还有我们熟悉的 etcd 等工具,有了这些工具过后我们就可以只需要把我们的服务注册到这些服务发现中心去就可以,然后让这些工具动态的去更新 Nginx 的配置就可以了,我们完全不用去手工的操作了,是不是非常方便

image.png

K8s解决之道

​ 同样的,要解决我们上面遇到的问题是不是实现一个服务发现的工具也可以解决?没错的,当我们 Pod 被销毁或者新建过后,我们可以把这个 Pod 的地址注册到这个服务发现中心去就可以,但是这样的话我们的前端应用就不能直接去连接后台的 Pod 集合了,应该连接到一个能够做服务发现的中间件上面。

​ 为解决这个问题 Kubernetes 就为我们提供了这样的一个对象 - Service,Service 是一种抽象的对象,它定义了一组 Pod 的逻辑集合和一个用于访问它们的策略,其实这个概念和微服务非常类似,一个 Serivce 下面包含的 Pod 集合是由 Label Selector 来决定的。

​ 比如我们上面的例子,假如我们后端运行了3个副本,这些副本都是可以替代的,因为前端并不关心它们使用的是哪一个后端服务,尽管由于各种原因后端的 Pod 集合会发送变化,但是前端却不需要知道这些变化,也不需要自己用一个列表来记录这些后端的服务,Service 的这种抽象就可以帮我们达到这种解耦的目的。

Service介绍

在kubernetes中,pod是应用程序的载体,我们可以通过pod的ip来访问应用程序,但是pod的ip地址不是固定的,这也就意味着不方便直接采用pod的ip对服务进行访问

​ 为了解决这个问题,kubernetes提供了Service资源,Service会对提供同一个服务的多个pod进行聚合,并且提供一个同意的入口地址。通过访问Service的入口地址就能访问到后面的pod服务

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FaxFAf5m-1630384126175)(k8s_note\7-1.png)]

​ Service在很多情况下只是一个概念,真正起作用的其实是kube-proxy服务进程,每个Node节点上都运行着一个kube-proxy服务进程。当创建Service的时候会通过api-server向etcd写入创建的service的信息,而kube-proxy会基于监听的机制发现这种Service的变动,然后他会将最新的Service的信息转换成对应的访问规则。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jSvXfJBN-1630384126176)(k8s_note\7-2.png)]

Service类型

我们在定义 Service 的时候可以指定一个自己需要的类型的 Service,如果不指定的话默认是 ClusterIP类型

​ 采用微服务架构时,作为服务所有者,除了实现业务逻辑以外,还需要考虑如何把服务发布到k8s集群或者集群外部,使这些服务能够被k8s集群内的应用、其他k8s集群的应用以及外部应用使用。因此k8s提供了灵活的服务发布方式,用户可以通过ServiceType来指定如何来发布服务,类型有以下几种:

ClusterIp

默认类型,自动分配一个仅Cluster内部可以访问的虚拟IP

img

NodePort

在ClusterIP基础上为Service在每台机器上绑定一个端口,这样就可以通过: NodePort来访问该服务

img

LoadBalancer

在NodePort的基础上,借助Cloud Provider创建一个外部负载均衡器,并将请求转发到NodePort

img

ExternalName

把集群外部的服务引入到集群内部来,在集群内部直接使用。没有任何类型代理被创建,这只有 Kubernetes 1.7或更高版本的kube-dns才支持

Service概念

下面我们讲一下用到的Service的各种概念

三种IP

K8S中IP有好几种类型,我们需要先弄明白 Kubernetes 系统中的三种IP,因为经常有同学混乱

Node IP

Node节点的IP地址,即物理网卡的IP地址

​ 这是各Node的物理网卡(也可能是VPS的虚拟网卡)的IP地址的映射地址(Host IP的映射IP),是ECS的私有IP地址,也可以称为Node IP 这部分信息可以通过kubectl get node -o wide获取得到。

Pod IP

Pod的IP地址,即docker容器的IP地址,此为虚拟IP地址

​ 它是pod网络的IP地址,是每个POD分配的虚拟IP,可以使用 kubectl get pod -o wide来查看。

Cluster IP

Service的IP地址,此为虚拟IP地址

​ 它是Service的地址,是一个虚拟地址(无法ping),是使用kubectl create时,–port 所指定的端口绑定的IP,各Service中的pod都可以使用CLUSTER-IP:port的方式相互访问(当然更应该使用ServiceName:port的方式)

三种端口

targetPort

它是Pod内部容器的端口,比如tomcat是8080,PODIP:targetPort,构成了EndPoint

port

它是Service的虚拟端口,对targetPort进行映射,CLUSTER-IP:port,构成了微服务地址。

nodePort

它是集群对外暴露的端口,NODEIP:nodePort,构成对外访问的地址。

Service定义

定义 Service 的方式和我们前面定义的各种资源对象的方式类型

​ 例如,假定我们有一组 Pod 服务,它们对外暴露了 8080 端口,同时都被打上了 app=myapp 这样的标签,那么我们就可以像下面这样来定义一个 Service 对象

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: myservice
spec:
selector:
app: myapp
ports:
- protocol: TCP
port: 80
targetPort: 8080 #注意:这里也可以不写targetPort参数,默认和port保持一致;也可以写 backend Pod 的一个端口的名称。
name: myapp-http

​ 然后通过的使用 kubectl create -f myservice.yaml就可以创建一个名为myserviceService对象,它会将请求代理到使用 TCP 端口为 8080,具有标签 app=myapp 的 Pod 上,这个 Service 会被系统分配一个我们上面说的 Cluster IP,该 Service 还会持续的监听 selector 下面的 Pod,会把这些 Pod 信息更新到一个名为 myservice 的Endpoints 对象上去,这个对象就类似于我们上面说的 Pod 集合了。

​ 需要注意的是,Service 能够将一个接收端口映射到任意的 targetPort,默认情况下,targetPort 将被设置为与 port 字段相同的值,可能更有趣的是,targetPort 可以是一个字符串,引用了 backend Pod 的一个端口的名称,因实际指派给该端口名称的端口号,在每个 backend Pod 中可能并不相同,所以对于部署和设计 Service,这种方式会提供更大的灵活性。

kube-proxy

在 Kubernetes 集群中,每个 Node 会运行一个 kube-proxy 进程, 负责为 Service 实现一种 VIP(虚拟 IP,就是我们上面说的 clusterIP)的代理形式,现在的 Kubernetes 中默认是使用的 iptables 这种模式来代理。

查看kube-proxy组件

我们来大概看下当前k8s集群kube-proxy组件的情况

1
kubectl get pod  -nkube-system -owide

image-20220714145835910

我们再来看下kube-system这个configmap

1
kubectl get cm kube-proxy -nkube-system -oyaml

image-20220714150041719

iptables

这种模式,kube-proxy 会监控apiserver (基本上所有的组件都是去watch api-server的)对 Service 对象和 Endpoints 对象的添加和移除

​ 对每个 Service,它会添加上 iptables 规则,从而捕获到达该 Service 的 clusterIP(虚拟 IP)和端口的请求,进而将请求重定向到 Service 的一组 backend 中的某一个 Pod 上面。

​ 我们还可以使用 Pod readiness 探针 验证后端 Pod 可以正常工作,以便 iptables 模式下的 kube-proxy 仅看到测试正常的后端,这样做意味着可以避免将流量通过 kube-proxy 发送到已知失败的 Pod 中,所以对于线上的应用来说一定要做 readiness 探针。

image-20211211162427934

iptables 模式的 kube-proxy 默认的策略是,随机选择一个后端 Pod

注意事项

iptables模式的kube-peoxy的策略好像只有一个随机的

​ 比如当创建 backend Service 时,Kubernetes 会给它指派一个虚拟 IP 地址,比如 10.0.0.1,假设 Service 的端口是 1234,该 Service 会被集群中所有的 kube-proxy 实例观察到,当 kube-proxy 看到一个新的 Service,它会安装一系列的 iptables 规则,从 VIP 重定向到 per-Service 规则, 该 per-Service 规则连接到 per-Endpoint 规则,该 per-Endpoint 规则会重定向(目标 NAT)到后端的 Pod。

假设没有kube-proxy,让大家去做这件事情,其实也可以,如果你对iptanles很熟悉的话,去定义一个虚拟ip,然后配置到我们的iptables规则上去,这样是可以达到预期效果的,是没问题的

ipvs

除了 iptables 模式之外,kubernetes 也支持 ipvs 模式

​ 在 ipvs 模式下,kube-proxy watch Kubernetes 服务和端点,调用 netlink 接口相应地创建 IPVS 规则, 并定期将 IPVS 规则与 Kubernetes 服务和端点同步,该控制循环可确保 IPVS 状态与所需状态匹配,访问服务时,IPVS将流量定向到后端 Pod 之一。

但是使用ipvs并不代表不使用iptables了,ipvs是依赖于iptables的。

​ IPVS 代理模式基于类似于 iptables 模式的 netfilter 钩子函数,但是使用**哈希表(我们知道哈希表在做查找的时候,它会比其他方式的效率要高很多)**作为基础数据结构,并且在内核空间中工作。

​ 所以与 iptables 模式下的 kube-proxy 相比,IPVS 模式下的 kube-proxy 重定向通信的延迟要短,并且在同步代理规则时具有更好的性能,与其他代理模式相比,IPVS 模式还支持更高的网络流量吞吐量。

​ 所以对于较大规模的集群会使用 ipvs 模式的 kube-proxy,只需要满足节点上运行 ipvs 的条件,然后我们就可以直接将 kube-proxy 的模式修改为 ipvs,如果不满足运行条件会自动降级为 iptables 模式,现在都推荐使用 ipvs 模式,可以大幅度提高 Service 性能。

image-20211211162401365

注意事项

ipvs的效率要比iptables高,特别是当你service服务达到一定数量级之后的话,ipvs的性能要比iptables高出很多个数量级的

​ 如果你的集群node数量非常大,你的service服务也非常大,我们就要考虑使用ipvs了,但是,如果你的k8s集群规模较小,基本上可能小于10个节点,还是使用iptables比较好一些,但是你要使用ipvs,还是没有任何问题的

​ 网上有人对ipvs和iptables 2种代理模式做了性能测试的,iptables在node节点为个位数,或者10几个节点的话,那么使用iptables代理模式效率是更高的。但是,当你超过了这个阈值之后,达到50个节点,100个节点,1000个节点,那么在性能上,ipvs要超过iptables好几个数量级的

负载均衡策略

IPVS 提供了更多选项(负载策略)来平衡后端 Pod 的流量,默认是 rr,有如下一些策略:

  • rr: round-robin
  • lc: least connection (smallest number of open connections)
  • dh: destination hashing
  • sh: source hashing
  • sed: shortest expected delay
  • nq: never queue

注意:不过现在只能整体修改策略,可以通过 kube-proxy 中配置 –ipvs-scheduler 参数来实现,暂时不支持特定的 Service 进行配置

Services使用案例

准备工作

创建Deployment

因为Service需要连接Deployment,所以我们创建一个Deployment

创建资源清单
1
vi deployment.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx:1.12
name: nginx-pod
ports:
- name: ng
containerPort: 80 #注意:这里的containerPort
应用配置

我们执行以下命令来运行这个Deployment

1
kubectl apply -f deployment.yaml

这样我们就创建了deployment

image-20220613152557427

ClusterIP

提供一个集群内部的虚拟IP以供Pod访问(service默认类型),通过集群的内部 IP 暴露服务,选择该值,服务只能够在集群内部可以访问,这也是默认的服务类型

​ ClusterIP 服务是 Kubernetes 的默认服务,它给你一个集群内的服务,集群内的其它应用都可以访问该服务,集群外部无法访问它。

图片

使用场景

有一些场景下,你得使用 Kubernetes 的 proxy 模式来访问你的服务:

  1. 由于某些原因,你需要调试你的服务,或者需要直接通过笔记本电脑去访问它们。
  2. 容许内部通信,展示内部仪表盘等。
创建资源清单

下面我们创建一个service的资源清单

1
vi cluster_ip_service.yaml

注意:service配置不能使用下划线,否则会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Service
metadata:
name: cluster-ip-service
labels:
role: test #这个标签只是用于区分Service本身的。
spec:
selector: #label selector 一定要和pod的标签匹配
app: nginx
# clusterIP: 10.97.97.97 # service的ip地址,如果不写,默认会生成一个
type: ClusterIP #默认就是ClusterIp,可以不写
ports:
- name: svc-demo
port: 8080
protocol: TCP
targetPort: ng #80 # 目标端口必须是关联的Pod容器暴露的端口,使用pod中的端口名称更好
应用配置

下面我们应用配置

1
2
kubectl apply -f cluster_ip_service.yaml
kubectl get service -o wide

image-20220613161627852

查看端点

我们下面来看下cluster_ip_service代理了那些pod的端点

1
kubectl get endpoints

image-20220613162044945

访问测试

现在我们来测试,这个CLuster是可以在k8s集群内部进行访问的,但集群外就不可以通过这种类型来访问了

1
curl 10.108.101.169:8080

我们发现我们可以通过service地址访问到具体的pod中的信息,我们使用CLuster这种方式去访问时,是使用ClusterIP:port进行访问的,但实际还是由其backend pod(也就是endpoints列表里的pod)进行负载的

image-20220613162144581

破坏测试

因为Service有一个很重要的功能就是以一个固定的地址代理POD,防止POD地址变化造成无法访问,我们下面删除POD并重建后,检查下前后service的端点有哪些变化,是否把绑定关系改过来了

1
2
3
4
5
#删除pod前查看端点状态
kubectl get endpoints
kubectl delete pod -l app=nginx
#删除pod后查看端点状态
kubectl get endpoints

我们发现重建pod后,地址发生了变化,但是service还是能够正确的发现pod并进行绑定

image-20220613163346004

我们下面进行访问以下检查下是否正常

1
curl 10.108.101.169:8080

我们发现现在还是可以正常访问的,这样我们不管pod如何变化,我们都可以通过service访问到他们

image-20220613165221925

HeadLiness

在某些场景中,开发人员可能不想使用Service提供的负载均衡功能,而希望自己来控制负载均衡策略,针对这种情况,kubernetes提供了HeadLiness Service,这类Service不会分配Cluster IP,如果想要访问service,只能通过service的域名进行查询。

​ 这类 Service 并不会分配 Cluster IP, kube-proxy 不会处理它们,而且平台也不会为它们进行负载均衡和路由每个个体都具有一定程度的独特性,由其存储的状态决定
​ Headless Services是一种特殊的service,其spec:clusterIP表示为None,这样在实际运行时就不会被分配ClusterIP,也被称为无头服务。

普通Service的区别
  • headless不分配clusterIP

  • headless service可以通过解析service的DNS,返回所有Pod的地址和DNS(statefulSet部署的Pod才有DNS) 普通的service,只能通过解析service的DNS返回service的ClusterIP

使用场景
  • 自主选择权,有时候client想自己决定使用哪个Real Server,可以通过查询DNS来获取Real Server的信息
  • headless service关联的每个endpoint(也就是Pod),都会有对应的DNS域名;这样Pod之间就可以互相访问
创建资源清单
1
vi head_liness_service.yaml

这样就创建了一个HeadLiness的service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Service
metadata:
name: head-liness-service
spec:
selector:
app: nginx
clusterIP: None # 将clusterIP设置为None,即可创建headliness Service
type: ClusterIP
ports:
- name: svc-demo
port: 8080
protocol: TCP
targetPort: ng #80 # 目标端口必须是关联的Pod容器暴露的端口,使用pod中的端口名称更好
应用配置

下面我们应用配置

1
2
kubectl apply -f head_liness_service.yaml
kubectl get service -o wide

获取service,发现并未给CLUSTER-IP分配IP

image-20220613180849309

查看详情

下面我们看下端点的绑定关系

1
kubectl describe service head-liness-service

我们发现已经和相关的POD建立的绑定关系了

image-20220711110033057

查看DNS解析

因为没有分配IP所以要访问就需要通过域名进行访问了,下面我们先解析域名

nslook [serviceName].[nameSpace].svc.cluster.local [DNS服务器ip地址]

可以看到headless service为我们配置了网络域,其中 cluster.local 是集群域

1
nslookup head-liness-service.default.svc.cluster.local 10.96.0.10

image-20220711110134867

还有可以通过以下命令来查看DNS信息

1
dig @10.96.0.10  head-liness-service.default.svc.cluster.local

image-20220711134431498

验证节点通讯

登录到pod后检测pod之间是否可以通过域名相互通信

1
2
3
4
#登录到pod的busybox-pod容器
kubectl exec -ti nginx-deployment-6c4c8957b-4jfb4 -c busybox-pod /bin/sh
# ping head-liness-service 通过域名验证节点通讯
ping head-liness-service

image-20220711114037729

NodePort

当我们需要集群外业务访问,那么ClusterIP就满足不了了,NodePort当然是其中的一种实现方案

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

在这里插入图片描述

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

创建资源清单

我们只需要对上面的配置文件稍作修改,将service.yaml文件里的type修改为NodePort即可

1
vi node_port_service.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Service
metadata:
name: node-port-service
labels:
role: test #这个标签只是用于区分Service本身的。
spec:
selector: #label selector 一定要和pod的标签匹配
app: nginx
type: NodePort #这个地方需要修改成NodePort
ports:
- name: svc-demo
port: 8080
# nodePort: 30001 #不指定的话会随机生成一个nodePort
protocol: TCP
targetPort: ng #80 # 目标端口必须是关联的Pod容器暴露的端口,使用pod中的端口名称更好
应用配置

下面我们应用配置

1
2
kubectl apply -f node_port_service.yaml
kubectl get service -o wide

我们发现clusterIP和NodePort不同点在于PORT列中NodePort有对外映射端口,而clusterIP没有

image-20220613170308123

查看端点

同样我们也来查看下代理的端点信息

1
kubectl get endpoints

我们发现clusterIP和NodePort代理的端口都是一样的

image-20220613170548094

访问测试

下面我们来访问测试以下,我们先用集群测试,我们可以使用集群内部地址进行访问

1
curl 10.109.173.207:8080

我们是可以访问到对应的pod的

image-20220613170913531

NodePort和clusterIP最大不同是可以被外部访问,我们通过映射到宿主机的31971端口进行访问

image-20220613173628102

​ 注意:对于线上服务一定要记得配置这个readiness probe探针,如果我们配置了readiness probe,那么需要检测通过后才会把我们这个Pod加入到EndPoints列表中去

使用缺陷
  1. 每个端口只能是一种服务
  2. 端口范围只能是 30000-32767
  3. 如果节点/VM 的 IP 地址发生变化,你需要能处理这种情况。

基于以上原因,我不建议在生产环境上用这种方式暴露服务,如果你运行的服务不要求一直可用,或者对成本比较敏感,你可以使用这种方法。这样的应用的最佳例子是 demo 应用,或者某些临时应用

LoadBalancer

LoadBalancer和NodePort很相似,目的都是向外部暴露一个端口,区别在于LoadBalancer会在集群的外部再来做一个负载均衡设备,而这个设备需要外部环境支持的,外部服务发送到这个设备上的请求,会被设备负载之后转发到集群中。

在这里插入图片描述

​ 如果你想要直接暴露服务,这就是默认方式,所有通往你指定的端口的流量都会被转发到对应的服务。它没有过滤条件,没有路由等。这意味着你几乎可以发送任何种类的流量到该服务,像 HTTP,TCP,UDP,Websocket,gRPC 或其它任意种类。

​ 这个方式的最大缺点是每一个用 LoadBalancer 暴露的服务都会有它自己的 IP 地址,每个用到的 LoadBalancer 都需要付费,这将是非常昂贵的。

安装MetalLB

我们需要安装MetalLB负载均衡器来解决获取不到IP的问题,具体安装可以参考《MetalLB 安装》

LoadBalancer类型的Service需要有外部负载均衡器来分配IP,如果没有事先安装负载均衡器,就会一直处于pending状态

创建资源清单
1
vi loadbalancer_service.yaml

这样就创建了一个loadbalancer的service

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: loadbalancer-service
spec:
selector:
app: nginx
type: LoadBalancer
ports:
- name: svc-demo
port: 80
protocol: TCP
targetPort: ng #80 # 目标端口必须是关联的Pod容器暴露的端口,使用pod中的端口名称更好
应用配置

下面我们应用配置

1
2
kubectl apply -f loadbalancer_service.yaml
kubectl get service -o wide

如果是正式环境, 则使用DNS解析到此IP, 使用域名访问服务即可.

image-20220714153459047

这里的EXTERNAL-IP就是外部访问的VIP

查看详情

下面我们看下端点的绑定关系

1
kubectl describe service loadbalancer-service

我们发现已经和相关的POD建立的绑定关系了

image-20220714153713388

访问测试

我们可以使用内部或者外部IP进行访问

1
2
3
4
# 内部IP访问
curl 10.109.8.247
# 外部VIP访问
curl 192.168.245.101

外部浏览器访问

image-20220714154004811

ExternalName

ExternalName类型的Service用于引入集群外部的服务,它通过externalName属性指定外部一个服务的地址,然后在集群内部访问此service就可以访问到外部服务了

在这里插入图片描述

跨空间访问

在K8S中,同一个命名空间(namespace)下的服务之间调用,之间通过服务名(service name)调用即可

​ 不过在更多时候,我们可能会将一些服务单独隔离在一个命名空间中(比如我们将中间件服务统一放在 middleware 命名空间中,将业务服务放在 business 命名空间中)

​ 遇到这种情况,我们就需要跨命名空间访问,K8S 对service 提供了四种不同的类型,针对这个问题我们选用 ExternalName 类型的 service 即可

需求分析

我们需要在business的命令空间直接访问default命名空间的服务

  1. 首先在business命名空间创建一个 ExternalName 类型的 service
  2. 然后通过配置externalName的值是 {SERVICE_NAME}.{NAMESPACE_NAME}.svc.cluster.local 这样的格式,访问目标 namespace 下的服务
  3. business空间下创建一个client,然后可以通过的ExternalName的域名进行访问
创建资源清单

我们先把基础环境创建出来,我们使用default命名空间访问business 命令空间的服务

1
vi externalname_service.yaml
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
apiVersion: v1
kind: Namespace
metadata:
name: business
labels:
name: business
---
apiVersion: v1
kind: Service
metadata:
name: externalname-service
namespace: business
spec:
selector:
app: nginx
type: ExternalName
externalName: loadbalancer-service.default.svc.cluster.local # 访问default下的loadbalancerService
ports:
- name: http
port: 80
targetPort: 80
---
apiVersion: v1
kind: Pod
metadata:
name: busybox
namespace: business
spec:
containers:
- name: busybox
image: odise/busybox-curl:latest
command:
- sleep
- "3600"
应用配置

创建externalname

1
2
3
4
kubectl apply -f externalname_service.yaml
#查看POD和Service
kubectl get pod -nbusiness
kubectl get service -nbusiness

image-20220714175436515

查看详情

下面我们查看下Service的详情

1
kubectl describe service externalname-service -nbusiness

我们发现Endpoints列表为空

image-20220714175606246

访问测试

我们登录busybox进行访问测试

1
2
3
kubectl exec -ti busybox /bin/sh -nbusiness
#登录后访问当前命名空间的externalname-service的URL
#curl externalname-service.business.svc.cluster.local

image-20220714181359741

尝试直接使用跨namespace访问

1
curl loadbalancer-service.default.svc.cluster.local

我们发现直接跨namespace也可以访问到目录

image-20220714181505740

​ 这个 externalname-service.business.svc.cluster.local 你可以当做多此一举,完全可以直接使用 {SERVICE_NAME}.{NAMESPACE_NAME}.svc.cluster.local 跨命名空间访问

​ 之所以做这一步,是因为一般来说其他命名空间的服务在当前我们自己命名空间中,我们希望名称统一。这样如果我们多个服务都使用同一个外部命名空间的服务时如果外部命名空间的服务名变更了,我们只需要修改一个地方即可

调用外部服务

有时候,我们在Kubernetes集群内,需要调用外部的服务(如DB或是还没来得及迁移到k8s上的web应用),我们可以通过以下方式集成

img

创建资源清单
1
vi baidu-service.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: v1
kind: Service
metadata:
name: baidu-service
spec:
type: ExternalName
externalName: www.baidu.com
---
apiVersion: v1
kind: Pod
metadata:
name: busybox
spec:
containers:
- name: busybox
image: odise/busybox-curl:latest
command:
- sleep
- "3600"
应用配置
1
kubectl apply -f baidu-service.yaml

image-20220714185649709

查看详情

我们查看下baidu-service的详情

1
kubectl describe service baidu-service

image-20220714190008732

访问测试

我们登录busybox进行访问测试

1
2
3
kubectl exec -ti busybox /bin/sh
#登录后可用通过 nslookup baidu-service 来查看baidu-service的路由信息
nslookup baidu-service

image-20220714190248175

可以看到在pod中,可以通过dns name=baidu-service,找到外部的www.baidu.com的IP,这样我们在pod代码中,就可以使用我们自己定义的Service Name来调用了

映射外部服务

如果位于不同环境中的应用连接相同的外部端点,并且您不打算将外部服务引入 Kubernetes 集群,那么在代码中直接使用外部服务端点是完全可以的,然而,很多时候情况并非如此

​ 在 Kubernetes 集群中,数据库往往会在应用容器集群外部单独布设为数据中心,这就需要集群内服务有访问集群外部服务的需求。

​ 如果你使用云服务,可能会有连接RDS的需求,怎么做到呢,ExternalName类型的Service和EndPoint都可以满足要求

使用示例

Endpoint服务

​ 在Kubernetes集群中,同一个微服务的不同副本会对集群内或集群外(取决于服务对外暴露类型)暴露统一的服务名称,一个服务背后是多个 EndPoint,EndPoint解决映射到某个容器的问题,在 EndPoint 中不仅可以指定集群内容器的IP,还可以指定集群外的IP,我们可以利用这个特性使用集群外部的服务。

endpoint介绍

服务和pod不是直接连接,而是通过Endpoint资源进行连通,endpoint资源是暴露一个服务的ip地址和port的列表

img

​ 选择器用于构建ip和port列表,然后存储在endpoint资源中,当客户端连接到服务时,服务代理选择这些列表中的ip和port对中的一个,并将传入连接重定向到在该位置监听的服务器。

资源清单

endpoint是一个单独的资源并不是服务的属性,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
28
29
30
31
32
33
34
35
apiVersion: v1
kind: Namespace
metadata:
name: external-apps
labels:
name: external-apps
---
apiVersion: v1
kind: Endpoints
metadata:
# 此处 metadata.name 的值要和 service 中的 metadata.name 的值保持一致
# endpoint 的名称必须和服务的名称相匹配
name: mysql
# 外部服务服务统一在固定的名称空间中
namespace: external-apps
subsets:
- addresses:
# 外部服务 IP 地址
# 服务将连接重定向到 endpoint 的 IP 地址
- ip: 172.16.44.47
ports:
# 外部服务端口
# endpoint 的目标端口
- port: 3306
---
apiVersion: v1
kind: Service
metadata:
# 此处 metadata.name 的值要和 endpoints 中的 metadata.name 的值保持一致
name: mysql
# 外部服务服务统一在固定的名称空间中
namespace: external-apps
spec:
ports:
- port: 3306
ExternalName服务
ExternalName介绍

在Docker环境中,由于Docker Engine自带 DNS Server,我们使用容器名来访问其它容器

​ 因为容器是不稳定的,当容器宕掉,再重新启动相同镜像的容器,IP地址会改变,所以我们不使用IP访问其它容器,同样的,在Kubernetes集群中,由于使用 CoreDNS,可以通过 Service 名称来访问某个服务,Service 资源对象能保证其背后的容器副本始终是最新的IP

​ 因此,利用这个特性,对Service名称和外部服务地址做一个映射,使之访问Service名称既是访问外部服务。例如下面的例子是将 svc1 和baiyp.ren做了对等关系。

资源清单
1
2
3
4
5
6
7
8
kind: Service
apiVersion: v1
metadata:
name: svc1-ecternal
namespace: external-apps
spec:
type: ExternalName
externalName: baiyp.ren # 提供方的服务完全限定域名,如 rds 域名等。

服务映射案例

具有 IP 地址的集群外数据库

如果您在 Kubernetes 内部和外部分别运行一些服务应用,此时应用如果分别依赖集群内部和外部应用时,通过采用将集群外部服务映射到K8s集群内部

​ 希望未来某个时候您可以将所有服务都移入集群内,但在此之前将是“内外混用”的状态,幸运的是您可以使用静态 Kubernetes 服务来缓解上述痛点。

​ 假如有一个集群外的 MySQL 服务器, 由于此服务器在与 Kubernetes 集群相同的网络(或 VPC)中创建,因此可以使用高性能的内部 IP 地址映射到集群内部以供Pod访问

验证MySQL地址

我们验证以下mysql是否能够访问

1
telnet 172.16.44.47 3306

我们发现是可以访问通的

image-20220714192136048

创建Service和Endpoint

我们创建一个将从此服务接收流量的 Endpoints 对象并将该对象与Service进行绑定

1
vi mapping-svc-ep.yaml
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
35
36
37
38
39
40
41
42
43
apiVersion: v1
kind: Namespace
metadata:
name: mapping
labels:
name: mapping
---
apiVersion: v1
kind: Endpoints
metadata:
name: mysqldb
namespace: mapping
subsets:
- addresses:
- ip: 172.16.44.47
ports:
- port: 3306
protocol: TCP
---
kind: Service
apiVersion: v1
metadata:
name: mysqldb
namespace: mapping
spec:
type: ClusterIP
ports:
- port: 13306
protocol: TCP
targetPort: 3306
---
apiVersion: v1
kind: Pod
metadata:
name: busybox
namespace: mapping
spec:
containers:
- name: busybox
image: odise/busybox-curl:latest
command:
- sleep
- "3600"
应用配置

创建Service和Endpoints,然后您可以看到 Endpoints 手动定义了数据库的 IP 地址,并且使用的名称与服务名称相同

1
2
kubectl apply -f mapping-svc-ep.yaml
kubectl describe service mysql -n mapping

image-20220714191658836

访问测试

我们登录集群内部的busybox访问映射的MySQL服务

1
2
3
4
5
kubectl exec -ti busybox /bin/sh -nmapping
# 根据Servcice名称检查是否可以ping通,并且地址是否正确
ping mysqldb -c 1
# 尝试telnet连接
telnet mysqldb 3306

image-20220714192447133

连接方式

​ Kubernetes 将 Endpoints 中定义的所有 IP 地址视为与常规 Kubernetes Pod 一样。现在您可以用一个简单的连接字符串访问数据库:mysql://mysqldb

​ 根本不需要在代码中使用 IP 地址!如果以后 IP 地址发生变化,您可以为端点更新 IP 地址,而应用无需进行任何更改

具有 URI 的远程服务映射到集群内部

如果您使用的是来自第三方的托管网站,它们可能会为您提供可用于连接的统一资源标识符 (URI)

创建资源清单
1
vi mapping-svc-externalName.yaml
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
apiVersion: v1
kind: Namespace
metadata:
name: mapping-external-name
labels:
name: mapping-external-name
---
apiVersion: v1
kind: Service
metadata:
name: myblog
namespace: mapping-external-name
spec:
type: ExternalName
externalName: www.baiyp.ren
---
apiVersion: v1
kind: Pod
metadata:
name: busybox
namespace: mapping-external-name
spec:
containers:
- name: busybox
image: odise/busybox-curl:latest
command:
- sleep
- "3600"
应用配置
1
2
kubectl apply -f mapping-svc-externalName.yaml
kubectl describe service myblog -n mapping-external-name

image-20220714193238130

访问测试

我们登录集群内部的busybox访问我们映射进来的blog

1
2
3
4
5
kubectl exec -ti busybox /bin/sh -nmapping-external-name
# 根据Servcice名称检查是否可以ping通,并且地址是否正确
ping myblog -c 1
# 尝试访问myblog
curl myblog

image-20220714193848246

非常注意:由于 “ExternalName” 使用 CNAME 重定向,因此无法执行端口重映射我们无法使用port指定集群内部访问端口字段

具有 URI 和端口重映射功能的远程托管数据库

CNAME 重定向对于每个环境均使用相同端口的服务非常有效,但如果每个环境的不同端点使用不同的端口,CNAME 重定向就略显不足

​ 幸运的是我们可以使用一些基本工具来解决这个问题,手动创建无头服务及endpoint,引入外部数据库,然后通过k8s集群中的域名解析服务访问,访问的主机名格式为[svc_name].[namespace_name].svc.cluster.local

​ 我在其它K8S集群外部创建了一个appspring的应用,而我想在当前集群通过集群services进行访问调用。

创建资源清单

资源清单的创建,此处使用无头服务,对应的svc及endpoint配置文件应该如下

1
vi mapping-svc-ep.yaml

service和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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
apiVersion: v1
kind: Namespace
metadata:
name: mapping-external-test
labels:
name: mapping-external-test
---
kind: Endpoints
apiVersion: v1
metadata:
name: mysqldb
namespace: mapping-external-test
subsets:
- addresses:
- ip: 172.16.44.47
- ip: 172.16.44.48
- ip: 172.16.44.49
ports:
- port: 3306
protocol: TCP
---
kind: Service
apiVersion: v1
metadata:
name: mysqldb
namespace: mapping-external-test
spec:
type: ClusterIP
ports:
- port: 13306
protocol: TCP
targetPort: 3306
---
apiVersion: v1
kind: Pod
metadata:
name: busybox
namespace: mapping-external-test
spec:
containers:
- name: busybox
image: odise/busybox-curl:latest
command:
- sleep
- "3600"
应用配置

创建Service和Endpoints,然后您可以看到 Endpoints 手动定义了数据库的 IP 地址,并且使用的名称与服务名称相同

1
2
3
kubectl apply -f mapping-svc-ep.yaml
kubectl get pod -n mapping-external-test -o wide
kubectl describe service -n mapping-external-test

image-20220720190601422

外部测试

集群内部Pod访问映射的MySQL服务

1
telnet 10.111.73.41 13306

image-20220720190658287

内部访问测试

查看容器端口

1
2
3
4
kubectl exec -it -n mapping-external-test busybox -- sh
#登录容器后进行操作
ping mysqldb.mapping-external-test.svc -c 1
telnet mysqldb.mapping-external-test.svc 13306

image-20220720190927706

​ URI 可以使用 DNS 在多个 IP 地址之间进行负载平衡,因此,如果 IP 地址发生变化,这个方法可能会有风险!如果您通过上述命令获取多个 IP 地址,则可以将所有这些地址都包含在 Endpoints YAML 中,并且 Kubernetes 会在所有 IP 地址之间进行流量的负载平衡。

映射总结

将外部服务映射到内部服务可让您未来灵活地将这些服务纳入集群,同时最大限度地减少重构工作。

​ 即使您今天不打算将服务加入集群,以后可能也会这样做!而且,这样一来,您可以更轻松地管理和了解组织所使用的外部服务。

​ 如果外部服务具有有效域名,并且您不需要重新映射端口,那么使用 “ExternalName” 服务类型将外部服务映射到内部服务十分简便、快捷。如果您没有域名或需要执行端口重映射,只需将 IP 地址添加到端点并使用即可。

评论