동적 Kubernetes 클라우드 환경에서 NGINX Ingress Controller 성능 테스트

점점 더 많은 기업이 프로덕션 환경에서 컨테이너화된 앱을 실행함에 따라 Kubernetes는 컨테이너 오케스트레이션을 위한 표준 도구로서의 입지를 계속 공고히 하고 있습니다. 동시에, COVID-19 전염병에 의해 촉발된 재택 근무 이니셔티브가 인터넷 트래픽의 성장을 가속화했기 때문에 클라우드 컴퓨팅에 대한 수요가 몇 년 앞당겨졌습니다. 기업은 고객이 주요 네트워크 중단 및 과부하를 경험하고 있기 때문에 인프라를 업그레이드하기 위해 빠르게 노력하고 있습니다.

클라우드 기반 마이크로서비스 환경에서 필요한 수준의 성능을 달성하려면 차세대 하이퍼스케일 데이터 센터의 확장성과 성능을 활용하는 빠르고 완전히 동적인 소프트웨어가 필요합니다. Kubernetes를 사용하여 컨테이너를 관리하는 많은 조직은 NGINX 기반 Ingress Controller에 의존하여 사용자에게 앱을 제공합니다.

이 성능 테스트에서는 인터넷을 통한 클라이언트 연결의 대기 시간을 측정하여 현실적인 다중 클라우드 환경에서 3개의 NGINX Ingress Controller에 대한 성능 테스트 결과를 보고합니다.

  • Kubernetes 커뮤니티에서 유지 관리하고 NGINX 오픈소스를 기반으로 하는 NGINX Ingress Controller. 여기에서는 커뮤니티 Ingress Controller라고 합니다. Google Container Registry에서 가져온 이미지를 사용하여 버전 0.34.1을 테스트했습니다.
  • NGINX에서 유지 관리하는 NGINX 오픈소스 Ingress Controller 버전 1.8.0.
  • NGINX Plus Ingress Controller 버전 1.8.0, NGINX에서 유지 관리합니다.

목차

1. 테스트 프로토콜 및 수집된 지표(Metrics)
2. 토폴로지(Topology)
3. 테스트 방법론(Testing Methodology)
  3-1. 클라이언트 배포(Client Deployment)
  3-2. Backend 애플리케이션 배포(Deployment)
4. Performance Results
  4-1. 정적 배포(Deployment)의 지연 시간 결과(Latency Results)
  4-2. 동적 배포(Deployment)의 지연 시간 결과(Latency Results)
  4-3. 동적 배포(Dynamic Deployment)에 대한 시간 초과 및 오류 결과
5. 결론
6. 부록(Appendix)
  6-1. 클라우드 머신 사양
  6-2. NGINX 오픈소스 및 NGINX Plus Ingress Controller 구성
  6-3. 커뮤니티 NGINX Ingress Controller 구성
  6-4. Backend 앱 구성

1. 테스트 프로토콜 및 수집된 지표(Metrics)

로드 생성 프로그램 wrk2를 사용하여 클라이언트를 에뮬레이트하여 정의된 기간 동안 HTTPS를 통해 지속적으로 요청했습니다. 테스트 중인 Ingress Controller(커뮤니티 Ingress Controller, NGINX 오픈소스 Ingress Controller 또는 NGINX Plus Ingress Controller)는 Kubernetes Pod에 배포된 Backend 애플리케이션에 요청을 전달하고 애플리케이션에서 생성된 응답을 클라이언트에 반환했습니다. Ingress Controller를 스트레스 테스트하기 위해 클라이언트 트래픽의 꾸준한 흐름을 생성하고 다음과 같은 성능 메트릭(Metrics)을 수집했습니다.

  • 대기 시간(Latency) – 클라이언트가 요청을 생성하고 응답을 수신하는 사이의 시간입니다. 백분위수 분포로 지연 시간을 보고합니다. 예를 들어 대기 시간 테스트의 샘플이 100개 있는 경우 99번째 백분위수 값은 100개의 테스트 실행에서 응답의 다음에서 가장 느린 대기 시간입니다.
  • 연결 시간 초과(Connection timeouts) – Ingress Controller가 특정 시간 내에 요청에 응답하지 못하여 자동으로 삭제되거나 삭제되는 TCP 연결입니다.
  • 읽기 오류(Read errors) – Ingress Controller의 소켓이 닫혀 실패한 연결에서 읽기를 시도합니다.
  • 연결 오류(Connection errors) – 설정되지 않은 클라이언트와 수신 컨트롤러 간의 TCP 연결입니다.

2. 토폴로지(Topology)

모든 테스트에서 AWS의 클라이언트 시스템에서 실행되는 wrk2 유틸리티를 사용하여 요청을 생성했습니다. Google Kubernetes Engine(GKE) 환경의 GKE-node-1에 Kubernetes DaemonSet으로 배포된 Ingress Controller의 외부 IP 주소에 연결된 AWS 클라이언트. Ingress Controller는 SSL termination(Kubernetes Secret 참조) 및 Layer 7 라우팅을 위해 구성되었으며 LoadBalancer 유형의 Kubernetes 서비스를 통해 노출되었습니다. Backend 애플리케이션은 GKE-node-2에서 Kubernetes 배포로 실행되었습니다.

클라우드 머신 유형 및 소프트웨어 구성에 대한 자세한 내용은 부록을 참조하십시오.

3. 테스트 방법론(Testing Methodology)

3-1. 클라이언트 배포(Client Deployment)

AWS 클라이언트 시스템에서 다음 wrk2(버전 4.0.0) 스크립트를 실행했습니다. GKE에 배포된 Ingress Controller에 대한 1000개의 연결을 함께 설정하는 2개의 wrk 스레드를 생성합니다. 각 3분 테스트 실행 동안 스크립트는 초당 30,000개의 요청(RPS)을 생성하며, 프로덕션 환경에서 Ingress Controller의 부하를 잘 시뮬레이션한 것으로 간주합니다.

wrk -t2 -c1000 -d180s -L -R30000 https://app.example.com:443/
  • -t – 스레드 수 설정(2)
  • -c – TCP 연결 수 설정(1000)
  • -d – 테스트 실행 기간을 초(180 또는 3분) 단위로 설정합니다.
  • -L – 분석 도구로 내보내기 위한 자세한 대기 시간 백분위수 정보를 생성합니다.
  • -R – RPS(30,000) 수를 설정합니다.

TLS 암호화의 경우 2048-bit key size 및 Perfect Forward Secrecy와 함께 RSA를 사용했습니다.

Backend 애플리케이션(https://app.example.com:443에서 액세스)의 각 응답은 200 OK HTTP 상태 코드와 함께 약 1KB의 기본 서버 메타데이터로 구성됩니다.

3-2. Backend 애플리케이션 배포(Deployment)

Backend 애플리케이션의 정적 및 동적 배포를 모두 사용하여 테스트 실행을 수행했습니다.

정적 배포에는 5개의 Pod 복제본이 있었고 Kubernetes API를 사용하여 변경 사항이 적용되지 않았습니다.

동적 배포의 경우 다음 스크립트를 사용하여 backend nginx 배포를 주기적으로 5개의 Pod 복제본에서 최대 7개로 확장한 다음 다시 5개로 축소했습니다. 이것은 동적 Kubernetes 환경을 에뮬레이트하고 Ingress Controller가 Endpoint 변경에 얼마나 효과적으로 적응하는지 테스트합니다.

while [ 1 -eq 1 ]
do
  kubectl scale deployment nginx --replicas=5
  sleep 12
  kubectl scale deployment nginx --replicas=7
  sleep 10
done

4. Performance Results

4-1. 정적 배포(Deployment)의 지연 시간 결과(Latency Results)

그래프에서 알 수 있듯이 세 가지 Ingress Controller 모두 Backend 애플리케이션의 정적 배포로 유사한 성능을 달성했습니다. 이는 모두 NGINX 오픈소스를 기반으로 하고 정적 배포가 Ingress Controller에서 재구성할 필요가 없다는 점을 감안하면 의미가 있습니다.

NGINX-IC-Kubernetes-cloud_static

4-2. 동적 배포(Deployment)의 지연 시간 결과(Latency Results)

그래프는 Backend 애플리케이션을 5개의 복제본 Pod에서 최대 7개로 주기적으로 확장한 동적 배포에서 각 Ingress Controller에서 발생하는 지연 시간(Latency)을 보여줍니다.

NGINX Plus Ingress Controller만이 이 환경에서 잘 수행되며 99.99번째 백분위수까지 대기 시간이 거의 없습니다. 커뮤니티와 NGINX 오픈소스 Ingress Controller 모두 패턴은 다르지만 상당히 낮은 백분위수에서 눈에 띄는 지연 시간을 경험합니다. 커뮤니티 Ingress Controller의 경우 지연 시간은 완만하지만 꾸준히 99번째 백분위수까지 올라가며 약 5000ms(5초)에서 수평을 이룹니다. NGINX 오픈소스 Ingress Controller의 경우 지연 시간은 99번째 백분위수에서 약 32초로 급격히 급증하고 99.99번째 백분위수에서 다시 60초로 급증합니다.

동적 배포에 대한 시간 초과 및 오류 결과에서 추가로 논의하는 것처럼 커뮤니티 및 NGINX 오픈소스 Ingress Controller에서 발생하는 대기 시간은 NGINX 구성이 업데이트되고 변경되는 Backend 애플리케이션의 Endpoint에 대한 응답으로 다시 로드된 후(reloaded) 발생하는 오류 및 시간 초과로 인해 발생합니다.

NGINX-IC-Kubernetes-cloud_dynamic

다음은 이전 그래프와 동일한 테스트 조건에서 커뮤니티 및 NGINX Plus Ingress Controller에 대한 결과를 보다 자세히 보여줍니다. NGINX Plus Ingress Controller는 99.99번째 백분위수까지 지연이 거의 발생하지 않으며, 99.9999번째 백분위수에서 254ms까지 상승하기 시작합니다. 커뮤니티 Ingress Controller의 대기 시간 패턴은 99번째 백분위수에서 5000ms 대기 시간으로 꾸준히 증가하며 이 지점에서 대기 시간이 감소합니다.

NGINX-IC-Kubernetes-cloud_dynamic-zoomed

4-3. 동적 배포(Dynamic Deployment)에 대한 시간 초과 및 오류 결과

이 표는 지연(latency) 결과의 원인을 더 자세히 보여줍니다.

NGINX 오픈소스커뮤니티NGINX Plus
연결 오류
(Connection errors)
3336500
연결 시간 초과
(Connection timeouts)
30988090
읽기 오류
(Read errors)
465000

NGINX 오픈소스 Ingress Controller를 사용하면 Backend 애플리케이션의 Endpoint에 대한 모든 변경 사항에 대해 NGINX 구성을 업데이트하고 다시 로드해야 하므로 많은 연결 오류, 연결 시간 초과 및 읽기 오류가 발생합니다. 연결/소켓 오류는 클라이언트가 NGINX 프로세스에 더 이상 할당되지 않은 소켓에 연결을 시도할 때 NGINX를 다시 로드하는 데 걸리는 짧은 시간 동안 발생합니다. 연결 시간 초과는 클라이언트가 Ingress Controller에 대한 연결을 설정했지만 Backend Endpoint를 더 이상 사용할 수 없을 때 발생합니다. 오류와 시간 초과는 모두 대기 시간에 심각한 영향을 미치며 99번째 백분위수에서 32초로 급증하고 99.99번째 백분위수에서 다시 60초로 급증합니다.

커뮤니티 Ingress Controller의 경우 Backend 애플리케이션이 확장 및 축소됨에 따라 Endpont 변경으로 인해 8,809개의 연결 시간 초과가 발생했습니다. 커뮤니티 Ingress Controller는 Lua 코드를 사용하여 Endpoint가 변경될 때 구성 다시 로드를 방지합니다. 결과는 Endpoint 변경을 감지하기 위해 NGINX 내에서 Lua 처리기를 실행하면 끝점에 대한 각 변경 후에 구성을 다시 로드해야 하는 요구 사항으로 인해 NGINX 오픈소스 버전의 일부 성능 제한을 해결한다는 것을 보여줍니다. 그럼에도 불구하고 연결 시간 초과가 계속 발생하고 더 높은 백분위수에서 상당한 대기 시간이 발생합니다.

NGINX Plus Ingress Controller를 사용하면 오류나 시간 초과가 발생하지 않았습니다. 동적 환경은 성능에 거의 영향을 미치지 않았습니다. NGINX Plus Ingress Controller는 NGINX Plus API를 사용하여 Endpoint가 변경될 때 NGINX 구성을 동적으로 업데이트하기 때문입니다. 언급했듯이 가장 높은 대기 시간은 254ms였으며 99.9999 백분위수에서만 발생했습니다.

5. 결론

성능 결과는 동적 Kubernetes 클라우드 환경에서 시간 초과 및 오류를 완전히 제거하기 위해 Ingress Controller가 이벤트 핸들러 또는 구성 다시 로드 없이 Backend Endpoint의 변경 사항에 동적으로 조정되어야 함을 보여줍니다. 결과를 바탕으로 NGINX Plus API는 동적 환경에서 NGINX를 동적으로 재구성하기 위한 최적의 솔루션이라고 말할 수 있습니다. 테스트에서 NGINX Plus Ingress Controller만 사용자를 만족시키는 데 필요한 매우 동적인 Kubernetes 환경에서 완벽한 성능을 달성했습니다.

6. 부록(Appendix)

6-1. 클라우드 머신 사양

머신클라우드 공급자머신 유형
ClientAWSm5a.4xlarge
GKE-node-1GCPe2-standard-32
GKE-node2GCPe2-standard-32

6-2. NGINX 오픈소스 및 NGINX Plus Ingress Controller 구성

Kubernetes 구성

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: nginx-ingress
  namespace: nginx-ingress
spec:
  selector:
    matchLabels:
      app: nginx-ingress
  template:
    metadata:
      labels:
        app: nginx-ingress
     #annotations:
       #prometheus.io/scrape: "true"
       #prometheus.io/port: "9113"
    spec:
      serviceAccountName: nginx-ingress
      nodeSelector:
        kubernetes.io/hostname: gke-rawdata-cluster-default-pool-3ac53622-6nzr
      hostNetwork: true    
      containers:
      - image: gcr.io/nginx-demos/nap-ingress:edge
        imagePullPolicy: Always
        name: nginx-plus-ingress
        ports:
        - name: http
          containerPort: 80
          hostPort: 80
        - name: https
          containerPort: 443
          hostPort: 443
        - name: readiness-port
          containerPort: 8081
       #- name: prometheus
         #containerPort: 9113
        readinessProbe:
          httpGet:
            path: /nginx-ready
            port: readiness-port
          periodSeconds: 1
        securityContext:
          allowPrivilegeEscalation: true
          runAsUser: 101 #nginx
          capabilities:
            drop:
            - ALL
            add:
            - NET_BIND_SERVICE
        env:
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        args:
          - -nginx-plus
          - -nginx-configmaps=$(POD_NAMESPACE)/nginx-config
          - -default-server-tls-secret=$(POD_NAMESPACE)/default-server-secret

Notes:

  • 이 구성은 NGINX Plus용입니다. NGINX 오픈소스 구성에서 필요에 따라 nginx‑plus에 대한 참조가 조정되었습니다.
  • NGINX App Protect는 이미지에 포함되어 있지만(gcr.io/nginx-demos/nap-ingress:edge) 비활성화되었습니다(-enable-app-protect 플래그는 생략됨).

ConfigMap

kind: ConfigMap
apiVersion: v1
metadata:
  name: nginx-config
  namespace: nginx-ingress
data:
  worker-connections: "10000"
  worker-rlimit-nofile: "10240"
  keepalive: "100"
  keepalive-requests: "100000000"

6-3. 커뮤니티 NGINX Ingress Controller 구성

Kubernetes 구성

apiVersion: apps/v1
kind: DaemonSet
metadata:
  labels:
    helm.sh/chart: ingress-nginx-2.11.1
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/instance: ingress-nginx
    app.kubernetes.io/version: 0.34.1
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/component: controller
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: ingress-nginx
      app.kubernetes.io/instance: ingress-nginx
      app.kubernetes.io/component: controller
  template:
    metadata:
      labels:
        app.kubernetes.io/name: ingress-nginx
        app.kubernetes.io/instance: ingress-nginx
        app.kubernetes.io/component: controller
    spec:
      nodeSelector: 
        kubernetes.io/hostname: gke-rawdata-cluster-default-pool-3ac53622-6nzr
      hostNetwork: true
      containers:
        - name: controller
          image: us.gcr.io/k8s-artifacts-prod/ingress-nginx/controller:v0.34.1@sha256:0e072dddd1f7f8fc8909a2ca6f65e76c5f0d2fcfb8be47935ae3457e8bbceb20
          imagePullPolicy: IfNotPresent
          lifecycle:
            preStop:
              exec:
                command:
                  - /wait-shutdown
          args:
            - /nginx-ingress-controller
            - --election-id=ingress-controller-leader
            - --ingress-class=nginx
            - --configmap=$(POD_NAMESPACE)/ingress-nginx-controller
            - --validating-webhook=:8443
            - --validating-webhook-certificate=/usr/local/certificates/cert
            - --validating-webhook-key=/usr/local/certificates/key
          securityContext:
            capabilities:
              drop:
                - ALL
              add:
                - NET_BIND_SERVICE
            runAsUser: 101
            allowPrivilegeEscalation: true
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          readinessProbe:
            httpGet:
              path: /healthz
              port: 10254
              scheme: HTTP
            periodSeconds: 1
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
            - name: https
              containerPort: 443
              protocol: TCP
            - name: webhook
              containerPort: 8443
              protocol: TCP
          volumeMounts:
            - name: webhook-cert
              mountPath: /usr/local/certificates/
              readOnly: true
      serviceAccountName: ingress-nginx
      terminationGracePeriodSeconds: 300
      volumes:
        - name: webhook-cert
          secret:
            secretName: ingress-nginx-admission

ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
data:
  max-worker-connections: "10000"
  max-worker-open-files: "10204"
  upstream-keepalive-connections: "100"
  keep-alive-requests: "100000000"

6-4. Backend 앱 구성

Kubernetes 구성

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  selector:
    matchLabels:
      app: nginx 
  template:
    metadata:
      labels:
        app: nginx
    spec:
      nodeSelector:
        kubernetes.io/hostname: gke-rawdata-cluster-default-pool-3ac53622-t2dz 
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 8080
        volumeMounts: 
        - name: main-config-volume
          mountPath: /etc/nginx
        - name: app-config-volume
          mountPath: /etc/nginx/conf.d
        readinessProbe:
          httpGet:
            path: /healthz
            port: 8080
          periodSeconds: 3
      volumes: 
      - name: main-config-volume
        configMap:
          name: main-conf
      - name: app-config-volume
        configMap: 
          name: app-conf
---

ConfigMaps

apiVersion: v1
kind: ConfigMap
metadata:
  name: main-conf
  namespace: default
data:
  nginx.conf: |+
    user nginx;
    worker_processes 16;
    worker_rlimit_nofile 102400;
    worker_cpu_affinity auto 1111111111111111;
    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_nodelay on;
 
        access_log off;
 
        include /etc/nginx/conf.d/*.conf;
    }
 
---
 
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-conf
  namespace: default
data:
  app.conf: "server {listen 8080;location / {default_type text/plain;expires -1;return 200 'Server address: $server_addr:$server_port\nServer name:$hostname\nDate: $time_local\nURI: $request_uri\nRequest ID: $request_id\n';}location /healthz {return 200 'I am happy and healthy :)';}}"
---

Service

apiVersion: v1
kind: Service
metadata:
  name: app-svc
spec:
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP
    name: http
  selector:
    app: nginx
---