- 병렬 커넥션, keep-alive 커넥션, 커넥션 파이프라인을 활용한 HTTP의 최적화
- 커넥션 관리를 위해 따라야 할 규칙들
1. 병렬 커넥션 parallel
- HTTP는 클라가 여러 개의 커넥션을 맺음으로써 여러 개의 HTTP 트랜잭션을 병렬로 처리할 수 있게 한다.
1.1 병렬 커넥션은 페이지를 더 빠르게 내려받는다.
- 하나의 커넥션으로 객체들을 로드할 때의 대역폭 제한과
대기시간을 줄일 수 있다면
더 빠르게 로드할 수 있을 것이다.
- 위 그림은 병렬 커넥션 방식을 개략적으로 보여준다.
- html을 머저 내려받고, 남은 3개의 트랜잭션이
각각 별도의 커넥션에서 동시에 처리된다. - 이미지들을 병렬로 내려받아 커넥션 지연이 겹쳐짐으로써 총 지연시간이 줄어든다.
1.2 병렬 커넥션이 항상 더 빠르지는 않다.
- 클라의 네트워크 대역폭이 좁을 때는 대부분 시간을 데이터 전송하는 데만 쓸 것이다.
- 다수의 커넥션은 메모리를 많이 소모하고, 자체적인 성능 문제를 발생시킨다.
- 브라우저는 실제로 병렬 커넥션을 사용하긴 하지만, 적은 수(대부분 4개, 최신 브라우저는 6~8개)의 병렬 커넥션만을 허용한다.
- 서버는 특정 클라로부터 과도한 수의 커넥션이 맺어졌을 경우, 그것을 임의로 끊어버릴 수 있다.
1.3 병렬 커넥션은 더 빠르게 느껴질 수 있다.
- 병렬 커넥션이 실제로 페이지를 더 빠르게 내려받는 것은 아니지만, 화면에 여러 개의 객체가 동시에 보이면서 내려받고 있는 상황을 볼 수 있기 때문에 사용자는 더 빠르게 내려받고 있는 것처럼 느낄 수 있다.
- 사실 사람들은 페이지의 총 다운로드 시간이 더 걸린다 하더라도, 화면 전체에서 여러 작업이 일어나는 것으 ㄹ눈으로 확인할 수 있으면 그것을 더 빠르다고 여긴다.
2. 지속 커넥션 persistent
웹 클라는 보통 같은 사이트에 여러 개의 커넥션을 맺는다. 즉 같은 서버에 계속 요청
- 사이트 지역성이라 부른다. site locality
처리가 완료된 후에도 계속 연결된 상태로 있는 TCP 커넥션을 지속 커넥션이라고 부른다.
- HTTP/1.1을 지원하는 기기는 처리가 완료된 후에도
TCP 커넥션을 유지하여 앞으로 있을 HTTP 요청에 재사용 할 수 있다.
- HTTP/1.1을 지원하는 기기는 처리가 완료된 후에도
- 커넥션을 맺기 위한 준비 작업에 따르는 시간을 절약할 수 있다.
- TCP의 느린 시작으로 인한 지연을 피할 수 있다.
2.1 지속 커넥션 vs 병렬 커넥션
병렬 커넥션의 단점
- 각 트랜잭션마다 새로운 커넥션을 맺고 끊기 때문에 시간과 대역폭이 소요된다.
- 새로운 커넥션은 TCP의 느린 시작 때문에 성능이 떨어진다.
- 실제로 연결할 수 있는 병렬 커넥션의 수에는 제한이 있다.
지속 커넥션의 장점
- 커넥션을 맺기 이한 사전 작업 지연을 줄여줌
- 튜닝된 커넥션을 유지
- 커넥션의 수를 줄여준다.
지속 커넥션의 단점
- 계속 연결된 상태로 있는 수많은 커넥션이 쌓이게 될 것이다.
지속 커넥션 + 병렬 커넥션은 가장 효과적
- HTTP/1.0+ 에는 keep-alive 커넥션이 있고,
HTTP/1.1에는 지속 커넥션이 있다.
2.2 HTTP/1.0+의 Keep-Alive 커넥션
- 초기의 지속 커넥션은 상호 운용과 관련된 설계에 문제가 있었지만,
아직 많은 클라와 서버는 이 초기 keep-alive 커넥션을 사용하고 있다.- 설계상의 문제는 HTTP/1.1에서 수정되었다.
2.2.1 Keep-Alive 동작
- HTTP/1.1에서는 keep-alive는 사용하지 않기로 결정
아직 keep-alive 핸드셰이크가 널리 사용되고 있음.
- HTTP/1.0 keep-alive 커넥션을 구현한 클라는 커넥션을 유지하기 위해서 요청에
Connection: Keep-Alive
헤더를 포함시킨다.
- HTTP/1.0 keep-alive 커넥션을 구현한 클라는 커넥션을 유지하기 위해서 요청에
이 요청을 받은 서버는 그다음 요청도 이 커넥션을 통해 받고자 한다면, 응답 메세지에 같은 헤더를 포함시켜 응답한다.
응답에
Connection: Keep-Alive
가 없으면, 클라는 서버가 keep-alive를 지원하지 않으며,
응답 메세지가 전송되고 나면 서버 커넥션을 끊을 것이라고 추정한다.
2.2.2 Keep-Alive 옵션 (헤더)
- keep-alive 헤더는 커넥션을 유지하기를 바라는 요청일 뿐이다.
- 클라나 서버가 keep-alive 요청을 받았다고 해서 무조건 그것을 따를 필요는 없다.
Connection: Keep-Alive
가 있을 때만 사용 가능하다.- HTTP/2에서는 무시된다.
keep-alive의 동작은 Keep-Alive
헤더의 옵션들로 제어 가능하다
1 | Keep-Alive: timeout=5, max=1000 |
- timeout
- 커넥션이 얼마간 유지될 것인지를 의미한다.
- 이대로 동작한다는 보장은 없다.
- max
- 커넥션이 몇 개의 HTTP 트렌젝션을 처리할 때까지 유지될 것인지를 의미한다.
- 이대로 동작한다는 보장은 없다.
2.2.3 Keep-Alive 커넥션 제한과 규칙
- HTTP/1.0과 HTTP/2.0에서는 사용되지 않는다.
Connection: Keep-Alive
Connection: Keep-Alive
가 있을 때만 사용 가능하다.Connection: Keep-Alive
가 없을 경우 서버는 요청을 처리한 후 커넥션을 끊을 것이다.- 클라는
Connection: Keep-Alive
가 없을 경우 서버가 응답 후에 커넥션을 끊을 것임을 알 수 있다.
Content-Length과 커넥션
- 커넥션이 끊어지기 전에 엔터티 본문의 길이를 알 수 있어야 커넥션을 유지할 수 있다.
- 엔터티 본문이 정확한 Content-Length 값과 함께 멀티파트 미디어 형식을 가지거나,
- 청크 전송 인코딩 chunked transfer encoding으로 인코드 되어야 함을 뜻한다.
- keep-alive 커넥션에서 잘못된 Content-Length 값을 보내는 것은 좋지 않다.
- 트랜잭션이 끝나는 시점에 기존 메세지의 끝과 새로운 메세지의 시작점을 정확히 알 수 없기 때문이다.
Connection 헤더
- 프락시와 게이트웨이는 Connection 헤더의 규칙을 철저히 지켜야 한다. (명시된 헤더는 다 제거되기 때문)
- keep-alive 커넥션은 connection 헤더를 인식하지 못하는 프락시 서버와 맺어지면 안 된다.
- HTTP/1.0 을 따르는 기기로부터 받는 모든 Connection 헤더 필드는 무시해야 한다.
오래된 프락시 서버로부터 실수로 전달될 수 있기 때문.
- HTTP/1.0 을 따르는 기기로부터 받는 모든 Connection 헤더 필드는 무시해야 한다.
2.2.4 Keep-Alive와 dumb(멍충한) 프락시
Connection 헤더의 무조건 전달
- 웹 클라는 프락시에
Connection: Keep-Alive
헤더와 함께 메세지를 보내고, 커넥션을 유지하기를 요청한다.
클라는 커넥션을 유지하자는 요청이 받아들여졌는지 확인하기 위해 응답을 기다린다. - 멍청한 프락시는
Connection
헤더를 이해하지 못해서 다음 서버에 메세지를 그대로 전달한다.
Connection
헤더는 홉별 헤더다.(hop-by-hop): 오직 한 개의 전송 링크에만 적용되며, 다음 서버로 전달되어서는 안 된다. Connection: Keep-Alive
를 받은 웹 서버는 커넥션을 유지하자고 요청하는 것으로 잘못 판단하게 된다.
클라가 아닌 프락시와 커넥션을 유지하는 것에 동의하고Connection: Keep-Alive
헤더를 포함하여 응답한다.
하지만 프락시는Connection
를 모른다.- 프락시는
Connection: Keep-Alive
헤더를 포함하고 있는 응답메세지를 클라에게 전달한다.
클라는 이 헤더를 통해 프락시가 커넥션을 유지하는 것에 동의했다고 추정한다.
클라와 서버는 커넥션을 유지한다고 생각하지만, 정작 프락시는 모름. Connection
를 모르는 프락시는 모든 데이터를 클라에게 전달하고 서버가 커넥션을 끊기를 기다린다.
하지만 서버는 커넥션을 유지하고, 프락시는 끊어지기 전까지 기다린다.- 클라는 유지되고 있다고 생각되는 커넥션으로 다음 요청을 보내는데, 프락시는 예상치 못한 요청이 들어온 거라 프락시로부터 무시되고,
브라우저는 아무런 응답 없이 로드 중이라는 표시만 나온다. - 브라우저는 자신이나 서버가 타임아웃이 나서 커넥션이 끊길 때까지 기다린다.
프락시와 홉별 헤더
위의 잘못된 통신을 피하려면
- 프락시는 Connection 헤더와 Connection 헤더에 명시된 헤더들은 절대 전달하면 안 된다.
- 프락시는 Connection 헤더, Keep-Alive 헤더도 전달하면 안 된다.
- 홉별 헤더들 역시 전달하거나 캐시하면 안 된다.
- Proxy-Authenticate, Proxy-Connection, Transfer-Encoding, Upgrade
2.2.5 Proxy-Connection 살펴보기
- Proxy-Connection은 비표준 헤더이다.
- 프록시가 종종 여러 계층으로 배치되어 있기 때문에 2.6와 같은 문제를 야기한다.
- 클라는 요청 시 Proxy-Connection 헤더 필드를 보내지 않도록 권장한다.
- Connection 헤더를 모르는 프락시에게 Connection 대신에 Proxy-Connection 확장 헤더를 프락시에게 전달한다.
- 웹 서버는 Proxy-Connection를 무시한다.
- 영리한 프락시라면, 의미 없는 Proxy-Connection헤더를 Connection 헤더로 바꿈으로써 원하던 효과를 얻게 된다.
RFC 7230
Proxy-Connection vs Connection header
2.3 HTTP/1.1의 지속 커넥션
- 설계가 더 개선된 지속 커넥션을 지원한다.
- default로 활성화되어있다.
- HTTP/1.1 어플리케이션은 트랜잭션이 끝난 다음 커넥션을 끊으려면
Connection:close
헤더를 명시해야 한다.
- HTTP/1.1 어플리케이션은 트랜잭션이 끝난 다음 커넥션을 끊으려면
- HTTP/1.1 클라는 응답에
Connection:close
헤더가 없으면 응답 후에도 커넥션을 계속 유지하는 것으로 추정한다.
- 클라와 서버는 언제든지 커넥션을 끊을 수 있다.
Connection:close
를 보내지 않는 것이 서버가 커넥션을 영원히 유지하겠다는 것은 아니다.
- HTTP/1.1 클라는 응답에
2.3.1 지속 커넥션의 제한과 규칙
- 클라가 요청에
Connection:close
헤더를 포함해 보내면,
클라는 그 커넥션으로 추가적인 요청을 보낼 수 없다. - 클라가 해당 커넥션으로 추가적인 요청을 보내지 않을 것이라면, 마지막 요청에
Connection:close
헤더를 보내야 한다.. - 커넥션이 끊어지기 전에 엔터티 본문의 길이를 알 수 있어야 커넥션을 유지할 수 있다.
- 엔터티 본문이 정확한 Content-Length 값을 가지거나,
- 청크 전송 인코딩 chunked transfer encoding으로 인코드 되어야 함을 뜻한다.
- Connection 헤더의 값과 상관없이 언제든지 커넥션을 끊을 수 있다.
3. 파이프라인 커넥션 pipelined
- HTTP/1.1은 지속 커넥션을 통해서 요청을 파이프라이팅할 수 있다.
- keep-alive 커넥션의 성능을 더 높여준다.
- 여러 개의 요청은 응답이 도착하기 전까지 큐에 쌓인다.
- 대기 시간이 긴 네트워크 상황에서 네트워크 상의 왕복으로 인한 시간을 줄여서 성능을 높여준다.
3.1 파이프라인의 제약사항
- HTTP 클라는 커넥션이 지속 커넥션인지 확인하기 전까지 파이프라인을 이어서는 안 된다.
- 응답은 요청 순서와 같게 와야 한다. 순서 없이 오면 순서에 맞게 정렬시킬 방법이 없다.
- HTTP 클라는 POST 요청같이(비멱등) 반복해서 보낼 경우, 문제가 생기는 요청은 파이프라인을 통해 보내면 안 된다.
- 에러가 발생하면 파이프라인을 통한 요청 중에 어떤 것들이 서버에서 처리되었는지 클라는 알 방법이 없다.
4. 커넥션 끊기에 대한 미스터리
4.1 마음대로 커넥션 끊기
- 보통 커넥션은 메세지를 다 보낸 다음 끊지만,
에러가 있는 상황에서는 헤더의 중간이나, 다른 엉뚱한 곳에서 끊길 수 있다.
4.2 Content-Length와 Truncation(끊기)
- 각 HTTP 응답은 본문의 정확한 크기 값을 가지는 Content-Length 헤더를 가지고 있어야 한다.
- 클라나 프락시가 커넥션이 끊어졌다는 HTTP 응답을 받은 후,
실제 전달된 엔터티의 길이와 Content-Length의 값이 일치하지 않거나
Content-Length 자체가 존재하지 않으면, 수신자는 데이터의 정확한 길이를 서버에게 물어봐야 한다.
4.3 커넥션 끊기의 허용, 재시도
- 커넥션은 에러가 없더라도 언제든 끊을 수 있다.
- 클라가 트랜잭션을 수행 중에 전송 커넥션이 끊기게 되면, 클라는 그 트랜잭션을 재시도 하더라도, 문제가 없다면(멱등성) 커넥션을 다시 맺고 한 번 더 전송을 시도해야 한다.
- 이건 파이프라인 커넥션에서는 어렵다.
- 클라는 여러 요청을 큐에 쌓아 놓을 수 있지만,
서버는 아직 처리되지 않고 스케줄이 조정되어야 하는 요청들을 남겨둔 채로 커넥션을 끊어버릴 수도 있다.
4.4 우아한 커넥션 끊기
- TCP 커넥션은 그림처럼 양방향이다.
- TCP 커넥션의 양쪽에는 데이터를 읽거나 쓰기 위한 입력 큐와 출력 큐가 있다.
전체 끊기와 절반 끊기
- 전체 끊기:
close()
를 호출하면 TCP 커넥션의 입력 채널과 출력 채널의 커넥션을 모두 끊는다. - 절반 끊기:
shutdonw()
을 호출하면 입력 채널이나 출력 채널 중에 하나만을 개별적으로 끊는다.
TCP 끊기와 리셋 에러
잘 모르겠다.
- 보통은 커넥션의 출력 채널을 끊는 것이 안전하다.
- 커넥션의 반대편에 있는 기기는 모든 데이터를 버퍼로부터 읽고 나서, 데이터 전송이 끝남과 동시에 당신이 커넥션을 끊었다는 것을 알게 될 것이다.
- 클라에서 더는 데이터를 보내지 않을 것임을 확신할 수 없는 이상, 커넥션의 입력 채널을 끊는 것은 위험하다.
- 서버의 운영체제는 TCP ‘connection reset by peer’ 메세지를 클라에게 보낼 것이다.
- 대부분의 운영체제는 이것을 심각한 에러로 취급하여 버퍼에 저장된, 아직 읽히지 않은 데이터를 모두 삭제한다.
- 파이프라인 커넥션에서는 이러한 상황은 더 악화된다.
우아하게 커넥션 끊기
- 우아하게 커넥션 끊기를 구현하는 것은 어플리케이션 자신의 출력 채널을 먼저 끊고, 다른 쪽에 있는 기기의 출력 채널이 끊기는 것을 기다리는 것이다.
- 양쪽에서 더는 데이터를 전송하지 않을 것이라고 알려주면 커넥션은 리셋의 위험 없이 온전히 종료된다.
- 상대방이 절반 끊기를 구현했다는 보장도 없어서, 출력 채널에 절반 끊기를 하고 난 후에도 데이터나 스트림의 끝을 식별하기 위해 입력 채널에 대해 상태 검사를 주기적으로 해야 한다.
- HTTP 완벽가이드 책을 보고 이해한 내용을 정리 한 글입니다.
참고자료