문제 현상

k8s 환경에서 service 앞단에 nginx ingress controller 를 두고 locust 부하 테스트툴을 사용하여 부하테스트를 진행하였다. 그런데 부하 테스트 중 간헐적으로 클라이언트(locust)와 nginx 서버간에 connection reset 에러가 발생하는 것을 확인하였다. 특이사항으로는 클라이언트의 http 요청의 HTTP Keep-Alive 가 enable 되어 있을때만 발생하고 disable 되어 있을때는 발생하지 않았다.

Untitled

locust 로 부하 테스트 중에 connection reset 에러가 발생한 화면

원인 추적

외부의 요청을 받는 nginx ingress controller 부터 살펴보기로 하고, nginx controller pod 로그에서 이상한 점이 없는지 관찰 하였다. 로그를 살펴본 결과 kubernetes ingress resource 에 변경사항이 발생하면 nginx ingress controller 가 ingress resource를 watch 하다가, nginx.conf 를 다시 만들어서 reload 하는 것을 볼 수 있었는데 이 시점에 connection reset 에러도 발생하는 것을 확인하였다.

nginx pod 로그 관찰

  1. foo namespace의 bar-master-ingress 이름의 ingress resource 에 변경 사항이 발생

    I0819 11:42:02.001910       7 store.go:640] updating annotations information for ingress foo/bar-master-ingress
    ****I0819 11:42:02.002008       7 main.go:167] "No default affinity found" ingress="bar-master-ingress"
    **I0819 11:42:02.002182       7 event.go:282] Event(v1.ObjectReference{Kind:"Ingress", Namespace:"foo", Name:"bar-master-ingress", UID:"b9bed8f0-3fb7-4fca-86d5-6b2c086d50a4", APIVersion:"networking.k8s.io/v1beta1", ResourceVersion:"74243302", FieldPath:""}): type: 'Normal' reason: 'Sync' Scheduled for sync**
    ...
    ...
    I0819 11:42:02.002785       7 queue.go:128] "syncing" key="foo/bar-master-ingress"
    
  2. nginx.conf 의 변경 전후 diff 내용이 출력된다

    I0819 11:42:05.825434       7 nginx.go:684] "**NGINX configuration change" diff**="--- /etc/nginx/nginx.conf\\t2022-08-19 11:36:50.785869106 +0900\\n+++ /tmp/new-nginx-cfg372163662\\t2022-08-19 11:42:05.818037692 +0900\\n@@ -1,5 +1,5 @@\\n \\n-# Configuration checksum: 12093455329089770281\\n+# Configuration checksum: 16068307183398161240\\n \\n # setup custom paths that do not require root access\\n pid /tmp/nginx.pid;\\n@@ -9145,151 +9145,6 @@\\n \\t}\\n \\t## end server ksp-ksp-cbt-847-0819112304.prod1.internal.domain\\n \\t\\n-\\t## start server bar.prod1.internal.domain\\n-\\tserver {\\n-\\t\\tserver_name bar.prod1.internal.domain ;\\n-\\t\\t\\n-\\t\\tlisten 80  ;\\n-\\t\\tlisten 443  ssl http2 ;\\n-\\t\\t\\n-\\t\\tset $proxy_upstream_name \\"-\\";\\n-\\t\\t\\n-\\t\\tssl_certificate_by_lua_block {\\n-\\t\\t\\tcertificate.call()\\n-\\t\\t}\\n-\\t\\t\\n-\\t\\tlocation / {\\n-\\t\\t\\t\\n-\\t\\t\\tset $namespace      \\"foo\\";\\n-\\t\\t\\tset $ingress_name   \\"bar-master-ingress\\";\\n-\\t\\t\\tset $service_name   \\"bar-master-service\\";\\n-\\t\\t\\tset $service_port   \\"8888\\";\\n-\\t\\t\\tset $location_path  \\"/\\";\\n-\\t\\t\\t\\n-\\t\\t\\trewrite_by_lua_block {\\n-\\t\\t\\t\\tlua_ingress.rewri
    ...
    ...
    \\n-\\t\\t}\\n-\\t\\t\\n-\\t\\terror_page 400 401 403 404 405 500 505 /error.html;\\n-\\t\\tlocation = /error.html {\\n-\\t\\t\\troot /etc/nginx/error;\\n-\\t\\t}\\n-\\t\\t\\n-\\t}\\n-\\t## end server bar.prod1.internal.domain\\n-\\t\\n \\t## start server ksp-ksp-cbt-851-0819100002.prod1.internal.domain\\n \\tserver {\\n \\t\\tserver_name ksp-ksp-cbt-851-0819100002.prod1.internal.domain ;\\n"
    

    위 로그에서 event 발생 전 후의 nginx.conf 의 diff 를 자세히 보면 nginx.conf 에서 아래처럼 bar.prod1.internal.domain 도메인을 갖는 server block 부분이 삭제된 것을 확인할 수 있는데, 이는 bar-master-ingress ingress resource 가 삭제된걸로 인한 것이다.

    --- /etc/nginx/nginx.conf  2022-08-19 11:36:50.785869106 +0900
    +++ /tmp/new-nginx-cfg372163662  2022-08-19 11:42:05.818037692 +0900
    @@ -1,5 +1,5 @@
    
    -# Configuration checksum: 12093455329089770281
    +# Configuration checksum: 16068307183398161240
    
     # setup custom paths that do not require root access
     pid /tmp/nginx.pid;
    @@ -9145,151 +9145,6 @@
       }
       ## end server ksp-ksp-cbt-847-0819112304.prod1.internal.domain
       
    -  ## start server bar.prod1.internal.domain
    -  server {
    **-    server_name bar.prod1.internal.domain ;**
    -    
    -    listen 80  ;
    -    listen 443  ssl http2 ;
    -    
    -    set $proxy_upstream_name \\"-\\";
    -    
    -    ssl_certificate_by_lua_block {
    -      certificate.call()
    -    }
    -    
    -    location / {
    -      
    -      set $namespace      \\"foo\\";
    -      set $ingress_name   \\"bar-master-ingress\\";
    -      set $service_name   \\"bar-master-service\\";
    -      set $service_port   \\"8888\\";
    -      set $location_path  \\"/\\";
    -      
    -      rewrite_by_lua_block {
    -        lua_ingress.rewrite({
    -          force_ssl_redirect = false,
    -          ssl_redirect = false,
    -          force_no_ssl_redirect = false,
    -          use_port_in_redirects = false,
    -        })
    -        balancer.rewrite()
    -        plugins.run()
    -      }
    -      
    -      # be careful with `access_by_lua_block` and `satisfy any` directives as satisfy any
    -      # will always succeed when there's `access_by_lua_block` that does not have any lua code doing `ngx.exit(ngx.DECLINED)`
    -      # other authentication method such as basic auth or external auth useless - all requests will be allowed.
    -      #access_by_lua_block {
    -      #}
    -      
    -      header_filter_by_lua_block {
    -        lua_ingress.header()
    -        plugins.run()
    -      }
    -      
    -      body_filter_by_lua_block {
    -      }
    -      
    -      log_by_lua_block {
    -        balancer.log()
    -        
    -        monitor.call()
    -        
    -        plugins.run()
    -      }
    -      
    -      port_in_redirect off;
    -      
    -      set $balancer_ewma_score -1;
    -      set $proxy_upstream_name \\"foo-bar-master-service-8888\\";
    -      set $proxy_host          $proxy_upstream_name;
    -      set $pass_access_scheme  $scheme;
    -      
    -      set $pass_server_port    $server_port;
    -      
    -      set $best_http_host      $http_host;
    -      set $pass_port           $pass_server_port;
    -      
    -      set $proxy_alternative_upstream_name \\"\\";
    -      
    -      client_max_body_size                    1m;
    -      
    -      proxy_set_header Host                   $best_http_host;
    -      
    -      # Pass the extracted client certificate to the backend
    -      
    -      # Allow websocket connections
    -      proxy_set_header                        Upgrade           $http_upgrade;
    -      
    -      proxy_set_header                        Connection        $connection_upgrade;
    -      
    -      proxy_set_header X-Request-ID           $req_id;
    -      proxy_set_header X-Real-IP              $remote_addr;
    -      
    -      proxy_set_header X-Forwarded-For        $remote_addr;
    -      
    -      proxy_set_header X-Forwarded-Host       $best_http_host;
    -      proxy_set_header X-Forwarded-Port       $pass_port;
    -      proxy_set_header X-Forwarded-Proto      $pass_access_scheme;
    -      
    -      proxy_set_header X-Scheme               $pass_access_scheme;
    -      
    -      # Pass the original X-Forwarded-For
    -      proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;
    -      
    -      # mitigate HTTPoxy Vulnerability
    -      # <https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/>
    -      proxy_set_header Proxy                  \\"\\";
    -      
    -      # Custom headers to proxied server
    -      
    -      proxy_set_header X-Content-Type-Options                    \\"nosniff;\\";
    -      
    -      proxy_set_header X-Frame-Options                    \\"sameorigin\\";
    -      
    -      proxy_set_header X-XSS-Protection                    \\"1; mode=block\\";
    -      
    -      proxy_connect_timeout                   2s;
    -      proxy_send_timeout                      60s;
    -      proxy_read_timeout                      60s;
    -      
    -      proxy_buffering                         off;
    -      proxy_buffer_size                       4k;
    -      proxy_buffers                           4 4k;
    -      
    -      proxy_max_temp_file_size                1024m;
    -      
    -      proxy_request_buffering                 on;
    -      proxy_http_version                      1.1;
    -      
    -      proxy_cookie_domain                     off;
    -      proxy_cookie_path                       off;
    -      
    -      # In case of errors try the next upstream server before returning an error
    -      proxy_next_upstream                     error timeout;
    -      proxy_next_upstream_timeout             0;
    -      proxy_next_upstream_tries               2;
    -      
    -      proxy_pass http://upstream_balancer;
    -      
    -      proxy_redirect                          off;
    -      
    -    }
    -    
    -    # Custom code snippet configured in the configuration configmap
    -    location /_hcheck.hdn {
    -      root /etc/nginx/hcheck;
    -    }
    -    
    -    error_page 400 401 403 404 405 500 505 /error.html;
    -    location = /error.html {
    -      root /etc/nginx/error;
    -    }
    -    
    -  }
    -  ## end server bar.prod1.internal.domain
    -  
       ## start server test.prod1.internal.domain
       server {
         server_name test.prod1.internal.domain ;
    

    참고로 nginx ingress controller 는 여러 kubernetes ingress resource 의 설정을 취합하여 하나의 nginx.conf 를 만들어 내는데 각 ingress resource 들은 nginx.conf 에서 각각 독립된 server block 으로 존재한다. 아래는 nginx.conf 파일의 예제이다

    http {
    
      // ingress resource 별로 아래와 같은 server block 이 생성된다.
      server {
        server_name domain1.foo.com;
        access_log logs/domain1.foo.access.log main;
    
        root /var/www/domain1.foo.com/htdocs;
      }
    
      // ingress resource 별로 아래와 같은 server block 이 생성된다.
      server {
        server_name www.domain2.foo.com;
        access_log  logs/domain2.foo.access.log main;
    
        root /var/www/domain2.foo.com/htdocs;
      }
    }
    
  3. nginx.conf 재생성 후 reload 하는 것을 확인할 수 있다.

    I0819 11:42:05.990019       7 controller.go:161] "Backend successfully reloaded"
    I0819 11:42:05.990172       7 event.go:282] Event(v1.ObjectReference{Kind:"Pod", Namespace:"managed-ingress-controller", Name:"managed-ingress-controller-ingress-nginx-controller-jcgfs", UID:"06fe1709-51c8-4ebc-a2a5-3c4acf5840f3", APIVersion:"v1", ResourceVersion:"64661823", FieldPath:""}): type: 'Normal' reason: 'RELOAD' **NGINX reload triggered due to a change in configuration**
    I0819 11:42:05.993141       7 controller.go:186] Dynamic reconfiguration succeeded.
    

nginx.conf 가 재생성되어 reload 되는 것과 connection reset 에러가 발생하는 것은 무슨 관계인가?

k8s nginx ingress controller 매뉴얼에 따르면 ingress resource 가 새로 생성되거나 삭제되면 nginx.conf 를 재생성후 reload 한다고 한다. 당연하겠지만 ingress resource 가 새로 생성되거나 삭제되면 nginx.conf 에서 server block 을 추가하거나 삭제해줘야 하기 때문이다.

Untitled

참고: https://kubernetes.github.io/ingress-nginx/how-it-works/#when-a-reload-is-required

그리고 nginx 는 reload 하기 위하여 아래와 같은 과정을 거친다.