K8s in Action 阅读笔记——【5】Services: enabling clients to discover and talk to pods
你已了解Pod以及如何通过ReplicaSets等资源部署它们以确保持续运行。虽然某些Pod可以独立完成工作,但现今许多应用程序需要响应外部请求。例如,在微服务的情况下,Pod通常会响应来自群集内部或群集外部客户端的HTTP请求。
如果Pod想要使用其他Pod提供的服务,它们需要找到这些Pod,但与非Kubernetes世界不同,系统管理员无法在客户端配置文件中指定提供服务的服务器的确切IP地址或主机名,因为Pod是临时的。
在Kubernetes中,Pod在调度到节点后在启动之前会被分配一个IP地址,因此客户端在调用之前无法知道Pod的IP地址。此外,由于水平扩展,多个Pod可能提供相同的服务,每个Pod都有自己的IP地址。因此,客户端应该通过单个IP地址访问所有这些Pod,而不需要关心支持服务的Pod数量及其IP地址,不必维护所有单个Pod IP的列表。
为解决上述问题,kubernetes提供了一个资源——Service。
5.1 Introducing services
Kubernetes Service
是为了创建一个单一和恒定的入口点到一组提供相同服务的Pod而创建的资源。每个服务都有一个IP地址和端口,只要该服务存在,它们不会改变。客户端可以打开到该IP和端口的连接,然后这些连接会被路由到支持该服务的Pod之一。这样,服务的客户端不需要知道提供服务的单个Pod的位置,从而允许这些Pod随时在集群中移动。
让我们回顾一下这样一个例子:你有一个前端Web服务器和一个后端数据库服务器。可能会有多个Pod都充当前端,但只有一个后端数据库Pod。你需要解决两个问题才能使系统正常运行:
- 外部客户端需要连接到前端Pod,而不需要关心是否只有一个Web服务器或数百个。
- 前端Pod需要连接到后端数据库。因为数据库运行在一个Pod内,所以它可能随着时间的推移在集群中移动,导致其IP地址发生变化。你不希望每次移动后端数据库时都重新配置前端Pod。
通过为前端Pod创建一个Service并配置为可从集群外部访问,你可以公开一个单一且固定的IP地址,外部客户端可以通过该地址连接到Pod。同样,通过为后端Pod创建一个Service,你为后端Pod创建了一个稳定的地址。即使Pod的IP地址发生变化,服务地址也不会改变。此外,通过创建Service,你还可以通过环境变量或DNS轻松地通过名称找到后端服务,以便前端Pod找到它。图5.1显示了系统的所有组件(两个Service、支持这些Service的两组Pod以及它们之间的相互依赖关系)。
现在你已经理解了服务背后的基本思想,接下来让我们深入了解如何创建它们。
5.1.1 Creating services
一个服务可以由多个Pod支持,连接会在这些支持Pod之间进行负载平衡。但是,怎么定义哪些Pod是服务的一部分呢?你可能还记得标签选择器在ReplicationController和其他Pod控制器中的使用,以指定同一集合内的Pod。服务也使用同样的机制,如图5.2所示。
通过kubectl expose创建服务
创建服务最简单的方法是通过kubectl expose
命令,在第2章中使用它来暴露先前创建的ReplicationController。该命令创建了一个Service资源,其Pod选择器与ReplicationController使用的相同,从而通过单个IP地址和端口公开所有Pod。
现在,你将不使用expose命令,而是通过向Kubernetes API服务器发布YAML手动创建服务。
通过YAML文件创建服务
# kubia-svc.yaml
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
selector:
app: kubia
ports:
- port: 80
targetPort: 8080
正在定义一个名为kubia的服务,它将接受端口80上的连接,并将每个连接路由到与app=kubia标签选择器匹配的一个Pod的端口8080。
测试服务
应用上述YAML后,可以查看已经创建的Service:
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 14d
kubia ClusterIP 10.97.113.157 <none> 80/TCP 6s
从列表中可以看出,分配给该服务的IP地址为10.97.113.157
。因为这是集群IP,所以只能从集群内部访问它。服务的主要目的是向集群中的其他pod公开pod组,但通常也希望向外部公开服务。
可以通过以下几种方式向集群内的服务发送请求:
- 最显而易见的方式是创建一个 pod,将请求发送到服务的集群 IP,记录响应。然后,可以检查 pod 的日志,看看服务的响应是什么。
- 可以 ssh 进入其中一个 Kubernetes 节点,使用 curl 命令。
- 可以通过 kubectl exec 命令在一个现有的 pod 中执行 curl 命令。
kubectl exec
命令允许你远程在 pod 中现有的容器内运行任意命令。当你想要检查一个容器的内容、状态和/或环境时,这个命令非常有用。使用 kubectl get pods 命令列出 pods 并选择一个 pod 作为 exec 命令的目标(在下面的示例中,我选择了 kubia-7nog1 pod 作为目标)。你还需要获取你的服务的集群 IP(例如使用 kubectl get svc 命令)。在自己运行下面的命令时,请务必使用你自己的 pod 名称和服务 IP 替换它们:
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
kubia-2k9d4 1/1 Running 0 12m
kubia-x4tl7 1/1 Running 0 12m
kubia-xqvlb 1/1 Running 0 12m
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 14d
kubia ClusterIP 10.97.113.157 <none> 80/TCP 12m
$ kubectl exec kubia-2k9d4 -- curl -s http://10.97.113.157
You've hit kubia-xqvlb
命令中的双破折线(–)表示命令选项的结束,后面的所有内容都是应该在 pod 中执行的命令。如果命令没有以破折号开头的参数,则不需要使用双破折线。但在你的情况下,如果你不在这里使用双破折线,则 -s 选项会被解释为 kubectl exec 的选项,并导致错误
让我们回顾一下运行该命令时发生的情况。图5.3显示了事件序列。你指示Kubernetes在一个pod的容器中执行curl命令。curl
向由三个pod支持的服务IP发送了一个HTTP请求。Kubernetes服务代理拦截了连接,从三个pod中选择了一个随机的pod,并将请求转发给它。运行在该pod中的Node.js然后处理了请求并返回了一个包含pod名称的HTTP响应。curl
然后将响应打印到标准输出中,kubectl拦截并将其打印到本地机器的标准输出中。
在服务上配置会话关联
如果你多次执行相同的命令,每次调用应该都会命中不同的pod,因为服务代理通常会将每个连接转发到一个随机选择的后备pod,即使连接来自同一个客户端。然而,如果你希望某个特定客户端的所有请求都被重定向到同一个pod,你可以将服务的sessionAffinity属性设置为ClientIP(而不是默认值None),如下所示。
这将使服务代理将所有源自同一个客户端IP的请求重定向到同一个pod。作为一个练习,你可以创建一个额外的服务,并将 session affinity设置为 ClientIP,然后尝试发送请求。Kubernetes仅支持两种类型的服务会话亲和性: None和ClientIP。Kubernetes服务不在HTTP级别上操作。服务处理TCP和UDP数据包,并不关心它们携带的有效负载。因为cookie是HTTP协议的一种构造,所以服务不知道它们,这就解释了为什么会话亲和性不能基于cookie。
在同一服务中公开多个端口
你的服务只暴露了一个端口,但服务也可以支持多个端口。例如,如果你的 pod 监听了两个端口,比如说 8080 用于 HTTP,8443 用于 HTTPS,你可以使用一个单一的服务将端口80和443都转发到 pod 的端口8080和8443上。
创建具有多个端口的服务时,必须为每个端口指定名称。
示例文件如下:
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
ports:
- name: http
port: 80
targetPort: 8080
- name: https
port: 443
targetPort: 8443
selector:
app: kubia
标签选择器作为一个整体应用于服务——它不能为每个端口单独配置。如果希望不同的端口映射到pod的不同子集,则需要创建两个服务。
使用命名端口
在所有这些示例中,你都是通过端口号来引用目标端口的,但是你也可以为每个pod的端口指定一个名称,并在服务规范中通过名称来引用它。这会使服务规范稍微清晰一些,特别是在端口号不为人所知的情况下。
示例如下:
# 在一个Pod中命名端口
kind: Pod
spec:
containers:
- name: kubia
ports:
- name: http
containerPort: 8080
- name: https
containerPort: 8443
然后,可以在服务规范中按名称引用这些端口,如下所示。
# 在Service中引用已经命名的端口
apiVersion: v1
kind: Service
spec: ports:
- name: http
port: 80
targetPort: http
- name: https
port: 443
targetPort: https
给端口命名能够使你以后在不改变Service配置的情况下更改端口号。
5.1.2 Discovering services
客户端 pods 怎么知道服务的 IP 和端口号呢?你需要先创建服务,然后手动查找它的 IP 地址,并将 IP 地址传递给客户端 pod 的配置选项中吗?并不是。Kubernetes 还提供了一些方式让客户端 pod 来发现服务的 IP 地址和端口号。
通过环境变量发现服务
当一个 pod 启动时,Kubernetes 会初始化一组环境变量,指向每个此时存在的服务。如果在创建客户端 pods 之前就创建了服务,这些 pods 中的进程可以通过检查它们的环境变量来获取服务的 IP 地址和端口。
为了查看这些环境变量的内容,让我们检查一个正在运行的 pod 中的环境变量。你已经学会了可以使用 kubectl exec 命令在 pod 中运行命令,但是由于你在创建 pods 之后才创建了服务,服务的环境变量可能还没有被设置。
因此,你需要首先解决这个问题。在查看服务的环境变量之前,你需要先删除所有的 pods,并让 ReplicationController 创建新的 pods:
$ kubectl delete pod --all
pod "kubia-2k9d4" deleted
pod "kubia-x4tl7" deleted
pod "kubia-xqvlb" deleted
再来看看重新创建的Pod中的环境变量:
$ kubectl exec kubia-ppvdj env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=kubia-ppvdj
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBIA_PORT_80_TCP_ADDR=10.97.113.157
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_PORT=443
KUBIA_SERVICE_PORT=80
KUBIA_PORT_80_TCP=tcp://10.97.113.157:80
KUBIA_PORT_80_TCP_PROTO=tcp
KUBERNETES_SERVICE_PORT=443
KUBIA_PORT=tcp://10.97.113.157:80
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBIA_SERVICE_HOST=10.97.113.157 # 服务的集群IP
KUBIA_PORT_80_TCP_PORT=80
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
NPM_CONFIG_LOGLEVEL=info
NODE_VERSION=7.9.0
YARN_VERSION=0.22.0
HOME=/root
通过DNS发现服务
还记得在第三章中当你列出 kube-system 命名空间中的 pods 吗?其中一个 pod 叫做 kube-dns。kube-system 命名空间还包括与该名称相同的相应服务。
正如其名称所示,该 pod 运行 DNS 服务器,所有在集群中运行的其他 pods 都会自动配置为使用该 DNS 服务器(Kubernetes 通过修改每个容器的 /etc/resolv.conf 文件来实现这一点)。在 pod 中运行的进程执行的任何 DNS 查询都将由 Kubernetes 自己的 DNS 服务器处理,该 DNS 服务器知道你的系统中运行的所有服务。
pod是否使用内部DNS服务器可以通过每个pod的规范中的
dnsPolicy
属性进行配置。
每个服务在内部DNS服务器中获得一个DNS条目,知道服务名称的客户机pod可以通过其完全限定域名(FQDN)访问它,而不是求助于环境变量。
在Pod容器中运行Shell
你可以使用kubectl exec命令在pod的容器中运行bash(或任何其他shell)。通过这种方式,你可以随意探索容器,而不必为要运行的每个命令执行kubectl exec。
要正确使用shell,你需要将-it
选项传递给kubectl exec
:
$ kubectl exec kubia-ppvdj -it -- bash
root@kubia-ppvdj:/#
你现在在容器里了。你可以使用curl命令以以下任何一种方式访问kubiaservice
root@kubia-ppvdj:/# curl http://kubia.default.svc.cluster.local
You've hit kubia-ppvdj
root@kubia-ppvdj:/# curl http://kubia.default
You've hit kubia-ppvdj
root@kubia-ppvdj:/# curl http://kubia
You've hit kubia-wxvx6
你可以使用服务的名称作为请求 URL 中的主机名来访问服务。因为每个 pod 容器内部的 DNS 解析器配置,你可以省略命名空间和 svc.cluster.local 后缀。查看容器中的 /etc/resolv.conf 文件:
root@kubia-ppvdj:/# cat /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local cs1cloud.internal
通过exit
命令退出shell
理解为什么不能ping通服务IP
尝试ping通服务IP
root@kubia-ppvdj:/# ping kubia
PING kubia.default.svc.cluster.local (10.97.113.157): 56 data bytes
^C--- kubia.default.svc.cluster.local ping statistics ---
4 packets transmitted, 0 packets received, 100% packet loss
发现无法Ping通,这是因为服务的集群 IP 是一个虚拟 IP,只有与服务端口结合才有意义。
5.2 Connecting to services living outside the cluster
到目前为止,我们已经讨论了由群集内运行的一个或多个 pod 支持的服务。但有些情况下,你想通过 Kubernetes 服务功能公开外部服务。而不是让服务重定向到群集中的 pod,你希望它重定向到外部 IP 和端口。
这使你能够利用服务负载均衡和服务发现。在群集中运行的客户端 pod 可以像连接内部服务一样连接到外部服务。
5.2.1 Introducing service endpoints
在介绍如何操作之前,先进一步解释服务的相关信息。服务并不会直接链接到 pod。相反,在它们之间会有一个资源存在——Endpoints 资源。如果在服务上使用了 kubectl describe 命令,你可能已经注意到了它的 endpoints,如下所示:
$ kubectl describe svc kubia
Name: kubia
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=kubia
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.97.113.157
IPs: 10.97.113.157
Port: <unset> 80/TCP
TargetPort: 8080/TCP
Endpoints: 10.244.1.200:8080,10.244.1.201:8080,10.244.1.202:8080
Session Affinity: None
Events: <none>
Endpoints资源是公开服务的IP地址和端口列表。端点资源和其他Kubernetes资源一样,所以你可以用kubectl get命令显示它的基本信息:
$ kubectl get endpoints kubia
NAME ENDPOINTS AGE
kubia 10.244.1.200:8080,10.244.1.201:8080,10.244.1.202:8080 94m
尽管pod选择器是在服务规范中定义的,但在重定向传入连接时并不直接使用它。相反,选择器用于构建ip和端口列表,然后将其存储在Endpoints资源中。当客户端连接到服务时,服务代理选择其中一个IP和端口对,并将传入的连接重定向到在该位置侦听的服务器。
5.2.2 Manually configuring service endpoints
服务端点与服务解耦使它们可以手动配置和更新。如果你创建了一个没有 Pod 选择器的服务,Kubernetes甚至不会创建Endpoints资源(毕竟,没有选择器,它不知道要包括哪些Pod在服务中)。你需要自己创建Endpoints资源来指定服务的端点列表。要创建具有手动管理端点的服务,你需要创建一个Service和一个Endpoints资源。
创建没有选择器的服务
# external-service.yaml
apiVersion: v1
kind: Service
metadata:
# 服务的名称必须与Endpoints的名称匹配
name: external-service
spec: # 没有定义选择器
ports:
- port: 80
为没有选择器的服务创建端点资源
Endpoints是一个单独的资源,而不是服务的属性。因为你没有使用选择器创建服务,因此相应的Endpoints资源没有自动创建,所以你需要自己创建。如下所示:
# external-service-endpoints.yaml
apiVersion: v1
kind: Endpoints
metadata:
# 服务的名称必须与Endpoints的名称匹配
name: external-service
subsets:
- addresses:
# 服务将转发连接到的端点的ip
- ip: 11.11.11.11
- ip: 22.22.22.22
ports:
# 端点的目标端口
- port: 80
Endpoints对象需要与服务具有相同的名称,并包含服务的目标IP地址和端口列表。在Service和Endpoints资源都被发送到服务器后,该服务就可以像具有Pod选择器的任何常规服务一样使用了。在创建服务之后创建的容器将包含服务的环境变量,并且所有连接到其IP:port
对的连接将在服务端点之间进行负载均衡。
图5.4显示了三个使用外部端点连接到服务的pod
如果后来你决定将外部服务迁移到在Kubernetes内运行的Pod,你可以向服务添加选择器,从而使它的Endpoints自动管理。相反地,去掉一个Service的选择器,就能让Kubernetes停止更新它的Endpoints。这意味着服务的IP地址可以保持不变,而服务的实际实现也可以发生变化。
5.2.3 Creating an alias for an external service
不必手动配置服务端点来暴露外部服务,还有一种更简单的方法,就是通过完全限定域名(fully qualified domain name FQDN
)引用外部服务。
创建ExternalName服务
要创建一个作为外部服务别名的服务,你可以创建一个类型字段设置为ExternalName的Service资源。例如,假设有一个公共API可用于api.somecompany.com
。你可以定义一个指向它的服务,如下所示。
# external-service-externalname.yaml
apiVersion: v1
kind: Service
metadata:
name: external-service
namespace: default
spec:
type: ExternalName
# 服务的完全限定域名
externalName: someapi.somecompany.com
ports:
- port: 80
创建服务之后,Pod可以通过external-service.default.svc.cluster.local
域名(甚至是externalservice)连接到外部服务,而无需使用服务的实际FQDN。
5.3 Exposing services to external clients
到目前为止,我们只讨论了如何在群集内部将服务提供给 Pod 使用。但是,你还想将某些服务(例如前端 Web 服务器)暴露给外部客户端,以便它们可以访问这些服务,如图 5.5 所示。
你有几种方式使服务可从外部访问:
- 将服务类型设置为
NodePort
——对于 NodePort 服务,每个群集节点都会在节点本身上打开一个端口(因此得名),并将接收到的流量重新定向到底层服务。该服务不仅可以在内部群集 IP 和端口上访问,还可以通过所有节点上的专用端口访问。 - 将服务类型设置为
LoadBalancer
,这是 NodePort 类型的扩展——这使服务通过云基础设施提供的专用负载均衡器可访问。负载均衡器会将流量重定向到所有节点上的节点端口。客户端通过负载均衡器的 IP 连接服务。 - 创建 Ingress 资源,这是一种通过单个 IP 地址暴露多个服务的根本不同的机制——它在 HTTP 层级(网络层 )运作,因此可以提供更多的功能。
5.3.1 Using a NodePort service
暴露一组pod给外部客户端的第一种方法是创建一个服务并将其类型设置为NodePort。通过创建一个NodePort服务,你让Kubernetes在所有节点上保留一个端口(相同的端口号在所有节点上使用),并将传入的连接转发给服务中的pod。
这类似于常规服务(实际类型为ClusterIP),但NodePort服务不仅可以通过服务的内部集群IP访问,还可以通过任何节点的IP和保留的节点端口访问。
创建NodePort服务
# kubia-svc-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
name: kubia-nodeport
namespace: default
spec:
selector:
app: kubia
type: NodePort
ports:
- port: 80 # 这是服务的内部集群IP的端口
targetPort: 8080 # 目标Pod的端口
nodePort: 30123 # 可以通过每个集群节点的端口30123访问该服务。
可以将类型设置为NodePort,并指定此服务应跨所有集群节点绑定到的节点端口。指定端口不是必需的;如果省略它,Kubernetes将选择一个随机端口。
测试NodePort服务
可以查看已经创建的NodePort服务
$ kubectl get svc kubia-nodeport
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubia-nodeport NodePort 10.105.54.75 <none> 80:30123/TCP 59s
看 EXTERNAL-IP 列。它显示 ,表示该服务可以通过任何群集节点的 IP 地址访问。PORT(S) 列显示了集群 IP 的内部端口 (80) 和节点端口 (30123)。该服务可以在以下地址访问:
- 10.105.54.75:80
- <节点一的IP>:30123
- <节点二的IP>:30123、等等
root@yjq-k8s1:~# curl 10.105.54.75:80
You've hit kubia-ppvdj
root@yjq-k8s1:~# curl 33.33.33.110:30123
You've hit kubia-ppvdj
yjq@DESKTOP-NKBDBDM:~$ curl 33.33.33.110:30123
You've hit kubia-qfwt4
图 5.6 显示了你的服务在集群节点的 30123 端口上暴露(如果你在 GKE 上运行,则适用此情况;Minikube 只有一个节点,但原理相同)。对其中任一端口的传入连接都将重定向到随机选择的一个 pod 上,该 pod 可能是正在运行传入连接所在的节点上的那个 pod,也可能不是。
30123 端口接收到的连接可能会被转发到第一个节点上运行的 pod 或者第二个节点上运行的 pod 中的其中一个。
5.3.2 Exposing a service through an external load balancer
在云提供商上运行的 Kubernetes 集群通常支持从云基础架构自动提供负载均衡器。你只需要将服务的类型设置为 LoadBalancer 而不是 NodePort。负载均衡器将拥有自己独特的、公开可访问的 IP 地址,并将所有连接重定向到你的服务。因此,你可以通过负载均衡器的 IP 地址访问你的服务。
如果 Kubernetes 运行在不支持 LoadBalancer 服务的环境中,负载均衡器将不会被提供,但服务仍将像 NodePort 服务一样运行。这是因为 LoadBalancer 服务是 NodePort 服务的一个扩展。
5.3.3 Understanding the peculiarities of external connections
理解和避免不必要的网络跳跃
当外部客户端通过节点端口连接到服务时(这也包括通过负载均衡器首先通过连接的情况),随机选择的 pod 可能正在运行与接收连接的节点不同的节点上。需要进行额外的网络跳跃才能到达 pod,但这可能并不总是理想的。
你可以通过将服务配置为仅将外部流量重定向到正在运行与接收连接的节点上的 pod,来防止这种额外跳跃。这是通过在服务的 spec 部分中设置 externalTrafficPolicy 字段来完成的:
spec:
externalTrafficPolicy: Local
如果一个服务定义包括这个设置,并且一个外部连接通过服务的节点端口打开,服务代理将选择一个本地运行的 pod。如果不存在本地的 pods,则连接将挂起(它不会像没有使用这个标注时那样被转发到一个随机的全局 pod)。因此,你需要确保负载均衡器仅将连接转发到至少有一个这样的 pod 的节点。
使用这个标注也有其他的缺点。通常,连接会均匀地分布在所有的 pods 中,但是当使用这个标注时,情况就不再如此了。
想象一下有两个节点和三个 pods。假设节点 A 运行一个 pod,节点 B 运行另外两个。如果负载均衡器均匀地将连接分布在这两个节点上,节点 A 上的 pod 将收到所有连接的 50%,但节点 B 上的两个 pods 将仅仅每个收到 25%,如图 5.8 所示。
注意客户端 IP 的非保留
通常情况下,当集群内部的客户端连接到服务时,支持服务的 pods 可以获得客户端的 IP 地址。但是当连接通过节点端口接收时,数据包的源 IP 会被更改,因为会对数据包执行源网络地址转换 (Source Network Address Translation,SNAT)。
支持 pod 无法看到实际的客户端 IP,这可能会对一些需要知道客户端 IP 的应用程序造成问题。例如,对于 web 服务器,这意味着访问日志将不会显示浏览器的 IP。
5.4 Exposing services externally through an Ingress resource
Ingress:入口
使用Ingress
其中一个重要的原因是,每个 LoadBalancer 服务都需要自己的负载均衡器和公共 IP 地址,而一个 Ingress 只需要一个,即使提供对数十个服务的访问。当客户端向 Ingress 发送 HTTP 请求时,请求中的主机和路径确定将请求转发到哪个服务,如图 5.9 所示。Ingress 在网络协议栈(HTTP)的应用程序层运行,并可以提供基于 cookie 的会话亲和性等功能,但服务不能提供这些功能。
本集群中使用Istio实现Ingress功能。
5.5 Signaling when a pod is ready to accept connections
如果 pod 的标签与服务的 pod 选择器匹配,那么它们将作为服务的endpoints包含在内。当创建一个带有正确标签的新 pod 时,它将成为服务的一部分,并且请求将开始被重定向到该 pod。但是,如果该 pod 暂时还没有准备好立即开始服务请求呢?
该 pod 可能需要时间来加载配置或数据,或者可能需要执行启动前的一些准备工作,以防止第一个用户请求花费太长时间,影响用户体验。在这种情况下,你不希望立即开始向 pod 发送请求,特别是当已经运行的实例可以适当地且快速地处理请求时。因此,在Pod完全准备好之前,不要将请求转发到正在启动的Pod是有意义的。
5.5.1 Introducing readiness probes
在前一章我们学习了存活探针(liveness probes),接下来我们介绍就绪探针(readiness probes)
readiness probe 周期性调用来确定特定 pod 是否应该接收客户端请求。当容器的 readiness probe 返回成功时,表示容器可以准备接受请求。
就绪探针的类型
跟存活探针类型,就绪探针也有三种类型:
HTTP GET
探针在你指定的容器IP地址、端口和路径上执行HTTP GET请求。如果探针接收到响应并且响应代码不表示错误(换句话说,如果HTTP响应代码是2xx或3xx),则该探针被认为是成功的。如果服务器返回错误响应代码或根本没有响应,则探针将被视为失败,容器将被重新启动。TCP Socket
探针尝试打开容器指定端口的 TCP 连接。如果连接成功建立,探针就算成功。否则,容器将会被重启。Exec
探针在容器内部执行任意命令,并检查该命令的退出状态码。如果状态码为 0,探针就算成功。其他所有状态码都被视为失败。
就绪探针的工作机制
当一个容器被启动时,可以配置Kubernetes在执行第一次readiness检查之前等待一段可配置的时间。此后,它会定期地调用该探针,并根据就绪探针的结果进行操作。如果一个Pod报告它不可用,它将被从服务中移除。如果Pod再次变为就绪状态,则重新添加。
与存活探针不同的是,如果一个容器不能通过就绪探测,它不会被杀死或重启。这是存活性和就绪性探针之间的重要区别。存活探针通过终止不健康的容器并将其替换为新的健康的容器来保持Pod的健康状态,而就绪探针确保只有准备好为请求提供服务的Pod才能接收它们。这在容器启动后大多是必要的,但在容器运行一段时间后也很有用。
正如图5.11中所示,如果Pod的就绪检查失败,则将其从Endpoints对象中删除。连接到服务的客户端将不会被重定向到Pod。这个效果与Pod根本不匹配服务的标签选择器时的效果相同。
5.5.2 Adding a readiness probe to a pod
向Pod模板中加入就绪探针
当ReplicationController的YAML文件在文本编辑器中打开时,在Pod的模板中查找容器规范,并将以下就绪性探针定义添加到spec.template.spec.containers下的第一个容器中:
就绪性探针将定期在容器内执行命令ls /var/ready。如果文件存在,则ls命令返回零退出代码;否则,返回非零退出代码。如果该文件存在,则就绪性探针将成功;否则,将失败。
观察就绪探测结果
删除现有的Pod,新定义的就绪探针就会起作用,会发现所有的Pod都没有就绪,这是因为他们当作还没有创建文件/var/ready。
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
kubia-94xhp 0/1 Running 0 57s
kubia-hddrx 0/1 Running 0 57s
kubia-jnw56 0/1 Running 0 57s
通过创建/var/ready文件使其中一个的就绪探测开始返回成功
$ kubectl exec kubia-94xhp -- touch /var/ready
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
kubia-94xhp 0/1 Running 0 112s
kubia-hddrx 0/1 Running 0 112s
kubia-jnw56 0/1 Running 0 112s
可以发现Pod还没有准备好,这是为什么呢,使用kubectl describe
命令来看看Pod:
$ kubectl describe kubia-94xhp
Readiness: exec [ls /var/ready] delay=0s timeout=1s period=10s #success=1 #failure=3
就绪性探针会定期检查,默认为每10秒检查一次。因为就绪性探针还没有被调用,所以该Pod目前还没有准备好。但是最迟在10秒钟后,该Pod应该变为就绪状态,并将其IP列为服务的唯一endpoint(运行kubectl get endpoints kubia-loadbalancer进行确认)。
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
kubia-94xhp 1/1 Running 0 5m1s
kubia-hddrx 0/1 Running 0 5m1s
kubia-jnw56 0/1 Running 0 5m1s
$ kubectl get endpoints
NAME ENDPOINTS AGE
kubernetes 33.33.33.110:6443 14d
kubia 10.244.1.232:8080 18h
kubia-nodeport 10.244.1.232:8080 110m
5.6 Using a headless service for discovering individual pods
你已经了解到如何通过服务提供稳定的IP地址,让客户端连接到支持每个服务的Pod(或其他终点)。服务的每个连接被转发到一个随机选择的支持Pod。但是,如果客户端需要连接到所有这些Pod?如果支持的Pod本身每个都需要连接到所有其他支持Pod?通过服务连接显然不是解决这个问题的方法。那么该怎么做呢?
为了让客户端连接到所有Pod,它需要找出每个单独Pod的IP。一个选项是让客户端调用Kubernetes API服务器,并通过API调用获取Pod列表及其IP地址,但由于你应该始终努力保持应用程序与Kubernetes松耦合,因此使用API服务器并不理想。
幸运的是,Kubernetes允许客户端通过DNS查找来发现Pod的IP。通常,当你对服务执行DNS查找时,DNS服务器会返回单个IP(服务的集群IP)。但是,如果你告诉Kubernetes你对服务不需要集群IP(通过在服务规范中将clusterIP字段设置为None来实现),则DNS服务器将返回Pod IP而不是单个服务IP。
与返回单个DNSA记录不同,DNS服务器将返回该服务的多个A记录,每个A记录指向当时支持该服务的单个Pod的IP。因此,客户端可以执行简单的DNSA记录查找,并获得属于服务的所有Pod的IP。然后,客户端可以使用该信息连接到其中的一个、多个或全部。
5.6.1 Creating a headless service
在服务规范中将clusterIP字段设置为None将使服务headless,因为Kubernetes不会通过IP分配为其分配集群IP,以便客户端可以通过此方式连接到其后面的Pod。现在你将创建一个名为kubia-headless的无头服务。如下所示:
# kubia-svc-headless.yaml
apiVersion: v1
kind: Service
metadata:
name: kubia-headless
namespace: default
spec:
selector:
app: kubia
clusterIP: None # 使服务变得headless
ports:
- port: 80 # 这是服务的内部集群IP的端口
targetPort: 8080 # 目标Pod的端口
你可以使用kubectl create命令创建服务,然后使用kubectl get和kubectl describe命令查看服务。你会看到它没有集群IP,而其Endpoints包括(部分)匹配其Pod选择器的Pod。我说“部分”,因为你的Pod包含一个就绪性探针,因此仅将准备就绪的Pod列为服务的终点。在继续之前,请确保至少两个Pod报告为就绪状态,方法是创建/var/ready文件,如同前一个示例中所示:
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
kubia-94xhp 1/1 Running 0 26m
kubia-hddrx 1/1 Running 0 26m
kubia-jnw56 0/1 Running 0 26m
5.6.2 Discovering pods through DNS
现在你的Pod已经就绪,可以尝试执行DNS查找,以查看是否获得了实际的Pod IP。你需要从其中一个Pod内部进行查找。不幸的是,你的kubia容器映像不包含nslookup(或dig)二进制文件,因此无法使用它来执行DNS查找。
你正在尝试的只是从集群内运行的Pod内部执行DNS查找。为什么不基于包含所需二进制文件的映像运行一个新的Pod呢?为了执行与DNS相关的操作,你可以使用Docker Hub上提供的tutum/dnsutils容器映像,该映像包含nslookup和dig二进制文件。要运行Pod,你可以通过创建一个YAML描述文件并将其传递给kubectl create的整个过程,有一种更快的方式。
创建如下Pod
$ kubectl run dnsutils --image=tutum/dnsutils --command -- sleep infinity
让我们使用新创建的Pod执行DNS查找:
$ kubectl exec dnsutils -- nslookup kubia-headless
Server: 10.96.0.10
Address: 10.96.0.10#53
Name: kubia-headless.default.svc.cluster.local
Address: 10.244.1.232
Name: kubia-headless.default.svc.cluster.local
Address: 10.244.1.231
DNS服务器为kubia-headless.default.svc.cluster.local FQDN返回了两个不同的IP地址。这些IP地址是报告准备就绪的两个Pod的IP地址。你可以使用kubectl get pods -o wide命令来列出显示Pod的IP地址以确认这一点。
这与DNS返回常规(非headless)服务(例如kubia服务)的IP地址不同,后者返回服务的集群IP地址。尽管headless服务可能与常规服务不同,但从客户端的角度来看,它们并没有太大区别。即使使用headless服务,客户端仍然可以通过连接服务的DNS名称连接到其Pod。但对于headless服务,因为DNS返回的是Pod的IP地址,所以客户端直接连接到Pod而不是通过服务代理连接。
5.7 Troubleshooting services
如果无法通过服务访问Pod,应该按照以下列表进行检查:
- 首先,请确保你是从集群内部连接到服务的集群IP而不是从外部连接。
- 不要浪费时间通过ping服务IP来判断服务是否可访问(记住,服务的集群IP是虚拟IP,ping它永远不会起作用)。
- 如果定义了就绪探针,请确保执行成功,否则Pod将不是服务的一部分。
- 要确认Pod是否是服务的一部分,请使用kubectl get endpoints命令检查相应的Endpoints对象。
- 如果你尝试通过服务的FQDN或部分FQDN(例如myservice.mynamespace.svc.cluster.local或myservice.mynamespace)来访问服务但无法访问,请尝试使用其集群IP而不是FQDN。
- 检查你是否连接到服务公开的端口而不是目标端口。
- 尝试直接连接到Pod IP以确认Pod是否在正确的端口上接受连接。
- 如果甚至无法通过Pod的IP访问应用程序,请确保应用程序未仅绑定到localhost。