Kubernetes in Action—服务:让客户端发现 pod 并与之通信

在没有 Kubernetes 的世界里,系统管理员要在用户端配置文件中指出服务的具体 IP 地址或主机名来让客户端应用,但这种方式在 Kubernetes 不适用,因为:

  • pod 是短暂的 — pod 会随时的启动或关闭,无论是因为减少了 pod 的数量,又或者集群中节点异常等等
  • Kubernetes 在 pod 启动前会给以及调度到节点上的 pod 分配 IP 地址 — 因此不能给客户端显示配置 pod 的 IP 地址
  • 水平伸缩意味着多个 pod 可能提供相同的服务 — 每个 pod 都有自己的 IP,客户端不应该关心 pod 的数量及对应的 IP 地址,而应该通过统一的 IP 地址进行访问

为了解决上述问题,Kubernetes 提供了服务(service)类型。

介绍服务

服务是一种为一组功能相同的 pod 提供单一不变入口点的资源,客户端通过和服务建立连接,这些连接会被路由到提供该服务的任一 pod 上,通过这种方式, pod 可以在集群中随时被创建或移除。

下图展示了前端组件和后端组件通过配置服务使客户端或前端 pod 能在 pod 随时变动的情况下而保持配置不变,其可以通过环境变量或 DNS 以及服务名来访问这些服务,间接访问提供服务的 pod。

截屏2019-12-23下午1.15.46

创建服务

服务和 ReplicationController 一样,通过使用标签选择器决定哪些 pod 属于同一组,间接决定哪些 pod 属于服务。

截屏2019-12-23下午1.18.37

通过 kubectl expose 创建服务

kubectl expose rc kubia --type=LoadBalancer --name kubia-http

也可以通过 pod 选择器选择 pod 来创建服务资源,从而通过单个 IP 和端口访问所有选择的 pod。

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

创建 kubia-svc.yaml 文件:

apiVersion: v1
kind: Service
metadata:
  name: kubia
spec:
  ports:
  - port: 80
    targetPort: 8080
  selector:
    app: kubia

创建名为 kubia 的服务,其将在 80 端口接收请求并连接路由到具体标签选择器是 app=kubia 的 pod 的 8080 端口上。

检测新的服务

➜ kubectl create -f kubia-svc.yaml
service/kubia created
➜ kubectl get svc
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP   4d8h
kubia        ClusterIP   10.96.202.155   <none>        80/TCP    4s

CLUSTER-IP 是集群的 IP 地址,只能在集群内部访问。

从内部集群测试服务

通过以下方法向服务发送请求测试服务:

  • 创建 pod,将请求发送到服务的集群 IP 并记录响应,通过查看 pod 日志检查服务器响应。
  • ssh 到 Kubernetes 节点上,使用 curl 命令请求。
  • 通过 kubectl exec 命令在一个已经存在的 pod 执行 curl 命令。

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

kubectl exec 命令远程地在一个已存在的 pod 容器上执行命令:

➜ kubectl exec kubia-c6g2b -- curl -s http://10.96.202.155
You've hit kubia-pk8tc

双横杠“—”代表 kubectl 命令项的结束。其之后是指在 pod 内部需要执行的命令,当不使用“—”时, -s 会被解析成 kubectl exec 的选项,造成歧义。

下图展示了运行命令时发生的事情:

截屏2019-12-23下午9.08.59

配置服务上的会话亲和性

如果多次执行同样的命令,会发现每次执行调用都在不同的 pod 上,服务通常将连接随机指向选中后端 pod 中的一个,即使是同一个客户端。通过设置服务的 sessionAffinity 属性为 ClientIP(默认值为 None),可以使特定客户端的所有请求都指向同一个 pod。

apiVersion: v1
kind: Service
spec:
	sessionAffinity: ClientIP
	...

同一个客户端的请求无论怎么解析都是到同一个 pod 上。

同一个服务暴露多个端口

创建的服务可以暴露多个端口,如 pod 监听两个端口,HTTP 监听 8080 端口,而 HTTPS 监听 8443 端口,那么就可以将 80 和 443 转发至 pod 端口的 8080 和 8443。

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

创建 kubia-svc-named-ports.yaml 文件:

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:

kind: Pod
spec:
	containers:
	- name: kubia
	  ports:
	  - name: http
	    containerPort: 8080
	  - name: https
	    containerPort: 8443

在 pod 端口定义命名,8080 为 http 端口,https 为 8443 端口。

在服务中引用命名了端口的 pod:

apiVersion: v1
kind: Service
spec:
  ports:
  - name: http
    port: 80
    targetPort: http
  - name: https
    port: 443
    targetPort: https

其中, 80 端口被映射到 pod 中名为 http 的端口,即 8080,https 同理。

使用端口命名最大的好处就是即使更换端口号也无须更改服务的 spec。假如过段时间决定把 pod 对应的端口修改了,采用命名的端口号,服务依然会正确地将请求转发到对应的端口号上。

服务发现

Kubernetes 为客户端提供了发现服务的 IP 和端口的方式。

通过环境变量发现服务

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

首先删除已存在的 pod,使 pod 的创建晚于服务:

➜ kubectl delete po --all
pod "kubia-c6g2b" deleted
pod "kubia-jqvhf" deleted
pod "kubia-pk8tc" deleted

查看 pod 容器中和服务相关的环境变量:

➜ kubectl exec kubia-k8xdc env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=kubia-k8xdc
KUBERNETES_SERVICE_PORT=443
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_PORT=443
KUBIA_PORT=tcp://10.96.187.176:80
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBIA_SERVICE_HOST=10.96.187.176
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBIA_PORT_80_TCP=tcp://10.96.187.176:80
KUBIA_PORT_80_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBIA_SERVICE_PORT=80
KUBIA_PORT_80_TCP_PORT=80
KUBIA_PORT_80_TCP_ADDR=10.96.187.176
...

集群中存在两个服务:kubernetes 和 kubia,因此列表中显示了 KUBERNETES_SERVICE_HOST/PORT 和 KUBIA_SERVICE_HOST/PORT 两个和服务相关的环境变量。

环境变量将服务名称的”-“转换为下划线”_“,并且当服务名称用作环境变量名称前缀时,所有字母为大写。

通过 DNS 发现服务

kube-system 命名空间下有个 pod 叫做 kube-dns,其运行 DNS 服务。Kubernetes 通过配置集群中的 pod 的容器的 /etc/resolv.conf 文件, 使运行在 pod 上的进程 DNS 查询都会通过 kube-dns 解析,即被 Kubernetes 自身的 DNS 服务器响应,该服务器知道系统中运行的所有服务。

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

客户端的 pod 从内部 DNS 服务器获得一个 DNS 条目,通过全限定域名(FQDN)来访问服务。

通过 FQDN 连接服务

在前端 web 服务器和后端数据库服务的例子中,配置名为 backend-database 的服务,则前端可以通过 BACKEND_DATABASE_SERVICE_HOST 和 BACKEND_DATABASE_SERVICE_PORT 来获得后端服务的 IP 地址和端口信息。

前端 pod 也可以通过打开以下 FQDN 连接来访问数据库服务:backend-database.default.svc.cluster.local,显然, backend-database 代表服务名称,default 表示服务在其定义的名称空间,而 svc.cluster.local 是所有集群本地服务名称中使用的可配置集群域后缀,当前端 pod 和数据库 pod 在同一命名空间下,可以省略 svc.cluster.local,甚至命名空间,即可以使用 backend-database 指代数据库服务。

➜ kubectl exec kubia-w4n8p -- curl -s kubia
You've hit kubia-k8xdc

在 pod 容器中运行 shell

➜ kubectl exec -it kubia-w4n8p bash
root@kubia-w4n8p:/# curl http://kubia.default.svc.cluster.local
You've hit kubia-wvzts
root@kubia-w4n8p:/# curl http://kubia.default
kYou've hit kubia-k8xdc
root@kubia-w4n8p:/# curl http://kubia
You've hit kubia-k8xdc

查看 /etc/resolve.conf 文件,就会发现其 DNS 服务解析的原理:

root@kubia-w4n8p:/# cat /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

无法 ping 通服务 IP 的原因

root@kubia-w4n8p:/# ping kubia
PING kubia.default.svc.cluster.local (10.96.187.176) 56(84) bytes of data.
^C
--- kubia.default.svc.cluster.local ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1029ms

进入 pod 中 ping 服务 IP 查看服务是否已经启动,但却无法 ping 通,因为服务的集群是一个虚拟 IP,并只有在服务端口结合时才有意义,因此当调试异常时,无法 ping 通服务 IP 是正常的事情。

连接集群外部的服务

当希望通过 Kubernetes 服务特效暴露外部服务的时候,不要让服务将连接重定向到集群中的 pod,而是让其重定向到外部 IP 和端口。

介绍服务 endpoint

服务并不是和 pod 直接相连的,而是通过一种介于两者之间的资源 — Endpoint 资源。查看 kubia 服务:

➜ kubectl describe svc kubia
Name:              kubia
Namespace:         default
Labels:            <none>
Annotations:       <none>
Selector:          app=kubia
Type:              ClusterIP
IP:                10.96.187.176
Port:              <unset>  80/TCP
TargetPort:        8080/TCP
Endpoints:         172.17.0.6:8080,172.17.0.7:8080,172.17.0.8:8080
Session Affinity:  None
Events:            <none>

Endpoint 资源暴露一个服务的 IP 地址和端口列表,使用 kubectl info 获取其基本信息:

➜ kubectl get endpoints kubia
NAME    ENDPOINTS                                         AGE
kubia   172.17.0.6:8080,172.17.0.7:8080,172.17.0.8:8080   168m

虽然在 spec 中定义了 pod 选择器,但选择器只用于构建 IP 和端口列表,然后存储在 Endpoint 资源中。之后客户端连接到服务时,服务代理从存储在 Endpoint 资源的 IP 和端口对选择一个,并将连接重定向到该位置监听的服务器。

手动配置服务的 endpoint

当创建不包含 pod 选择器的服务时,则 Kubernetes 将不会创建 Endpoint 资源。

创建没有选择器的服务

创建 external-service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: external-service
spec:
  ports:
  - port: 80

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

手动创建 Endpoint 资源,创建 external-service-endpoints.yaml:

apiVersion: v1
kind: Endpoints
metadata:
  name: extern-service
subsets:
  - addresses:
    - ip: 11.11.11.11
    - ip: 22.22.22.22
    ports:
    - port: 80

Endpoints 对象和服务具有相同名称,并包含该服务的目标 IP 地址和端口列表。当服务和 Endpoint 资源都发布到服务器后,服务就可以和具有 pod 选择器一样的服务正常使用了。

下图显示了 3 个 pod 连接到具有外部 endpoint 的服务:

截屏2019-12-24上午12.51.15

为外部服务创建别名

除了手动配置 Endpoint 代替公开外部服务方法,还可以通过其完全限定域名(FQDN)访问外部服务。

创建 ExternalName 类型的服务

将创建服务资源的 type 字段配置为 ExternalName,可以创建一个具有别名的外部服务的服务。设想在 api.somecompany.com 上有公共 API 可用,定义一个指向该域名的服务,创建 external-service-externalname.yaml:

apiVersion: v1
kind: Service
metadata:
  name: external-service
spec:
  type: ExternalName
  externalName: someapi.somecompany.com
  ports:
  - port: 80

服务创建后,可以通过 external-service.default.svc.cluster.local 域名(甚至是 external-service)连接到外部服务,而不是使用服务的实际 FQDN。

这样做的好处是隐藏了实际的服务名称及其使用该服务的 pod 的位置,当以后要将其指向其他服务,只需修改 externalName 属性,或者将类型重新变回 ClusterIP 并为服务创建 Endpoint。

ExternalName 的原理是为服务创建了简单的 CNAME DNS 记录,因此连接到服务的客户端将直接连接到外部服务,完全绕过服务代理。

将服务暴露给外部客户端