一、概述
当我们使用 K8S 对容器进行编排时,基于负载均衡和高可用方面考虑,且设计上 Pod 易失态,不能直接使用 PodIP 作为外部访问的方式。因此,K8S 官方提供了一些负载均衡的解决方案。这其中有四层和七层两种,本文主要介绍 K8S 内的七层负载均衡实现方案 Ingress,它是主流的 K8S 南北向七层负载均衡实现。
二、功能介绍
1. 功能入口
项目 -> 应用列表 -> 流量接入 -> Ingress

2. 创建 Ingress 规则
首先需要确定的是暴露到内网还是外网,选择要发布的地域和集群。

然后最核心的是 ingress 规则,需要填写域名和路径匹配的方式,以及最终处理流量的 backend service。

如果用户有高级需求,可以使用注释进行实现。
3. HTTPS TLS 证书配置
Ingress 默认提供 HTTP 的流量处理,如果需要可以把 ingress 配置为支持 HTTPS 的类型,其中 tls 证书则由 secret 提供。

需要注意的是,创建的 Secret 类型必须为 kubernetes.io/tls,然后添入 cert 和 key 即可。

4. 验证
创建完 ingress,可以在 ingress 的详细页面看到可以访问的 IP 地址。我们把刚刚配置的域名DNS解析到这个 IP,然后就可以进行访问了。

三、技术选型
1. 常见的三种负载均衡方案

(1)NodePort
NodePort 如其名,直接使用宿主机的 Port 进行暴露。缺点是 端口随机(通常30000+)且有限,使用不便利,维护成本高。唯一的优点是 原生支持,不需要引入额外的组件。但如果想实现高可用和负载均衡,也需要在上层再挂一个四层反向代理。
(2)LoadBalancer
LB 与 服务 一对一 创建,这种模式 最灵活、性能最高、安全性最好。但是对 IP 资源消耗较大,每个云厂商实现不同。适合做四层负载。
(3)Ingress
一个流量入口对应多个 SVC,类似 Nginx,拥有处理七层流量的能力,一般可根据域名和路径等策略进行路由和相关处理。支持限流、TLS、黑名单等诸多功能,用户体验与 网关 相似,适合做七层负载。
2. Ingress 资源配置
让我们看看创建一个 Ingress,通常会填写的一些基本参数有哪些:
properties
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-wildcard-host
spec:
rules:
- host: "foo.bar.com"
http:
paths:
- pathType: Prefix
path: "/bar"
backend:
service:
name: service1
port:
number: 80
可以看到,通常我们会通过使用基于不用的域名和路径的策略,来访问不同的后端服务。所以配置文件中我们填写了 host、path 用来进行规则匹配,backend.service 则是指明了转发给哪个K8S服务。
所以,我们可以把 Ingress 理解成集成在 K8S 中的七层网关,每当我们 创建/修改 Ingress 资源时,就是对网关的配置进行修改。
3. IngressController 选型方案对比

图中 Kubernetes Ingress 即为 IngressNginx,为K8S官方开发支持。而与之相邻的 NginxIngress 则是 NGINX 官方所开发支持。
如无特殊说明,本文中所提及的 IngressNginx 均指 Kubernetes Ingress。
最终,我们选择了 IngressNGINX 方案。
IngressNginx 优点:
➡️基本等同于传统 Nginx 的方案,优势是性能、简单、稳定可靠 和 低成本。
➡️社区默认支持的 IngressController。
IngressNginx 缺点:
➡️由于Nginx的模式是 Watch K8S 集群的相关资源事件,当发现有更新时,会重新生成配置文件,并进行 Reload。然而 Reload 的成本会随着配置文件的大小增长,当配置文件过大,热更新配置可能会有几秒的延迟,这期间可能会有问题(当然,也有方案进行解决,例如在集群中部署多个 IngressNginx 分摊压力,但会增加维护成本)。
➡️另一个缺点就是可扩展性,当我们想对它进行功能扩展时,只能使用 Lua 的方式,这个成本也是很高的。当然,原生支持的功能基本足够使用。
四、IngressNGINX 技术原理
1. IngressNGINX 架构
Ingress 一般由 Ingress资源对象、IngressController 和 GW 三部分组成(对于 IngressNGINX来说 GW 就是 NGINX)。

IngressController 实际上就是一种适配器模式,把原本毫无关系的 Ingress 和 NGINX 集成起来。IC 作为适配器,使 NGINX 拥有了感知 K8S 集群资源变化的能力。

2. IngressNGINX 模型原理
IN 的 POD 由一个容器组成,该容器又包括以下内容:
-
IC进程,它根据 Ingress 和集群中创建的其他资源配置 NGINX。
-
NGINX Master进程,它是负责控制NGINX的管理进程。
-
NGINX Worker进程,它负责处理客户端通信,并对后端应用程序的流量进行负载均衡。
下面是一个模块图,它展示了这些流程如何在一起交互,以及如何与一些外部流程/实体交互:

下面的编号列表用花括号描述了每个连接的类型:
-
(HTTP)Prometheus通过IC公开的HTTP端点获取IC和NGINX指标。默认值为:9113/metrics。注意:Prometheus不是IC所需要的,端点可以关闭。
-
(HTTPS)IC读取Kubernetes API以获取集群中资源的最新版本,并写入API以更新已处理资源的状态并发出事件。
-
(HTTP)Kubelet探测IC就绪探针(默认值为:8081/nginx-ready),以考虑IC-Pod就绪。
4.(文件I/O)当IC启动时,它从文件系统中读取配置生成所需的配置模板。模板位于容器的 /etc/nginxtemplate/ 目录中,扩展名为 .tmpl。
- (文件I/O)IC将日志写入容器运行时收集的stdout和stderr。
6. (文件I/O)IC根据集群中创建的资源生成NGINX配置,并将其写入文件系统的 /etc/nginx 目录中。配置文件的扩展名为 .conf。
7.(文件I/O)IC将TLS证书和密钥从入口和其他资源中引用的任何TLS机密写入文件系统。
8.(HTTP)IC通过UNIX:/var/lib/NGINX/nginx-status.sock UNIX套接字获取NGINX指标,并将其转换为#1中使用的普罗米修斯格式。
9.(HTTP)为考虑配置重新加载成功,IC确保至少有一个NGINX Worker具有新配置。为此,IC通过UNIX:/var/lib/nginx/nginx-config-version.sock UNIX套接字检查特定端点。
10.(N/A)为了启动NGINX,IC运行NGINX命令,该命令启动NGINX主机。
11.(信号)为了重新加载NGINX,IC 运行 nginx-s reload 命令,该命令验证配置并将重新加载信号发送给NGINX主机。
12.(信号)为了关闭NGINX,IC执行nginx-s quit命令,该命令将优美的关闭信号发送给NGINX主机。
13.(文件I/O)NGINX主服务器将日志发送到它的stdout和stderr,这两个日志由容器运行时收集。
14.(文件I/O)NGINX主机在启动或重新加载时读取配置中引用的TLS证书和密钥。
15.(文件I/O)NGINX主机在启动时或重新加载期间读取配置文件。
16.(信号)NGINX主程序控制NGINX Worker的生命周期,它使用新配置创建 Worker,并使用旧配置关闭Worker。
17.(文件I/O)NGINX工作人员将日志写入容器运行时收集的stdout和stderr。
18.(UDP)NGINX Worker通过UNIX套接字/var/lib/NGINX/nginx-syslog.sock通过Syslog协议将HTTP上游服务器响应延迟日志发送到IC。反过来,IC分析并将日志转换为普罗米修斯度量。
19.(HTTP、HTTPS、TCP、UDP)客户端向端口80和443以及GlobalConfiguration资源公开的任何其他端口上的任何NGINX工作端口发送通信量,并从其接收通信量。
20.(HTTP、HTTPS、TCP、UDP)NGINX工作器向后端发送通信量,并从后端接收通信量。
21.(HTTP)Admin 可以通过NGINX工作器使用端口8080连接到NGINX stub_status。注意:默认情况下,NGINX只允许来自本地主机的连接。
3. 创建一个 Ingress 的处理流程

核心步骤如下:
-
用户 创建 Ingress。
-
IC 感知到 Ingress 资源发生变化。
-
IC 重新生成配置文件。
-
IC 触发 NGINX Reload。
7.2 NGINX 读取配置文件。
4. 何时进行 NGINX Reload

通常,Kubernetes 控制器使用SyncLoop模式来检查控制器中所需的状态是否更新或需要更改。为此,我们需要使用集群中的不同对象来构建模型,特别是 Ingress、Service、Endpoint、Secret 以及 Configmap 来生成一个反映集群状态的时间点配置文件。
为了从集群中获得这个对象,我们使用了 Kubernetes Informer。当添加、修改或删除一个新对象时,该informer允许对使用回调对单个更改的更改作出反应。不幸的是,没有办法知道某个特定的更改是否会影响最终的配置文件。因此,**对于每一个变化,必须根据集群的状态从零开始重建一个新的模型,并与当前的模型进行比较。**如果新模型等于当前模型,那么我们就避免生成新的 NGINX 配置并触发重新加载。否则,将检查差异是否仅限于 Endpoints。如果是这样,那么就使用 HTTP POST 请求将 Endpoints 的新列表发送给在 NGINX 中运行的 Lua 处理程序,同样避免生成新的 NGINX 配置并触发重新加载。如果运行和新模型之间的差异不仅仅是 Endpoints,那么将基于新模型创建一个新的 NGINX 配置,替换当前模型并触发Reload。
该模型的一个用途是避免不必要的重载,当状态没有变化时,并检测定义中的冲突。NGINX 配置的最终表示是从一个 Go 模板生成的,该模板使用新模型作为模板所需变量的输入。
一些需要 Reload 的场景(括号中表示 NGINX配置文件对应需要做出的改动):
➡️创建新的 Ingress 资源(添加 server)。
➡️TLS 部分添加到现有的 Ingress(增加 tls 配置)。
➡️从 Ingress 添加/删除路径(增加/删除 location)。
➡️删除 Ingress、Svc、Secret 等(需要删除 server 等)。
需要注意的是,NGINX 使用 balancer_by_lua_block 模块进行负载均衡,这个lua模块可以在不 Reload 的情况下,动态更新upstream上游节点 。换句话说,如果只是 Endpoint 有更新,则无需进行 Reload 。
下面是 NGINX 的配置文件 nginx.conf 简略:
bash
server {
server_name test-msc.qihoo.net ;
listen 80 ;
listen 443 ssl http2 ;
......
location / {
// 一些和 K8S 资源相关的标识
set $namespace "nlzx-chy";
set $ingress_name "qc-safety-brain-test-msc";
set $service_name "qc-safety-brain-test-frontend";
set $service_port "80";
set $location_path "/";
set $global_rate_limit_exceeding n;
// 最终转发到 upstream_balancer 上游
proxy_pass http://upstream_balancer;
......
upstream upstream_balancer {
// 使用 balancer_by_lua_block lua模块进行负载均衡
balancer_by_lua_block {
tcp_udp_balancer.balance()
}
}
5. NGINX Reload 原理
我们在前面提了很多次 NGINX Reload,接下来讲一下 NGINX Reload 的原理。让我们看看 NG 是如何在不影响流量的情况下,进行配置重载。
首先我们看一下 NG 的进程模型,NG 是多进程模型。Master 作为父进程,负责管理所有子进程,不处理流量。Worker作为子进程,负责处理流量。
Master 和 Worker 之间主要通过 signal 进行通信。
进程模型抽象:

早期 MasterListen + WorkerAccept 模型:

现在的 Worker:ReusePort + Accept 模型:

早期 NGINX 的模型是,Master 负责 Listen,然后在 fork 时把 socket 传递给子进程,每个子进程进行 Accept 取客户端链接。但多个进程 Accept 同一个 Socket 有惊群问题,需要使用互斥锁处理,会对性能有一定影响。
而现在的 ReusePort 模型,socket 的 listen 也交给子进程,这样每个子进程都持有自己独有的 socket,就可以不用使用互斥锁,直接交给内核进行负载均衡,性能更佳。
该模型的原理是通过内核提供的 ReusePort 功能实现,性能提升的数据如下:

通常我们需要使用 nginx -s reload 指令进行 Reload,在现在的模型下,这个指令原理如下:

使用nginx -s reload进行平滑重启。nginx启动时会通过参数 -s 发现目前要进行信号处理而不是启动nginx服务,然后他会查看nginx的pid文件,pid文件中保存有master的进程号,然后向master进行发送相应的信号,reload对应的是HUP信号,所以nginx --s reload 与 kill -1 <MasterPid> 一样。
Master收到HUP信号后的处理流程如下:
-
master解析新的配置文件。
-
master fork出新的worker进程,此时新的worker会和旧的worker共存。
-
master向旧的worker发送QUIT命令。
-
旧的worker会关闭监听端口,不再接受新的网络请求,并等待所有正在处理的请求完成后,退出。
-
此时只有新的worker存在,nginx完成了重启。
NGINX 是 IO 密集型系统,关于 IO 密集型系统的各种性能优化,这里就不展开讲了。
6. IngressController 高可用
至此,我们对 IngressController 的原理有了一定的了解。最后我们聊一下 IC 本身的高可用,即 IC 本身的多副本部署架构。
我们使用 DaemonSet + NodeSelector(ingress=on) 方式,只在 Master 节点上部署 IC。
在 IC 的上层使用了 LVS,把 IC 挂载到 lvs 后面,对多个 IC 进行负载均衡。

优点:IC 和 Master 组件集成在一起,不需要额外的机器部署,运维成本低。
缺点:正因为 IC 和 Master 组件部署在同一节点上,当 IC 接入的流量压力过大时,可能会与 Master 组件争抢节点的资源。
五、配置定制化
IngressNGINX 提供的 ConfigMap + Annotation 的方式,基本就可以解决用户的高级需求。但在某些场景下,我们可能仍需要更进一步的定制 NGINX 本身。对于这种情况,我们可以修改 NGINX 的 模板文件 nginx.tmpl。这个模板文件提供了一些语法支持,最终通过模板来定制生成的 nginx 配置文件,以达成需求。
由于 nginx.tmpl 有近 2k 行,这里就不展开介绍该模板的详细内容。
需要注意的是,直接修改配置模板虽然可以深度定制 NGINX,但是需要自行适配后续的 IngressNGINX 升级带来的兼容性变动。
六、未来规划
1. IngressNGINX 自身
由于 IngressNginx 是比较成熟的软件,几乎不用担心他的稳定性、性能 和 功能完善度。所以可优化的层面主要是部署架构:
-
IN 和 Master 分离开,单独机器部署,避免影响到集群基础组件运行。
-
在一个集群里部署多个IC,分担流量压力。
按照目前集群对 Ingress 的使用规模来看,还没有达到这个地步,可以持续观察着,在遇到瓶颈时再进行变动,以节省成本。
2. Gateway API
GatewayAPI 人如其名,设计上也是用来处理流量,在职责定义上囊括了 Ingress 的能力。而社区之所以在有 Ingress 的情况下,又重新定义一个 GatewayAPI 出来,其实是为了解决 Ingress 设计上的不足。但截止目前,其仍处于发展期,还需要一些时间。
更多产品和技术文章,敬请关注👆
360智汇云是以"汇聚数据价值,助力智能未来"为目标的企业应用开放服务平台,融合360丰富的产品、技术力量,为客户提供平台服务。
目前,智汇云提供数据库、中间件、存储、大数据、人工智能、计算、网络、视联物联与通信等多种产品服务以及一站式解决方案,助力客户降本增效,累计服务业务1000+。
智汇云致力于为各行各业的业务及应用提供强有力的产品、技术服务,帮助企业和业务实现更大的商业价值。
客服电话:4000052360