티스토리 뷰

 

올클 서비스는 학교 API를 사용하여 사용자가 학교 구성원임을 인증받는다. 사용자가 처음 서비스에 진입하여 로그인 API를 호출하면 올클 서버는 학교 API에 요청을 보낸다. 사용자에게 서비스를 보이는 첫인상인 만큼 로그인 API를 빠른 응답 속도로 제공하고 싶다. 가장 효과적으로 응답 시간을 줄이는 방법은 외부 API의 응답 시간을 줄이는 것이다. 외부 API를 호출하면 가장 긴 시간이 걸리는 부분은 어디일까? 

 

외부 API 호출에서 응답 시간 분석

외부 API를 직접 호출해 보았다. 테스트에는 curl 명령어와 --verbose 옵션을 사용하여 상세한 응답 시간을 분석했다. 민감한 정보는 모두 가렸다. 

 

➜ curl -v -o /dev/null -s -w 
"HTTP Status: %{http_code}\nDNS Lookup: %{time_namelookup}\n
Connect: %{time_connect}\nPre-transfer: %{time_pretransfer}\n
Start-transfer: %{time_starttransfer}\nTotal Time: %{time_total}\n"
"{http://***.sejong.***.***}"; 

* Host ***.sejong.***.***:80 was resolved.
* IPv6: (none)
* IPv4: ***.***.***.***
*   Trying ***.***.***.***:80...
* Connected to *** (***.***.***.***) port 80
> POST /userLogin.do HTTP/1.1
> Host: ***
> User-Agent: curl/8.7.1
> Accept: */*
> Content-Length: 42
> Content-Type: application/x-www-form-urlencoded
> 
} [42 bytes data]
* upload completely sent off: 42 bytes
< HTTP/1.1 302 Found
< Server: Apache-Coyote/1.1
< X-FRAME-OPTIONS: SAMEORIGIN
< Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
< Access-Control-Max-Age: 3600
< Access-Control-Allow-Headers: x-requested-with
< Access-Control-Allow-Origin: *
< Set-Cookie: JSESSIONID=***; Path=/; HttpOnly
< Location: /index.jsp
< Content-Length: 0
< Date: Fri, 30 Aug 2024 12:24:27 GMT
< 
* Connection #0 to host ***.sejong.***.*** left intact
HTTP Status: 302
DNS Lookup: 0.129258
Connect: 0.602096
Pre-transfer: 0.602450
Start-transfer: 0.820694
Total Time: 0.820833

 

마지막 부분이 중요하다. 

  • DNS Lookup: DNS 조회에 걸린 시간을 의미한다. 129ms가 소요됐다. 
  • Connect: TCP 커넥션을 맺는데 소요된 시간을 의미한다. 602ms - 129ms = 473ms가 소요됐다. 
  • Pre-transfer: TCP 연결이 완료되고 SSL/TLS 핸드셰이크가 포함된다. 0ms가 소요됐다. 
  • Start-transfer: 클라이언트가 서버로부터 첫 번째 바이트의 응답을 받기까지의 시간을 의미한다. 즉, 서버에서 응답을 준비하고 전송을 시작하기까지의 시간을 나타낸다. 820ms - 602ms = 218ms가 소요됐다. 

TCP 커넥션을 맺는 데에 473ms로 가장 오래 걸린 것을 확인할 수 있다. TCP 커넥션을 재사용하면 820ms의 총 응답 시간을 약 50% 줄일 수 있을 것으로 기대된다. 

 

 

HTTP keep-alive로 커넥션 재사용

그럼, TCP 커넥션을 어떻게 재사용할 수 있을까? 한 가지 방법은 keep-alive 옵션을 사용하는 것이다. 위의 응답 헤더를 다시 보자. Connection 헤더와 Keep-Alive 헤더를 확인할 수 없다. 이것은 HTTP/1.1부터 keep-alive를 사용하지 않고 persistence connection을 지원하기 때문이다. 

 

< HTTP/1.1 302 Found
< Server: Apache-Coyote/1.1
< X-FRAME-OPTIONS: SAMEORIGIN
< Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
< Access-Control-Max-Age: 3600
< Access-Control-Allow-Headers: x-requested-with
< Access-Control-Allow-Origin: *
< Set-Cookie: JSESSIONID=***; Path=/; HttpOnly
< Location: /index.jsp
< Content-Length: 0
< Date: Fri, 30 Aug 2024 12:24:27 GMT

 

curl 명령어에 HTTP/1.0으로 요청을 보내도록 설정하고 Connection 헤더를 추가했다. 응답에서 keep-alive가 설정되어 돌아온 것을 확인할 수 있다. 서버가 커넥션 유지를 지원한다는 의미다. 

 

➜ curl --http1.0 -v -o /dev/null -s -w 
"HTTP Status: %{http_code}\nDNS Lookup: %{time_namelookup}\n
Connect: %{time_connect}\nPre-transfer: %{time_pretransfer}\n
Start-transfer: %{time_starttransfer}\nTotal Time: %{time_total}\n"
-H "Connection: keep-alive"
"{http://***.sejong.***.***}"; 

* Host ***.sejong.***.***:80 was resolved.
* IPv6: (none)
* IPv4: ***.***.***.***
*   Trying ***.***.***.***:80...
* Connected to *** (***.***.***.***) port 80
> POST /userLogin.do HTTP/1.0
> Host: ***
> User-Agent: curl/8.7.1
> Accept: */*
> Connection: keep-alive
...
> 
} [42 bytes data]
* upload completely sent off: 42 bytes
< HTTP/1.1 302 Found
...
< Content-Length: 0
< Date: Fri, 30 Aug 2024 12:24:27 GMT
< Connection: keep-alive
< 
* Connection #0 to host ***.sejong.***.*** left intact

 

이번에는 HTTP/1.0과 HTTP/1.1 요청에 대한 응답 마지막 부분에 집중해 보자. '커넥션이 손상되지 않은 채로 남았다'라고 해석된다. 이것은 서버가 커넥션을 유지하려 했지만 curl 프로세스가 커넥션을 필요로 하지 않아 닫았다는 의미다. 이것으로 미루어 보았을 때, HTTP/1.0과 HTTP/1.1 모두 서버는 커넥션을 유지하려 한다. 

 

* Connection #0 to host ***.sejong.***.*** left intact

 

 

Apache Bench로 TCP 커넥션 재사용 확인

이론적으로 재사용될 것이라 생각된다. 하지만 실제로 재사용하는지 확인이 필요하다. 학교 API를 연속해서 호출하고 TCP 커넥션을 재사용하는지 확인하려 한다. 

 

TCP 커넥션 재사용을 확인할 방법이 필요하다. 학교 API로 요청 보내는 시점에 watch와 netstat 명령어를 사용하여 로컬 네트워크를 모니터링하면 TCP 커넥션이 생성되는 것을 확인할 수 있다. 

 

 

 

서버는 기본적으로 TCP 커넥션을 유지하려 한다. 하지만 여러 클라이언트를 대상으로 테스트해본 결과, 상황에 클라이언트가 TCP 커넥션을 종료한다. 다음은 시도해 본 클라이언트 별 특징을 정리했다. 

 

HTTP 요청 클라이언트 특징
curl 클라이언트 매 요청마다 연결 종료
spring 테스트 툴 - RestAssured 매 요청마다 TCP 커넥션은 유지하지만 새로운 TCP 커넥션 생성
Python request 모듈 TCP 커넥션 상태를 보기 어려움
Apache Bench 요청마다 TCP 커넥션 유지

 

 

요청마다 TCP 커넥션이 유지되고, 요청 통계를 제공하는 Apache Bench를 사용하기로 결정했다. Apache Bench로 2번의 요청을 순차적으로 보냈다. 다음 코드를 사용했다. post_date_txt는 인증에 필요한 정보를 담았다. 

 

ab -n 2 -c 1 -p post_data.txt -T application/x-www-form-urlencoded \
http://***.sejong.***.***/userLogin.do

 

결과는 다음과 같다. 커넥션 부분의 최소 소요 시간은 261ms이고 최대 소요 시간은 600ms이다. 최대 소요 시간은 첫 번째 요청에서 발생했고, 최소 소요 시간은 두 번째 요청에서 커넥션을 재사용하여 감소한 것으로 추정된다. Apache Bench가 상세 정보는 보여주지 않기 때문에 정확히는 알기 어렵다. 

 

Concurrency Level:      1
Time taken for tests:   1.342 seconds
Complete requests:      2
Failed requests:        0
Non-2xx responses:      2
Total transferred:      890 bytes
Total body sent:        426
HTML transferred:       0 bytes
Requests per second:    1.49 [#/sec] (mean)
Time per request:       670.808 [ms] (mean)
Time per request:       670.808 [ms] (mean, across all concurrent requests)
Transfer rate:          0.65 [Kbytes/sec] received
                        0.31 kb/s sent
                        0.96 kb/s total

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      261  430 239.3    600     600
Processing:   240  240   0.3    240     240
Waiting:      240  240   0.3    240     240
Total:        502  671 238.9    840     840

 

좀더 확실히 하고자, 요청 수를 20개로 늘렸다. TCP 커넥션의 최소 소요 시간은 217ms이고 최대 소요 시간은 905ms이다. 주의 깊게 볼 부분은 중앙값 220ms과 평균 257ms, 그리고 표준 편차다. 대부분의 요청이 200ms 대에서 이루어 졌고, 큰 데이터로 인해 평균이 높아진 것을 확인할 수 있다. 이를 미루어 보았을 때, 첫 번째 요청이 TCP 커넥션을 생성하는데에 905ms가 걸리고 뒤의 요청은 해당 커넥션을 재사용했다고 확신했다. 

 

Concurrency Level:      1
Time taken for tests:   9.790 seconds
Complete requests:      20
Failed requests:        0
Non-2xx responses:      20
Total transferred:      8900 bytes
Total body sent:        4260
HTML transferred:       0 bytes
Requests per second:    2.04 [#/sec] (mean)
Time per request:       489.510 [ms] (mean)
Time per request:       489.510 [ms] (mean, across all concurrent requests)
Transfer rate:          0.89 [Kbytes/sec] received
                        0.42 kb/s sent
                        1.31 kb/s total

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      217  257 152.8    220     905
Processing:   226  233  10.0    230     271
Waiting:      225  233  10.0    230     271
Total:        443  489 153.2    454    1138

 

실제로 watch와 netstat로 네트워크를 모니터링한 결과 TCP 커넥션 수가 1개 이상으로 늘어나지 않음을 확인했다. 

 

 

TCP 커넥션은 유지된다!

학교 API를 호출한 후에 TCP 커넥션은 유지된다. 단, 클라이언트에 따라 다르다. 

 

처음 세운 가설도 어느 정도 맞았다. API 호출에서 TCP 커넥션은 응답 시간을 지연하는 데에 큰 역할을 한다. TCP 커넥션을 재사용하면 응답 시간이 50% 정도 감소할 것으로 예상했다. 네트워크 지연 시간과 처리 시간 등 많은 변수가 있지만 말이다. 위 Apache Bench로 테스트한 결과 총 응답 시간의 중앙값은 454ms이고 최대값은 1138ms인 것을 미루어 보아, 응답 시간이 60% 감소했음을 알 수 있다. 

 

 

참고

https://speedtestdemon.com/a-guide-to-curls-performance-metrics-how-to-analyze-a-speed-test-result/#What_does_curls_time_pretransfer_mean

https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Keep-Alive

https://httpd.apache.org/docs/current/programs/ab.html

링크
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday