Service

Service 是kubernetes提供一种为一组功能相同的pod提供单一不变的接入点的资源。当服务存在时,它的IP地址和端口都不会改变。客户端通过IP地址和端口建立连接。这些连接会被路由到提供该服务的任意一个pod上。这样客户端就不要知道每个单独的提供服务的pod地址,这些pod就可以在集群中随时创建与注销。

service通过标签来确定哪些pod是属于服务,与前面rc、rs、ds一样都通过pod控制器中的标签选择器来进行指定。

service-demo.yaml

apiVersion: v1
kind:Service
metadata:
  name: svc-demo
spec:
  selector:
    app: svc-backend  # 具有app=svc-backend 标签的pod属于服务
  ports:
  - port: 80     # 该服务可用的端口
   targetPort: 8080        # 服务链转发到容器的端口

在运行的容器中远程执行命令
可用使用kubectl exec 命令远程地在一个已存在的pod容器上指定任何命令
例如:kubectl exec podName – curl -s http://x.x.x.x
“–” 表示kubectl 命令项结束。在双横杠之后表示是在容器中要执行的命令。如果没有使用双横杠来隔开,后续的参数可能会被解析成kubectl命令的参数,这样会造成结果的歧义。

设置会话亲和性
每次连接都会被执行到不同的pod上,即使是同一个客户端发来的请求。如果希望特定客户端产生的所有的请求每次都指向同一个pod,可以设置服务的sessionAffinity属性值为clientIP(默认值是nano)

apiVersion: v1
kind: Service
spec:
    sessionAffinity: clinetIP

kubernetes 服务处理的是TCP和UDP包,而不是应用层的HTTP,所以不支持cookie的会话亲和性

同一个服务暴露多个端口

apiVersion: v1
kind: Service
metadata:
  name: svc-demo
spec:
  ports:
  - name: http  # 在创建同时有多个端口的时候,必须为每个端口设置名字
    port: 80
    targetPort: 8080
 - name: https
   port: 443
   targetPort: 8443
 selector:
   app: svc-backend  

即可同时暴露两个对外的端口,80会被映射到后端pod的8080端口上,443会被映射到后端pod的8443端口上

使用命名的端口
假如在后端pod上端口命名,则可在service中使用端口名进行映射

kind: Pod
spec:
  containes:
  - name: http
    containerPort: 8080   # 端口8080 被命名为http
  - name: https
    containerPort: 8443   # 端口8443 被命名为https
       
kind:Service
spec:
  ports:
  - name: port1
    port: 80
    targetPort: http  # 即可与pod中命名为http的端口进行映射
  - name: port2
    port: 443
    targetPort: https  # 即可与pod中命名为https的端口进行映射  

采用命名的方式会让以后更改pod的端口更方便,假如以后pod的端口该为其他值,service中的映射端口值就不需要改变

服务发现

通过环境变量发现服务
pod开始运行时,kubernetes会初始化一系列的环境变量指向现在的服务。这样pod就能通过环境变量来知道服务的IP地址与端口,但前提是服务必须在pod之前创建
可通过kubectl 命令查看pod当前的环境变量
kubectl exec podName env
服务转换成环境变量时遵循以下原则

  • 服务名称中的横杠会转换成下划线
  • 服务名称会作为环境变量的前缀,且都会转换成大写

DNS服务发现
kubernetes 通过修改每个容器的 /etc/resolv.conf 文件来实现将集群中运行的dns-pod 作为所有容器的DNS服务。运行在pod上的进程DNS查询都会被kubernetes自身的DNS服务响应,该服务知道集群中运行的所有服务。
注意:pod是否使用内部的DNS服务器是根据pod中spec的dnsPolicy属性来决定。
每个服务从内部DNS服务器中获得一个DNS条目,客户端的pod在指定服务名称的情况下可以通过全限定域名(FQDN)来访问,而不是通过环境变量。

通过FQDN连接服务
例:
前端pod可以通过打开一下FQDN连接访问后端的服务
backend-database.default.svc.cluster.local
其中 backend-databash 为服务名称
default 表示服务所在的命名空间
svc.cluster.local 是在所有集群本地服务名称中使用的可配置集群域后缀

注意:客户端然要知道服务的端口号,如果服务使用默认的端口号(例如HTTP为80,postgres为5432等),这样是没问题的,如果不是标准的端口,则可以从环境变量中获得

连接时,如果前端pod与后端服务在同一个命名空间,则可省略svc.cluster.local,甚至命名空间也可以省略。
注意:服务正常工作时,ping不通服务的IP,因为服务的集群IP是一个虚拟IP,只有当与服务端口结合时才有意义。

连接集群外部的服务

endpoint介绍
服务并不是和pod直接两联,相反,有一种介于两者之间——就是Endpoint资源。Endpoint资源就是暴露一个服务的IP地址和端口的列表。尽管在spec服务中定义了pod选择器,但是在重定向传入连接时不会直接使用它,相反,选择器用于构建IP和端口列表,然后存入Endpoint资源中。当客户端连接到服务时,服务代理选择这些IP端口对中的一个,并接入连接重定向到现在为止监听的服务器。

手动配置服务的Endpoint
服务的endpoint与服务解耦后,可以分别手动配置个更新它们
如果创建了不带选择器的服务,kubernetes将不会为服务创建Endpoint资源,这样就需要创建Endpoint资源来指定该服务的endpoint列表。
创建没有选择器的服务

apiVersion: v1
kind: Service
metadata:
  name: test-svc    #服务的名称必须与Endpoint对象的名称一致
spec:     # 没有指定选择器
  ports:
  - port: 80 

为没有选择器的服务创建Endpoint资源

apiVersion: v1
kind: Endpoints
metadata:
  name: test-svc    #endpoint的名称必须与服务的名称一致
subsets:
  - addresses:
    - ip: 1.1.1.1   # 服务将连接重定向到endpoint的IP地址
    - ip: 2.2.2.2
    ports:
    - port: 80      # endpoint的目标端口

Endpoint必须与服务具有相同的名称,且需要包含该服务的目标IP地址与端口列表。服务和Endpoint资源都发布到服务器后,服务就可像具有pod选择器一样的正常使用。在服务创建后创建的容器将包含服务的环境变量,并且与其IP:port对的所有连接都在服务端点之间进行负载均衡

也可以根据需要为服务添加选择器,从而对Endpoint进行自动管理。相反,也可将选择器从服务端移除,kubernetes将停止更新Endpoint。这意味着服务的IP地址可以保持不变,同时服务的实际实现却发生了改变。

为外部服务创建别名
除了使用Endpoint来代替公开外部服务方法外,还可以通过其完全限定域名访问外部服务
要创建一个具有别名的外部服务时,要将创建的服务资源的一个type字段设置为ExternalName。
external-svc.yaml

apiVersion: v1
kind: Service
metadata:
  name: external-svc
spec:
  type: ExternalName    # 设置type类型
  externalName: www.baidu.com   # 外部服务的完全限定域名
  ports:
  - port: 80    

服务创建后,pod可以通过external-svc。default.svc.cluster.local 域名连接到外部服务(如果pod与服务是在同一命名空间下,则可通过external-svc来访问),而不是使用服务实际的FQDN,这样隐藏了实际服务的名称及其使用该服务的pod位置,可以修改externalName属性来将其指向不同的服务。也可以修改类型重新变回clusterIP,或手动创建Endpoint资源,或添加选择器。

将服务暴露给外部客户端

以下方式可在外部访问服务:

  1. 将服务类型设置成
    NodePort——每个集群节点都会在节点上打开一个端口,对于NodePort服务,每个集群节点都在节点本身上打开一个端口,并将在该端口上接收的流量重定向到基础服务。该服务仅在内部集群IP和端口上才可以访问,但也可以通过所有节点上的专用端口访问
  2. 将服务类型设置成LoadBalance,NodePort的一种扩展——这使得服务可以通过一个专用的负责均衡器来访问,这是有kubernetes中正在运行的云基础设施提供的。负载均衡器将流量重定向到跨所有节点的节点端口,接护短通过负载均衡器的IP连接到服务。
  3. 创建一个Ingress资源,这是一个完全不同的机制,通过一个IP地址公开多个服务——它运行在HTTP层上,因此可以提供比工作在第四层的服务更多的功能。

使用NodePort类型服务
将一组pod的公开个外部服务客户端的一种办法是创建一个服务并将其类型设置为NodePort。通过创建NodePort服务,可以让kubernetes在所有的节点上保留一个端口(所有节点上都使用相同的端口号),并将传入的连接转发给作为服务器部分的pod。
不仅可以通过服务内部集群IP访问NodePort服务,还可以通过任何节点的IP和预留节点端口访问NodePort服务。

svc-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
  name: svc-nodeport
spec:
  type: NodePort      # 为NodePort 设置服务类型
  ports:
  - port: 80          # 服务集群IP的端口号
    targePort: 8080   # 背后pod的目标端口号
   nodePort: 30123   # 通过集群节点的30123端口可以访问到该服务
  selector:          # 选择器
    app: test    

其中nodePort:30123 不是必要的,可以不指定节点端口,kubernetes会随机分配一个端口
kubectl get svc svc-nodeport 获取服务信息
查看EXTERNAL-IP 列 将显示为 nodes
查看PORT(S) 列显示集群IP的内部端口(80)和几点端口(30123)
访问过程:
外部客户端 -> 节点 -> 服务 -> pod

通过JSONPath获取所有节点的IP
可以通过节点的json或yaml描述中找到IP,但并不是在很大的惊悚中筛选,而是可以利用
kubectl命令值打印出节点IP而不是整个服务的定义

kubectl get nodes -o -jsonpath=’{.item[*].status.addresse[?(@.type==“ExternalIP”)].address}’

通过指定kubectl的JSONPath,使得其只输出需要的信息,上列中的JSONPath指示kubectl执行以下操作
1、浏览item属性
2、对于每个元素,输入status属性
3、过滤address属性的元素,仅包含哪些具有将type属性设置为ExternalIP的元素
4、最后打印过滤元素的address属性
知道节点IP后,就可以通过 curl http://nodeIP:30123 对服务进行访问

正如所看到的,整个互联网上都可以通过任何节点上的30123端口访问到你的pod,但是,若果客户端指向的节点发生故障,客户端将无法再访问服务。这就是为什么要将负载均衡器放在节点前面,以确保发送请求传播到所有健康的节点上,且不会连接到处于脱机的节点的原因。
如果kubernetes集群支持它(当kubernetes部署在云基础设施上时,一般都会支持负载均衡器),name可以创建一个Load Balance 而不是NodePort服务自动生成的负载均衡器。

通过负载均衡器将服务暴露出来
在云提供商上运行的kubernetes集群通常都支持从云基础架构自动提供负载均衡。只要将服务类型设置为Load Balance而不是NodePort,负载均衡器拥有自己独一无二的可公开访问的IP地址,并将所有连接重定向到服务,可以通过负载均衡的IP地址访问服务。
如果kubernetes在不支持Load Balance的环境中运行,则不会调用负载均衡,但是服务任然将表现得像一个NodePort服务,这是因为Load Balance是NodePort的一种扩展。

loadBalance-demo.yaml
apiVersion: v1
kind: Service
metadata:
  name: loadBalance
spec:
  type: LoadBalance
  selector:
    app: test
  ports:
 - port: 80
   targetPort: 8080

创建服务后,云基础架构需要一段时间才能创建负载均衡器并将IP地址写入服务对象,这样IP地址就被列为服务的外部IP地址。
kubectl get svc loadBalance
查看负载均衡器的地址,并且可以通过该地址访问服务

了解并防止不必要的网络跳数
但外部客户端通过服务连接到服务时,随机选择的pod的并不一定在接收连接的节点上,因此可能需要额外的网络跳转才能到达pod,我们可以通过设置服务的属性来避免该情况的产生,让外部连接只重定向到接收连接的节点上运行的pod。设置spec.externalTrafficPolicy=Local 即可。但同样存在问题,如果接收连接的节点上没有运行pod,则连接会被挂起,而不会重定向到其他的pod,因此需要确保负载均衡器将连接转发给至少具有一个pod的节点
同时该方法还有一个缺点,当节点A上有一个pod,节点B上有两个pod,如果负载均衡器在两个节点间均匀的分布连接,则A节点会承担50%的负载,而B上的pod各承担25%的负载

使用headless服务来发现独立的pod
服务一般提供稳定的IP地址,让客户端来连接支持服务的每个pod。到服务的每个链接只会被转发到一个随机的pod上,但如果想访问所有的pod呢?
如果客户端要链接每一个pod,那需要知道pod的IP地址,可以让客户端调用kubernetes API服务器,通过api获取pod及其IP地址列表。但是我们应该尽量保证,应用程序与kubernetes无关,所有尽量不要采用该种方法。
kubernetes运行客户端通过DNS来查找发现podIP。通过执行DNS服务时,kubernetes会返回单个IP——服务的集群IP。但可以告诉kubernetes,我们不需要集群IP,则DSN将会返回podIP,而不是单个服务的IP。只要将服务spec中的cluster字段设置为None即可。
DNS会返回多条记录,每条记录会指向一个支持该服务的单个pod的IP。客户端可以通过这些记录连接到其中一个、多个或全部。需要注意的是,这里只会返回部分的记录,那是因为没有就绪的pod没有添加到服务中,所以它们的地址不会被返回。
将服务的clusterIP设置为none,会使服务变成headless服务。此时kubernetes不会为服务分配集群IP。
kube-svc-headless.yaml

apiVersion: v1
kind: Service
metadata:
  name: svc-headless
spec:
  clusterIP: None
  Ports:
  - port: 80
    targetPort: 8080
 selecter:
   app:headless-pod

有时候我们需要发现所有匹配服务标签选择器的pod,不管是否就绪

kind: Service
metadata:
    annotations:
        service.alpha.kubernetes.io/tolerrate-unready-endpoints: "true"

(本文章是学习《kubernetes in action》 一书时做的笔记,由于本人是刚学习k8s,且自学方式就是把它敲一遍,然后加一些自己的理解。所以会发现很多内容是跟书中内容一模一样,如果本文是对本书的侵权,敬请谅解,告知后会删除。如果引用或转载,请注明出处——谢谢)

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐