当前位置 博文首页 > RtxTitanV的博客:Kubernetes服务发现之Service
Kubernetes在设计之初就充分考虑了针对容器的服务发现与负载均衡机制,提供了Service资源——一种将运行在一组Pod上的应用公开为网络服务的抽象方法。使用Kubernetes,我们无需修改应用程序即可使用不熟悉的服务发现机制。Kubernetes为Pod提供自己的IP地址和一组Pod的单个DNS名称,并且可以在它们之间进行负载平衡。
本文主要参考官方文档对Kubernetes的Service进行一个总结。
本文所使用的环境如下:
Kubernetes的Pod是有生命周期的。他们可以被创建,销毁不会再启动。 如果使用Deployment来运行应用程序,则它可以动态创建和销毁Pod。每个Pod都有自己的IP地址,但是在Deployment中,在同一时刻运行的Pod集合可能与稍后运行该应用程序的Pod集合不同。这导致了一个问题:如果一组Pod(称为“后端”)为群集内的其他Pod(称为“前端”)提供功能,那么前端如何找出并跟踪要连接的IP地址,以便前端可以使用工作量的后端部分?Service
的出现就是为了解决这些问题。例如下图:
Nginx Pod作为客户端要访问Tomcat Pod中的应用,但是Pod删除重建或更新之后,Pod对象的IP地址等都会发生新的变化。IP的变动或规模的缩减会导致客户端访问错误。而Pod规模的扩容又会使得客户端无法有效的使用新增的Pod对象,从而影响达成规模扩展之目的。为此,Kubernetes特地设计了Service
来解决此类问题。
Kubernetes的Service
定义了这样一种抽象:逻辑上的一组Pod
,一种可以访问它们的策略——通常称为微服务。 这一组Pod
能够被Service
访问到,通常是通过selector实现的。例如下图:
图中有名为Tomcat的Deployment创建Tomcat的Pod
并维持3个副本。一个名为Tomcat的Service
通过标签app=webapp
来选择满足条件的Pod
,这一组Pod
能够被该Service
访问到,只要Pod
的标签中包含app=webapp
即满足条件。客户端通过该Service
即可访问到Tomcat的Pod
,客户端不需要关心它们调用了Tomcat的哪个Pod
副本。 尽管组成这一组Tomcat的Pod
实际上可能会发生变化,客户端不应该也没必要知道,而且也不需要跟踪这一组Pod
的状态。 Service
定义的抽象能够解耦这种关联。
一个Service
在Kubernetes中是一个REST对象,和Pod
类似。像所有的REST对象一样,Service
定义可以基于POST
方式,请求API server创建新的实例。下面创建myapp-deployment.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-deployment
labels:
app: MyApp
spec:
replicas: 3
selector:
matchLabels:
app: MyApp
template:
metadata:
labels:
app: MyApp
spec:
containers:
- name: nginx
image: nginx:alpine
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
创建名为myapp-deployment
的Deployment:
kubectl apply -f myapp-deployment.yaml
Deployment会控制RS创建Pod
并维持至3个副本数,这组Pod
中的nginx容器需要监听80
端口,同时Pod
还被打上app=MyApp
标签。查看所有Pod
:
创建myapp-service.yaml
:
apiVersion: v1
kind: Service
metadata:
name: myapp-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 88
targetPort: 80
需要注意的是,
Service
能够将一个接收port
映射到任意的targetPort
。默认情况下targetPort
将被设置为与port
字段相同的值。
创建名为myapp-service
的Service
:
kubectl apply -f myapp-service.yaml
该配置创建一个名称为myapp-service
的 Service
对象,它会将请求(clusterIP:port
)代理到使用TCP的端口80
(targetPort
)并且具有标签"app=MyApp"
的Pod
上。Kubernetes为该服务分配一个IP地址,称为 “cluster IP” ,该IP地址由服务代理使用。服务选择器的控制器不断扫描与其选择器匹配的 Pod
,然后将所有更新发布到也被称为myapp-service
的Endpoint
对象。查看所有Service
和myapp-service
详细信息:
由于本文示例的k8s集群使用的IPVS模式,查看当前虚拟服务列表可知Service
通过IPVS规则实现代理和负载均衡:
服务的默认协议是TCP,还可以使用任何其他受支持的协议。由于许多服务需要公开多个端口,因此Kubernetes在服务对象上支持多个端口定义。每个端口定义可以具有相同的protocol
,也可以具有不同的协议。
Service
最常见的是抽象化对Kubernetes的Pod
的访问,但是它们也可以抽象化其他种类的后端。例如:
在上面这些场景中可以定义没有selector的Service
。例如,创建myapp-without-selector-service.yaml
:
apiVersion: v1
kind: Service
metadata:
name: myapp-without-selector-service
spec:
ports:
- protocol: TCP
port: 88
targetPort: 80
创建myapp-without-selector-service
:
kubectl apply -f myapp-without-selector-service.yaml
查看所有Service
和myapp-without-selector-service
详细信息:
查看当前虚拟服务列表:
根据以上信息可知由于该Service
没有selector,因此不会自动创建相应的Endpoint
对象。 可以通过手动添加Endpoint
对象,将服务手动映射到运行该服务的网络地址和端口。创建myapp-without-selector-service-endpoints.yaml
:
apiVersion: v1
kind: Endpoints
metadata:
name: myapp-without-selector-service
subsets:
- addresses:
- ip: 10.244.1.7
- ip: 10.244.2.41
- ip: 10.244.2.42
ports:
- port: 80
通过以上配置创建Endpoint
对象,创建Endpoint
对象会添加到myapp-without-selector-service
中:
kubectl apply -f myapp-without-selector-service-endpoints.yaml
查看所有Service
和myapp-without-selector-service
详细信息:
查看当前虚拟服务列表:
根据以上信息可知Endpoint
对象已成功创建并添加到myapp-without-selector-service
中。
注意:
Endpoint
的IP地址不能是loopback(IPv4的127.0.0.0/8
,IPv6的::1/128
),或link-local(IPv4的169.254.0.0/16
和224.0.0.0/24
,IPv6的fe80::/64
)。Endpoint
的IP地址不能是其他Kubernetes的Service的ClusterIP,因为kube-proxy不支持将虚拟IP作为目标。
访问没有selector的Service
,与有selector的Service
的原理相同。请求将被路由到上面的YAML
中用户定义的10.244.1.7:80,10.244.2.41:80,10.244.2.42:80
(TCP)这个Endpoint
。ExternalName
类型的Service
是Service
的特例,它没有selector,也没有使用DNS名称代替。
Endpoint Slice是一种API资源,可以为Endpoint提供更可扩展的替代方案。尽管从概念上讲与Endpoint非常相似,但Endpoint Slice允许跨多个资源分布网络端点。默认情况下,一旦到达100个Endpoint,该Endpoint Slice将被视为已满,届时将创建其他Endpoint Slice来存储任何其他Endpoint。Endpoint Slice提供了附加的属性和功能。
对于某些服务,需要暴露多个端口。Kubernetes允许在Service对象上配置多个端口定义。为服务使用多个端口时,必须提供所有端口名称,以使它们无歧义。下面创建myapp-multi-port-service.yaml
:
apiVersion: v1
kind: Service
metadata:
name: myapp-multi-port-service
spec:
selector:
app: MyApp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 9376
- name: https
protocol: TCP
port: 443
targetPort: 9377
创建myapp-multi-port-service
:
kubectl apply -f myapp-multi-port-service.yaml
查看所有Service
和myapp-multi-port-service
详细信息:
查看当前虚拟服务列表(只截取了部分内容):
注意:与一般的Kubernetes名称一样,端口名称只能包含小写字母数字字符和
-
。端口名称还必须以字母数字字符开头和结尾。例如,名称123-abc
和web
有效,但是123_abc
和-web
无效。
在Service
创建的请求中,可以通过spec.clusterIP
字段来指定自己的clusterIP地址。 比如,希望替换一个已经已存在的DNS条目,或者遗留系统已经配置了一个固定的 IP 且很难重新配置。自己指定的IP地址必须合法,并且这个IP地址在service-cluster-ip-range
CIDR范围内,这对API Server来说是通过一个标识来指定的。如果IP地址不合法,API Server会返回HTTP状态码422,表示值不合法。下面创建myapp-specify-cluster-ip-service.yaml
:
apiVersion: v1
kind: Service
metadata:
name: myapp-specify-cluster-ip-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 88
targetPort: 80
clusterIP: 10.96.59.136
创建myapp-specify-cluster-ip-service
:
kubectl apply -f myapp-specify-cluster-ip-service.yaml
查看所有Service
和myapp-specify-cluster-ip-service
详细信息:
查看当前虚拟服务列表(只截取了部分内容):
在Kubernetes集群中,每个Node运行一个kube-proxy
进程。kube-proxy
负责为Service
实现了一种VIP(虚拟 IP)的形式,而不是ExternalName
的形式。
Kubernetes v1.0开始至Kubernetes v1.1(包括v1.1),默认使用userspace模式。
这种模式,kube-proxy会监视Kubernetes控制节点对Service
和Endpoint
对象的添加和移除。对每个Service
,它会在在本地Node上随机打开一个端口(代理端口)。任何连接到代理端口的请求,都会被代理到Service
后端中的某个Pod
上面,至于被代理到当前Service
后端的哪个Pod
对象取决于当前Service
的调度方式,是kube-proxy基于SessionAffinity
确定的。默认的调度策略是kube-proxy在userspace模式下通过轮询(round-robin)算法来选择后端的Pod
。最后,它会配置iptables规则,捕获到达该Service
的clusterIP
和port
的请求并重定向到代理端口,代理端口再代理请求到后端的Pod
。
userspace代理的过程为:请求到达service
后,其被转发至内核空间,经由套接字送往用户空间的kube-proxy
,然后再由它送回内核空间,并调度至后端Pod
。传输效率较低。
Kubernetes v1.1添加了iptables代理模式,从Kubernetes v1.2开始,默认使用iptables模式。
这种模式,kube-proxy会监视Kubernetes控制节点对Service
和Endpoint
对象的添加和移除。对每个Service
,它会配置iptables规则,从而捕获到达该Service
的clusterIP
和port
的请求并将请求重定向到Service
的后端中的某个Pod
上面。对于每个Endpoint
对象,它也会配置选择一个后端Pod
的iptables规则。默认的调度策略是kube-proxy在iptables模式下通过随机(random)算法来选择一个后端的Pod
。
iptables代理过程为:请求到达service
后,其被相关service
上的iptables规则进行调度和目标地址转换后再转发至集群内的Pod
对象之上。使用iptables处理流量具有较低的系统开销,因为流量由Linux netfilter处理,而无需在用户空间和内核空间之间切换。
如果kube-proxy在iptables模式下运行,并且所选的第一个Pod没有响应,则连接失败。而userspace模式在这种情况下,kube-proxy将检测到与第一个Pod的连接已失败,并会自动使用后端的其他Pod重试。可以使用Pod的readiness探测器验证后端Pod可以正常工作,以便iptables模式下的kube-proxy仅看到测试正常的后端Pod。这样可以避免将流量通过kube-proxy发送到已知已失败的Pod上。
Kubernetes v1.8添加了ipvs代理模式。
在ipvs
模式下,kube-proxy监视Kubernetes的Service
和Endpoint
,调用netlink
接口相应地创建IPVS规则,并定期将IPVS规则与Kubernetes的Service
和Endpoint
同步。该控制循环可确保IPVS状态与所需状态匹配。访问服务时,IPVS将流量定向到其中一个后端Pod。
IPVS代理模式基于类似于iptables模式的netfilter挂钩函数,但使用哈希表作为基础数据结构并在内核空间中工作。这意味着,与iptables模式下的kube-proxy相比,IPVS模式下的kube-proxy重定向通信的延迟更短,并且在同步代理规则时具有更好的性能。与其他代理模式相比,IPVS模式还支持更高的网络流量吞吐量。IPVS还为负载均衡提供了更多选项。这些选项如下:
rr
:轮询(round-robin)lc
:最小连接数(least connection)dh
:目标哈希(destination hashing)sh
:源哈希(source hashing)sed
:最短期望延迟(shortest expected delay)nq
:永不排队(never queue)注意:要在IPVS模式下运行kube-proxy,必须在启动kube-proxy之前使IPVS Linux在节点上可用。当 kube-proxy以IPVS代理模式启动时,它将验证IPVS内核模块是否可用。如果未检测到IPVS内核模块,则kube-proxy将退回到以iptables代理模式运行。
如果要确保每次都将来自特定客户端的连接传递到同一Pod,可以通过将service.spec.sessionAffinity
设置为"ClientIP",默认值是 “None”,来基于客户端的IP地址选择会话关联。可以通过service.spec.sessionAffinityConfig.clientIP.timeoutSeconds
来设置最大会话停留时间,默认值为10800秒,即3小时。
为了使用户能够为他们的Service选择一个端口号,必须确保不能有2个Service发生冲突。Kubernetes通过为每个Service分配它们自己的 IP地址来实现。为了保证每个Service被分配到一个唯一的IP,需要一个内部的分配器能够原子地更新etcd中的一个全局分配映射表,这个更新操作要先于创建每一个Service。为了使Service能够获取到IP,这个映射表对象必须在注册中心存在,否则Service创建将会失败并显示一条指示一个IP不能被分配的消息。
在控制平面中,一个后台Controller负责创建映射表(需要支持从使用了内存锁的Kubernetes的旧版本迁移过来)。同时Kubernetes会通过控制器检查不合理的分配(如管理员干预导致的)以及清理已被分配但不再被任何Service使用的IP地址。
Service
的IP不像Pod
的IP地址实际路由到一个固定的目的地,它实际上不能通过单个主机来进行应答。所以使用iptables来定义一个虚拟IP地址(VIP),它可以根据需要透明地进行重定向。当客户端连接到VIP时,它们的流量会自动地传输到一个合适的Endpoint。Service
的环境变量和DNS实际上会根据Service
的虚拟IP地址和端口来进行填充。kube-proxy支持三种代理模式各自的操作略有不同。
userspace模式,创建Service
时,Kubernetes master会给它分配一个虚拟IP地址,该Service
会被集群中所有的kube-proxy
实例观察到。当代理看到一个新的Service
时,它会打开一个新的随机端口(代理端口),建立一个从该虚拟IP地址重定向到新端口的iptables,并开始接收请求连接。当一个客户端连接到一个Service
的虚拟IP地址时,iptables规则开始起作用,它会重定向该数据包到Service
的代理端口。Service
代理选择一个后端的Pod
并将客户端的流量代理到该后端Pod
上。这意味着Service
的所有者能够选择他们想使用的任何端口而不会发生冲突。客户端可以简单地连接到一个IP和端口,而不需要知道实际访问了哪些Pod
。
iptables模式,创建Service
时,Kubernetes控制面板会给它分配一个虚拟IP地址,该Service
会被集群中所有的kube-proxy
实例观察到。当代理看到一个新的Service
时, 它会配置一系列从虚拟IP地址重定向per-Service
规则的iptables规则。该per-Service
规则链接到重定向(使用目标NAT)到后端的Pod
的per-Endpoint
规则。当一个客户端连接到一个Service
的虚拟IP地址时,iptables规则开始起作用。一个后端Pod
会被选择(或者根据会话亲和性或者随机)并且数据包被重定向到该后端Pod
。与userspace代理不同,数据包从来不会拷贝到用户空间,kube-proxy不是必须为该虚拟IP工作而运行,并且节点会看到来自未更改的客户端IP地址的流量。当流量通过节点端口或负载均衡器进入时,尽管在这种情况下,客户端IP发生更改,也会执行相同的基本流程。
IPVS模式,专为负载均衡设计,并基于内核中的哈希表。在大规模集群中,iptables操作会显着降低速度时。可以通过基于IPVS的kube-proxy在大量服务中实现性能一致性。 同时,基于IPVS的kube-proxy具有更复杂的负载均衡算法。
Kubernetes支持2种基本的服务发现模式——环境变量和DNS。
当Pod
运行在 Node
上,kubelet会为每个活跃的Service
添加一组环境变量。它同时支持Docker links兼容变量、简单的{SVCNAME}_SERVICE_HOST
和{SVCNAME}_SERVICE_PORT
变量,这里Service
的名称需大写,横线被转换成下划线。举个例子,一个名为redis-master
的Service
暴露了TCP端口6379,同时给它分配了Cluster IP地址10.0.0.11
,这个Service
生成了如下环境变量:
REDIS_MASTER_SERVICE_HOST=10.0.0.11
REDIS_MASTER_SERVICE_PORT=6379
REDIS_MASTER_PORT=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
REDIS_MASTER_PORT_6379_TCP_PORT=6379
REDIS_MASTER_PORT_6379_TCP_ADDR=10.0.0.11
注意:当有需要访问服务的Pod时,并且正在使用环境变量方法将端口和群集IP发布到客户端Pod时,必须在客户端Pod出现之前创建服务。 否则,这些客户端Pod将不会设定其环境变量。如果仅使用DNS查找服务的群集IP,则无需担心此设定问题。
可以使用附加组件为Kubernetes集群设置DNS服务。支持群集的DNS服务器(例如CoreDNS)监视Kubernetes API中的新服务,并为每个服务创建一组DNS记录。如果在整个群集中都启用了DNS,则所有Pod都应该能够通过其DNS名称自动解析服务。例如,如果在 Kubernetes命名空间my-ns
中有一个名为my-service"
的服务,则控制平面和DNS服务共同为my-service.my-ns
创建DNS记录。my-ns
命名空间中的Pod应该能够通过简单地对my-service
进行名称查找来找到它( my-service.my-ns
也可以)。其他命名空间中的Pod必须将名称限定为my-service.my-ns
。这些名称将解析为为服务分配的群集IP。
Kubernetes还支持命名端口的DNS SRV(服务)记录。如果my-service.my-ns
服务具有名为http
的端口,且协议设置为TCP
,则可以对_http._tcp.my-service.my-ns
执行DNS SRV查询查询以发现该端口号,http
以及IP地址。Kubernetes DNS服务器是唯一的一种能够访问ExternalName
类型的Service
的方式。
对于应用的某些部分,比如前端,可能希望暴露一个服务到Kubernetes集群外部的一个外部IP地址上。Kubernetes的ServiceTypes
允许指定Service的类型,Service有