Kubernetes之Service

  特定的pod可以独立地应对外部请求,现在大多数应用都需要根据外部请求做出响应。例如,就微服务而言,pod通常需要对来自集群内部其他pod,以及来自集群外部的客户端的HTTP请求做出响应。

  pod需要一种寻找其他pod的方法来使用其他pod提供的服务,不像在没有Kubernetes的世界,系统管理员要在用户端配置文件中明确指出服务的精确的IP地址或者主机名来配置每个客户端应用,但是同样的方式在Kubernetes中并不适用,因为

    • pod是短暂的一它们随时会启动或者关闭,无论是为了给其他pod提供空间而从节点中被移除,或者是减少了pod的数量,又或者是因为集群中存在节点异常。
    • Kubernetes在pod启动前会给已经调度到节点上的pod分配IP地址---因此客户端不能提前知道提供服务的pod的IP地址。
    • 水平伸缩意味着多个pod可能会提供相同的服务---—每个pod都有自己的IP地址,客户端无须关心后端提供服务pod的数量,以及各自对应的IP地址。它们无须记录每个pod的IP地址。相反,所有的pod可以通过一个单一的IP地址进行访问。

  为了解决上述问题,Kubernetes提供了一种资源类型——服务(service),在本章中将对其进行介绍。

 

1.介绍服务

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

  结合实例解释服务

  回顾一下有前端web服务器和后端数据库服务器的例子。有很多pod提供前端服务,而只有一个pod提供后台数据库服务。需要解决两个问题才能使系统发挥作用。

    • 外部客户端无须关心服务器数量而连接到前端pod上。
    • 前端的pod需要连接后端的数据库。由于数据库运行在pod中,它可能会在集群中移来移去,导致IP地址变化。当后台数据库被移动时,无须对前端pod重新配置。

  通过为前端pod创建服务,并且将其配置成可以在集群外部访问,可以暴露一个单一不变的IP地址让外部的客户端连接pod。同理,可以为后台数据库pod创建服务,并为其分配一个固定的IP地址。尽管pod的IP地址会改变,但是服务的IP地址固定不变。另外,通过创建服务,能够让前端的pod通过环境变量或DNS以及服务名来访问后端服务。系统中所有的元素都在图5.1中展示出来(两种服务、支持这些服务的两套pod,以及它们之间的相互依赖关系)。

 

1.1 创建服务

  服务的后端可以有不止一个pod。服务的连接对所有的后端pod是负载均衡的。但是要如何准确地定义哪些pod属于服务哪些不属于呢?

  在ReplicationController和其他的pod控制器中使用标签选择器来指定哪些pod属于同一组。服务使用相同的机制,可以参考图5.2。

  在kubernetes之RS/RC中,通过创建ReplicationController运行了三个包含Nodejs应用的pod。再次创建ReplicationController并且确认pod启动运行,现在将会为这三个pod创建一个服务。

  通过kubectI expose创建服务

  创建服务的最简单的方法是通过kubectl expose,在Kubernetes之RC/RS中来暴露创建的ReplicationController。像创建ReplicationController时使用的pod选择器那样,利用expose命令和pod选择器来创建服务资源,从而通过单个的IP和端口来访问所有的pod。

  通过YAML描述文件来创建服务

apiVersion: v1
kind: Service
metadata:
  name: kubia
spec:
  ports:
  - port: 80               #服务的可用端口
    targetPort: 8080  #服务将连接转发到的容器端口
  selector:
    app: kubia             #具有app=kubia签的pod都属于该服务

  创建了一个名叫kubia的服务,它将在端口80接收请求并将连接路由到具有标签选择器是app=kubia的pod的8080端口上。

  接下来通过使用kubectl create发布文件来创建服务。

  检测新的服务

  在发布完YAML文件后,可以在命名空间下列出来所有的服务资源,并可以发现新的服务已经被分配了一个内部集群IP。

$ kubectl get svc
NAME         CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
kubernetes   10.111.240.1     <none>        443/TCP   30d
kubia        10.111.249.153   <none>        80/TCP    6m        

  列表显示分配给服务的IP地址是10.111.249.153。因为只是集群的IP地址,只能在集群内部可以被访问。服务的主要目标就是使集群内部的其他pod可以访问当前这组pod,但通常也希望对外暴露服务。如何实现将在之后讲解。现在,从集群内部使用创建好的服务并了解服务的功能。

  从内部集群测试服务

  可以通过以下几种方法向服务发送请求:

    • 显而易见的方法是创建一个pod,它将请求发送到服务的集群IP并记录响应。可以通过查看pod日志检查服务的响应。
    • 使用ssh远程登录到其中一个Kubernetes节点上,然后使用curl命令。
    • 可以通过kubectl exec命令在一个己经存在的pod中执行curl命令。

  来学习最后一种方法——如何在己有的pod中运行命令。

  在运行的容器中远程执行命令

  可以使用kubectl exec命令远程地在一个己经存在的pod容器上执行任何命令。这样就可以很方便地了解pod的内容、状态及环境。用kubectl get pod 命令列出所有的pod,并且选择其中一个作为exec命令的执行目标(在下述例子中,选择kubia-7nog1 pod作为目标)。也可以获得服务的集群IP(比如使用 kubectl get svc命令)。

$ kubectl exec kubia-7nog1 -- curl -s http://10.111.249.153
You've hit kubia-gzwli

  为什么是双横杠?

  双横杠(--)代表着kubectl命令项的结束。在两个横杠之后的内容是指在pod内部需要执行的命令。如果需要执行的命令并没有以横杠开始的参数,横杠也不是必需的。如下情况,如果这里不使用横杠号,-s选项会被解析成kubectl exec选项,会导致结果异常和歧义错误。

$ kubectl exec kubia-7nog1 curl -s http://10.111.249.153
The connection to the server 10.111.249.153 was refused – did you specify the right host or port?

  服务除拒绝连接外什么都不做。这是因为kubectl并不能连接到位于10.111.249.153的API服务器(-s选项用来告诉kubectl需要连接一个不同的API服务器而不是默认的)。

 

  回顾一下在运行命令时发生了什么。图5.3展示了事件发生的顺序。在一个pod容器上,利用Kubernetes去执行curl命令。curl命令向一个后端有三个pod服务的IP发送了HTTP请求,Kubernetes服务代理截取该连接,在三个pod中任意选择了一个pod,然后将请求转发给它。Node.js在pod中运行处理请求,并返回带有pod名称的HTTP响应。接着,curl命令向标准输出打印返回值,该返回值被kubectl截取并打印到宕主机的标准输出。

  在之前的例子中,在pod主容器中以独立进程的方式执行了curl命令。这与容器真正的主进程和服务通信并没有什么区别。

  配置服务上的会话亲和性

  如果多次执行同样的命令,每次调用执行应该在不同的pod上。因为服务代理通常将每个连接随机指向选中的后端pod中的一个,即使连接来自于同一个客户端。

  另一方面,如果希望特定客户端产生的所有请求每次都指向同一个pod,可以设置服务的sessionAffinity属性为ClientIP(而不是None,None是默认值),如下面的代码清单所示。

代码清单5.2会话亲和性被设置成ClientIP的服务的例子
apiVersion: v1
kind: Service 
spec:
  sessionAffinity: ClientIP

  这种方式将会使服务代理将来自同一个ClientIP的所有请求转发至同一个pod上。作为练习,创建额外的服务并将会话亲和性设置为ClientIP,并尝试向其发送请求。

  Kubernetes仅仅支持两种形式的会话亲和性服务:None和ClientIP。你或许惊讶竟然不支持基于cookie的会话亲和性的选项,但是你要了解Kubernetes服务不是在HTTP层面上工作。服务处理TCP和UDP包,并不关心其中的载荷内容。因为cookie是HTTP协议中的一部分,服务并不知道它们,这就解释了为什么会话亲和性不能基于cookie

  同一个服务暴露多个端口

  创建的服务可以暴露一个端口,也可以暴露多个端口。比如,你的pod监听两个端口,比如HTTP监听8080端口、HTTPS监听8443端口,可以使用一个服务从端口80和443转发至pod端口 8080和8443。在这种情况下,无须创建两个不同的服务。通过一个集群IP,使用一个服务就可以将多个端口全部暴露出来。

  注意:在创建一个有多个端口的服务的时候,必须给每个端口指定名字。

  以下代码清单中展示了多端口服务的规格。

#代码清单,在服务定义中自定多端口
apiVersion: v1
kind: Service 
metadata:
  name: kubia 
spec:
  ports:
  - name: http
    port: 80                  #pod的8080端口映射成80端口
    targetPort: 8080            
  - name: https
    port: 443               #pod的8443端口映射成443端口
    targetPort: 8443
  selector:
    app: kubia

  注意:标签选择器应用于整个服务,不能对每个端口做单独的配置。如果不同的pod有不同的端口映射关系,需要创建两个服务。

  之前创建的kubia pod不在多个端口上侦听,因此可以练习创建一个多端口服务和一个多端口pod。

  使用命名的端口

  在这些例子中,通过数字来指定端口,但是在服spec中也可以给不同的端口号命名,通过名称来指定。这样对于一些不是众所周知的端口号,使得服务spec更加清晰。

  举个例子,假设你的pod端口定义命名如下面的代码清单所示。

#代码清单5.4 在pod的定义中指定port名称
kind: Pod 
spec:
  containers:
  - name: kubia 
    ports:
  - name: http                      #端口8080被命名为http
    containerPort: 8080
  - name: https                    #端口8443被命名为https
    containerPort: 8443
#代码清单5.5 在服务中引用命名pod
kind: Service
spec:
  ports:
  - name: http                  #将端口80映射到容器中被称为http的端口
    port: 80
    targetPort: http
  - name: https                 #将端口443映射到容器中被称为https的端口
    port: 443
    targetPort: https

  为什么要采用命名端口的方式?最大的好处就是即使更换端口号也无须更改服务spec。你的pod现在对http服务用的是8080,但是假设过段时间你决定将端口更换为80呢?

  如果你采用了命名的端口,仅仅需要做的就是改变spec pod中的端口号(当然端口号的名称没有改变)。在pod向新端口更新时,根据pod收到的连接(8080端口在旧的pod上、80端口在新的pod上),用户连接将会转发到对应的端口号上。

 

1.2 服务发现

  通过创建服务,现在就可以通过一个单一稳定的IP地址访问到pod。在服务整个生命周期内这个地址保持不变。在服务后面的pod可能删除重建,它们的IP地址可能改变,数量也会增减,但是始终可以通过服务的单一不变的IP地址访问到这些pod。

  但客户端pod如何知道服务的IP和端口?是否需要先创建服务,然后手动查找其IP地址并将IP传递给客户端pod的配置选项?当然不是。Kubernetes还为客户端提供了发现服务的IP和端口的方式。

  通过环境变量发现服务

  在pod开始运行的时候,Kubernetes会初始化一系列的环境变量指向现在存在的服务。如果你创建的服务早于客户端pod的创建,pod上的进程可以根据环境变量获得服务的IP地址和端口号。

  在一个运行pod上检查环境,去了解这些环境变量。现在己经了解了通过kubectl exec命令在pod上运行一个命令,但是由于服务的创建晚于pod的创建,那么关于这个服务的环境变量并没有设置,这个问题也需要解决。

  在查看服务的环境变量之前,首先需要删除所有的pod使得ReplicationController创建全新的pod。在无须知道pod的名字的情况下就能删除所有的pod,就像这样:

$ kubectl delete po --all
pod "kubia-7nog1" deleted 
pod "kubia-bf50t" deleted 
pod "kubia-gzwli" deleted

  现在列出所有新的pod,然后选择一个作为kubectl exec命令的执行目标。一旦选择了目标pod,通过在容器中运行env来列出所有的环境变量,如下面的代码清单所示。

$ kubectl exec kubia-3inly env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=kubia-3inly
KUBERNETES_SERVICE_HOST=10.111.240.1 
KUBERNETES SERVICE PORT=443
KUBIA_SERVICE_HOST=10.111.249.153   #这是服务的集群ip
KUBIA SERVICE PORT=80                        #这是服务所在的端口
 

  在集群中定义了两个服务:kubernetes和kubia(之前在用kubectl get svc命令的时候应该见过);所以,列表中显示了和这两个服务相关的环境变量。在本章开始部分,创了kubia服务,在和其有关的环境变量中有KUBIA_SERVICE_HOST和KUBIA_SERVICE_PORT,分别代表了kubia服务的IP地址和端□号。

  回顾本章开始部分的前后端的例子,当前端pod需要后端数据库服务pod时,可以通过名为backend-database的服务将后端pod暴露出来,然后前端pod 通过环境变量BACKEND_DATABASE_SERVICE_HOST和BACKEND_DATABASE_SERVICE_PORT去获得IP地址和端口信息。

  注意: 服务名称中的横杠被转换为下画线,并且当服务名称用作环境变量名称中的前缀时,所有的字母都是大写的。

  环境变量是获得服务IP地址和端口号的一种方式,为什么不用DNS域名?为什么kubernetes中没有DNS服务器,并且允许通过DNS来获得所有服务的IP地址? 事实证明,它的确如此!

  通过DNS发现服务

  在kube-system命名空间下其中一个pod被称作kube-dns,当前的kube-system的命名空间中也包含了一个具有相同名字的响应服务。

  就像名字的暗示,这个pod运行DNS服务,在集群中的其他pod都被配置成使用其作为dns(kubernetes通过修改每个容器的/etc/resolv.conf文件实现)。运行在pod上的进程DNS查询都会被kubernetes自身的DNS服务器响应,该服务器知道系统中运行的所有服务。

  注意: pod是否使用内部的DNS服务器是根据pod中spec的dnsPolicy属性来决定的。

  每个服务从内部DNS服务器中获得一个DNS条目,客户端的pod在知道服务名称的情况下可以通过全限定域名(FQDN)来访问,而不是诉诸于环境变量。

  通过FQDN连接服务

  再次回顾前端-后端的例子,前端pod可以通过打开以下FQDN的连接来访问后端数据库服务:

backend-database.default.svc.cluster.local

  backend-database对应于服务名称,default表示服务在其中定义的名称空间,而svc.cluster.local是在所有集群本地服务名称中使用的可配置集群域后缀。

  注意:客户端仍然必须知道服务的端口号。如果服务使用标准端口号(例如,HTTP的80端口或Postgres的5432端口),这样是没问题的。如果并不是标准端口,客户端可以从环境变量中获取端口号。

  连接一个服务可能比这更简单。如果前端pod和数据库pod在同一个命名空间下,可以省略svc.cluster.local后缀,甚至命名空间。因此可以使用backend-database来指代服务。这简单到不可思议,不是吗?

  尝试使用FQDN来代替IP去访问kubia服务。另外,必须在一个存在的pod上才能这样做。己经知道如何通过kubectl exec在一个pod的容器上去执行一个简单的命令,但是这一次不是直接运行curl命令,而是运行bash shell,这样可以在容器上运行多条命令。这和调用docker exec -it bash命令,这与此很相似。

  在pod容器中运行shell

  可以通过kubectl exec命令在一个pod容器上运行bash(或者其他形式的shell)。通过这种方式,可以随意浏览容器,而无须为每个要运行的命令执行kubectl exec。

  注意:shell的二进制可执行文件必须在容器镜像中可用才能使用。

  为了正常地使用shell,kubectl exec命令需要添加-it选项:

$ kubectl exec -it kubia-3inly bash
root@kubia-3inly:/#

  现在进入容器内部,根据下述的任何一种方式使用curl命令来访问kubia服务:

root@kubia-3inly:/# curl http://kubia.default.svc.cluster.local
You've hit kubia-5asi2 
root@kubia-3inly:/# curl http://kubia.default
You've hit kubia-3inly 
root@kubia-3inly:/# curl http://kubia
You've hit kubia-8awf3

  在请求的URL中,可以将服务的名称作为主机名来访问服务。因为根据每个pod容器DNS解析器配置的方式,可以将命名空间和svc.cluster.local后缀省略掉。查看一下容器中的/etc/resolv.conf文件就明白了。

root@kubia-3inly:/# cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local ...

  无法ping通服务IP的原因(在k8s1.12版本可以ping通,本章根据1.8做为讲解)

  在继续之前还有最后一问题。了解了如何创建服务,很快地去自己创建一个。但是,不知道什么原因,无法访问创建的服务。

  大家可能会尝试通过进入现有的Pod,并尝试像上一个示例那样访问该服务来找出问题所在。然后,如果仍然无法使用简单的curl命令访问服务,也许会尝试ping服务IP以查看服务是否己启动。现在来尝试一下:

root@kubia-3inly:/# ping kubia
PING kubia.default.svc.cluster.local (10.111.249.153): 56 data bytes
^C--- kubia.default.svc.cluster.local ping statistics ---
54 packets transmitted, 0 packets received, 100% packet loss

  嗯,curl这个服务是工作的,但是却ping不通。这是因为服务的集群IP是一个虚拟IP,并且只有在与服务端口结合时才有意义。以及服务是如何工作的。在这里提到这个问题,因为这是用户在尝试调试异常服务时会做的第一件事(ping服务的IP),而服务的IP无法ping通会让大多数人措手不及。

  为什么在k8s有些版本中可以ping通,有些无法ping通,因为具体就要设置到设置的网络模式原因,iptables不支持,但是ipvs支持,1.12以上版本基本都是ipvs的方式。具体介绍见以下连接。

 

2.连接集群外部的服务

  到现在为止,己经讨论了后端是集群中运行的一个或多个pod的服务。但也存在希望通过Kubernetes服务特性暴露外部服务的情况。不要让服务将连接重定向到集群中的pod,而是让它重定向到外部IP和端口。

  这样做可以充分利用服务负载平衡和服务发现。在集群中运行的客户端pod可以像连接到内部服务一样连接到外部服务。

 

2.1 介绍服务endpoint

  在进入如何做到这一点之前,先阐述一下服务。服务并不是和pod直接相连的。 相反,有一种资源介于两者之间——-它就是Endpoint资源。如果之前在服务上运行过kubectl describe,可能已经注意到了endpoint,如下面的代码清单所示。

$ kubectl describe svc kubia
Name:              kubia
Namespace:         default
Labels:            <none>
Selector:          app=kubia                        #用于创建endpoint列表的服务pod选择器
Type:              ClusterIP 
IP:                10.111.249.153
Port:              <unset>  80/TCP
Endpoints:         10.108.1.4:8080,10.108.2.5:8080,10.108.2.6:8080             #代表服务endpoint的pod的IP和端口列表
Session Affinity:  None
Events:            <none>

  Endpoint资源就是暴露一个服务的IP地址和端口的列表,Endpoint资源和其他 Kubernetes资源一样,所以可以使用kubectl info来获取它的基本信息。

$ kubectl get endpoints kubia
NAME    ENDPOINTS                                           AGE
kubia    10.108.1.4:8080,10.108.2.5:8080,10.108.2.6:8080    1h

  尽管在spec服务中定义了pod选择器,但在重定向传入连接时不会直接使用它。 相反,选择器用于构建IP和端口列表,然后存储在Endpoint资源中。当客户端连接到服务时,服务代理选择这些IP和端口对中的一个,并将传入连接重定向到在该位置监听的服务器。

 

2.2 手动配置服务的endpoint

  或许己经意识到这一点,服务的endpoint与服务解耦后,可以分别手动配置和更新它们。

  如果创建了不包含pod选择器的服务,Kubernetes将不会创建Endpoint资源(毕竟,缺少选择器,将不会知道服务中包含哪些pod)。这样就需要创建Endpoint资源 来指定该服务的endpoint列表。

  要使用手动配置endpoint的方式创建服务,需要创建服务和Endpoint资源。

  创建没有选择器的服务

  首先为服务创建一个YAML文件,如下面的代码清单所示。

#代码5.8 不含pod选择器的服务
apiVersion: v1
kind: Service
metadata:
  name: external-service           #服务的名字必须和Endpoint对象的名字相匹配
spec:                                            #服务中没有定义选择器
  ports:
  - port: 80

  定义一个名为external-service的服务,它将接收端口80上的传入连接。并没有为服务定义一个pod选择器。

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

  Endpoint是一个单独的资源并不是服务的一个属性。由于创建的资源中并不包含选择器,相关的Endpoints资源并没有自动创建,所以必须手动创建。如下所示的代码清单中列出了YAML manifest。

apiVersion: v1
kind: Endpoints
metadata:
  name: external-service                     #Endpoint的名称必须和服务的名称相匹配
subsets:
  - addresses:                                      #服务连接重定向到endpoint的IP地址
    - ip: 11.11.11.11
    - ip: 22.22.22.22
    ports:                           
    - port: 80                                         #endpoint的目标端口

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

  图5.4显示了三个pod连接到具有外部endpoint的服务。

  如果稍后决定将外部服务迁移到Kubernetes中运行的pod,可以为服务添加选择器,从而对Endpoint进行自动管理。反过来也是一样的——将选择器从服务中移除, Kubernetes将停止更新Endpoints。这意味着服务的IP地址可以保持不变,同时服务的实际实现却发生了改变。

 

2.3 为外部服务创建别名

  除了手动配置服务的Endpoint来代替公开外部服务方法,有一种更简单的方法, 就是通过其完全限定域名(FQDN)访问外部服务

  创建ExtemalName类型的服务

  要创建一个具有别名的外部服务的服务时,要将创建服务资源的一个type字段设置为ExternalName。例如,设想一下在api.somecompany.com上有公共可用的API,可以定义一个指向它的服务,如下面的代码清单所示。

#代码5.10 ExternalName类型的服务
apiVersion: v1
kind: Service
metadata:
  name: external-service           
spec:
  type: ExternalName                    #type类型为ExternalName
  externalName: someapi.somecompany.com           #实际服务的完全限定域名                                   
  ports:
  - port: 80

  服务创建完成,pod可以通过external-service.default.svc.cluster.local域名(甚至是external-service)连接到外部服务,而不是使用服务的实际FQDN。这隐藏了实际的服务名称及其使用该服务的pod的位置,允许修改服务定义,并且在以后如果将其指向不同的服务,只需简单地修改externalNamie属性,或者将类型重新变回ClusterIP并为服务创建Endpoint---无论是手动创建,还是对服务上指定标签选择器使其自动创建。

  ExternalName服务仅在DNS级别实施-为服务创建了简单的CNAME DNS记录。因此,连接到服务的客户端将直接连接到外部服务,完全绕过服务代理。出于这个原因,这些类型的服务甚至不会获得集群IP。

  注意:CNAME记录指向完全限定的域名而不是数字IP地址。

 

3.将服务暴露给外部客户端

  到目前为止,只讨论了集群内服务如何被pod使用;但是,还需要向外部公开某些服务。例如前端web服务器,以便外部客户端可以访问它们。

  有几种方式可以在外部访问服务:

    • 将服务的类型设置成NodePort---每个集群节点都会在节点上打开一个端口,对于NodePort服务,每个集群节点在节点本身(因此得名叫NodePort)上打开一个端口,并将在该端口上接收到的流量重定向到基础服务。该服务仅在内部集群IP和端口上才可访问,但也可通过所有节点上的专用端口访问。
    • 将服务的类型设置成LoadBalance,NodePort类型的一种扩展---这使得服务可以通过一个专用的负载均衡器来访问,这是由Kubernetes中正在运行的云基础设施提供的。负载均衡器将流量重定向到跨所有节点的节点端口。客户端通过负载均衡器的IP连接到服务。
    • 创建一个Ingress资源,这是一个完全不同的机制,通过一个IP地址公开多个服务---它运行在HTTP层(网络协议第7层)上,因此可以提供比工作在第4层的服务更多的功能。Ingress在另外讲解。

 

3.1 使用NodePort类型的服务

  将一组pod公开给外部客户端的第一种方法是创建一个服务并将其类型设置为NodePort。通过创建NodePort服务,可以让Kubernetes在其所有节点上保留一个端口(所有节点上都使用相同的端口号),并将传入的连接转发给作为服务部分的pod。

  这与常规服务类似(它们的实际类型是ClusterIP),但是不仅可以通过服务的内部集群IP访问NodePort服务,还可以通过任何节点的IP和预留节点端口访问NodePort服务。

  当尝试与NodePort服务交互时,意义更加重大。

  创建NodePort类型的服务

  现在将创建一个NodePort服务,以查看如何使用它。下面的代码清单显示了服务的YAML。

apiVersion: v1
kind: Service
metadata:
  name: kubia-nodeport
spec:
  type: NodePort        #为NodePort设置服务类型
  ports:
  - port: 80                  #服务集群IP的端口号
    targetPort: 8080 
    nodePort: 30123    #通过集群节点的30123端口可以访问该服务
  selector:
    app: kubia

  将类型设置为NodePort并指定该服务应该绑定到的所有集群节点的节点端口。指定端口不是强制性的。如果忽略它,Kubernetes将选择一个随机端口。

  注意:如果在GKE中创建服务时,kubectl打印出一个关于必须配置防火墙规则的警告。接下来的章节将讲述如何处理。

  查看NodePort类型的服务

  查看该服务的基础信息:

$ kubectl get svc kubia-nodeport
NAME             CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
kubia-nodeport   10.111.254.223   <nodes>       80:30123/TCP   2m

  看看EXTERNAL-IP列。它显示nodes,表明服务可通过任何集群节点的IP地址访问。PORT(S)列显示集群IP(80)的内部端口和节点端口(30123),可以通过以下地址访问该服务:

    • 10.11.254.223:80
    • <1stnode'sIP>:30123
    • <2ndnode'sIP>:30123,等等

  图5.6显示了服务暴露在两个集群节点的端口 30123上(这适用于在GKE上运行的情况;Minikube只有一个节点,但原理相同)。到达任何一个端口的传入连接将被重定向到一个随机选择的pod,该pod是否位于接收到连接的节点上是不确定的。

  在第一个节点的端口30123收到的连接,可以被重定向到第一节点个上运行的pod,也可能是第二个节点上运行的pod。

  更改防火墙规则,让外部客户端访问NodePort服务

  如前所述,在通过节点端口访问服务之前,需要配置谷歌云平台的防火墙,以允许外部连接到该端口上的节点,如下所示。

$ gcloud compute firewall-rules create kubia-svc-rule --allow=tcp:30123
Created [https://www.googleapis.com/compute/v1/projects/kubia-
     1295/global/firewalls/kubia-svc-rule].
NAME            NETWORK  SRC_RANGES  RULES      SRC_TAGS  TARGET_TAGS
kubia-svc-rule  default  0.0.0.0/0   tcp:30123

  可以通过其中一个节点的IP的端口30123访问服务,但是需要首先找出节点的IP请 阅补充内容了解如何做到这一点。

  正如所看到的,现在整个互联网可以通过任何节点上的30123端口访问到pod。客户端发送请求的节点并不重要。但是,如果只将客户端指向第一个节点,那么当该节点发生故障时,客户端无法再访问该服务。这就是为什么将负载均衡器放在节点前面以确保发送的请求传播到所有健康节点,并且从不将它们发送到当时处于脱机状态的节点的原因。

  如果Kubernetes集群支持它(当Kubernetes部署在云基础设施上时,大多数情况都是如此),那么可以通过创建一个Load Badancer而不是NodePort服务自动生成负载均衡器。接下来介绍此部分。

 

3.2 通过负载均衡器将服务暴露出来

  在云提供商上运行的Kubernetes集群通常支持从云基础架构自动提供负载平衡器。所有需要做的就是设置服务的类型为Load Badancer而不是NodePort。负载均衡器拥有自己独一无二的可公开访问的IP地址,并将所有连接重定向到服务。可以通过负载均衡器的IP地址访问服务。

  如果Kubernetes在不支持Load Badancer服务的环境中运行,则不会调配负载平衡器,但该服务仍将表现得像一个NodePort服务。这是因为Load Badancer服务是NodePort服务的扩展。可以在支持Load Badancer服务的 GoogleKubemetesEngine上运行此示例。Minikube没有。

  当然,具体还有腾讯云,阿里云以及AWS的具体操作会在单独的篇章中讲解。

  创建LoadBalance服务

 要使用服务前面的负载均衡器,请按照以下YAML manifest创建服务,代码清单如下所示。

apiVersion: v1
kind: Service
metadata:
  name: kubia-loadbalancer
spec:
  type: LoadBalancer         #该服务从Kubernetes集群的基础架构获取负载平衡器
  ports:
  - port: 80
    targetPort: 8080
  selector:
    app: kubia

  服务类型设置为LoadBalancer而不是NodePort。如果没有指定特定的节点端口,Kubernetes将会选择一个端口。

  通过负载均衡器连接服务

  创建服务后,云基础架构需要一段时间才能创建负载均衡器并将其IP地址写入服务对象。一旦这样做了,IP地址将被列为服务的外部IP地址:

$ kubectl get svc kubia-loadbalancer
NAME                 CLUSTER-IP       EXTERNAL-IP      PORT(S)         AGE
kubia-loadbalancer   10.111.241.153   130.211.53.173   80:32143/TCP    1m

  在这种情况下,负载均衡器的IP地址为130.211.53.173,因此现在可以通过该IP地址访问该服务:

$ curl http://130.211.53.173
You've hit kubia-xueq1
  提示:会话亲和性和Web浏览器
  由于服务现在已暴露在外,因此可以尝试使用网络浏览器访问它。但是会看到一些可能觉得奇怪的东西——每次浏览器都会碰到同一个pod。此时服务的会话亲和性是否发生变化?使用kubectl explain,可以再次检查服务的会话亲缘性是否仍然设置为None,那么为什么不同的浏览器请求不会碰到不同的pod,就像使用curl时那样?
  现在阐述为什么会这样。浏览器使用keep-alive连接并通过单个连接发送所有请求,而curl每次都会打开一个新连接。服务在连接级别工作,所以当首次打开与服务的连接时,会选择一个随机集群,然后将属于该连接的所有网络数据包全部发送到单个集群。即使会话亲和性设置为None,用户也会始终使用相同的pod(直到连接关闭)。

  请参阅图5.7, 了解HTTP请求如何传递到该pod。外部客户端(可以使用curl)连接到负载均衡器的80端口,并路由到其中一个节点上的隐式分配节点端口。之后该连接被转发到一个pod实例。

  如前所述,LoadBalancer类型的服务是一个具有额外的基础设施提供的负载平衡器NodePort服务。如果使用kubectl describe来显示有关该服务的其他信息,则会看到为该服务选择了一个节点端口。如果要为此端口打开防火墙,就像在上一节中对NodePort服务所做的那样,也可以通过节点IP访问服务。

  提示如果使用的是Minikube,尽管负载平衡器不会被分配,仍然可以通过节点端口(位于MnikubeVM的IP地址)访问该服务,

 

3.3 了解外部连接的特性

  必须了解与服务的外部发起的连接有关的几件事情。

  了解并防止不必要的网络跳数

  当外部客户端通过节点端口连接到服务时(这也包括先通过负载均衡器时的情况),随机选择的pod并不一定在接收连接的同一节点上运行。可能需要额外的网络跳转才能到达pod,但这种行为并不符合期望。

  可以通过将服务配置为仅将外部通信重定向到接收连接的节点上运行的pod来阻止此额外跳数。这是通过在服务的spec部分中设置externalTrafficPolicy 字段来完成的:

spec:
    externalTrafficPolicy: Local

  如果服务定义包含此设置,并且通过服务的节点端口打开外部连接,则服务代理将选择本地运行的pod。如果没有本地pod存在,则连接将挂起(它不会像不使用注解那样,将其转发到随机的全局pod)。因此,需要确保负载平衡器将连接转发给至少具有一个pod的节点。

  使用这个注解还有其他缺点。通常情况下,连接均匀分布在所有的pod上,但使用此注解时,情况就不再一样了。

  想象一下两个节点有三个pod。假设节点A运行一个pod,节点B运行另外两个pod。如果负载平衡器在两个节点间均匀分布连接,则节点A上的pod将接收所有连接的50%,但节点B上的两个pod每个只能接收25%,如图5.8所示。

  EKS上的实践(接上一节)

  在AWS的EKS上实践,如果改成了externalTrafficPolicy: Local的模式,那么就会看到LoadBalancer的后端存活节点不是所有的。

  因是因为这是一个假象,要充分理解改为local之后的后果。现象为:aws的LoadBalancer后端只有一个节点存活了。因为3个节点上只有一个pod,只有有pod的节点才会接收请求,导致在LoadBlancer上之看到只有一个node节点是存活的。

  记住客户端IP是不记录的

  通常,当集群内的客户端连接到服务时,支持服务的pod可以获取客户端的IP地址。但是,当通过节点端口接收到连接时,由于对数据包执行了源网络地址转换(SNAT),因此数据包的源IP将发生更改。

  后端的pod无法看到实际的客户端IP,这对于某些需要了解客户端IP的应用程序来说可能是个问题。例如,对于Web服务器,这意味着访问日志无法显示浏览器的IP。

  上一节中描述的local外部流量策略会影响客户端IP的保留,因为在接收连接的节点和托管目标pod的节点之间没有额外的跳跃(不执行SNAT)。

将服务给暴露出去其实还有其他办法,比如,ingress等,其他篇幅讲解。

posted @ 2021-05-17 14:36  小家电维修  阅读(574)  评论(0编辑  收藏  举报