Kubernetes

nginx reload와 keep-alive (부제: zero-downtime은 사기일까?)

토마스.dev 2021. 1. 16. 20:42

* 분석내용이 틀릴 수도 있습니다. 지적해주시면 감사하겠습니다.

 

해당 이슈를 얘기하기 전에 Ingress 리소스정보 변경에 따른 Ingress-nginx controller 의 config reload flow를 먼저 설명한다.

예를 들어, Ingress의 host를 변경한다고 가정하자. 변경이 되면 Ingress 리소스를 watch하는 Ingress-nginx controller(nginx가 아니다!)에서 Ingress를 읽어 nginx.conf를 새로 만든다. 그리고나서 nginx -s reload 명령을 날리게 된다.

 

이 때, nginx 에서는 어떤 방식으로 config을 반영할까?

 

구글링으로 reload 시 내부적으로 일어나는 구체적인 flow를 찾을 수 있었다.

serverfault.com/questions/378581/nginx-config-reload-without-downtime/1019121#1019121

Nginx Reload

Nginx reload (HUP signal) is more specifically implemented as several steps [1,2]:
  • The master process checks the syntax validity.
  • Applies new configuration, that is, to open log files and new listen sockets.
  • If this fails, it rolls back changes and continues to work with old configuration.
  • If this succeeds, it starts new worker processes, and sends messages to old worker processes requesting them to shut down gracefully.
  • Old worker processes close listen sockets and continue to service old clients.
  • After all clients are serviced, old worker processes are shut down.

간단히 말해, 새로운 worker들을 시작하고, 기존 worker들은 현재 request 처리 후 종료된다는 것이다. 이것만 보면, 아~ zero-downtime이군. 맘껏 reload해도 되겠네? 라고 착각하게 된다.

 

하지만 이것은 반은 맞고 반은 틀리다.

 

필자는 ingress 변경시에 nginx 쪽에서 에러가 난다는 문의를 받은 적 있다. 그래서 몇 가지 벤치마크 툴로 테스트를 해보았다.

 

jmeter

jmeter는 두가지 타입의 클라이언트를 지원한다. HTTPClient4와 Java이다. 이 중 HTTPClient4 에서만 Ingress 변경시에 에러가 났다.

 

wrk에서도 에러가 났다.

$ wrk -v -t8 -c8 -d1m --timeout 1m http://thomas
wrk  [kqueue] Copyright (C) 2012 Will Glozer
Running 1m test @ http://thomas
  8 threads and 8 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    10.11ms    3.50ms  99.20ms   80.84%
    Req/Sec    99.28     10.31   138.00     70.60%
  47514 requests in 1.00m, 37.52MB read
  Socket errors: connect 0, read 10, write 0, timeout 0
Requests/sec:    790.69
Transfer/sec:    639.31KB

 

ab로는 에러가 나지 않았다.

$ ab -t 60 -c 8 http://thomas/
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
 
Benchmarking 10.61.205.71 (be patient)
Completed 5000 requests
Completed 10000 requests
Completed 15000 requests
Completed 20000 requests
Finished 21302 requests
 
 
Server Software:
Server Hostname:        thomas
Server Port:            80
 
Document Path:          /
Document Length:        612 bytes
 
Concurrency Level:      8
Time taken for tests:   60.000 seconds
Complete requests:      21302
Failed requests:        0
Total transferred:      17531546 bytes
HTML transferred:       13036824 bytes
Requests per second:    355.03 [#/sec] (mean)
Time per request:       22.533 [ms] (mean)
Time per request:       2.817 [ms] (mean, across all concurrent requests)
Transfer rate:          285.34 [Kbytes/sec] received
 
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        4   10   5.9      7      39
Processing:     5   12   6.2      9      77
Waiting:        5   12   6.1      9      77
Total:         11   22   7.7     19      83
 
Percentage of the requests served within a certain time (ms)
  50%     19
  66%     27
  75%     30
  80%     31
  90%     33
  95%     34
  98%     36
  99%     37
 100%     83 (longest request)

 

왜 이런 차이가 존재하는 것일까? 구글링을 좀 더 하니 힌트를 찾을 수 있었다.

soonoo.me/docs/posts/2020/03/26/nginx-reload.html

 

바로 keep-alive를 지원할 경우 끊김이 발생한다는 것이다. 조건을 확인해 다시 테스트해보았다.

 

jmeter: keep-alive 옵션이 기본적으로 켜져있다. 이걸 끄니 HTTPClient4에서도 발생하지 않았다.

wrk: 기본적으로 keep-alive옵션을 지원한다. "Connection: Close" 헤더 옵션을 줘서 keep-alive를 끄니 발생하지 않았다.

$ wrk -v -t8 -c8 -d1m --timeout 1m -H "Connection: Close" http://thomas
wrk  [kqueue] Copyright (C) 2012 Will Glozer
Running 1m test @ http://thomas
  8 threads and 8 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    13.26ms   12.80ms 244.69ms   95.81%
    Req/Sec    42.91      7.61    60.00     92.46%
  20553 requests in 1.00m, 16.13MB read
Requests/sec:    342.00
Transfer/sec:    274.87KB

ab: 기본적으로 keep-alive옵션이 꺼져 있다. -k (keep-alive) 옵션을 주니 에러가 발생했다.

$ ab -k -t 60 -c 8 http://thomas/
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
 
Benchmarking thomas-nginx.dev.9rum.cc (be patient)
Completed 5000 requests
Completed 10000 requests
Completed 15000 requests
Completed 20000 requests
Completed 25000 requests
Completed 30000 requests
Completed 35000 requests
Completed 40000 requests
Finished 41280 requests
 
 
Server Software:
Server Hostname:        thomas
Server Port:            80
 
Document Path:          /
Document Length:        612 bytes
 
Concurrency Level:      8
Time taken for tests:   60.008 seconds
Complete requests:      41280
Failed requests:        13
   (Connect: 0, Receive: 0, Length: 13, Exceptions: 0)
Keep-Alive requests:    40864
Total transferred:      34167061 bytes
HTML transferred:       25255404 bytes
Requests per second:    687.91 [#/sec] (mean)
Time per request:       11.629 [ms] (mean)
Time per request:       1.454 [ms] (mean, across all concurrent requests)
Transfer rate:          556.03 [Kbytes/sec] received
 
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   1.0      0      26
Processing:     0   12   6.0      9     129
Waiting:        0   11   6.0      9     129
Total:          0   12   6.1      9     137
 
Percentage of the requests served within a certain time (ms)
  50%      9
  66%     10
  75%     13
  80%     18
  90%     23
  95%     24
  98%     25
  99%     26
 100%    137 (longest request)

 

여기까지 보면, 아~ nginx는 keep-alive 클라이언트에 대해 zero-downtime을 보장하지 않는구나~ 라고 생각할수 있다.

정말 nginx의 문제일까? 이것이 사실인지 좀 더 분석해보자.

 

wireshark로 keep-alive가 있을 때와 없을 때 패킷을 비교해보자

keep-alive가 없을 때,

우리가 알고 있듯이 3way handshake를 맺고 GET을 날린다. 그리고 나서 200OK가 떨어지고, 서버쪽에서 FIN,ACK가 옴으로써 문제없이 termination된다.

 

keep-alive가 있을 때,

GET으로 데이터를 받은 후 서버에서 FIN,ACK나 날라온다. 헌데 클라이언트는 FIN,ACK로 종료하기 전에 GET을 한 번 더 날린다. 이러니까 서버에서는 RST가 날라온다(2번이 날라오는 이유는 처음 GET에 대한 RST와 그 다음 클라이언트의 FIN,ACK에 대한 것으로 보인다)

 

이걸보면, nginx에서는 정상적으로 커넥션을 종료하라는 FIN을 보낸다. 헌데 클라이언트에서 GET을 보낸다. 왜 그럴까?

사실 Termnation Handshake할때 한 쪽에서 FIN을 보냈다 한들 다른 쪽에서 바로 FIN,ACK를 보낼 필요는 없다. 그 사이에 Request가 날라갈 수가 있는 것이다.  패킷을 다시 보면, GET 요청을 할때 서버의 FIN에 이어서 요청하는 것을 알 수 있다.

클라이언트 입장에서는 "어? 끝냄? 잠깐, 하나만 더 요청할게" 인거다.

 

아까 jmeter에서 java 클라이언트로 했을때는 에러가 없었다고 했다. 어떻게 해서 그럴까?

java 클라이언트로도 똑같이 RST이 온다. 하지만 클라이언트 쪽에서 에러를 잘 처리?하고 Retry를 해서 그런 것 같다.
어찌 생각해보면 벤치마크 입장에서 RST 에러를 표시해주는 것이 정확한 측정일 수도 있다. 어쨌든 RST이 온건 온거니까.

 

위에서 nginx reload의 flow을 다시 한번 보자.

"old worker processes requesting them to shut down gracefully."

이걸 보면, Request한번 받아주고 끝내면 되지 왜 안받아주고 RST 날릴까? 쪼잔하게 시리?

forum.nginx.org/read.php?2,197927,289673#msg-289673

여기를 보면, nginx 기존 코드로는 유지를 하고 종료했던 것으로 보인다. 하지만 이렇게 되면 Heavy한 Request가 왔을때 worker가 제때 종료되지 않는 문제가 있어서 다시 없앴다고 한다.

 

더 생각해서 worker의 종료를 강제적으로 지연하는 방법은 없을까? 있긴하다. 바로 worker_shutdown_timeout 이다. 

medium.com/statuscode/nginxs-new-worker-shutdown-timeout-directive-d60f9c1142f8

하지만, 사실 테스트할 때 이미 해당 옵션값은 240s였다. 그런데도 불구하고 바로 Close처리가 됐다. 왜그럴까?

stackoverflow.com/questions/63821421/nginx-reload-ignores-worker-shutdown-timeout

이 설명대로라면 해당 옵션은 종료 시그널을 받기 전에 받은 request 의 처리 대기시간인 것이다.

 

HTTP/1.1 스펙을 살펴보면, 클라이언트는 keep-alive를 사용할 경우 서버쪽에서 언제든지 끊길수 있다는 점을 고려하여 이에 대한 적절한 에러처리를 해야한다고 나와있다.

 

다시, soonoo.me/docs/posts/2020/03/26/nginx-reload.html 에서 마지막 링크를 살펴보면, trac.nginx.org/nginx/ticket/1022#comment:1

 

keep-alive커넥션을 끊기 위한 스펙은 강제하고 있지 않다고 한다. 서버가 Connection: Close를 명시적으로 날려주면 좋겠지만, 이럴 경우 클라이언트로부터 Request를 한 번 더 받아야 하고, 위에서 얘기한대로 worker 미종료 문제가 생길수 있다. 그렇게 되면 성능 문제와 더불어 변경된 옵션도 늦게 반영되거나 아예 반영이 안될수도 있는 이슈가 존재하는 것이고, 그래서 nginx는 FIN을 날린 것이다. 

 

결론은, keep-alive를 사용하는 클라이언트는 nginx 에서 "Connection: Close" 를 주지 않고 끊어버리니 그걸 감안해서 적절한 에러처리를 해야한다는 것이다.

 

+추가: openresty/1.15.8.1 를 사용하는 nginx의 경우 증상이 좀 달랐다.(위 테스트는 nginx/1.19.3 이었다)

특이하게 RST를 보내지 않고, 정상적으로 ACK를 보내왔다. 다만, 서버쪽 FIN 직후 클라이언트에서 날린 GET은 응답이 오지 않아 timeout처리 되었다. 서로 처리방법이 다른가보다. ㅡㅡㅋ

'Kubernetes' 카테고리의 다른 글

CustomResource Version Converting 과정 분석  (0) 2021.09.15
CKA 후기  (0) 2021.03.27
Git Flow 와 CI/CD  (0) 2020.03.19
Shared Informer  (0) 2019.12.11