CS/네트워크

HTTP/TCP 지연과 성능개선법

Junuuu 2023. 8. 13. 00:01
728x90

개요

"HTTP 완벽 가이드"라는 책을 읽으며 HTTP 프로그래머에게 영향을 주는 가장 일반적인 TCP 관련 지연들에 대해 정리해보고자 합니다.

 

TCP 커넥션 핸드셰이크 지연

어떤 데이터를 전송하든 새로운 TCP 커넥션을 열고 IP 패킷을 교환합니다.

SYN, ACK 등의 플래그를 포함하는 패킷을 주고받고 커넥션이 잘 맺어졌음을 서버에게 알리고 데이터 전송이 이루어집니다.

HTTP 트랜잭션이 아주 큰 데이터를 주고받지 않는 평범한 경우라면 이런 핸드셰이크과정이 눈에 띄는 지연을 발생시킵니다.

예를 들어 크기가 작은 HTTP 트랜잭션은 50% 이상의 시간을 여기에 사용합니다.

이미 존재하는 커넥션을 재활용하여 이 문제를 해결합니다.

 

확인응답 지연

인터넷 자체가 패킷 전송을 완벽하게 보장하지 않기 때문에 TCP는 데이터 전송 보장을 위해 자체적인 확인 체계를 거칩니다.

예를 들어 세그먼트 순번, 무결성 체크섬이 있습니다.

 

이때 확인응답은 크기가 작기 때문에 같은 방향으로 송출되는 데이터 패킷에 묶어 편승시킵니다.

하지만 막상 편승할 패킷을 찾으려고 하면 해당 방향으로 송출될 패킷이 많지 않기 때문에 확인응답지연 알고리즘 때문에 오히려 지연이 자주 발생합니다.

 

운영체제마다 다르지만, 지연의 원인이 되는 확인응답 지연 관련 기능을 수정하거나 비활성화할 수 있습니다.

 

TCP 느린 시작

인터넷의 혼잡제어를 위해 TCP는 한 번에 전송할 수 있는 패킷의 수를 제한합니다.

예를 들어 처음에는 1개의 패킷을 보내고 응답을 받으면 2개의 패킷을 보내고 응답을 받으면 4개의 패킷을 보냅니다.

 

이런 특성으로 통해 이미 어느정도 데이터를 주고받은 튜닝된 커넥션보다 느립니다.

이를 해결하기 위해서 이미 존재하는 커넥션을 재사용할 수 있습니다.

 

네이글 알고리즘과 TCP_NODELAY

TCP가 작은 크기의 데이터를 포함한 많은 수의 패킷을 전송하면 네트워크 성능은 크게 떨어집니다.

이를 해결하기 위해 많은 양의 TCP 데이터를 한 개의 덩어리로 합치는 것이 네이글 알고리즘입니다.

 

네이글 알고리즘은 세그먼트 최대 크기가 되지 않으면 전송을 하지 않습니다.

다만 다른 모든 패킷이 확인응답을 받았을 때는 최대 크기보다 작은 패킷의 전송을 허락합니다.

 

이런 특성으로 크기가 작은 HTTP 메시지는 앞으로 생길지 모르는 추가적인 데이터를 기다리며 지연됩니다.

추가적으로 확인응답지연과 함께쓰이면 형편없이 동작합니다.

 

이를 위해 TCP_NODELAY 파라미터 값을 설정하여 네이글 알고리즘을 비활성화할 수 있습니다.

하지만 작은 크기의 패킷이 너무 많이 생기지 않도록 큰 크기의 데이터 덩어리를 만들어야 합니다.

 

TIME_WAIT의 누적과 포트 고갈

보통 성능 측정을 할때 문제를 발생시킵니다.

HTTP 커넥션을 구분할때는 IP와 포트를 통해 구분합니다.

 

같은 IP와 포트를 사용하면 커넥션을 일정기간 동안 생성되지 않도록 하기 위해 보통 세그먼트의 최대 생명주기의 2배인 2분 동안 TIME_WAIT가 유지됩니다.

 

서버가 초당 500개 이상의 트랜잭션을 처리할만큼 빠르지 않다면 포트 고갈 문제를 겪지 않습니다.

패킷이 중복되고 TCP 데이터의 충돌을 막기위해 생긴 기능이지만 현대는 빠른 라우터들 덕분에 중복으로 패킷이 생기는 경우가 없어졌습니다.

 

HTTP의 커넥션 성능을 향상시킬 수 있는 기술들

  • 병렬 커넥션
  • 지속 커넥션
  • 파이프라인 커넥션
  • 다중커넥션

 

병렬 커넥션

HTTP 클라이언트가 여러 개의 컬렉션을 맺어 여러 개의 HTTP 트랜잭션을 병렬로 처리하는 방식입니다.

복잡한 웹페이지는 수십개에서 수백 개의 객체를 포함합니다.

하지만 서버는 여러 사용자의 요청도 함께 처리해야 하기 때문에 수백 개의 커넥션을 허용하는 경우는 드뭅니다.

 

브라우저는 실제로 병렬 커넥션을 사용하긴 하지만 대부분 4개의 병렬 커넥션만 허용합니다.

 

HTTP/1.0+의 Keep-Alive 커넥션

초기에 지속 커넥션은 설계에 문제가 있었지만 많은 클라이언트와 서버는 이 keep-alive 커넥션을 활용하고 있습니다.

설계상의 문제는 HTTP/1.1에서 수정되었습니다.

keep-alive 커넥션의 성능상의 장점은 핸드셰이크 과정이 사라져 시간을 단축시킵니다.

 

클라이언트는 요청에 Connection:Keep-Alive 헤더를 포함시키고 서버로부터도 동일한 요청을 받으면 커넥션을 유지합니다.

하지만 클라이언트나 서버는 keep-alive 요청을 무조건 따를 필요는 없고 언제든지 커넥션을 끊을 수 있습니다.

 

이런 특성으로 클라이언트는 GET요청의 경우에는 상관없지만 주문등의 POST 요청을 중복될 수 있기 때문에 반복을 피해야 합니다.

이런 멱등성을 유지하면 안되는 요청은 keep-alive 옵션을 하면 안됩니다.

 

우아하게 커넥션 끊기

HTTP 명세에서는 클라이언트와 서버가 예기치 않게 커넥션을 끊어야 한다면 "우아하게" = graceful shutdown을 해야 한다고 합니다.

이를 위해서는 애플리케이션이 자신의 출력 채널을 먼저 끊고 다른 쪽에 있는 기기의 출력 채널이 끊기는 것을 기다립니다.

 

양쪽에서 더는 데이터를 전송하지 않을 것이라고 알려주면 커넥션은 리셋의 위험 없이 온전히 종료됩니다.

하지만 상대 쪽에서 동일하게 대처할 것이라는 보장은 없습니다.

 

따라서 출력 채널만 끊고, 데이터나 스트림의 끝을 식별하기 위해 입력 채널에 대해 상태 검사를 주기적으로 수행해야 합니다.

만약 특정 타임아웃 시간 내에 끊어지지 않으면, 애플리케이션은 리소스를 보호하기 위해 커넥션을 강제로 끊을 수 있습니다.