k8s Service资源概念:通过规则定义出由多个pod对象组合而成的逻辑集合,以及访问这组pod的策略。service关联pod资源的规则要借助于标签选择器来完成
由deploy等控制器管理的pod对象中断后会由新建的资源对象所取代,而扩缩容后的应用则会带来pod对象群体的变动,随之变化的还有pod的IP地址访问接口等。pod规模的扩容又会使得客户端无法有效地使用新增的pod对象,从而影响达成规模扩展的目的。为此,service资源可以解决此类问题。
service资源基于标签选择器将一组pod定义成一个逻辑组合,并通过自己的IP地址和端口调度代理至组内的pod对象之上。service对象的IP地址也称为cluster IP,它位于k8s集群配置指定专用IP地址范围之内,而且是一种虚拟IP地址,它在service对象创建后即保持不变,并且能够被同一集群中的pod资源所访问。service端口用于接收客户端请求并将其转发至其后端的pod中应用的相应端口上,因此,这种代理机制也称为端口代理或四层代理,它工作于TCP/IP协议栈的传输层。
通过其标签选择器匹配到的后端pod资源不止一个时,service资源能够以负载均衡的方式进行流量调度,实现了请求流量的分发机制。service与pod对象之间的关联关系通过标签选择器以松耦合的方式建立,它可以先于pod对象创建而不会发生错误,于是,创建service和pod资源的任务可由不同的用户分别完成。
service资源会通过api server持续监视着标签选择器匹配到的后端pod对象,并实时跟踪各对象的变动。service并不直接链接至pod对象,它们之间还有一个中间层---endpoint资源对象,它是一个由IP地址和端口组成的列表,这些IP地址和端口则来自于由service的标签选择器匹配到的pod资源。
一个service对象就是工作节点上的一些iptables或ipvs规则,用于将到达service对象IP地址的流量调度转发至相应的endpoints对象指向的IP地址和端口之上。工作于每个工作节点的kube-proxy组件通过api server持续监控着各service及与其关联的pod对象,并将其创建和更新实时反映至当前节点上相应的iptables或ipvs规则上。
service IP事实上是用于生成iptables或ipvs规则时使用的IP地址,它仅用于实现k8s集群网络的内部通信,并且进能够将规则中定义的转发服务的请求作为目标地址予以响应。kube-proxy将请求代理至相应端点的方式有三种:userspace、iptables和ipvs。
(1) userspace代理模型
这种模型中,kube-proxy负责跟踪aip server上service和endpoint对象的变动,并根据此调整service资源的定义,对于每个service对象,它会随机打开一个本地端口,任何到达此端口的连接请求都将被代理至当前service资源后端的各pod对象上。这种代理模型中,请求流量到达内核空间后经由套接字送往用户空间的kube-proxy,而后由它送回内核空间,并调度至后端pod。这种方式中,请求在内核空间和用户空间来回转发必然导致效率低下。
(2) iptables代理模型
iptables代理模型中,kube-proxy负责跟踪api server上service和endpoint对象的变动,并据此做出service资源定义的变动。同时,对于每个service,他都会创建iptables规则直接捕获到达cluster IP和pod的流量,并将其重定向至当前service的后端。在创建service资源时,集群中每个节点上的kube-proxy都会收到通知并将其定义为当前节点上的iptables规则,用于转发工作接口接收到的与此service资源的clusterIP和端口的相关流量。客户端发来的请求流量被相关的iptables规则进行调度和目标地址转换后再转发至集群内的pod对象之上。其缺点是iptables代理模型不会在被选中的后端pod资源无响应时自动进行重定向,而userspace可以。
(3) ipvs代理模型
此模型中,kube-proxy跟踪apiserver上service和endpoint对象的变动,据此来调用netlink接口创建ipvs规则,并确保与api server中的变动保持同步。类似于iptables模型,ipvs构建于netfilter的钩子函数之上,但它使用hash表作为底层数据结构并工作于内核空间,因此具有流量转发速度快、规则同步性能好的特性。另外,ipvs支持众多调度算法,如rr、lc、dh、sh、sed和nq等。
(1) 创建service资源
创建service资源对象有两种方法:一种是直接使用kubectl expose命令,另一种是使用资源配置文件。定义service对象时,spec的两个较为常用的内嵌字段分别为selector和ports,分别用于定义使用的标签选择器和要暴露的端口。
示例:
~]# cat myapp-svc.yaml kind: Service apiVersion: v1 metadata: name: myapp-svc spec: selector: app: myapp ports: - protocol: TCP port: 80 targetPort: 80
service资源myapp-svc通过标签选择器关联至标签为“app=myapp”的各pod对象,它会自动创建名为myapp-svc的endpoint资源对象,并自动配置一个clusterIP,暴露的端口由port资源进行指定,后端各pod对象的端口则由targetPort给出,也可以使用同port字段的默认值。
查看创建的service资源对象
~]# kubectl get svc myapp-svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE myapp-svc ClusterIP 10.105.145.67 <none> 80/TCP 4h59myapp-svc ClusterIP 10.105.145.67 <none> 80/TCP 5s
作为此service对象的后端Endpoint对象,并负责接受相应的请求流量,查看Endpoint资源的端点列表,如下
~]# kubectl get endpoints myapp-svc NAME ENDPOINTS AGE myapp-svc 10.244.0.4:80,10.244.0.5:80,10.244.0.6:80 27h
(2) service会话粘性
service资源还支持session affinity机制,它能够将来自同一个客户端的请求始终转发至同一个后端pod对象,这意味着它会影响调度算法的流量分发功用,进而降低负载均衡的效果。
session affinity的效果仅会在一定时间期限内生效,默认值为10800s,超出时长后,客户端的再次访问会被调度算法重新调度。另外,session affinity机制仅能基于客户端IP地址识别客户端身份,它会把经由同一个nat服务器进行源地址转换的所有客户端识别为同一个客户端,调度粒度粗糙且效果不佳。
(1) 服务发现概述
服务发现就是服务或者应用之间互相定位的过程。服务发现机制的基本实现,一般是先部署好一个网络为止较为稳定的服务注册中心(也称为服务总线),服务提供者向注册中心注册自己的位置信息,并在变动后及时予以更新,相应地,服务消费者则周期性地从注册中心获取服务提供者的最新位置信息从而发现要访问的目标服务资源。复杂的服务发现机制还能够让服务提供者提供其描述信息、状态信息及资源使用信息等,以供消费者实现更为复杂的服务选择逻辑。根据服务发现过程的实现方式,服务发现还可分为两种类型:客户端发现和服务端发现:
客户端发现:由客户端到服务注册中心发现其依赖到的服务的相关信息,因此,它需要内置特定的服务发现程序和发现逻辑 服务端发现:这种方式需要额外用到一个称为中央路由器或服务均衡器的组件;服务消费者将请求发往中央路由器或者负载均衡器,由它们负责查询服务注册中心获取服务提供者的位置信息,并将服务消费者的请求转发给服务提供者。
Netflix的eureka是目前较为流行的服务发现系统之一,它是专门开发用来实现服务发现的系统,以可用性目的为先,可以在多种故障期间保持服务发现和服务注册的功能可用。另一个同级别的实现是consul。
(2) 服务发现方式:环境变量
创建pod资源时,kubelet会将其所属名称空间内的每个活动的service对象以一系列环境变量的形式注入其中。它支持使用service环境变量以及docker的link兼容的变量
k8s service环境变量
k8s为每个service资源生成包括以下形式的环境变量在内的一系列环境变量,在同一名称空间中创建的pod对象都会自动拥有这些变量
docker link形式的环境变量
docker使用--link选项实现容器连接时说设置的环境变量形式,在创建pod对象时,k8s也会将与此形式兼容的一系列环境变量注入pod对象中。基于环境变量的服务发现其功能简单、易用,但存在一定局限,例如,仅有那些与创建的pod对象在同一名称空间中且事先存在的service对象的信息才会以环境变量的形式注入,那些处于非同一名称空间,或者是在pod资源创建之后才创建的service对象的相关环境变量则不会被添加。基于dns的发现机制并不存在此类限制。
clusterdns和服务发现
k8s系统之上用于名称解析和服务发现的clusterdns是集群的核心组件之一,集群中创建的每个service对象,都会由其自动生成相关的资源记录。默认情况下,集群内各pod资源会自动配置器作为名称解析服务器,并在其dns搜索列表中包含它所属名称空间的域名后缀。
service的IP地址仅在集群内可达,然而,总会有些服务需要暴露到外部网络中接受各类客户端的访问,此时,就需要在集群的边缘为其添加一层转发机制,以实现将外部请求流量转入到集群的service资源之上,这种操作也成为发布服务到外部网络中。
(1) service类型
service共有四种类型:
ClusterIP:通过集群内部IP地址暴露服务,此地址仅在集群内部可达,而无法被集群外部的客户端访问 NodePort:这种类型建立在ClusterIP之上,其在每个节点的IP地址的某静态端口暴露服务,因此,它依然会为service分配集群IP地址,并将此作为NodePort的路由目标 LoadBalancer:这种类型建构在NodePort之上,其通过cloud provider提供的负载均衡器将服务暴露到集群外部,因此LoadBalancer一样具有NodePort和ClusterIP ExternalName:其通过将service映射至由externalName字段的内容指定的主机名来暴露服务,此主机名需要被dns服务解析至cname类型的记录。此种类型并非定义由k8s集群提供的服务,而是把集群外部的某服务以dns cname记录的方式映射到集群内,从而让集群内的pod资源能够访问外部的service的一种实现方式。
(2) NodePort类型的service资源
NodePort即节点port,通常在安装部署k8s集群系统时会预留一个端口范围用于NodePort,默认为3000-32767之间。定义NodePort类型的service资源时,需要通过此属性明确指定其类型名称。
~]# cat myapp-svc-nodeport kind: Service apiVersion: v1 metadata: name: myapp-svc-nodeport spec: type: NodePort selector: app: myapp ports: - protocol: TCP port: 80 targetPort: 80 nodePort: 32223 # cluster IP类型的service,不需要指定spec.type类型 ~]# kubectl get svc myapp-svc-nodeport NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE myapp-svc-nodeport NodePort 10.109.178.236 <none> 80:32223/TCP 30m
一般并不推荐用户自定义使用nodePort,除非事先能够明确知道它不会与某个现存的service资源产生冲突。对于集群外部的客户端来说,它们可经由任何一个节点的节点IP及端口访问NodePort类型的service资源,而对于集群内部的pod客户端来说,依然可通过clusterIP对其进行访问。
(3) LoadBalancer类型的service资源
NodePort类型的service资源虽然能够于集群外部访问得到,但外部客户端必须得事先得知NodePort和集群中至少一个节点的IP地址,且选定的节点发生故障时,客户端还得自行选择请求访问其他的节点。另外,集群节点很可能是某IaaS云环境中使用私有IP地址的VM,或者是idc中使用私有地址的物理机,这类地址对互联网客户端不可达,因此,一般还应该在集群之外创建一个具有公网IP地址的负载均衡器,由它接入外部客户端的请求并调度至节点相应的NodePort之上。
创建LoadBalancer的servcie资源配置清单
~]# cat locadbalancer.yaml kind: Service apiVersion: v1 metadata: name: myapp-svc-lb spec: type: LoadBalancer selector: app: myapp ports: - protocol: TCP port: 80 targetPort: 80 nodePort: 32223
(4) ExternalName service
ExternalName类型的service资源用于将集群外部的服务发布到集群中以供pod中的应用程序访问,因此,它不需要使用标签选择器关联任何的pod对象,但必须要使用spec.externalName属性定义一个cname记录用于返回外部真正提供服务的主机的别名,而后通过cname记录值获取到相关主机的IP地址。
示例:
~]# cat external-redis-svc kind: Service apiVersion: v1 metadata: name: external-redis-svc namespace: default spec: type: ExternalName externalName: redis.ilinux.io ports: - protocol: TCP port: 6379 targetPort: 6379 nodePort: 0 selector: {}
servicce资源创建完成后,可以在pod内通过esternal-redis-svc访问相应的外部服务,可在pod内nslookup出外部服务的IP地址
由于ExternalName类型的service资源实现于dns级别,客户端将直接接入外部的服务而完全不需要服务代理,因此,它也无须配置cluster IP,此种类型的服务也称为HeadlessService。
service对象隐藏了各pod资源,并负责将客户端的请求流量调度至该组pod对象之上。不过,有时候也会存在这样的需求:客户端需要直接访问service资源后端的所有pod资源,这时就应该向客户端暴露每个pod资源的IP地址,而不再是中间层service对象的clusterIP,这种类型的service资源便称为Headless Service。
Headless Service对象没有clusterIP,于是kube-proxy便无须处理此类请求,也就更没有了负载均衡或代理它的需要。在前端应用拥有自有的其他服务发现机制时,Headless service即可省去定义clusterIP的需求。至于如何为此类service资源配置IP地址,取决于它的标签选择器的定义。
具有标签选择器:端点控制器会在api中为其创建endpoint记录,并将clusterdns服务中的A记录直接解析到此service后端的pod对象的IP地址上 没有标签选择器:端点控制器不会在api中为其创建endpoints记录,clusterdns的配置分为两种情形,对externalName类型的服务创建cname记录,对其他三种类型来说,为那些与当前service共享名称的所有endpoints对象创建一条记录。
(1) 创建Headless Service资源
配置service资源清单时,只需要将clusterIP字段的值设置为None即可,例如:
~]# cat myapp-headless-svc kind: Service apiVersion: v1 metadata: name: myapp-headless-svc spec: clusterIP: None selector: app: myapp ports: - port: 80 targetPort: 80 name: httpport ~]# kubectl get svc myapp-headless-svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE myapp-headless-svc ClusterIP None <none> 80/TCP 40s
会发现,创建的service没有clusterIP,如果标签选择器匹配到相关pod资源,它便拥有Endpoints记录,这些Endpoints对象会作为DNS资源记录名称myapp-headless-svc查询时的A记录解析结果
(2) Pod资源发现
记录于clusterdns的A记录的相关解析结果是后端pod资源的IP地址,这就意味着客户端通过此service资源的名称发现的是各pod资源。客户端向此service对象发起的请求将直接接入到pod资源中的应用之上,而不再由service资源进行代理转发,它每次接入的pod资源则由DNS服务器接收到查询请求时以轮询的方式返回的IP地址。