深圳幻海软件技术有限公司 欢迎您!

万字解读云原生时代,如何从 0 到 1 构建 K8s 容器平台的 LB(Nginx)负载均衡体系

2023-03-20

云原生时代,基于Kubernetes的容器编排方案是当下最优选择,各个中型、大型互联网公司全都拥抱Kubernetes,没有其他方案可以与Kubernetes匹敌。所有业务(尤其是高并发业务)的访问必然要通过负载均衡LB代理层,服务端高并发系统离不开负载均衡,大中型公司下,负载均衡代理层都是有专人进

云原生时代,基于 Kubernetes 的容器编排方案是当下最优选择,各个中型、大型互联网公司全都拥抱 Kubernetes,没有其他方案可以与 Kubernetes 匹敌。

所有业务(尤其是高并发业务)的访问必然要通过负载均衡 LB 代理层,服务端高并发系统离不开负载均衡,大中型公司下,负载均衡代理层都是有专人进行独立开发和建设的,云原生 Kubernetes 容器平台下的 LB 代理层,同样需要有专人来负责建设和维护。那么 Kubernetes 容器平台基础下的的 LB(Nginx) 负载均衡代理层要怎么建设?和非容器平台下的 LB 建设有什么异同?建设的核心要点和当下最优的方案是什么?相信看完本文,都会对 Kubernetes 容器平台的 LB(Nginx)负载均衡了然于心,并且可以快速深入建设 Kubernetes LB(Nginx)负载均衡体系。还可以了解到,一个中大型公司,是如何从 0 到 1 来构建大规模 Kubernetes 容器平台的 LB(Nginx)负载均衡体系的一些非常宝贵的实战经验。

适应人群 :Kubernetes 开发者、LB 开发者、Kubernetes 基础运维人员、LB(Nginx)从业者、容器平台开发 or 架构设计人员。

一、容器 LB 建设的背景

PS:如果对 Kubernetes 基本概念还不熟,那么需要先理解一下 Kubernetes,本文是针对对 Kubernetes 基本概念有一定理解的基础上来进行分析和设计。

1.初识负载均衡(LB)

负载均衡(Load Balancer,简称 LB)是指把客户端访问的流量通过负载均衡器,然后根据指定的一些负载均衡策略进行转发,最终可以均匀的分摊到后端上游服务器上,然后上游服务器进行响应后再返回数据给客户端。负载均衡的最常见应用是充当反向代理,通过负载均衡,可以大大的提高服务的响应速度、提高并发请求、提高稳定性(防止单点故障)。

  • 负载均衡的基本实现方案,从业界来看,一般分为软件和硬件两大类,软件负载均衡又可以分层如4层、7层负载均衡,如下:
  • 硬件负载均衡
  • 如 F5,性能好,但是贵。一般的互联网公司都没有采集硬件负载均衡
  • 软件负载均衡
  • 目前这两个都可以实现 4 层,但是更多的还是使用 Nginx 的 7 层功能。
  • 4 层:典型的如 LVS
  • 7 层:典型的如 Nginx、HAProxy

2.容器化下 LB 的异同点

在物理机时代,还没有容器化之前,典型的负载均衡的建设方案就是搭建一套 Nginx 集群,提供 7 层的代理;搭建一套 LVS 集群,提供 4 层代理方案。并且同时,一般 7 层之上,都有一个 4 层代理,流量的基本流向就是 client -> LVS(4 层) -> Nginx(7层) -> server 。

在物理机这个时代,运维人员对 Nginx 的 upstream 的配置,基本都是手动添加修改各个 server,然后推送配置上线应用。传统的物理机时代的维护方式,是基于后端 server 的 IP 基本是固定的,比如,你上线一个 WebServer 的服务,要部署到哪些机器上,这个是事先确定好的了,IP 会固定不变,不管你怎么升级,服务都还是固定在这些机器上,因此这个时代这样的维护方式,并没有太多问题,大家以往也都维护的挺和谐。

在容器化时代,基于 Kubernetes 的容器化平台下,LB 的建设有哪些差异呢?主要分为两大块:

• 后端服务的 IP,会由于集群的调度,IP 是可变的,每当你部署、升级等操作的时候,IP 都会改变,那么这个时候,我们显然不能够再继续采用原有写死 IP 的方式来进行 7 层代理的维护了。由于服务 IP 的不确定性,我们必须要改变姿势,不能由人为填充 Nginx 的 upstream 的 server ip 的方式,只能通过动态的获取和变更,这个就需要 LB 能够主动发现后端服务并且动态更新

• Kubernetes 的容器化平台下,集群内部的网络是虚拟的,虚拟网络的 IP 在集群外部是无法访问的,因此还需要解决好容器集群内外的网络互通问题。

二、容器 LB 负载均衡怎么建设

1.Kubernetes 的负载均衡

Kubernetes 本身有内置一个集群内部的负载均衡方案,叫 kube-proxy,但是这个只能内部访问,并且功能稍显不足;而实际上,我们的容器平台,必须要提供集群外部访问的功能,因为你的用户(客户端)都是在集群外部。

Kubernetes 负载均衡相关的方案,包括:

  • 集群内部负载均衡【内置】

Pod IP 在集群内部都是互通的,因此集群内部无需考虑网络互通问题

每个 Node 节点上的 kube-proxy,就是集群内置的内部负载均衡的解决方案;但是只限于集群内部,并且功能有限

  • 集群外部负载均衡【额外添加】

社区提供的 nginx-ingress-controller 方案可以满足需求

云厂商的 Cloud provider 也可以满足需求

参考 nginx-ingress-controller 的模式,自建 LB 方案

由此可见,如果是在自己 IDC 内部建设容器 LB 方案,那么只能采用自建方案 或者基于 nginx-ingress-controller 方案来建设;如果是上云的话,那么可以自建,也可以直接采用云厂商的方案。

下面所有的介绍,都是基于自建方案来设计,在 IDC 内部,我们要怎么从 0 到 1 来建设 K8s 容器的 LB 体系。

2.业务需求

业务功能需求就在于,业务(开发)使用容器 LB 体系的时候,他们会需要哪些需求,包括怎么使用、需要哪些功能、需要哪些策略,作为容器 LB 建设的开发人员,我们需要能够站在业务方的角度去考虑,如下图所示,有这些业务需求:

详细说明如下:

  • 体验需求

LB 分组:这个业务非常核心,需要独立的 LB 集群,也就是 LB 代理层需要分组

域名解析线路:如果是多集群、多 IDC,那么服务暴露的域名,要怎么解析,是全 IDC 都解析,还是只解析到某一个集群

7 层代理的一些高级配置,如 uri 的 rewrite 规则、自定义一些特殊配置

大部分用户:业务要暴露自己的服务只需要足够简单的配置和理解,他们不需要也不想关注服务暴露的细节,要的就是一个结果,我的服务部署了,我要暴露出去给 client 端调用

小众用户:业务非常核心,有各种不确定因素存在,业务开发人员需要关注细节

  • 负载均衡代理层的常规功能需求

要能够统计 SLA ,包括 QPS、慢请求、错误数 等

要能够针对异常进行告警

要能够支持常见的负载均衡算法,如轮询、最小连接、hash 等

负载均衡代理层要能够支持超时、重试等基本功能

负载均衡代理层还必须要能够支持对后端服务的健康检查

基本的服务暴露:支持 4 层、7 层的代理方案,支持 7 层的 HTTP、HTTPS,也支持基本的 PATH 路由

域名:服务暴露的时候,每个服务肯定需要有自己的域名,那么这个域名需要能够支持默认按照一定规则生成,还需要能够支持自定义域名;具体怎么选择就看业务自己的需求

内外网的需求:有些业务是直接给 APP 调用的,那么必然需要暴露到外网;而有些业务只是需要集群内部访问,那么就暴露到内网即可;

upstream 上游(后端)服务的基本策略

监控和统计

  • 负载均衡代理层的高级策略需求

限流策略:高可用服务必须要有的功能,通过 LB 代理层进行限流,防止流量太大从而导致后端过载引发整体故障

熔断保护机制:当服务发现异常,并且通过限流还不能解决的时候,需要能够直接熔断,也就是直接断开请求,防止影响到其他业务

灰度放量:当业务新上线一个功能(版本迭代)的时候,首先需要进行灰度放量,然后观察,看是否满足预期,如果满足预期则继续灰度放量;如果有异常则需要马上回滚

3.运维需求

我们建设的容器 LB 方案,最终是要交付给运维同学去使用的,运维必须要把控好整个公司的流量入口,LB 就是整个公司的流量入口;而且一般业务同学也没有权限去操作 LB 相关的配置。那么,站在运维的角度来看,容器 LB 需要提供哪些功能呢?如下图所示,有这些运维需求:

详细说明如下:

  • 负载均衡器的相关管理

负载均衡器的自动化脚本部署,因为运维需要部署负载均衡器,那么怎么样能够实现更为智能的自动化脚本部署,而不是零散的各个命令去操作呢?这块依赖于我们提供的一些操作步骤和子命令,然后结合 ansible 来封装实现

负载均衡器的扩缩容,部署完了之后,后续还可能有扩缩容需求,比如国庆期间、春节期间、大促期间,这是需要提前扩容的,那么怎么能够快速扩缩容?怎么更自动化?这块同样也是需要结合 ansible 来封装实现

负载均衡器的分组,对运维而言,稳定性是首要的,那么线上的业务,有重要的服务,也有非重要的服务,一般而言,对重要核心的服务、流量非常大的服务,都需要单独的分组,用来进行物理上的隔离和管控

  • 权限管控和审计

权限,一般而言,公司建设 Kubernetes 容器平台,都会有一套管理平台系统,所有人都是通过管理平台来操作,包括运维和开发。如部署业务服务、上下线、LB 的操作和管理等等。那么既然是这样,那么必须要控制好权限,不同角色有不同的操作权限,避免所有人都能够操作负载均衡的相关配置,只有管理员 或者 运维人员才能够操作

审计,线上的所有变更,都需要有审计,方便回溯问题

  • 业务服务的配置操作

Nginx 负载均衡的基本配置检测,要能够通过管理平台来实现,包括基本检测和异常检测,检测通过才能执行变更

Nginx 负载均衡配置的灰度和回滚机制,灰度是说变更之前,需要先灰度 1 个 Nginx 节点,确保这次变更没有问题之后,才能全量变更;回滚是说如果灰度出现问题,那么需要能够快速回滚到上一个版本

Nginx 负载均衡配置的基本查看、搜索;可以全局管理所有配置;可以搜索关键字来快速定位配置

  • 稳定性的相关操作(流控)

业务限流,当业务流量过大之后,根据实际情况进行限流,避免打满后端服务

灰度放量,业务更新之前需要一个灰度逐步放量的过程

  • LB 系统和域名管理系统打通

中大型公司而言,都会有内部的域名管理系统,每个服务都会有一个对外暴露的域名来访问,那么域名管理系统必须要和 LB 系统打通并且联动起来,形成一个完整的操作链。这就需要用户暴露一个服务的时候,并不用事先申请域名,直接在 LB 系统这里进行申请即可。

4.基本方案和基本原则

Kubernetes 下,后端服务都是 Pod 的形态,Pod 要能够实现对外的负载均衡,就必须要成为 nginx 的 upstream。而 Pod 的 IP 是随时都可能变化的,为此,就需要一个 Nginx-Controller 来动态发现 Pod,然后渲染为 nginx 的 upstream;Nginx-Controller 就是一个 Nginx 再加上一个 Controller(发现 Pod 并渲染为 upstream)。

所以,就需要我们能够自研一个 Nginx-Controller 组件来实现了,那么这个 Nginx-Controller 有些什么要求 ?

A,集群内外的网络要能互通

基本要求就是:

  • 集群内,Nginx-Controller 要能够将流量分发给 Pod

需要将 Nginx-Controller 纳入到 Kubernetes 的节点中,也就是部署 Nginx-Controller 的机器必须是 Kubernetes 的 Node 节点

  • 集群外,外网的请求要能够转发到 Nginx-Controller 中

这就需要部署 Nginx-Controller 的机器能够和外部互通,一个最简单的方式就是,Nginx-Controller 采用二进制部署,使用 Node 主机的网络,这样就可以了

因为 Node IP 是互通的,只有 Pod IP 不互通

B,动态发现 Pod 并且渲染为 nginx 配置

首先,我们需要能够 watch 到 Pod、Service、 Endpoints 等资源的变化,这个就需要和 K8s API Server 交互,一般我们现在都是使用 Golang 语言来实现,因此可以基于官方的 client-go 来实现

在这,我们需要提供一套统一的模板配置,方便业务配置,然后自动渲染。因为 Nginx-Controller 要 watch 的业务服务资源是未知的,随时可以增加或者删除,那么最好能够有一套模板机制来实现,对于 Golang,可以通过 Golang 的 template包来封装模板的实现,结合模版和当前 Service、Endpoints 的情况,渲染成对应的 nginx 配置。比如:

upstream test-api {
          {{ k8sBuildUpstream  "default.test-back" "port=8080" "max_fails=3" "fail_timeout=3s"  }}
  • 1.
  • 2.

会渲染成相应服务的节点列表和端口:

upstream test-api {
          server 10.1.1.7:8080  max_fails=3  fail_timeout=3s;
          server 10.1.1.9:8080  max_fails=3  fail_timeout=3s;
       }
  • 1.
  • 2.
  • 3.
  • 4.

C,实现灰度、全量、回滚的机制

Nginx-Controller 虽然可以动态渲染 nginx 配置了,但是作为线上服务,必须需要有灰度、全量、回滚的机制。

因为我们的容器 LB 是需要分组的,每一组 LB 也都会有多个 nginx 节点,灰度就是指,我们的配置要发布,首先灰度一个节点,确保这个节点 OK 之后,再灰度到下一个 nginx 节点,或者可以全量到所有 nginx 节点。回滚则是指当我们灰度一个节点之后发现有问题,则回滚这个节点的配置。

怎么实现呢?可以通过两个 configmap 来解决灰度和全量更新的问题,configmap-canary 这个作为灰度的 configmap,并且通过 annotation 来标记哪些是要灰度的 nginx 节点的 IP,这样 nginx controller 如果识别到configmap-canary 里面的变化,则通过 annotation 的 IP 来判断是否是本节点的,如果是本节点的则渲染配置并且 reload nginx,从而生效,如果不是本节点的,那么则丢弃。当要全量的时候,则:

  • 首先,将所有的全量节点追加到 configmap-canary 的annotation["ip"]字段中,nginx-controller 读取该字段,匹配ip字段,匹配节点更新配置
  • 然后,如果确保已经全量成功,那么则先将 configmap-canary 的内容覆盖到 configmap-release 中,然后再清空 configmap-canary 中的 IP 列表;这样就可以完成整个灰度和全量的过程。

如果灰度的时候,发现异常了,需要回滚,那么直接清空 configmap-canary 中的 IP 列表;然后再回滚到上一个版本后,重新再走一遍发布流程来完成回滚操作

D,容器 LB 组件本身的管理和部署

上面说到容器 LB 组件本身(Nginx-Controller)需要二进制部署到 Node 主机上,那么要合理的管理这种二进制部署的需要一直运行的程序,一个较常见并且优雅的姿势就是通过 systemd 来管理。示例配置如下:

[Unit]
Descriptinotallow=nginx-controller daemon
Documentatinotallow=/www/nginx-controller/bin/nginx-controller -h
After=nginx.service
Wants=nginx.service

[Service]
Type=simple
ExecStart=/www/nginx-controller/bin/nginx-controller --slow-start=true  --is_dynamic=true ${OPTIONS}
ExecStop=/bin/kill -SIGTERM $MAINPID
ExecReload=/bin/kill -HUP $MAINPID
KillSignal=SIGQUIT
Restart=on-failure
RestartSec=3s

[Install]
WantedBy=multi-user.target
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

只要将这个配置放到 /usr/lib/systemd/system/ 中,systemd 就可以管理起来了。

E,各种统计和监控

Nginx-Controller 代理层所需的监控包括如下:

  • 进程的监控

进程是否存活、是否出现 panic 等

  • 日志监控

日志首先要采集,然后要对错误日志进行监控,可以使用 ELK

  • 基本指标监控

Nginx-Controller 的一些基本指标监控,可以使用 Prometheus

比如 reload 次数、更新次数、更新是否失败 等。。。。

  • LB 所在主机的机器性能监控

CPU:idle、system、user 等指标

网卡软中断

网络带宽:流入和流出带宽指标、网卡丢包指标

内存使用、swap 使用

磁盘 IO:读、写两方面

剩余句柄数

  • LB 代理层的基本业务指标监控

SLA

错误统计

延迟统计

域名维度、path 维度等

三、容器 LB 体验优化(LB 架构产品设计)

1.初期的架构图

我们既然是从 0 到 1 来构建 K8s 的 负载均衡体系,那么初期必然是需要从物理机转向容器,一般的选择是为了能够保证项目可以正常实施,容器 LB 这块的抉择,会结合着运维同学的一些习惯、可接受性以及更少的改动、更高的稳定性来做一些架构上的取舍。

没有容器化之前,7 层代理的架构一般是 client -> CDN -> LVS -> 物理机 Nginx -> server ;

为了满足上述诉求,在容器化之初,容器 LB 可能还不稳定,需要逐步导量过来,因此整体架构会是client -> CDN -> LVS -> 物理LB -> 容器LB(Nginx-Controller) -> POD ,如下:

LVS 和 Nginx 都需要做高可用,因此:

  • LVS 就是通过 keepalive 本身来做高可用,并且 LVS 需要配置万兆网卡,因为所有流量都要经过 LVS。
  • Nginx 的高可用和高并发就是建立一组 Nginx(多个 Nginx 实例),然后挂到 LVS 下面做心跳检测和流量分发

LVS 4 层代理可以对 Nginx 做检测来保证高可用

LVS 4 层代理可以基于 4 层做流量分发到 Nginx 上

  • 容器 LB(Nginx-Controller) 和 Pod 的网络需要能够互通,因此 容器 LB 也需要建立在 Kubernetes 集群之内,在同一个网络架构下

Kubernetes 容器平台的网络可以选择 Calico

2.最优的架构图

在项目中后期,容器 LB 倾向稳定之后,那么我们要考虑的就是性能问题、成本问题、体验问题了,为此,架构需要逐步演进。

  • 首先,物理机 Nginx 的存在,会导致多了一层链路

增加响应耗时

增加配置管理的复杂度

增加问题排查的链路分析

增加机器成本

  • 其次,Nginx-Controller 这个方案,有更优的替代方案,那就是nginx-ingress-controller

整体的最优的架构流向就是: client -> CDN -> LVS -> Nginx-Ingress-Controller -> Pod

Nginx-Ingress-Controller 的具体介绍在后面章节进行分析。

3.体验优化

优化 1:实现动态 upstream,减少 Nginx Reload 带来的 502

为何需要支持动态 upstream 呢?这是因为,在 K8s 下,服务的 Pod IP 会经常改变,比如每次发布更新的时候 Pod IP 都会变化,这也就意味着,nginx 的 upstream 的 server 列表会经常改变,那么每次 IP 有变化的时候,nginx 都需要 reload 的话,那么在线上高并发、大流量的场景下,长连接的服务会经常在 nginx reload 的时候出现 502,这个是不能接受的,非常影响业务的 SLA

那么为何长连接的服务会经常在 nginx reload 的时候出现 502 呢?这个要重点分析下 nginx 在进行 reload 的时候,对于老连接是怎么处理的,一个确定的流程是:

  • 如果当前连接是空闲状态,那么直接关闭
  • 如果当前连接还在等待 upstream response,那么会等待请求处理结束或者超时 (proxy_read_timeout),再关闭

这一过程对于短连接的请求,是挺合理的,表现也挺正常的。但是对于长连接场景,nginx 有些处理不好的地方。对于长连接请求,nginx 在处理完最后一个请求,返回 response 的时候,他依然是返回 Connection: keepalive 的 response header。这样就会导致会有一个时间窗口差,在 nginx 对于这个连接进行 close 以及到 Linux 内核完整 close 这个连接,并且发出 FIN 到 client 这个时间段内,client 端如果是高并发的场景,那么由于是长连接,因此很也可能会继续复用这个连接来发起新的请求给 Nginx,这样 Nginx 机器所在的 Linux 内核看到对于一个已关闭的连接还有新的请求,那么就会直接返回 RST 包,从而导致了 client 的一些 502 的错误。

优化 2:实现 SlowStart 功能,减少 Pod 启动初期的 SLA 性能下降

SlowStart 策略,指的是,在 Pod 初次启动并且能够对外提供服务之后,刚开始给一个缓冲时间,在这个缓冲时间内,先提供小流量的请求,进行有 weight 权重的 RR 算法,只允许非常小比例的流量;这个缓冲时间之后,再开始无权重的 RR 算法。

一般而言,Pod 的 Readiness 探针是可 worker 之后,就认为这个 Pod 可以开始对外提供服务了。但是针对某些 Java 服务,Readiness 探针 OK 后,还不能马上提供大量服务,因为 Java 需要启动 Java 虚拟机,初始化相关系统、组件;还有一些各种内存池、线程池 等初始化工作要做;而这些初始化工作在某些情况下可能需要一点耗时;或者某些情况下是有请求过来后才进行初始化,但是由于初始化需要时间,因此 Readiness 探针 OK 之后,还不能马上提供大量服务,否则在启动的时候就可能造成服务的些许不稳定,从而降低 SLA,给业务带来影响。这个是我们实际 Java 项目所得出的结论,因为 jit 的影响,如果在低流量下完成 jit 编译,这样给一个缓冲时间,最终效果就是可以提高 SLA。目前这个功能其实是一个规避措施,按理来说需要业务方自己解决的,因为不同的业务方可能情况也有些区别。

具体怎么实现呢?这就要结合 Kubernetes 本身机制来综合实现了。一般 Kubernetes 中服务的部署是通过 Deployment + Service 来部署一个服务;那么这样的话,服务就可以支持 Deployment 的滚动更新的特性,通过配置MaxSurge(如 25%),MaxUnavailable(如 25%),minReadySeconds(如 30s),progressDeadlineSeconds(如 600s) 几个参数来控制滚动策略,可以实现每次滚动升级过程中新旧一起加起来的总的 Pod 数会小于等于(1+MaxSurge)* desiredPods,而 available 可以的 Pod 节点数可以保证大于等于 MaxUnavailable * desiredPods,新增 Pod 节点 ready 后等待最少 minReadySeconds 后成 available,整个滚动流程超过 progressDeadlineSeconds 600s 停滞则认为失败,回滚旧版本。

为此,SlowStart 的机制实现就可以利用这个特性了,如果开启了 SlowStart 功能,那么就判断 Pod 节点是否是本次更新新启动的节点,如果是新启动的的 Pod 节点则调整其 Pod 的 weight 成预设比例(一般是较小权重),当节点 ready 时间超过 MinReadySeconds 后 ,恢复 weight 成正常权重(默认:100) ,从而实现 SlowStart 慢启动。这个机制的 SlowStart 功能实现的慢启动针对的是整个业务的 Service 级别的。利用这个特性来判断节点是否为新增节点,总结来看需要满足的条件如下:

# 1. 节点在deployment发布周期内
LatestPod.ReadyStatus.LastTransitionTime + progressDeadlineSeconds > CurTime

# 2. 节点ready后未超过minReadySeconds窗口
CurPod.ReadyStatus.LastTransitionTime + minReadySeconds > CurTime

# 3. 当前节点初始化时间与最新节点初始化时间差值未超过minReadySeconds窗口,防止扩步长限流
CurPod.InitializedStatus.LastTransitionTime + minReadySeconds > LatestPod.InitializedStatus.LastTransitionTime
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

如果是新增节点的话,则设置其 weight 为 100 * slow-start-weight,并且设置 service 级别的触发器,在LatestPod.ReadyStatus.LastTransitionTime + minReadySeconds + 10s - CurTime 时间后恢复为默认权重( weight=100)。

优化 3:LB 配置发布和运维域名管理系统打通,减少服务暴露的流程步骤

一般的互联网公司,运维这边都会有自己的域名管理系统,开发人员可以通过提单的方式,让运维给自己的服务分配一个域名(内网、外网);然后开发人员拿到这个域名之后呢,再和自己的服务绑定,这个绑定的过程就是服务暴露的过程。服务暴露就是指在 LB 这边建立对应的规则,然后让就可以通过这个域名来访问对应的服务了。

这个服务暴露的过程,首先需要人工提单,拿到域名后再进行手动配置,为此,如果公司有合适的机制和契机,那么应该需要将容器 LB 进行服务暴露的过程和域名管理系统打通,当业务需要进行服务暴露的时候,不再需要通过多个平台的操作来完成,只需要在容器 LB 这边的管理平台中进行服务暴露,然后内部可以自动生成域名或者自定义域名,然后自动和域名管理系统打通,然后正式生效对外提供服务。

这样的优化主要的目的就是为了提升用户体验,减少中间的人工操作环境,从而也可以进一步减少人力成本。

优化 4:移除物理机 Nginx,优化链路,降低成本

我们前面说到,在初期的时候,为了保证稳定和过渡,还是需要有物理机 Nginx 的存在,物理机 Nginx 的主要作用有两方面:

  • 其一,可以通过物理机 Nginx 这一层来对容器 LB 的流量进行灰度放量,同时可以能及时回滚
  • 其二,整个公司的业务服务,会有很多依然部署在物理机上,初期只会有小部分服务会开始逐步往容器进行迁移,因此物理机 Nginx 还必须要保留

但是在项目中后期,容器 LB 会逐步趋于稳定,此时,就需要逐步移除物理机 Nginx,直接是 LVS 到容器 LB,但是移除物理机 Nginx 需要有大量的工作要去梳理,因为物理机 Nginx 的配置是手动配置的,可能有很多差异化、特性化的配置。

优化 5:采用 nginx-ingress-controller 方案,减少 nginx 配置的干预,一步到位

前面说到 nginx-ingress-controller 可以作为最优方案来替代 Nginx-Controller, nginx-ingress-controller 产生的主要目的就在于能够将 Kubernetes 中的 Service 所代理的 Pod 服务暴露在 Kubernetes 集群之外,这样就能够打通集群内外的访问问题,通过 ingress 可以直接进行七层的负载均衡,并且可以对外访问,同时减少了一些复杂的配置。

因此,请求流程 client -> LVS VIP -> ingress-controller -> 业务 POD

具体的 nginx-ingress-controller 方案参看下面最后的说明。

四、容器 LB 开发设计的核心考量点

容器 LB 开发设计的核心考量点有如下:

详细说明如下:

1.支持动态 upstream 的实现【非常重要】

K8s 容器平台下,业务服务的 Pod 的是动态变化的,比如再每次重新部署、滚动升级、被驱逐重建等情况之后, Pod 的 IP 都是会发生改变。每次 Pod IP 改变,那么就意味着 Nginx 的 upstream 发生了变化,如果没有实现动态 upstream,那么将会导致每次 Pod IP 变化,Nginx 都需要进行异常 Reload 操作。在线上大规模集群下,如果业务的 QPS 请求很高,Nginx 频繁 Reload 会导致 client 端的长连接请求在 Nginx Reload 的时候出现 502,这样将降低业务的 SLA,故而无法提供高可靠的服务保障。

故而,只要我们实现了动态 upstream,比如基于 lua 模块的实现,那么不管后端 Pod IP 如何变化,Nginx 后端 upstream 的 IP 将会通过 lua 共享内存传递并进行负载均衡,因此 Nginx 将不会进行 Reload,从而会大大提高 SLA 服务质量。

2.支持后端 pod 的健康检查

Pod 本身,K8s 的 kubelet 会做健康检查,那么容器 LB 层面为何还需要对 Pod(业务服务)做健康检查呢?

  • kubelet 本身可能会出现故障导致不能及时摘除异常的 Pod,因此我们不能完全信任 kubelet
  • 如果 Node 节点出现异常,那么 kubelet 把 pod 标记不可用,基本需要几十秒,也就是影响几十秒之后才能检测到

3.SlowStart 策略

Nginx 的商业版本有支持 slow_start 功能,使用如下:

upstream backend {
    server backend1.example.com slow_start=30s;
    server backend2.example.com;
}
  • 1.
  • 2.
  • 3.
  • 4.

SlowStart 策略是指配置了 SlowStart 策略的 server,在 SlowStart 时间范围内,先给一定量的流量(比如 0% - 1%),在过了 SlowStart 时间之后,再恢复 100% 的流量。

这样,在 SlowStart 时间范围内,这个 server 就可以在低流量下处理一些服务内部初期的一些事情,比如 Java 服务,可以在低流量下完成 jit 编译、完成 Java 虚拟机初始化等,这样,当过了 SlowStart 时间之后,等一切就绪在恢复 100% 的流量,可以保证服务可以对外提供更好的质量。

当前,这个是商业版本的实现,开源版本无法使用,因此就需要我们自己实现,在 K8s 下,容器 LB 的 SlowStart 功能的具体实现可以参考文章前面的说明。

4.巡检模块

巡检模块不仅仅是针对容器 LB,可以是针对所有容器基础模块,这个的目的就在于,人为模拟一些实际情况,通过巡检,把容器 LB 的各个环节都定期检测一遍。这个在上线初期尤为重要。

巡检模块的实现至少包括如下:

  • 解耦待巡检服务(利于增加不同的巡检模块)
  • 多久检测一次(间隔、重试)
  • 检测的异常定义(比如 latency、error 等)
  • 出现异常的处理机制(比如告警、输出日志等)

通过巡检模块,可以有如下优势:

  • 首先可以保证容器 LB 出现问题能够及时发现,因为是自定义任务来检测容器 LB 的各个环节,因此大概率可以先于业务本身发现。巡检模块出现问题之后,需要及时告警给相关人员进行处理
  • 然后因为巡检了容器 LB 的各个环节,因此如果巡检模块没有出现问题,那么容器 LB 的整体就基本是正常的,这个对于维护人员的信心度可以大大增强。

5.Nginx SLA 统计模块

业界用的多是 tengine 的 ngx_http_reqstat_module,如果想要更优化,可以在此基础上进行扩展,增加如下这些功能:

  • 慢请求统计
  • 支持 http 自定义错误码(如 6xx 7xx) 等的统计
  • 自定义 http status 的统计
  • 支持以 upstream 为维度来统计

6.性能压测和优化

容器 LB 必须要进行大量压测和优化,以求达到最优的性能,提供最稳定的服务。

本文转载自微信公众号「 后端系统和架构」,作者「AllenWu」,可以通过以下二维码关注。

转载本文请联系「 后端系统和架构」公众号。