Kubernetes 设计模式笔记 —— Service Discovery

Service Discovery 主要负责提供一类稳定的入口,利用这些入口服务的客户端能够访问到提供服务的后台实例。

部署到 Kubernetes 的应用很少是单独存在的,它们往往需要与集群内部的其他服务甚至集群外部的系统产生交互。
比如 DaemonSet 或者 ReplicaSet 中的 long-running Pod,通常需要处理来自外部的比如 HTTP 连接请求。这些时候,服务的消费者就需要某种机制发现 Pod 的位置,因为 Pod 会动态地由 scheduler 分配给节点,其位置在应用扩展和收缩时也会发生变化。

在 Kubernetes 之前,最常用的机制是 client-side discovery。当一个消费者需要访问另一个有可能扩展到多个实例上的服务时,消费者本身会有一个 agent 负责查询记录有实例信息的注册表,选择其中一个实例进行访问。这个 agent 有可能嵌入到消费者中(比如 Zookeeper client、Consul client 或 Ribbon),也有可能作为并置进程存在(比如 Prana)。
Client-side service discovery

在后 Kubernetes 时代,分布式系统的很多非功能性职责比如资源调度、健康检查、自恢复、资源隔离等都交由平台负责。服务发现和负载均衡也属于这部分职责。
在 Kubernetes 里,所有服务实例注册以及注册信息的访问这类工作,都在后台完成。服务的消费者访问一个固定的虚拟服务端点,这个端点能够动态地发现提供服务的 Pod。
Server-side service discovery

Internal Service Discovery

当我们创建一个包含多个副本的 Deployment,scheduler 会将 Pod 调度到合适的节点,每个 Pod 在启动之前都会获得一个集群 IP。
如果另一个客户端服务想要访问 Deployment 部署的服务,想要获悉集群 IP 的具体地址并没有简单直接的方式。
因而 Kubernetes 提供了 Service 组件。Service 可以为一组相同功能的 Pod 提供一个不变的稳定入口。

1
2
3
4
5
6
7
8
9
10
11
apiVersion: 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 都会作为转发的目标。
Internal service discovery

需要注意的一点是,当 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
9
apiVersion: v1
kind: Service
metadata:
name: external-service
spec:
type: ClusterIP
ports:
- protocol: TCP
port: 80

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Endpoints
metadata:
name: external-service
subsets:
- addresses:
- ip: 1.1.1.1
- ip: 2.2.2.2
ports:
- port: 8080

上面创建的 Service 和之前的一样,都只能在集群内部访问。区别在于其 endpoint 是手动维护的,并且指向了集群外部的 IP 地址。
Manual service discovery

上述机制主要应用在需要访问外部资源的时候。Endpoint 还可以绑定 Pod 的 IP 地址,但是不支持其他 Service 的虚拟 IP。

Service 的一个优势在于它允许添加或者删除 selector,随意指向外部或内部的服务提供者,而不需要删除自身的资源定义。从而 Service 的 IP 地址保持不变。因此 Service 的客户端可以继续使用原来的访问地址,而 Service 本身指向的服务提供者可能已经从 on-premise 转到了 Kubernetes,客户端不受任何影响。

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Service
metadata:
name: database-service
spec:
type: ExternalName
externalName: my.database.example.com
ports:
- port: 80

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
13
apiVersion: 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。
Node port Service Discovery

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 还会借助云服务提供商的负载均衡器将服务暴露给外部使用。
Load balancer service discovery

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: Service
metadata:
name: random-generator
spec:
type: LoadBalancer
clusterIP: 10.0.171.239
loadBalancerIP: 78.11.24.19
selector:
app: random-generator
ports:
- port: 80
targetPort: 8080
protocol: TCP
status:
loadBalancer:
ingress:
- ip: 146.148.47.155

应用层服务发现(Ingress)

不同于之前的服务发现机制,Ingress 并不是一种 Service 类型,而是一个独立的 Kubernetes 资源,部署在 Service 前端作为 smart router 和集群的入口。
Ingress 通常会提供基于 HTTP 协议的对 Service 的访问, 通过外部可见的 URL、负载均衡、SSL termination、基于名称的 virtual hosting 等。
为了 Ingress 能生效,集群本身必须要有一个或者多个 Ingress controller 在运行。

1
2
3
4
5
6
7
8
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: random-generator
spec:
backend:
serviceName: random-generator
servicePort: 8080

上述配置会分配一个可供外部访问的 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
18
apiVersion: 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

Application layer service discovery

Ingress 是 Kubernetes 上最强大同时也最复杂的服务发现机制,其最常用的场景是当多个服务需要在同一个 IP 地址下,同时这些服务又都使用同样的第七层协议(通常是 HTTP)。

总结

Service Discovery mechanisms

参考资料

Kubernetes Patterns