NGINX.COM
Web Server Load Balancing with NGINX Plus

Kubernetes 已成为容器化应用的标准管理系统,并受到了许多企业在其生产环境中的采用。在本博文中,我们将描述通过 NGINX Ingress Controller for Kubernetes 可实现的性能,并详细分析三个指标(每秒请求数、每秒 SSL/TLS 事务和吞吐量)性能测试的结果。此外,将介绍我们所用 NGINX 和 Kubernetes 完整的配置。

您可以使用我们的性能数据和配置来决定适合您应用所需性能和可扩展性级别的拓扑结构、规格和 Kubernetes 配置。

拓扑结构

在所有测试中,客户端计算机上运行的 wrk 实用程序生成流量,并通过一个 40 Gb 的以太网链路将其发送到由 10 Gb 链路连接到组成 Kubernetes 集群的两个节点(一个主要节点和一个辅助节点)。

NGINX Ingress Controller 作为 Kubernetes Pod 部署在主要节点上,以执行 SSL 终止和 7 层路由。在辅助节点上运行的上游 Pod 是一台可提供各种大小静态内容的 NGINX Web 服务器。

所用硬件

以下是用于测试的硬件。

计算 CPU 网络 内存
客户端 2 颗英特尔 (R) 至强 (R) CPU E5-2699 v4 @ 2.20GHz,44 个真正(或 88 HT)内核 1 个英特尔 XL710 40GbE QSFP+ 128 GB
主要节点 2 颗英特尔 (R) 至强 (R) 奔腾 8168 CPU @ 2.70GHz,48 个真正(或 96 HT)内核 1 个英特尔 XL710 40GbE QSFP+ 192 GB
辅助节点 2 颗英特尔 (R) 至强 (R) CPU E5-2699 v4 @ 2.20GHz,44 个真正(或 88 HT)内核 1 个英特尔 10Gb X540-`AT2 128 GB

所用软件

测试用软件如下:

收集的指标

我们运行了测试收集的三项性能指标:

  • 每秒请求数 (RPS) —— NGINX Ingress Controller 每秒可处理的请求数(取固定时间段内平均值)。客户端发出的每个请求定向到 NGINX Ingress Controller。其将请求代理到上游 Pod,以获取客户端请求的静态内容。静态内容是一个大小约为一个小型 CSS 或 JavaScript 文件,或者一个非常小的图像的 1 KB 文件。
  • 每秒 SSL/TLS 事务 (TPS) —— NGINX Ingress Controller 每秒可建立并支持的新 HTTPS 的连接数(取固定时间段内平均值)。客户端所发出的每个 HTTPS 请求都来源一个新链接。客户端和 NGINX Ingress Controller 通过 TLS 握手以建立安全连接,然后 NGINX Ingress Controller 分析其请求并发回 0 KB 的响应。请求在得到满足后连接即关闭。
  • 吞吐量 —— 固定时间段内,NGINX 在处理静态内容的 HTTP 请求的同时可保持的数据传输速率。静态内容量是 1 MB 的文件,更大文件的请求则会增加系统的整体吞吐量。

测试方法

我们使用以下的 wrk 选项生成了客户端流量:

  • -c 选项指定要创建的 TCP 连接数量。在我们的测试中,我们将其设置为 1000 个连接。
  • -d 选项指定生成流量的用时。我们的每项测试均持续了 180 秒(3 分钟)。
  • -t 选项指定要创建的线程数量。我们指定了 44 个线程。

为了充分利用每个 CPU,我们使用了可以将单个 wrk 进程绑定到 CPU单位上的 taskset。与增加 wrk 数量相比,该方法能够产生更加一致的结果。

在测试 HTTPS 性能时,我们使用了一个 2048 位密钥长度和完美正向加密的 RSA;SSL 密码是 ECDHE-RSA-AES256-GCM-SHA384

测试 RPS

我们在客户端计算机上运行了以下脚本来测试 RPS:

taskset -c 0-21,44-65 wrk -t 44 -c 1000 -d 180s https://host.example.com:443/1kb.bin

该脚本在 44 个 CPU 上均生成了 1 个 wrk 线程。每个线程创建 1000 个 TCP 连接,并通过每个连接在 3 分钟内持续请求一个 1 KB 文件。

测试 TPS

我们运行了以下脚本来测试TPS:

taskset -c 0-21,44-65 wrk -t 44 -c 1000 -d 180s -H ‘Connection: Close’ https://host.example.com:443

该测试使用了与 RPS 测试相同的 wrk 选项,但因其重点是处理 SSL/TLS 连接,所以与 RPS 测试在两方面存在显著差异:

  • 客户端在每个请求打开并关闭连接(-H 选项设置 HTTP Connection: close 标头)。
  • 所请求的文件大小为 0 KB,而非 1 KB。

测试 HTTP 吞吐量

我们运行了以下脚本来测试吞吐量:

taskset -c 0-21,44-65 wrk -t 44 -c 1000 -d 180s https://host.example.com:443/1mb.bin

该测试以 wrk 选项作为 RPS 测试,但所请求的文件大小为 1 MB,而非 1 KB。

性能分析

HTTP 请求的 RPS

该表格和图表展现了 NGINX Ingress 控制器在不同 CPU 数量中每秒所处理 1 KB 文件的 HTTP 请求数。

性能与 CPU 数量大致成正比增长,直到 16 个 CPU。我们发现,较之 16个 CPU,24 个 CPU 的性能几乎没有优势。这是因为对资源的竞争最终会抵消 CPU 数量增加所带来的性能提升。

CPUs RPS
1 36,647
2 74,192
4 148,936
8 300,625
16 342,651
24 342,785

HTTPS 请求的 RPS

与未加密 HTTP 请求相比,HTTPS 请求所需的加、解密增加了计算开销。

采用 8 个 CPU 执行的测试结果符合预期:HTTPS 的 RPS 比 HTTP 低约 20%。

采用 16 或 24 个 CPU 执行的测试,HTTPS 和 HTTP 之间,同样因为资源争用,基本没有差异。

CPUs RPS
1 28,640
2 58,041
4 117,255
8 236,703
16 341,232
24 342,785

TPS

以下表格和图表澄明了在采用和不采用英特尔® 超线程技术(HT)的情况下,NGINX Ingress Controller 基于不同数量 CPU 的 TPS 性能。

因为 TLS 握手受到 CPU 限制且加密任务具有可并行性, HT 显著地提高该指标的性能。我们发现,在搭载多至 16 个 CPU 的情况下,性能提高了 50-65%。

CPUs SSL TPS(禁用 HT) SSL TPS(启用 HT)
1 4,433 7,325
2 8,707 14,254
4 17,433 26,950
8 31,485 47,638
16 54,877 56,715
24 58,126 58,811

吞吐量

该表格和图表显示了 NGINX 搭载不同数量的 CPU 时,3 分钟可保持的 HTTP 请求吞吐量(单位:Gbps)。

吞吐量与客户端发出的请求大小成正比。与预期一样,性能的峰值达到了略低于 10 Gbps :Kubernetes 集群的辅助节点配有英特尔 10 Gb X540AT2 网络接口卡。

CPUs 吞吐量 (Gbps)
1 1.91
2 4.78
4 8.72
8 8.64
16 8.80
24 8.80

测试技术细节

用于部署 NGINX Ingress Controller 的 Kubernetes 配置

我们使用以下 Kubernetes 配置文件部署了 NGINX Ingress Controller。它所定义 Kubernetes DaemonSet 使用一个专用于我们 DockerHub 存储库中的 nginx/nginx-ingress:edge 容器镜像 Pod 的副本。为了将 Pod 分配至辅助节点,我们在 nodeSelector 字段中指定了主要节点的标签 (npq3)。

运行以下命令列出集群节点附加的标签:

$ kubectl get nodes --show-labels

容器镜像对端口 80 和 443 开放,分别对应出入向 HTTP 和 HTTPS 连接。我们在 args 字段中包括了两个参数:

  • -nginx-configmaps 参数让我们使用 ConfigMaps 将配置变更部署到 NGINX Ingress Controller Pod 中。
  • -default-server-tls-secret 设置用于到达 NGINX Ingress Controller 入口点 TLS 请求的默认 Secret,该入口点与 Ingress 资源中指定的 TLS 路径的规则不匹配。
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  name: nginx-ingress
  namespace: nginx-ingress
spec:
  selector:
    matchLabels:
      app: nginx-ingress
  template:
    metadata:
      labels:
        app: nginx-ingress
    spec:
      serviceAccountName: nginx-ingress
      nodeSelector:
        kubernetes.io/hostname: npq3
      hostNetwork: true
      containers:
      - image: nginx/nginx-ingress:edge
        imagePullPolicy: IfNotPresent
        name: nginx-ingress
        ports:
        - name: http
          containerPort: 80
          hostPort: 80
        - name: https
          containerPort: 443
          hostPort: 443
        env:
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        args:
          - -nginx-configmaps=$(POD_NAMESPACE)/nginx-config
          - -default-server-tls-secret=$(POD_NAMESPACE)/default-server-secret

使用 Kubernetes 服务配置以暴露 NGINX Ingress Controller

以下配置创建了一个可在静态端口(80 和 443)向主机 IP 暴露NGINX Ingress Controller 的 Kubernetes 服务。在测试环境中,我们在 externalIPs 字段中为 NGINX Ingress Controller Pod 分配一个外部 IP 地址 (10.10.16.10),以便将其提供给客户端计算机。该地址必须是分配给主要节点上 40 GbE 网络接口的地址。

apiVersion: v1
kind: Service
metadata:
  name: nginx-ingress
  namespace: nginx-ingress
spec:
  externalTrafficPolicy: Local
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
    name: http
  - port: 443
    targetPort: 443
    protocol: TCP
    name: https
  externalIPs:
  - 10.10.16.10
  selector:
    app: nginx-ingress

用于 NGINX Ingress Controller 的 Kubernetes ConfigMap

ConfigMap 为您提供更精细的 NGINX 配置控制 ,以使用更高级的 NGINX 特性并自定义 NGINX 行为。您可通过 data 条目下的可用 ConfigMap 键列表并为其赋值,以设置 NGINX 指令。

有些 NGINX 模块和参数无法通过通用 ConfigMap 和注释直接设置。但通过 main-template ConfigMap 键,您可以加载自定义 NGINX 配置模板,从而在 Kubernetes 环境中完全控制 NGINX 配置。

例如,在该配置中,我们修改了 nginx 模版,并设置了以下指令,以进一步提高运行性能:

然后,如下所示,我们将修改后的模板粘贴到了 main-template ConfigMap 键的值中。我们缩短了 http{} 上下文中的配置,以突显最重要的指令。修改后的内容显示为橙色。

kind: ConfigMap
apiVersion: v1
metadata:
  name: nginx-config
  namespace: nginx-ingress
data:
  worker-processes: "24"
  worker-connections: "100000"
  worker-rlimit-nofile: "102400"
  worker-cpu-affinity: "auto 111111111111111111111111"
  keepalive: "200"
  main-template: | 
   user nginx;
   worker_processes  {{.WorkerProcesses}};
   {{- if .WorkerRlimitNofile}}
   worker_rlimit_nofile {{.WorkerRlimitNofile}};{{end}}
   {{- if .WorkerCPUAffinity}}
   worker_cpu_affinity {{.WorkerCPUAffinity}};{{end}}
   {{- if .WorkerShutdownTimeout}}
   worker_shutdown_timeout {{.WorkerShutdownTimeout}};{{end}}
   daemon off;

   error_log  /var/log/nginx/error.log {{.ErrorLogLevel}};
   pid        /var/run/nginx.pid;

   {{- if .MainSnippets}}
   {{range $value := .MainSnippets}}
   {{$value}}{{end}}
   {{- end}}

   events {
       worker_connections  {{.WorkerConnections}};
   }

   http {
       include       /etc/nginx/mime.types;
       default_type  application/octet-stream;
       
       ...
      

       sendfile        on;
       access_log  off;
       tcp_nopush  on;
       tcp_nodelay on;

       keepalive_timeout  315;
       keepalive_requests 10000000;

       #gzip  on;   
        ...
   }

注:在测试时,我们需使用模板以设置 access_logkeepalive_timeoutkeepalive_requests 参数。在 NGINX Ingress Controller 后续的 commit 中,添加了额外的 ConfigMap 键使您无需使用模版配置上述参数。您还可以使用 http-snippets 键配置其他参数(例如 tcp_nodelaytcp_nopush),这是模版的替代方案。

用于后端的 Kubernetes DaemonSet

本节描述了我们在测试中对 NGINX Ingress Controller 和上游 Pod 之间的网络所进行的性能优化。优化项目包括 Flannel 网络堆栈和用于 Web 服务器上游的 DaemonSet。

网络优化

Flannel 是 Kubernetes 环境中 3 层网络结构的配置工具。当集群中的节点不在同一个子网中且无法通过 2 层连接访问时,我们建议使用 Flannel 的 VXLAN 后端选项。当跨主机容器通信需在不同子网进行时,每个节点上的 flannel 设备就是一个将 TCP 数据包封装入 UDP 数据包的 VXLAN 设备,并将其发送至远程 VXLAN 设备。但因为每个请求均需通过 flannel 设备才可到达正确的 Pod 目标位置 ,VXLAN 后端选项并非提高性能的最佳方案。

我们测试环境中的集群节点都在同一子网,并通过 2 层网络连接。因此,进行跨主机容器通信无需使用 VXLAN 设备。我们能够在同一局域网内创建到远程机器 IP 地址的直接内核 IP 路由。

因此,每当 NGINX Ingress Controller 将连接代理到 Web 服务器 Pod 时,我们都将数据包绕过 flannel 设备转发至目标主机,而非使用 flannel 进程 (flanneld) 将 UDP 数据包封装到远程主机上。本操作不仅提高了性能,也减少了依赖项的数量。设置为该选项最简单的方法是在部署 Flannel 网络前,将 Flannel ConfigMap 中的后端选项更改为 "Type": "host-gw" 或者,您也可以手动更改内核路由表

后端 DaemonSet 部署

用于后端 Pod 的 NGINX 配置由 app-confmain-conf 定义。binary ConfigMap 创建 1 KB 文件,在 RPS 测试中将该文件返回至客户端。

apiVersion: v1
data:
  app.conf: "server {\n listen 80;\nlocation / {\n root /usr/share/nginx/bin;
    \n  }\n}\n"
kind: ConfigMap
metadata:
  name: app-conf
  namespace: default

---

apiVersion: v1
data:
  nginx.conf: |+
    user  nginx;
    worker_processes  44;
    worker_rlimit_nofile 102400;
    worker_cpu_affinity auto 0000000000000000000000111111111111111111111100000000000000000000001111111111111111111111;

    error_log  /var/log/nginx/error.log notice;
    pid        /var/run/nginx.pid;

    events {
        worker_connections  100000;
    }

    http {

        log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                          '$status $body_bytes_sent "$http_referer" '
                          '"$http_user_agent" "$http_x_forwarded_for"';

        sendfile        on;
        tcp_nopush     on;
        tcp_nodelay on;

        access_log off;

        include /etc/nginx/conf.d/*.conf;
    }

---

apiVersion: v1
data:
  1kb.bin: "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
kind: ConfigMap
metadata:
  name: binary
  namespace: default

现在,我们创建了一个 Kubernetes DaemonSet,其中包含了一个 NGINX 后端 Pod,专用于返回静态文件的辅助节点(web-server-payload)。ConfigMap(app-confmain-conf、和 binary)作为卷挂载到 nginx 镜像上。我们在 nodeSelector 字段中指定辅助节点的标签(nbdw34),以将 Pod 分配至辅助节点。请确保您指定的标签已附加到辅助节点中。

apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  name: web-server-payload
spec:
  selector:
    matchLabels:
      app: web-server-payload
  template:
    metadata:
      labels:
        app: web-server-payload
    spec:
      hostNetwork: true
      containers:
      - name: web-server-payload
        image: nginx
        ports:
        - containerPort: 80
        volumeMounts:
        - name: app-config-volume
          mountPath: /etc/nginx/conf.d
        - name: main-config-volume
          mountPath: /etc/nginx
        - name: binary-payload
          mountPath: /usr/share/nginx/bin
      volumes: 
      - name: app-config-volume
        configMap:
          name: app-conf
      - name: main-config-volume
        configMap:
          name: main-conf
      - name: binary-payload
        configMap:
          name: binary 
      nodeSelector: 
        kubernetes.io/hostname: nbdw34

为了进一步将网络性能最大化,我们将 hostNetwork 字段设置为 true,从而将 web-server-payload Pod 设置为主机网络命名空间。这一设置可直接在端口 80 将 web-server-payload Pod 暴露给主机(辅助节点)网络驱动程序,而非执行将其在容器网络命名空间 (cni0) 中暴露的默认行为。在将 hostNetwork 设置为 true 后,RPS 将平均增加 10–15%。

最后,我们将创立向 NGINX Ingress Controller 暴露 web-server-payload Pod 的 web-server-svc 服务。

apiVersion: v1
kind: Service
metadata:
  name: web-server-svc
spec:
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
    name: http
  selector:
    app: web-server-payload

Kubernetes Ingress 资源

Ingress 资源允许您设置基本的 NGINX 特性,例如 SSL 终止和 7 层基于路径的路由。我们使用 Kubernetes Secret 执行双向的 TLS 身份验证,并规定所有带有 host.example.com 主机标头的请求 URI 均通过端口 80 代理至 web-server-svc 服务。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: example-ingress
spec:
  tls:
  - hosts:
    - host.example.com
    secretName: example-secret
  rules:
  - host: host.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: web-server-svc
          servicePort: 80

结语

我们提供了在 Kubernetes 环境中使用 NGINX Ingress Controller 生成最佳性能测试结果的配置和部署信息。当 CPU 数量扩展到超过 16 个时,我们的 RPS 结果达到约 340 K 的最大值。而一台仅搭载 16 个 CPU 的计算机可能与一台采用 24 个 CPU 的计算机提供相同的 RPS。在 Kubernetes 中部署生产工作负载时,您可使用以上的信息制定既满足您对性能和扩展要求又经济实惠的解决方案。

立即测试 NGINX Plus 附带的 NGINX Ingress Controller 和 NGINX App Protect 吧,您可以从现在马上开始

如欲试用 NGINX Open Source 附带的 NGINX Ingress Controller,您可以获取源代码,或者从 DockerHub 下载预编译的容器。

Hero image
Ebook: Cloud Native DevOps with Kubernetes

Download the excerpt of this O’Reilly book to learn how to apply industry‑standard DevOps practices to Kubernetes in a cloud‑native context.

关于作者

Amir Rawdat

解决方案工程师

Amir Rawdat 是 NGINX 的技术营销工程师,专门负责各种技术内容的撰写。他在计算机网络、计算机编程、故障排除和内容撰写方面拥有深厚的背景。此前,Amir 是诺基亚(Nokia)的客户应用工程师。

关于 F5 NGINX

F5, Inc. 是备受欢迎的开源软件 NGINX 背后的商业公司。我们为现代应用的开发和交付提供一整套技术。我们的联合解决方案弥合了 NetOps 和 DevOps 之间的横沟,提供从代码到用户的多云应用服务。访问 nginx-cn.net 了解更多相关信息。