背景简介
K8s & K3s 将外部流量引入群集有哪些不同方式,在何种场景选用那种方式,我们进行如下探讨:
- NodePort
NodePort在集群中的主机节点上为Service提供一个代理端口,以允许从主机网络上对Service进行访问。
NodePort的流量转发机制和Cluster IP的iptables模式类似,唯一不同之处是在主机网络上开了一个“NodePort”来接受外部流量。从上面的规则也可以看出,在创建Nodeport时,Kube-proxy也会同时为Service创建Cluster IP相关的iptables规则。
NodePort提供了一种从外部网络访问Kubernetes集群内部Service的方法,但该方法存在下面一些限制,导致这种方式主要适用于程序开发,不适合用于产品部署。
Kubernetes cluster host的IP必须是一个well-known IP,即客户端必须知道该IP。但Cluster中的host是被作为资源池看待的,可以增加删除,每个host的IP一般也是动态分配的,因此并不能认为host IP对客户端而言是well-known IP。
客户端访问某一个固定的host IP的方式存在单点故障。假如一台host宕机了,kubernetes cluster会把应用 reload到另一节点上,但客户端就无法通过该host的nodeport访问应用了。
通过一个主机节点作为网络入口,在网络流量较大时存在性能瓶颈。
- LoadBalancer
Kubernetes提供了LoadBalancer。通过将Service定义为LoadBalancer类型,Kubernetes在主机节点的NodePort前提供了一个四层的负载均衡器。该四层负载均衡器负责将外部网络流量分发到后面的多个节点的NodePort端口上。
下图展示了Kubernetes如何通过LoadBalancer方式对外提供流量入口,图中LoadBalancer后面接入了两个主机节点上的NodePort,后端部署了三个Pod提供服务。根据集群的规模,可以在LoadBalancer后面可以接入更多的主机节点,以进行负荷分担。
备注:LoadBalancer类型需要云服务提供商的支持,Service中的定义只是在Kubernetes配置文件中提出了一个要求,即为该Service创建Load Balancer,至于如何创建则是由Google Cloud或Amazon Cloud等云服务商提供的,创建的Load Balancer的过程不在Kubernetes Cluster的管理范围中。
目前WS, Azure, CloudStack, GCE 和 OpenStack 等主流的公有云和私有云提供商都可以为Kubernetes提供Load Balancer。一般来说,公有云提供商还会为Load Balancer提供一个External IP,以提供Internet接入。如果你的产品没有使用云提供商,而是自建Kubernetes Cluster,则需要自己提供LoadBalancer。
- Ingress
LoadBalancer类型的Service提供的是四层负载均衡器,当只需要向外暴露一个服务的时候,采用这种方式是没有问题的。但当一个应用需要对外提供多个服务时,采用该方式则要求为每一个四层服务(IP+Port)都创建一个外部load balancer。
一般来说,同一个应用的多个服务/资源会放在同一个域名下,在这种情况下,创建多个Load balancer是完全没有必要的,反而带来了额外的开销和管理成本。另外直接将服务暴露给外部用户也会导致了前端和后端的耦合,影响了后端架构的灵活性,如果以后由于业务需求对服务进行调整会直接影响到客户端。为了解决该问题,可以通过使用Kubernetes Ingress来作为网络入口。
Kubernetes Ingress声明了一个应用层(OSI七层)的负载均衡器,可以根据HTTP请求的内容将来自同一个TCP端口的请求分发到不同的Kubernetes Service,其功能包括:
-
按HTTP请求的URL进行路由
同一个TCP端口进来的流量可以根据URL路由到Cluster中的不同服务,如下图所示: -
按HTTP请求的Host进行路由
同一个IP进来的流量可以根据HTTP请求的Host路由到Cluster中的不同服务,如下图所示:
Ingress 规则定义了对七层网关的要求,包括URL分发规则,基于不同域名的虚拟主机,SSL证书等。Kubernetes使用Ingress Controller 来监控Ingress规则,并通过一个七层网关来实现这些要求,一般可以使用Nginx,HAProxy,Envoy等。
Ingress配合NodePort和LoadBalancer提供对外流量入口
虽然Ingress Controller通过七层网关为后端的多个Service提供了统一的入口,但由于其部署在集群中,因此并不能直接对外提供服务。实际上Ingress需要配合NodePort和LoadBalancer才能提供对外的流量入口,如下图所示:
上图描述了如何采用Ingress配合NodePort和Load Balancer为集群提供外部流量入口,从该拓扑图中可以看到该架构的伸缩性非常好,在NodePort,Ingress,Pod等不同的接入层面都可以对系统进行水平扩展,以应对不同的外部流量要求。
上图只展示了逻辑架构,下面的图展示了具体的实现原理:
流量从外部网络到达Pod的完整路径如下:
- 外部请求先通过四层Load Balancer进入内部网络
- Load Balancer将流量分发到后端多个主机节点上的NodePort (userspace转发)
- 请求从NodePort进入到Ingress Controller (iptabes规则,Ingress Controller本身是一个NodePort类型的Service)
- Ingress Controller根据Ingress rule进行七层分发,根据HTTP的URL和Host将请求分发给不同的Service (userspace转发)
- Service将请求最终导入到后端提供服务的Pod中 (iptabes规则)
从前面的介绍可以看到,K8S Ingress提供了一个基础的七层网关功能的抽象定义,其作用是对外提供一个七层服务的统一入口,并根据URL/HOST将请求路由到集群内部不同的服务上。
场景应用分析
ClusterIP
ClusterIP 服务是 Kubernetes 的默认服务。它给你一个集群内的服务,集群内的其它应用都可以访问该服务。集群外部无法访问它。
ClusterIP 服务的 YAML 文件类似如下:
apiVersion: v1
kind: Service
metadata:
name: my-internal-service
selector:
app: my-app
spec:
type: ClusterIP
ports:
- name: http
port: 80
targetPort: 80
protocol: TC
从Internet 没法访问 ClusterIP 服务,那么为什么要讨论它呢?那是因为我们可以通过 Kubernetes 的 proxy 模式来访问该服务!
# 启动 Kubernetes proxy 模式:
kubectl proxy --port=8080
# 通过Kubernetes API,使用如下模式来访问这个服务:
http://localhost:8080/api/v1/proxy/namespaces/<NAMESPACE>/services/<SERVICE-NAME>:<PORT-NAME>
# 要访问上面定义的服务,可以使用如下地址:
http://localhost:8080/api/v1/proxy/namespaces/default/services/my-internal-service:http/
何时使用这种方式?
有一些场景下,你得使用 Kubernetes 的 proxy 模式来访问你的服务:
由于某些原因,你需要调试你的服务,或者需要直接通过笔记本电脑去访问它们。
容许内部通信,展示内部仪表盘等。
这种方式要求我们运行 kubectl 作为一个未认证的用户,因此我们不能用这种方式把服务暴露到 internet 或者在生产环境使用。
NodePort
NodePort 服务是引导外部流量到你的服务的最原始方式。NodePort,正如这个名字所示,在所有节点(虚拟机)上开放一个特定端口,任何发送到该端口的流量都被转发到对应服务。
NodePort 服务的 YAML 文件类似如下:
apiVersion: v1
kind: Service
metadata:
name: my-nodeport-service
selector:
app: my-app
spec:
type: NodePort
ports:
- name: http
port: 80
targetPort: 80
nodePort: 30036
protocol: TC
NodePort 服务主要有两点区别于普通的“ClusterIP”服务。第一,它的类型是“NodePort”。有一个额外的端口,称为 nodePort,它指定节点上开放的端口值 。如果你不指定这个端口,系统将选择一个随机端口。大多数时候我们应该让 Kubernetes 来选择端口,因为如评论中 thockin 所说,用户自己来选择可用端口代价太大。
何时使用这种方式?
这种方法有许多缺点:
每个端口只能是一种服务
端口范围只能是 30000-32767
如果节点/VM 的 IP 地址发生变化,你需要能处理这种情况。
基于以上原因,我不建议在生产环境上用这种方式暴露服务。如果你运行的服务不要求一直可用,或者对成本比较敏感,你可以使用这种方法。这样的应用的最佳例子是 demo 应用,或者某些临时应用。
LoadBalancer
LoadBalancer 服务是暴露服务到 internet 的标准方式。在 GKE 上,这种方式会启动一个 Network Load Balancer,它将给你一个单独的 IP 地址,转发所有流量到你的服务。
何时使用这种方式?
如果你想要直接暴露服务,这就是默认方式。所有通往你指定的端口的流量都会被转发到对应的服务。它没有过滤条件,没有路由等。这意味着你几乎可以发送任何种类的流量到该服务,像 HTTP,TCP,UDP,Websocket,gRPC 或其它任意种类。
这个方式的最大缺点是每一个用 LoadBalancer 暴露的服务都会有它自己的 IP 地址,每个用到的 LoadBalancer 都需要付费,这将是非常昂贵的。
Ingress
有别于以上所有例子,Ingress 事实上不是一种服务类型。相反,它处于多个服务的前端,扮演着“智能路由”或者集群入口的角色。
你可以用 Ingress 来做许多不同的事情,各种不同类型的 Ingress 控制器也有不同的能力。
GKE 上的默认 ingress 控制器是启动一个 HTTP(S) Load Balancer。它允许你基于路径或者子域名来路由流量到后端服务。例如,你可以将任何发往域名 foo.yourdomain.com 的流量转到 foo 服务,将路径 yourdomain.com/bar/path 的流量转到 bar 服务。
GKE 上用 L7 HTTP Load Balancer 生成的 Ingress 对象的 YAML 文件类似如下:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: my-ingress
spec:
backend:
serviceName: other
servicePort: 8080
rules:
- host: foo.mydomain.com
http:
paths:
- backend:
serviceName: foo
servicePort: 8080
- host: mydomain.com
http:
paths:
- path: /bar/*
backend:
serviceName: bar
servicePort: 8080
何时使用这种方式?
Ingress 可能是暴露服务的最强大方式,但同时也是最复杂的。Ingress 控制器有各种类型,包括 Google Cloud Load Balancer, Nginx,Contour,Istio,等等。它还有各种插件,比如 cert-manager,它可以为你的服务自动提供 SSL 证书。
如果你想要使用同一个 IP 暴露多个服务,这些服务都是使用相同的七层协议(典型如 HTTP),那么Ingress 就是最有用的。如果你使用本地的 GCP 集成,你只需要为一个负载均衡器付费,且由于 Ingress是“智能”的,你还可以获取各种开箱即用的特性(比如 SSL,认证,路由,等等)。
拓展阅读
针对网络流量的负载均衡,在实际生产工作中都是机其重要的角色的,我们几乎所有的流量通信服务都是需要对外暴露,这是第一步,之后才会开始考虑鉴权,负载,限流等诸多问题的,所以如何安全合适的暴露我们的流量是一个值得探讨的话题,尤其在微服务领域,当开发人员的所有开发能力永远和 IP+Port 时,技术人员也应该反思下自己的能力了,流量分析的问题是一个很大的面,也是从初级开发人员迈向高级/资深工程师的很关键的一步。