NGINX.COM
Web Server Load Balancing with NGINX Plus

Kubernetes has become the de facto standard for managing containerized applications, with many enterprises adopting it in their production environments. In this blog, we describe the kind of performance you can achieve with NGINX Ingress Controller for Kubernetes, detailing the results of our performance testing of three metrics: requests per second, SSL/TLS transactions per second, and throughput. We also include the full NGINX and Kubernetes configurations we used.

You can use our performance numbers and configuration to help you determine the right topology, specs, and Kubernetes configuration for delivering your own apps at the level of performance and scale you require.

Topology

For all tests, the wrk utility running on a client machine generated traffic and sent it over a 40‑Gigabit Ethernet link to a Kubernetes cluster of two nodes (one primary and one secondary), which were connected by a 10‑Gigabit link.

NGINX Ingress Controller was deployed as a Kubernetes Pod on the primary node to perform SSL termination and Layer 7 routing. The upstream Pod running on the secondary node was an NGINX web server serving static content of various sizes.

Hardware Used

The following hardware was used for the testing.

Machine CPU Network Memory
Client 2x Intel(R) Xeon(R) CPU E5‑2699 v4 @ 2.20GHz, 44 real (or 88 HT) cores 1x Intel XL710 40GbE QSFP+ 128 GB
Primary Node 2x Intel(R) Xeon(R) Platinum 8168 CPU @ 2.70GHz, 48 real (or 96 HT) cores 1x Intel XL710 40GbE QSFP+ 192 GB
Secondary Node 2x Intel(R) Xeon(R) CPU E5‑2699 v4 @ 2.20GHz, 44 real (or 88 HT) cores 1x Intel 10‑Gigabit X540‑AT2 128 GB

Software Used

We used the following software for testing:

Metrics Collected

We ran tests to collect three performance metrics:

  • Requests per second (RPS) – The number of requests NGINX Ingress Controller can process per second, averaged over a fixed time period. The client initiated each request and directed it to NGINX Ingress Controller. NGINX Ingress Controller proxied the request to the upstream Pod to fetch the static content requested by the client. The static content was a 1‑KB file, which is roughly the size of a small CSS or JavaScript file, or a very small image.
  • SSL/TLS transactions per second (TPS) – The number of new HTTPS connections NGINX Ingress Controller can establish and serve per second, averaged over a fixed time period. Clients sent a series of HTTPS requests, each on a new connection. The client and NGINX Ingress Controller performed a TLS handshake to establish a secure connection, then NGINX Ingress Controller parsed the request and sent back a 0‑KB response. The connection closed after the request was fully satisfied.
  • Throughput – The data transmission rate that NGINX is able to sustain while processing HTTP requests for static content over a fixed time period. The static content was a 1‑MB file, because requests for larger files increase the overall throughput of the system.

Testing Methodology

We generated client traffic using the following wrk options:

  • The -c option specifies the number of TCP connections to create. For our testing, we set this to 1000 connections.
  • The -d option specifies how long to generate traffic. We ran our tests for 180 seconds (3 minutes) each.
  • The -t option specifies the number of threads to create. We specified 44 threads.

To fully exercise each CPU we used taskset, which can pin a single wrk process to a CPU. This method yields more consistent results than increasing the number of wrk threads.

When testing HTTPS performance, we used RSA with a 2048‑bit key size and Perfect Forward Secrecy; the SSL cipher was ECDHE-RSA-AES256-GCM-SHA384.

Testing RPS

To measure RPS, we ran the following script on the client machine:

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

This script spawns one wrk thread on each of 44 CPUs. Each thread creates 1000 TCP connections and for 3 minutes continuously requests a 1‑KB file over each connection.

Testing TPS

To measure TPS, we ran the following script:

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

This test uses the same wrk options as the RPS test, but differs in two notable ways because the focus is on processing of SSL/TLS connections:

  • The client opens and closes a connection for each request (the -H option sets the HTTP Connection: close header).
  • The requested file is 0 KB in size instead of 1 KB.

Testing HTTP Throughput

To measure throughput, we ran the following script:

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

This test uses the wrk options as the RPS test, except that the requested file is 1 MB in size instead of 1 KB.

Performance Analysis

RPS for HTTP Requests

The table and graph show the number of HTTP requests for a 1‑KB file that NGINX Ingress Controller processed per second on different numbers of CPUs.

Performance increases in roughly direct proportion with the number of CPUs, up to 16 CPUs. We consistently found that there is little to no benefit in performance with 24 CPUs compared to 16. This is because resource contention eventually outweighs the increased performance expected from a higher number of CPUs.

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

RPS for HTTPS Requests

The encryption and decryption required for HTTPS requests increases computing expenditure compared to unencrypted HTTP requests.

For the tests with up to 8 CPUs, results are as expected: RPS is lower for HTTPS compared to HTTP, by about 20%.

For the tests with 16 or 24 CPUs, there is essentially no difference between HTTPS and HTTP, again because of resource contention.

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

TPS

The table and graph depict NGINX Ingress Controller’s performance for TPS on varying numbers of CPUs, with and without Intel® Hyper‑Threading Technology (HT).

Because TLS handshakes are CPU bound and encryption tasks are parallelizable, HT can significantly increase performance for this metric. We found it improved performance by 50–65% up to 16 CPUs.

CPUs SSL TPS (HT Disabled) SSL TPS (HT Enabled)
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

Throughput

The table and graph show the throughput of HTTP requests (in Gbps) that NGINX is able to sustain over a period of 3 minutes, on different numbers of CPUs.

Throughput is proportional to the size of requests issued by the client. Performance peaked at just under 10 Gbps, as expected: the secondary node of the Kubernetes cluster is equipped with an Intel 10‑Gigabit X540‑AT2 Network Interface Card.

CPUs Throughput (Gbps)
1 1.91
2 4.78
4 8.72
8 8.64
16 8.80
24 8.80

Technical Details of Testing

Kubernetes Configuration for Deployment of NGINX Ingress Controller

We deployed NGINX Ingress Controller with the following Kubernetes configuration file. It defines a Kubernetes DaemonSet running one copy of a Pod dedicated to the nginx/nginx-ingress:edge container image from our DockerHub repository. We assign the Pod to the primary node by specifying the primary node’s label (npq3) in the nodeSelector field.

To list the labels attached to your cluster nodes, run the following command:

$ kubectl get nodes --show-labels

The container image is exposed on port 80 and 443 for incoming and outgoing HTTP and HTTPS connections, respectively. We include two arguments in the args field:

  • The -nginx-configmaps argument enables us to apply configuration changes to the NGINX Ingress Controller Pod using ConfigMaps.
  • The -default-server-tls-secret sets the default Secret used for TLS requests reaching the NGINX Ingress Controller entry point that do not match a TLS path‑based rule specified in the Ingress resource.
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 Service Configuration for Exposing NGINX Ingress Controller

The configuration below creates a Kubernetes Service to expose the NGINX Ingress Controller Pod to a host IP address at static ports (80 and 443). In the testing environment, we expose the NGINX Ingress Controller Pod to the client machine by assigning it an external IP address (10.10.16.10) in the externalIPs field. The address must be the one assigned to the 40GbE network interface on the primary node.

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

Kubernetes ConfigMap for NGINX Ingress Controller

ConfigMaps give you more granular control over NGINX configurations, allowing you to use advanced NGINX features and customize NGINX behavior. You can set NGINX directives by assigning values to a list of available ConfigMap keys under the data entry.

Some NGINX modules and parameters cannot be set directly through general ConfigMaps and Annotations. However, using the main-template ConfigMap key, you can load custom NGINX configuration templates, giving you full control over the NGINX configuration in a Kubernetes environment.

For example, in this configuration we modify the main nginx template and set the following directives to further increase performance:

We then paste the modified template into the value of the main-template ConfigMap key as shown below. We’ve abridged the configuration in the http{} context to highlight the most relevant directives, and the modified ones appear in orange.

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;   
        ...
   }

Note: At the time of testing, we had to use the template to set the access_log, keepalive_timeout, and keepalive_requests directives. In a subsequent commit of NGINX Ingress Controller, additional ConfigMap keys were added so that you don’t need to use templates to configure those directives. You can also use the http-snippets key to configure other directives (such as tcp_nodelay and tcp_nopush) as an alternative to dealing with templates.

Kubernetes DaemonSet for the Backend

This section describes the performance optimizations we made to the network between the NGINX Ingress Controller and upstream Pods in our testing. The optimizations involve the Flannel networking stack and the DaemonSet for the web server upstream.

Networking Optimization

Flannel is a tool for configuring a Layer 3 network fabric in a Kubernetes environment. We recommend using Flannel’s VXLAN backend option when nodes in your cluster are not in the same subnet and not reachable with Layer 2 connectivity. When cross‑host container communication needs to happen on different subnets, the flannel device on each node is a VXLAN device that encapsulates TCP packets into UDP packets and sends them to the remote VXLAN device. However, the VXLAN backend option is not optimal for performance, because every request needs to go through the flannel device to arrive at the correct Pod destination.

The cluster nodes in our testing environment were on the same subnet and had Layer 2 connectivity. Therefore, we didn’t need a VXLAN device for cross-host container communication. We were able to create direct kernel IP routes to remote machine IP addresses within the same LAN.

Thus each time NGINX Ingress Controller proxied connections to the web server Pod, we forwarded the packets to the destination host and bypassed the flannel device, instead of the flannel daemon (flanneld) encapsulating UDP packets to remote hosts. This increased performance and reduced the number of dependencies. The easiest way to set this option is to change the backend option in the Flannel ConfigMap to "Type": "host-gw" before deploying the Flannel network. Alternatively, you can manually change the kernel route tables.

Backend DaemonSet Deployment

The NGINX configuration used for the backend Pod is defined by the app-conf and main-conf ConfigMaps. The binary ConfigMap creates the 1‑KB file returned to clients in the RPS tests.

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

Now we create a Kubernetes DaemonSet consisting of one NGINX backend Pod dedicated to the secondary node (web-server-payload) that returns a static file. The ConfigMaps (app-conf, main-conf, and binary) are mounted on the nginx image as volumes. We assign the Pod to the secondary node by specifying the secondary node’s label (nbdw34) in the nodeSelector field. Make sure the label you specify is attached to the secondary node.

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

To further maximize networking performance, we set the web-server-payload Pod to the host network namespace by setting the hostNetwork field to true. This directly exposes the web-server-payload Pod to the host (secondary node) network driver at port 80, instead of exposing it in the container networking namespace (cni0), which is the default behavior. You can expect an average 10–15% increase in RPS from setting hostNetwork to true.

Finally, we create the web-server-svc service to expose the web-server-payload Pod to NGINX Ingress Controller.

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 Resource

The Ingress resource allows you to set basic NGINX features such as SSL termination and Layer 7 path‑based routing. We use a Kubernetes Secret for SSL termination and set a rule stating that all request URIs with the host.example.com host header are proxied to the web-server-svc service through port 80.

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

Conclusion

We have provided the configuration and deployment details used for producing optimal performance test results with NGINX Ingress Controller in a Kubernetes environment. Our results for RPS converged to a maximum of roughly 340K as we scaled past 16 CPUs. A machine with only 16 CPUs might deliver the same RPS as one with 24 CPUs. Use this information to provision an affordable solution that satisfies your performance and scaling requirements when deploying your production workload in Kubernetes.

Test the performance of NGINX Ingress Controller with NGINX Plus and NGINX App Protect for yourself –
get started today!

To try NGINX Ingress Controller with NGINX Open Source, you can obtain the source code, or download a prebuilt container from 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.



About The Author

Amir Rawdat

Solutions Engineer

Amir Rawdat is a technical marketing engineer at NGINX, where he specializes in content creation of various technical topics. He has a strong background in computer networking, computer programming, troubleshooting, and content creation. Previously, Amir was a customer application engineer at Nokia.

About F5 NGINX

F5, Inc. is the company behind NGINX, the popular open source project. We offer a suite of technologies for developing and delivering modern applications. Together with F5, our combined solution bridges the gap between NetOps and DevOps, with multi-cloud application services that span from code to customer.

Learn more at nginx.com or join the conversation by following @nginx on Twitter.