TCP 커넥션
•
HTTP는 패킷 교환 네트워크 프로토콜의 계층화된 집합인 TCP/IP를 통해 이루어진다.
•
메시지가 손실, 손상되거나 순서가 바뀌지 않고 안전하게 전달된다.
•
주소 입력 → TCP 커넥션 생성 → 요청 → 응답 → TCP 커넥션 해제.
신뢰할 수 있는 데이터 전송통로인 TCP
•
TCP 커넥션은 인터넷을 안정적으로 연결해준다.
•
TCP 커넥션을 통해 한 바이트씩 순서대로 전달된다.
TCP 스트림은 세그먼트로 나뉘어 IP 패킷을 통해 전송된다
•
HTTP는 '프로토콜 스택'의 최상위 계층이다.
•
HTTP + TLS(SSL) ⇒ HTTPS
•
TCP는 세그먼트 단위로 데이터 스트림을 잘게 나누고, IP 패킷에 담아서 전달한다.
TCP 커넥션 유지하기
•
TCP는 포트 번호를 통해서 여러 개의 커넥션을 유지한다.
•
<발신 IP, 발신 포트, 수신 IP, 수신 포트 >
•
네가지 구성요소가 모두 동일한 커넥션을 중복 생성할 수 없다.
TCP 소켓 프로그래밍
•
운영체제는 TCP 커넥션의 생성과 관련된 여러 기능을 제공 → 소켓 API
◦
socket() : 소켓 생성
◦
connect() : TCP 커넥션 생성
◦
read() : 읽기
◦
write() : 쓰기
◦
close() : TCP 커넥션 끊음
•
TCP 양 끝에 데이터 구조를 생성하고 연결하여 데이터 스트림을 읽고 쓸 수 있다.
•
프로토콜의 세부 사항을 숨긴다.
TCP의 성능에 대한 고려
•
HTTP는 TCP의 상위 계층이기 때문에, HTTP 트랜잭션의 성능은 TCP의 성능에 영향을 받는다.
HTTP 트랜잭션 지연
•
트랜잭션이 처리되는 시간 << TCP 커넥션 설정, 요청/응답 전송 시간
•
HTTP의 지연은 TCP 네트워크 지연으로 인해 발생
◦
DNS 주소 → IP주소 변환 시간 소요 ( 인프라 발전 및 캐시 사용으로 단축 )
◦
커넥션 설정 시간 소요
◦
요청 메시지 - 처리 - 응답 메시지 전송 시간 소요
•
하드웨어, 네트워크, 서버, 메시지 크기, 거리 등
성능 관련 중요 요소
TCP 커넥션 핸드셰이크 지연
•
작은 크기의 트랜잭션을 처리하기 위해 커넥션 생성에 50% 이상의 시간 소요
◦
클라이언트는 커넥션을 생성을 요청하기 위해 'SYN' 패킷 전송
◦
서버는 요청을 받아들여 응답으로 'SYN + ACK' 패킷 전송
◦
클라이언트는 커넥션이 잘 맺어졌음을 'ACK' ( 확인응답 ) 패킷 전송
확인응답 지연
•
확인 응답을 보내기 위해 수신지가 같은 데이터 패킷에 'ACK' 패킷을 편승
•
특정 시간 동안 버퍼에 저장해 두고, 편승시킬 송출 데이터 패킷을 찾는다.
•
요청과 응답으로 이루어지는 HTTP 동작 방식에서 편승 기회가 낮음.
•
확인응답 지연 관련 기능 수정 및 비활성화
TCP 느린 시작(slow start)
•
TCP 커넥션 생성 후 데이터 전송 속도를 제한하고 성공적으로 데이터를 전송하면 점차 속도 제한을 높인다.
•
2개 → 성공 → 4개 → 성공 → ...
•
튜닝된 커넥션을 사용하는 '지속 커넥션' 기능
네이글(Nagle) 알고리즘과 TCP_NODELAY
•
TCP 세그먼트의 플래그와 헤더는 대략 40바이트
◦
작은 크기의 데이터를 포함한 패킷을 다량 보내면 성능 저하
•
TCP 세그먼트가 일정 크기가 되지 않으면 전송하지 않는다.
◦
다만 다른 모든 패킷이 'ACK'를 받으면 전송한다.
•
크기가 작은 HTTP 메시지는 패킷을 채우지 못해, 계속 대기하여 지연 발생
•
확인응답 지연과 함께 사용되어 지연 발생
•
HTTP 스택 파라미터로 TCP_NODELAY 설정하여 네이글 알고리즘 비활성화
TIME_WAIT의 누적과 포트 고갈
•
TCP 커넥션을 끊으면 종단간 IP와 포트를 메모리에 기록
•
일정 시간동안 해당 주소를 사용하는 커넥션의 재생성 방지
HTTP 커넥션 관리
•
커넥션을 생성하고 최적화하는 HTTP 기술
흔히 잘못 이해하는 Connection 헤더
•
클라이언트와 서버 사이에 중개 서버가 존재( 프락시, 캐시 서버 )
•
인접한 두 개의 HTTP 어플리케이션 커넥션에만 적용될 옵션을 지정하는 경우
◦
커넥션 토큰을 쉼표로 구분하여 갖고, 다른 커넥션에 전달되지 않아야 한다.
•
Connection 헤더에는 세 가지 종류의 토큰이 전달될 수 있다.
◦
HTTP 헤더 필드명은 이 커넥션에만 해당되는 헤더를 나열
◦
임시적인 토큰 값은, 커넥션에 대한 비표준 옵션을 의미
◦
close 값은, 커넥션 작업이 완료되면 종료되어야 함을 의미
•
커넥션 토큰이 HTTP 헤더 필드명을 갖고 있으면 현재 커넥션만을 위한 정보이므로 다음 커넥션에 전달해서는 안된다.
•
Connection 헤더에 있는 모든 헤더 필드는 다음 커넥션에 전달되는 시점에 삭제되어야 한다.
•
홉별(hop-by-hop) 헤더 명을 기술 → '헤더 보호하기'
순차적인 트랜잭션 처리에 의한 지연
•
HTML과 리소스를 받기 위해서 각기 다른 커넥션 생성 → 지연 발생
•
사용자는 빈 화면에 하나씩 나타나는 것을 보고있다.
•
HTTP 커넥션의 성능을 향상시키는 기술 네 가지에 대해 알아보자.
병렬 커넥션
•
여러 개의 TCP 커넥션을 통한 동시 HTTP 요청
병렬 커넥션은 페이지를 더 빠르게 내려받는다
•
단일 커넥션의 대역폭 제한과 커넥션이 동작하지 않는 시간을 활용
◦
각 커넥션의 지연시간을 겹치게 하여 총 지연 시간을 감소
◦
클라이언트의 인터넷 대역폭을 여러 커넥션이 나누어 사용
병렬 커넥션이 항상 더 빠르지는 않다
•
대역폭이 작은 경우에는 오히려 성능 저하
•
다수의 커넥션 생성 과정의 부하와 메모리 소모
•
브라우저는 적은 수의 병렬 커넥션을 허용
•
서버는 여러 사용자를 고려하여 과도한 수의 커넥션 제한
병렬 커넥션은 더 빠르게 '느껴질 수' 있다
•
화면에 객체가 동시에 보여 사용자는 심리적으로 더 빠르다고 느낀다.
지속 커넥션
•
동일한 서버에 HTTP 요청을 여러번 전송 → 사이트 지역성
•
HTTP 트랜잭션 처리 완료 후 TCP 커넥션을 유지하여 재사용
◦
새로운 커넥션 설정 및 느린 시작으로 인한 지연 감소
지속 커넥션 vs 병렬 커넥션
•
병렬 커넥션의 단점
◦
각 트랜잭션 마다 새로운 커넥션을 생성하고 끊음 → 시간, 대역폭 소모
◦
느린 시작으로 인한 성능 저하
◦
실제 연결 가능한 병렬 커넥션 수의 제한
•
지속 커넥션의 단점
◦
지속 커넥션이 쌓여 리소스의 불필요한 소모 발생
•
지속 커넥션과 병렬 커넥션을 함께 사용하는 것이 효과적
HTTP/1.0+의 Keep-Alive 커넥션
•
HTTP/1.0 에서 시작된 keep-alive 커넥션 → HTTP/1.1에서 수정 및 보완
•
새로운 커넥션 생성에 소요되는 시간을 절약
Keep-Alive 동작
•
HTTP/1.1 명세에서 제외되었지만 여전히 사용중
•
클라이언트는 요청에 'Connection:Keep-Alive' 헤더를 포함하여 전송
•
서버는 응답 메시지에 같은 헤더를 포함하여 전송
◦
응답에 헤더가 없으면 클라이언트는 서버가 커넥션을 끊을 것이라 추정
Keep-Alive 옵션
•
keep-alive의 동작은 Keep-Alive 헤더의 쉼표로 구분된 옵션을 추가하여 제어
•
timeout : 커넥션을 유지할 시간 ( 응답 )
•
max : 커넥션을 유지할 트랜잭션 처리 개수 ( 응답 )
Keep-Alive 커넥션 제한과 규칙
•
클라이언트는 keep-alive 커넥션을 사용하기 위해 Connection : Keep-Alive 요청 헤더를 전송한다.
•
커넥션을 유지하기 위해 Connection : Keep-Alive 헤더를 포함해 전송한다.
•
클라이언트는 Connection : Keep-Alive 헤더가 없으면 서버가 응답 후에 커넥션을 끊을 것을 알 수 있다.
•
엔터티 본문의 길이를 명확하게 표시해야 한다. (Content-Length)
•
프락시와 게이트웨이는 메시지를 전달하거나 캐시에 저장하기 전에 Connection 헤더에 명시된 모든 헤더 필드와 Connection 헤더를 제거한다.
•
keep-alive 커넥션은 Connection 헤더를 인식하지 못하는 멍청한 프락시(dumb proxy)와 맺어져서는 안된다.
•
HTTP/1.0을 따르는 기기로부터 받는 모든 Connection 헤더 필드는 무시해야하 한다.
•
클라이언트는 응답 전체를 모두 받기 전에 커넥션이 끊어진 경우, 다시 요청을 보낼 수 있는 준비를 해야한다.
Keep-Alive와 멍청한(dumb) 프락시
•
클라이언트의 요청 메시지에 Connection:Keep-Alive 헤더가 포함된 경우, 처리가 끝난 뒤에 이 커넥션을 유지하려는 것
•
서버가 keep-alive를 지원한다면 응답에 Connection:Keep-Alive 헤더를 포함하여 전송한다.
Connection 헤더의 무조건 전달
•
프락시는 Connection 헤더를 이해하지 못해서 해당 헤더들을 삭제하지 않고 요청 그대로를 다음 프락시에 전달
•
클라이언트는 프락시에 'Connection:Keep-Alive' 헤더가 포함된 요청 메시지를 전송. 클라이언트는 프락시의 응답을 대기하고 있다.
•
멍청한 프락시는 'Connection:Keep-Alive' 헤더를 이해하지 못해 서버에 그대로 전달한다. Connection 헤더는 홉별 헤더이므로 인접한 애플리케이션간에 적용하고 삭제한 뒤에 전달해야 한다.
•
서버는 프락시로부터 'Connection:Keep-Alive' 헤더가 포함된 메시지를 받아 프락시가 커넥션을 유지하자고 요청하는 것으로 판단하여 해당 헤더를 포함한 응답 메시지를 전송.
•
프락시는 여전히 해당 헤더를 이해하지 못해 클라이언트에게 그대로 전달.
•
클라이언트는 프락시로부터 'Connection:Keep-Alive'헤더가 포함된 메시지를 응답받아 커넥션을 유지한다.
•
프락시는 서버로부터 응답 메시지를 받았기 때문에 커넥션이 끊어지기를 기다림.
•
이후 클라이언트가 요청 메시지를 전송하고 프락시는 같은 커넥션상에서 다른 요청이 오는 경우를 예상하지 못하여 모든 요청 메시지를 무시한다.
•
브라우저는 아무런 응답 메시지를 받지 못하고 이후 타임아웃이 발생하여 커넥션이 끊어질 때 까지 기다린다.
프락시와 홉별 헤더
•
위와 같은 문제를 회피하기 위해, 프락시는 Connection 헤더와 여기에 명시된 헤더를 전달하면 안된다.
•
홉별 헤더들 역시 전달하거나 캐시해서는 안된다.
Proxy-Connection 살펴보기
•
모든 헤더를 무조건 전달하는 문제를 해결하기 위한 차선책
•
Connection 헤더 대신에 비표준인 Proxy-Connection 확장 헤더를 프락시에게 전달한다.
◦
프락시가 무조건 전달하더라도 서버는 해당 헤더를 무시하여 문제가 발생하지 않는다.
◦
영리한 프락시는 Connection 헤더로 변환하여 올바르게 처리한다.
◦
클라이언트와 서버사이에 한 개의 프락시가 있는 경우에 성립된다.
•
멍청한 프락시 옆에 영리한 프락시가 있는 경우 또다른 문제가 발생함.
HTTP/1.1의 지속 커넥션
•
HTTP/1.1 명세에서 제외된 keep-alive 대신 '지속 커넥션'을 지원
•
HTTP/1.1의 커넥션은 기본적으로 지속 커넥션이 활성화 되어있다.
•
트랜잭션이 끝나면 Connection:close 헤더를 포함한 응답을 전송한다.
지속 커넥션의 제한과 규칙
•
클라이언트가 요청에 Connection:close 헤더를 포함하여 보내면 해당 커넥션은 더이상 사용할 수 없다.
•
클라이언트가 해당 커넥션으로 추가적인 요청을 보내지 않을 것이라면 Connection:close 헤더를 포함한 메시지를 보내야 한다.
•
커넥션에 있는 모든 메시지가 자신의 길이 정보를 정확하게 갖고 있어야 커넥션을 유지할 수 있다.
•
프락시는 클라이언트와 서버 각각에 대해 별도의 지속 커넥션을 맺고 관리해야 한다.
•
프락시 서버는 클라이언트가 커넥션 관련 기능에 대한 클라이언트의 지원 범위를 알고있지 않다면 지속 커넥션을 맺어서는 안된다.
•
지속 커넥션도 언제든지 끊길 수 있다.
•
중간에 끊어지는 커넥션을 복구할 수 있어야 한다. 클라이언트는 다시 보내도 문제가 없는 요청은 가능한 한 다시 보내야 한다.
•
클라이언트는 전체 응답을 받기 전에 커넥션이 끊어지면, 요청을 반복해서 보내도 문제가 없는 경우에는 요청을 다시 보낼 준비가 되어 있어야 한다.
•
클라이언트 사용자는 서버의 과부하 방지를 위해 두개의 지속 커넥션만을 유지해야 한다.
파이프라인 커넥션
•
HTTP/1.1은 지속 커넥션을 통해서 요청을 파이프라이닝 할 수 있다.
◦
여러 개의 요청은 응답이 도착하기 전까지 큐에 쌓인다.
•
파이프라인의 제약 사항
◦
클라이언트는 지속 커넥션 요청에 대한 응답을 받기 전까지 파이프라인을 이어서는 안된다.
◦
응답은 요청을 보낸 순서와 같게 와야한다.
◦
클라이언트는 커넥션이 끊어지더라도 큐에 쌓인 요청을 언제든 다시 보낼 준비를 해야한다.
◦
POST 요청과 같이 반복해서 보낼 경우 문제가 생기는 요청은 파이프라인을 통해 보내서는 안된다 → 비멱등 요청
커넥션 끊기에 대한 미스터리
•
커넥션 관리에 대한 명확한 기준은 없다.
'마음대로' 커넥션 끊기
•
누구든지 아무때나 커넥션을 끊을 수 있다.
•
파이프라인의 경우, 일정 시간 동안 아무런 요청 메시지를 전송하지 않으면 서버는 그 커넥션을 끊을 수 있다.
•
하지만 서버가 커넥션을 끊는 시점에 클라이언트가 요청 메시지를 전송하였다고 확신할 수는 없다.
Content-Length와 Truncation
•
HTTP 응답은 본문의 정확한 크기 값을 갖는 Content-Length 헤더를 가지고 있어야 한다.
•
서버가 잘못된 길이 정보로 응답 메시지를 보내고 커넥션을 끊은 경우, 클라이언트는 데이터의 정확한 길이를 물어봐야 하는데 물어볼 수 가 없다.
커넥션 끊기의 허용, 재시도, 멱등성
•
커넥션은 에러가 없어도 언제든 끊을 수 있다.
•
예상치 못하게 커넥션이 끊기게 되면 재전송 할 수 있는 준비를 해야한다.
•
파이프라인의 경우, 응답이 오기전에 커넥션이 끊어지면 요청한 메시지가 얼마나 처리되었는지 알 수 없다.
◦
POST와 같이 비멱등한 메소드를 재전송하면 잘못된 결과를 초래한다.
◦
비멱등인 요청을 재전송해야 하는 경우, 이전 요청에 대한 응답을 기다린다.
우아한 커넥션 끊기
•
TCP 커넥션의 양쪽 끝에는 입력 큐와 출력 큐가 있다.
◦
한쪽의 출력 큐에있는 데이터는 다른 한쪽의 입력 큐로 보내진다.
전체 끊기와 절반 끊기
•
HTTP 애플리케이션은 입력 채널과 출력 채널 중 하나 이상을 끊을 수 있다.
•
전체 끊기 : 입출력 채널의 커넥션을 모두 끊는다. → close()
•
절반 끊기 : 입력 채널이나 출력 채널중 하나만 끊는다. → shutdown()
TCP 끊기와 리셋 에러
•
단순한 HTTP 애플리케이션은 전체 끊기만 사용
•
각기 다른 대상과 통신할 때, 파이프라인 지속 커넥션 사용, 에러 방지를 위해 절반 끊기 사용
◦
보통 커넥션의 출력 채널을 끊는 것이 안전하다.
◦
클라이언트가 더이상 데이터를 보내지 않는다고 확신할 수 없는 이상 입력 채널을 끊는 것은 위험하다.
◦
클라이언트가 이미 끊긴 입력 채널에 요청 메시지를 보내는 경우 서버의 운영체제는 'connection reset by peer' 메시지를 클라이언트에게 보낸다.
◦
운영체제는 심각한 에러로 취급하여 버퍼에 아직 읽지 않은 데이터를 모두 삭제한다.
◦
파이프라인 지속 커넥션에서 더욱 악화된 상황으로 발생
▪
클라이언트가 요청에 대한 응답을 읽지 않아 버퍼에 저장되어 있다.
▪
서버가 커넥션을 끊는다.
▪
클라이언트는 새로운 요청을 보내지만 이미 커넥션이 끊겨 connection reset by peer 메시지를 수신한다.
▪
버퍼에 있는 응답 데이터는 모두 삭제된다.
우아하게 커넥션 끊기
•
HTTP 명세에서 언급되지만 방법에 대해 설명하지 않음.
•
일반적인 '우아하게 커넥션 끊기'는 자신의 출력 채널을 먼저 끊고 다른 쪽의 출력 채널이 끊기는 것을 기다리며 더이상 데이터를 전송하지 않을 것을 확신한 뒤에 커넥션을 끊는 것.
◦
하지만 상대방이 절반 끊기 기능이 구현되지 않거나 절반 끊기를 했는지 보장할 수 없다.
•
출력 채널에 절반 끊기를 하고 난 뒤에도 입력 채널에 대해 주기적으로 데이터나 스트림의 끝을 식별하는 검사를 수행한다.