Kubernetes 设计解读 – Service

By | 9月 13, 2020

由于重新调度的原因,Pod 在 Kubernetes 上的地址是不固定的,因此需要一个代理来确保需要使用 Pod 的应用不需要知道 Pod 的真实地址。另一个原因是当使用 Replication Controller 创建了多个Pod 的副本时,需要一个代理来为这些 Pod 做负载均衡。

定义 service

Service 主要由一个 IP 地址和一个 label selector 组成。在创建之初,每个 service 被分配了一个独一无二的 IP 地址,该 IP 地址与 service 的生命周期相同且不再更改。

另外虽然默认是随机分配的,但是可以通过 service 配置信息中的 spec.clusterIP 字段手动指定 service 的虚拟 IP。

注意当一个 service 被创建的时候,系统会随之创建一个同样名称的 Endpoints 对象,该对象保存了所有匹配 label selector 后端 Pod 的 IP 地址和端口。

kind: service
apiVersion: v1
metadata:
  name: my-service
  labels:
    environment: testing
spec:
  selector:
    app: MyApp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9376

使用 service 来代理遗留系统

我们注意到 service 对象的 .spec.selector 属性是可选项,即允许存在没有 label selector 的 service,这是为了用户能够使用 service 代理一些并非 Pod,或者说得不到 label 的资源:

  • 访问 Kubernetes 集群外部的数据库
  • 访问其他 namespace 或者其他集群的 service
  • 任何其他类型的外部遗留系统

注意,因为在上述场景中没有定义 selector 属性,所以系统不会自动创建 Endpoints 对象。这个时候可以通过自定义一个 Endpoints 对象来显示地将上述 service 对象映射到一个或多个后端(例如被代理的遗留系统地址),如下:

kind: service
apiVersion: v1
metadata:
  name: my-service
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9376
---
kind: Endpoints
apiVersion: v1
metadata:
  name: my-service
subsets:
- address:
  - IP: 1.2.3.4
  ports:
  - port: 9376

这个时候访问该 service,流量会被分发给用户自定义的 endpoints。

工作原理

Kubernetes 集群的每个节点上都运行着一个服务代理 kube-proxy,它是负责实现 service 的主要组件。

kube-proxy 有 userspace 和 iptables 两种工作模式。

userspace 模式

对于每个 service,kube-proxy 都会在宿主机上随机监听一个端口与这个 service 对应起来,并在宿主机上建立起 iptables 规则,将 service IP:service port 流量重定向到上述端口。

这样所有发往 service IP:service port 的流量都会经过 iptables 重定向到它对应的随机端口,再经过 kube-proxy 的代理到某个后端 Pod。kube-proxy 里会维护本地端口与 service 的映射关系,以及 service 代理的 Pod 清单,至于具体选择哪个 Pod,则由路由策略(默认是 Round-Robin)以及用户设定的 .spec.sessionAffinity 决定。

kube-proxy 还会实时监测 master 节点上 etcd 中 service 和 Endpoints 对象的增加和删除信息,从而保证了后端被代理 Pod 的 IP 和端口变化可以及时更新到它维护的路由信息当中。

iptables 模式

iptables 模式下的 kube-proxy 将只负责创建及维护 iptables 的路由规则,其余的工作均由内核态的 iptables 完成。与 userspace 模式相比,它的速度更快,可靠性也更高。

自发现机制

一旦一个 service 被创建,该 service 的 service IP 和 service port 等信息都可以被注入到 Pod 中供他们使用。目前主要支持两种方式:

环境变量方式

kubelet 创建 Pod 时会自动添加所有可用的 service 环境变量到该 Pod 中。

环境变量形式诸如 {SVCNAME}_SERVICE_HOST{SVCNAME}_SERVICE_PORT 这样的形式,其中 {SVCNAME} 部分将 service 名字全部替换成大写且将破折号(-)替换成下划线(_)。

环境变量的注入只发生在 Pod 创建时,且不会被自动更新。这个特点暗含了 service 和需要访问该 service 的 Pod 的创建时间的先后次序,即任何想要访问 service 的 Pod 都需要在 service 已经存在后创建,否则与 service 相关的环境变量就无法注入该 Pod 的容器中。使用下面的 DNS 模式就没有这个问题。

DNS 方式

假设有一个名为 my-service 的 service,且该 service 处在 namespace my-ns 中,就形成了一条 DNS 记录:my-service.my-ns。

任何在 my-ns 中的 Pod 只要访问域名 my-service 即可访问到该 service,处在其他 namespace 中的 Pod 则必须提供完整的记录 my-service.my-ns 才可以访问。

DNS 方式的缺点:

  • 因为有 DNS TTL 的问题,服务器通常会将域名查找的结果进行缓存,如果 service 在 TTL 的时间内故障,客户端会解析到错误的 DNS 结果;另外很多客户端也是进行一次域名查找之后就将结果缓存起来,同样会带来上面的问题
  • 即使应用程序和 DNS 函数库会进行恰当的域名重解析操作,每个客户端频繁的域名重解析请求也会给系统带来极大的负荷

DNS 方式的优点:

  • 可以一定程度上解决 service 环境变量泛滥的问题
  • 可以解决 service 和 Pod 的先后顺序依赖问题,不存在这个限制了

外部可路由

service 分三种类型:

  • ClusterIP
    • ClusterIP 是最基本的类型,即在默认情况下只能在集群内部进行访问,另外两种则与实现从集群外部路由有着密不可分的关系。
  • NodePort
    • 如果将 service 的类型设置为 NodePort ,那么系统会从 service-node-port-range 范围中为其分配一个端口,并且在每个工作节点上都打开该端口,使得访问该端口 .spec.ports.nodesPort 即可访问到这个 service。当然用户也可以自定义这个端口
    • 使用 NodePort 的时候,在集群外部使用 .nodePort 或者在集群内部使用 .port 都可以访问到这个 service
  • LoadBalancer
    • 该功能需要云服务上支持,并定义 spec.loadBalancerIP 字段

External IP

每种 service 类型都支持 externalIPs 字段,在这个场景中,用户需要维护一个外部 IP 地址池,然后在 service 的描述文件中添加。

这里注意 Kubernetes 本身并不维护 externalIPs 的路由,需要由 IaaS 平来为负责维护。

Kubernetes 的 service 设计原则是:任何一个工作节点上的 kube-proxy 都能够正确地将流量导向任何一个被代理的 Pod,而这个 kube-proxy 不需要和被代理的 Pod 在同一个宿主机上。

多个 service 如何避免地址和端口冲突?

Kubernetes 会为每一个 service 分配一个唯一的 Cluster IP,所以当使用 cluster ip:port 的组合访问一个 service 的时候,不管 port 是什么,这个组合是一定不会发生重复的。

另一方面,kube-proxy 为每个 service 真正打开的是一个绝对不会重复的随机端口,用户在 service 描述文件中指定的访问端口会被映射到这个随机端口上。这就是为什么 用户可以在创建 service 时可以随意指定访问端口。

service 目前存在的不足

Kubernetes 使用 iptables 和 kube-proxy 解析 service 的入口地址在中小规模的集群中运行良好,但是当 service 的数量超过一定规模的时候,仍然有一些小问题:

  • service 环境变量泛滥
  • service 与使用 service 的 Pod 两者创建时间先后的制约关系

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注