Service Discovery 主要负责提供一类稳定的入口,利用这些入口服务的客户端能够访问到提供服务的后台实例。
部署到 Kubernetes 的应用很少是单独存在的,它们往往需要与集群内部的其他服务甚至集群外部的系统产生交互。
比如 DaemonSet 或者 ReplicaSet 中的 long-running Pod,通常需要处理来自外部的比如 HTTP 连接请求。这些时候,服务的消费者就需要某种机制发现 Pod 的位置,因为 Pod 会动态地由 scheduler 分配给节点,其位置在应用扩展和收缩时也会发生变化。
在 Kubernetes 之前,最常用的机制是 client-side discovery。当一个消费者需要访问另一个有可能扩展到多个实例上的服务时,消费者本身会有一个 agent 负责查询记录有实例信息的注册表,选择其中一个实例进行访问。这个 agent 有可能嵌入到消费者中(比如 Zookeeper client、Consul client 或 Ribbon),也有可能作为并置进程存在(比如 Prana)。
在后 Kubernetes 时代,分布式系统的很多非功能性职责比如资源调度、健康检查、自恢复、资源隔离等都交由平台负责。服务发现和负载均衡也属于这部分职责。
在 Kubernetes 里,所有服务实例注册以及注册信息的访问这类工作,都在后台完成。服务的消费者访问一个固定的虚拟服务端点,这个端点能够动态地发现提供服务的 Pod。
Internal Service Discovery
当我们创建一个包含多个副本的 Deployment,scheduler 会将 Pod 调度到合适的节点,每个 Pod 在启动之前都会获得一个集群 IP。
如果另一个客户端服务想要访问 Deployment 部署的服务,想要获悉集群 IP 的具体地址并没有简单直接的方式。
因而 Kubernetes 提供了 Service 组件。Service 可以为一组相同功能的 Pod 提供一个不变的稳定入口。1
2
3
4
5
6
7
8
9
10
11apiVersion: v1
kind: Service
metadata:
name: random-generator
spec:
selector:
app: random-generator
ports:
- port: 80
targetPort: 8080
protocol: TCP
上述配置会创建一个名为 random-generator
的 Service,其类型为 ClusterIP
(默认值),会在 80 端口上监听 TCP 连接,并转发到所有匹配的 Pod。
匹配条件 app: random-generator
由 selector 指定。
不管 Pod 是何时或者怎样创建的,任何带有 app: random-generator
标签的 Pod 都会作为转发的目标。
需要注意的一点是,当 Service 创建完成时,它获取到的 ClusterIP 只允许 Kubernetes 集群内部访问。只要 Service 定义一直存在,IP 就保持不变。
关于集群内部的其他应用如何知晓这个动态的 ClusterIP 具体是多少,有两种方式:
- 环境变量:此方式的主要问题是依赖于 Service 创建的时间,即 Service 必须在环境变量注入之前创建。因为环境变量无法注入到已经在运行的 Pod 中
- DNS lookup:Kubernetes 包含一个内置的 DNS 服务,被所有的 Pod 默认配置使用。当一个新的 Service 创建后,它会自动获得一条新的 DNS 记录,能够被所有 Pod 使用。比如
random-generator.default.svc.cluster.local
,其中random-generator
表示 Service 的名称,default
表示命名空间,svc
表示这是一个 Service,cluster.local
是集群前缀,可以省略
Service 的高级特性
Multiple ports
一个 Service 定义可以支持多个源端口和目标端口。
Session affinity
当新的请求出现时,Service 默认会随机挑选一个 Pod 作为转发的目标。可以配置 sessionAffinity: ClientIP
,从而来自同一个客户端 IP 的请求都会转发给同一个 Pod。
Readiness Probes
如果 Pod 定义了 readiness 检查,当它失效时,即便标签匹配,该 Pod 也会从 Service 端点中移除。
Virtual IP
ClusterIP 类型的 Service 在创建时会获得一个稳定的虚拟 IP,这个 IP 与任何网络接口都不相关,在现实中并不真实存在。
是每个节点上都有的 kube-proxy 意识到 Service 的存在后,更新节点上的 iptables,设置规则捕获目标是这个虚拟 IP 的网络包,将目标地址替换为选定的 Pod IP 地址。
iptables 中添加的规则并不包含 ICMP 协议,因此 Service 的 IP 地址无法被 ping。
Choosing ClusterIP
在 Service 创建过程中,可以通过 .spec.clusterIP
指定其使用的 IP 地址。
Manual Service Discovery
当我们创建一个带有 selector
的 Service 时,Kubernetes 会负责在 endpoint 资源列表里记录所有可提供服务的 Pod。可以使用类似 kubectl get endpoints random-generator
的命令查看 endpoint 列表。
除了将请求转发给集群内部的 Pod,还可以将连接转发给外部的 IP 和端口,比如像下面这样创建一个不带 selector
的 Service 并手动创建 endpoint 资源:1
2
3
4
5
6
7
8
9apiVersion: v1
kind: Service
metadata:
name: external-service
spec:
type: ClusterIP
ports:
- protocol: TCP
port: 80
1 | apiVersion: v1 |
上面创建的 Service 和之前的一样,都只能在集群内部访问。区别在于其 endpoint 是手动维护的,并且指向了集群外部的 IP 地址。
上述机制主要应用在需要访问外部资源的时候。Endpoint 还可以绑定 Pod 的 IP 地址,但是不支持其他 Service 的虚拟 IP。
Service 的一个优势在于它允许添加或者删除 selector
,随意指向外部或内部的服务提供者,而不需要删除自身的资源定义。从而 Service 的 IP 地址保持不变。因此 Service 的客户端可以继续使用原来的访问地址,而 Service 本身指向的服务提供者可能已经从 on-premise 转到了 Kubernetes,客户端不受任何影响。
1 | apiVersion: v1 |
ExternalName
是另外一种方式,通过 DNS CNAME 为外部的 endpoint 创建别名,而不是借助 IP 地址和代理。
面向集群外部的服务发现
前面提到的服务发现机制都使用了虚拟 IP,而这个虚拟 IP 本身只支持从集群内部访问。但是 Kubernetes 集群并不是与外部世界完全隔离的,除了 Pod 有时候需要访问外部资源以外,相反方向的访问也是经常发生的,即外部应用需要访问 Pod 提供的 endpoint。
NodePort
第一种创建 Service 并将其暴露给外部世界的方式是 NodePort
。1
2
3
4
5
6
7
8
9
10
11
12
13apiVersion: v1
kind: Service
metadata:
name: random-generator
spec:
type: NodePort
selector:
app: random-generator
ports:
- port: 80
targetPort: 8080
nodePort: 30036
protocol: TCP
上述配置会创建一个 Service,在虚拟 IP 的 80 端口接收客户端连接,并有选择地转发给匹配 selector app: random-generator
的 后端 Pod 上的 8080 端口。
除此之外,上面的配置还会保留所有节点上的 30036 端口,并将所有访问此端口的连接转发给 Service。从而不仅可以在集群内部通过虚拟 IP 连接 Service,还可以借助每个节点上的保留端口从集群外部访问 Service。
NodePort
的特点和问题:
- Port number:除了可以指定端口号以外(
nodePort: 30036
),还可以让 Kubernetes 自己选择可用的端口 - Firewall rules:
NodePort
会使用每一个节点上的指定端口,因此可能需要配置防火墙允许外部客户端连接指定端口 - Node selection:客户端可以连接集群中的任意一个节点,潜在的问题是,当该节点不可用时,客户端应用负责实现访问另一个健康节点的功能。因此,在节点前部署一个负载均衡器会是一个好的措施
- Pod selection:当客户端尝试通过节点端口连接服务时,其请求会被路由到随机选择的 Pod,该 Pod 可能位于同一个节点上,也可能位于另外的节点上。可以在 Service 的定义中使用
externalTrafficPolicy: Local
选项强制将请求只转发给当前节点上的 Pod。这同时会引发另一个问题,即必须确保每个节点上都有 Pod 部署(比如 daemon service),或者客户端知道哪个节点上有健康的 Pod 在运行 - Source address:当 Service 类型为
NodePort
时,网络包中的源 IP 地址(即客户端 IP)会被替换成节点的内部 IP。比如客户端发送网络包给 node1,假如说 Pod 位于 node2 上,网络包从 node1 转发到 node2,网络包中的源 IP 地址会被替换成 node1 的 IP,目标地址替换成 Pod 的地址。当 Pod 最终接收到请求,源 IP 地址已经被替换成 node1 的地址。这类行为同样可以通过externalTrafficPolicy: Local
避免
LoadBalancer
另一种针对外部客户端的服务发现方式是通过负载均衡器。NodePort
类型的 Service 构建在默认的 Service(type: ClusterIP
)之上,额外在每个节点上开放了一个端口。
该方式的缺点就是,我们仍需要一个负载均衡器来为客户端应用选择健康的节点。而 LoadBalancer
类型的 Service 则解决了这个问题。
除了 NodePort
类型所做的操作以外,LoadBalancer
类型的 Service 还会借助云服务提供商的负载均衡器将服务暴露给外部使用。
1 | apiVersion: v1 |
应用层服务发现(Ingress)
不同于之前的服务发现机制,Ingress 并不是一种 Service 类型,而是一个独立的 Kubernetes 资源,部署在 Service 前端作为 smart router 和集群的入口。
Ingress 通常会提供基于 HTTP 协议的对 Service 的访问, 通过外部可见的 URL、负载均衡、SSL termination、基于名称的 virtual hosting 等。
为了 Ingress 能生效,集群本身必须要有一个或者多个 Ingress controller 在运行。
1 | apiVersion: extensions/v1beta1 |
上述配置会分配一个可供外部访问的 IP 地址并在 80 端口对外暴露 Service。看上去与 type: LoadBalancer
并没有什么区别。实际上 Ingress 能够重复使用同一个外部负载均衡器和 IP 指向多个 Service。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: random-generator
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /
backend:
serviceName: random-generator
servicePort: 8080
- path: /cluster-status
backend:
serviceName: cluster-status
servicePort: 80
Ingress 是 Kubernetes 上最强大同时也最复杂的服务发现机制,其最常用的场景是当多个服务需要在同一个 IP 地址下,同时这些服务又都使用同样的第七层协议(通常是 HTTP)。