阅读本文前提条件:理解 k8s Service 的大致原理;会使用 Ingress。

原理概述

ClusterIP 类型的 Service 可以供内部程序使用,若不在路由设备上配置相应规则,外部节点无法访问 Service 的 IP 或某 Pod 的 IP。

一个比较容易想到的办法是,运用“转化思想”:既然 Pod 可以访问集群内的 Service,那就让几个 Pod 监听几个宿主机的端口,让这些 Pod 把流量转发到集群里面去。这样就把集群外访问 k8s Service 的问题转化成两个已经解决的问题:

  • 外部访问宿主机
  • Pod 访问 Service

具体来说:挑几个节点(记为 A、B、C),通过 nodeSelector 在这些节点上运行一个 DaemonSet,该 DaemonSet 的 Pod 配置 hostPort 字段绑定宿主机端口(例如 80)。

这样一来,外部程序访问 A、B、C 的 80 端口时,Pod 中运行的程序(如 nginx、HAProxy)就可以根据某种规则把流量转发给特定 Service。

其实,这已经是 Ingress 的原理了。

Ingress 也是 k8s 的一个 API 对象,但 IngressController 不是。IngressController 的存在形式可以是 Deployment 或 DaemonSet,也可以是某个 Operator 所控制的一组 Pod,总之是若干持续运行的 Pod。通常来说,Pod 中会运行一个有反向代理功能的程序(如 nginx)的容器,再运行一个容器(记为 c1)监视 Ingress 的变化,当 Ingress 改变时修改方向代理程序的配置文件。以 nginx 为例,c1 感知到 Ingress 变化后,修改 nginx.conf 并重启 nginx。当然了,如果反向代理程序原生支持监视 Ingress 并热更新自己的配置,那就不用 c1 了。

一般来说,小集群情形,可以将 IngressController 部署为 DaemonSet 并运行在所有节点上。

大集群情形,可以将 IngressController 部署为 DaemonSet,并通过污点、容忍、nodeSelector 让 IngressController 独占一些节点。

将 IngressController 部署为 Deployment 不太多见,因为 Pod 的停靠点不好控制。

IngressClass

很多时候,我们需要在集群中部署多个 IngressController 以满足不同的用途。为此,k8s 提供了 IngressClass 资源。

每个 IngressController 有自己的名字(一般都支持在启动时指定),而 IngressClass 可以指定用哪个 IngressController。

apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
  name: ing-cls-nginx
  annotations:
    ingressclass.kubernetes.io/is-default-class: "false"
spec:
  controller: my-nginx-clr
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
  name: ing-cls-traefik
  annotations:
    ingressclass.kubernetes.io/is-default-class: "true"
spec:
  controller: my-traefik-clr

这是两个 IngressClass,分别关联着名字为 my-nginx-clring-cls-traefik 的 IngressController。于是当如下一个 Ingress

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ing
  # ...
spec:
  ingressClassName: ing-cls-traefik
  # ...

被创建或修改时,IngressController 先通过 Ingress 的 .spec.ingressClassName 找到 IngressClass,再拿到 IngressClass 的 .spec.controller,和自己名字比较,就知道自己是否应该响应这个变化。

.metadata.annotations.ingressclass.kubernetes.io/is-default-class 表示该 IngressClass 是否为默认的。如果一个 Ingress 没有指定 IngressClass,则默认的 IngressController 会响应。一个集群之多有一个默认的 IngressClass。当然最好的方式是:确保集群中有且只有一个默认的 IngressClass。

注意事项

使用 Ingress、IngressClass 要特别注意的是,确保每个 Ingress 最多被一个 IngressController 响应。

也就是避免出现类似于

  • 多个 IngressController 名字相同
  • 有多个 IngressController,没有默认的,且 Ingress 没有指定 ingressClassName

的情况。否则会造成难以预测的后果。

思维扩展

从上面的叙述可以看出,IngressController 能代理什么协议,取决于 IngressController Pod 里面运行什么程序,以及 Ingress 资源中写什么内容。

例如,我们可以把某个 MySQL 代理程序作为 IngressController,将读写分离或分库分表的信息写入 Ingress。当然了,Ingress 的正文没有写这种信息的地方,只能全塞进 annotations 里面。于是,我们就得到一个对集群外暴露 MySQL 协议(而不是 TCP)的方式。

类似的,如果要向集群外暴露自定义的 A 协议,则在 Ingress 的 annotations 填写代理相关配置,并在 Controller 程序中监视并读取相关的配置,就实现了一个 A 的 IngressController。

甚至,我们可以通过 CRD 方式,实现一个 FooIngress 资源,在它的正文中配置 A 协议的代理,并实现一套类似 FooIngressClass 的机制,以打到代理 A 协议的目的。

事实上,我们可以更简单的用一个 ConfigMap 和一个带 hostPort 的 DaemonSet 实现自定义协议的接入,只不过此做法的“非标”程度更大一些。失去了“声明和实现分离”的效果。

Ingress 与 NodePort 类型的 Service 的区别

为简便,记 np-svc 为 NodePort 类型的 Service。

np-svc 只能转发 L4 流量,Ingress 可以转发 L7,如果自定义类似于 Ingress 的 CRD 并开发相应的 IngressController,还可以转发自定义协议的流量。

np-svc 会让所有的节点监听端口,Ingress 可以规划、选择节点。