5.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.1 创建服务
服务的后端可以有不止一个pod。服务的连接对所有的后端pod是负载均衡的。但是要如何准确地定义哪些pod属于服务哪些不属于呢?
或许还记得在ReplicationController和其他的pod控制器中使用标签选择器来指定哪些pod属于同一组。服务使用相同的机制。
在前面的章节中,通过创建ReplicationController运行了三个包含Node.js应用的pod。再次创建ReplicationController并且确认pod启动运行,在这之后将会为这三个pod创建一个服务。
通过kubectl expose创建服务
创建服务的最简单的方法是通过kubectl expose,在第2章中曾使用这种方法来暴露创建的ReplicationController。像创建ReplicationController时使用的pod选择器那样,利用expose命令和pod选择器来创建服务资源,从而通过单个的IP和端口来访问所有的pod。
现在,除了使用expose命令,可以通过将配置的YAML文件传递到Kubernetes API服务器来手动创建服务。
通过YAML描述文件来创建服务
使用以下代码清单中的内容创建一个名为kubia-svc.yaml的文件。
代码清单5.1 服务的定义:kubia-svc.yaml
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
ports:
- port: 80 #服务可用端口
targetPort: 8080 #转发的容器端口
selector:
app: kubia #具有app=kubia标签的pod都属于该服务
附:deployment文件
apiVersion: apps/v1
kind: Deployment
metadata:
name: kubia
labels:
app: kubia
spec:
replicas: 3
selector:
matchLabels:
app: kubia
template:
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia
image: luksa/kubia:1.0
ports:
- containerPort: 8080
创建了一个名叫kubia的服务,它将在端口80接收请求并将连接路由到具有标签选择器是app=kubia的pod的8080端口上。
接下来通过使用kubectl create发布文件来创建服务。
检测新的服务
在发布完YAML文件后,可以在命名空间下列出来所有的服务资源,并可以发现新的服务已经被分配了一个内部集群IP。
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubia ClusterIP 10.111.56.158 <none> 80/TCP 11s
列表显示分配给服务的IP地址是 10.111.56.158
。因为只是集群的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
命令),当执行下述命令时,请确保替换对应pod的名称及服务IP地址。
$ kubectl exec kubia-7d5b548867-fnvl5 -- curl -s http://10.111.56.158
如果之前使用过ssh命令登录到一个远程系统,会发现kubectl exec没有特别大的不同之处。
为什么是双横杠?
双横杠(--)代表着kubectl命令项的结束。在两个横杠之后的内容是指在pod内部需要执行的命令。如果需要执行的命令并没有以横杠开始的参数,横杠也不是必需的。
回顾一下在运行命令时发生了什么。在一个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
......
这种方式将会使服务代理将来自同一个client IP的所有请求转发至同一个pod上。作为练习,创建额外的服务并将会话亲和性设置为ClientIP,并尝试向其发送请求。
Kubernetes仅仅支持两种形式的会话亲和性服务:None和ClientIP。你或许惊讶竟然不支持基于cookie的会话亲和性的选项,但是你要了解Kubernetes 服务不是在HTTP层面上工作。服务处理TCP和UDP包,并不关心其中的载荷内容。因为cookie是HTTP协议中的一部分,服务并不知道它们,这就解释了为什么会话亲和性不能基于cookie。
同一个服务暴露多个端口
创建的服务可以暴露一个端口,也可以暴露多个端口。比如,你的pod监听两个端口,比如HTTP监听8080端口、HTTPS监听8443端口,可以使用一个服务从端口80和443转发至pod端口8080和8443。在这种情况下,无须创建两个不同的服务。通过一个集群IP,使用一个服务就可以将多个端口全部暴露出来。
注意 在创建一个有多个端口的服务的时候,必须给每个端口指定名字。
以下代码清单中展示了多端口服务的规格。
代码清单5.3 在服务定义中指定多端口
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
selector:
app: kubia #标签选择器适用于整个服务
ports:
- name: http
protocol: TCP
port: 80
targetPort: 8080
- name: https
protocol: TCP
port: 443
targetPort: 8081
注意 标签选择器应用于整个服务,不能对每个端口做单独的配置。如果不同的pod有不同的端口映射关系,需要创建两个服务。
之前创建的kubia pod不在多个端口上侦听,因此可以练习创建一个多端口服务和一个多端口pod。
使用命名的端口 在这些例子中,通过数字来指定端口,但是在服务spec中也可以给不同的端口号命名,通过名称来指定。这样对于一些不是众所周知的端口号,使得服务spec更加清晰。在服务spec中按名称引用这些端口,如下面的代码清单所示。
代码清单5.5 在服务中引用命名pod
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
selector:
app: kubia #标签选择器适用于整个服务
ports:
- name: http
port: 80
targetPort: http #将80端口映射到容器中被称为http的端口
- name: https
port: 443
targetPort: https #将443端口映射到容器中被称为https的端口
为什么要采用命名端口的方式?最大的好处就是即使更换端口号也无须更改服务spec。你的pod现在对http服务用的是8080,但是假设过段时间你决定将端口更换为80呢?
如果你采用了命名的端口,仅仅需要做的就是改变spec pod 中的端口号(当然你的端口号的名称没有改变)。在你的pod向新端口更新时,根据pod收到的连接(8080端口在旧的pod上、80端口在新的pod上),用户连接将会转发到对应的端口号上。
5.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使得ReplicatSet创建全新的pod。在无须知道pod的名字的情况下就能删除所有的pod,就像这样:
$ kubectl delete po --all
现在列出所有新的pod,然后选择一个作为kubectl exec命令的执行目标。一旦选择了目标pod,通过在容器中运行env来列出所有的环境变量,如下面的代码清单所示。
代码清单5.6 容器中和服务相关的环境变量
$ k exec kubia-7d5b548867-4qn2x -- env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
KUBIA_SERVICE_HOST=10.111.56.158 # 服务的集群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发现服务
还记得第3章中在kube-system命名空间下列出的所有pod的名称吗?其中一个pod被称作kube-dns,当前的kube-system的命名空间中也包含了一个具有相同名字的响应服务。
k get svc -A
就像名字的暗示,这个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,这样可以在容器上运行多条命令。在第2章中,当想进入容器启动Docker时,调用 docker exec-it bash
命令,这与此很相似。
在pod容器中运行shell
可以通过kubectl exec命令在一个pod容器上运行bash(或者其他形式的shell)。通过这种方式,可以随意浏览容器,而无须为每个要运行的命令执行kubectl exec。
注意 shell的二进制可执行文件必须在容器镜像中可用才能使用。
为了正常地使用shell,kubectl exec
命令需要添加–it选项:
$ kubectl exec -it kubia-3inly -- bash
现在进入容器内部,根据下述的任何一种方式使用curl命令来访问kubia服务:
# curl http://kubia.custom.svc.cluster.local
Hey there, this is kubia-7d5b548867-8wnls. Your IP is ::ffff:172.18.0.1.
# curl http://kubia.custom
Hey there, this is kubia-7d5b548867-8wnls. Your IP is ::ffff:172.18.0.1.
# curl http://kubia
Hey there, this is kubia-7d5b548867-m4bfz. Your IP is ::ffff:172.18.0.1.
在请求的URL中,可以将服务的名称作为主机名来访问服务。因为根据每个pod容器DNS解析器配置的方式,可以将命名空间和svc.cluster.local后缀省略掉。查看一下容器中的/etc/resilv.conf文件就明白了。
# cat /etc/resolv.conf
nameserver 10.96.0.10
search test.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
无法ping通服务IP的原因
在继续之前还有最后一问题。了解了如何创建服务,很快地去自己创建一个。但是,不知道什么原因,无法访问创建的服务。
大家可能会尝试通过进入现有的pod,并尝试像上一个示例那样访问该服务来找出问题所在。然后,如果仍然无法使用简单的curl命令访问服务,也许会尝试ping 服务 IP以查看服务是否已启动。现在来尝试一下:
# ping kubia
PING kubia.test.svc.cluster.local (10.99.93.78): 56 data bytes
^C--- kubia.test.svc.cluster.local ping statistics ---
91 packets transmitted, 0 packets received, 100% packet loss
嗯,curl这个服务是工作的,但是却ping不通。这是因为服务的集群IP是一个虚拟IP,并且只有在与服务端口结合时才有意义。将在第11章中解释这意味着什么,以及服务是如何工作的。在这里提到这个问题,因为这是用户在尝试调试异常服务时会做的第一件事(ping服务的IP),而服务的IP无法ping通会让大多数人措手不及。
网友评论