1. 병렬 커넥션 (Parallel Connection)
클라이언트는 여러개의 커넥션을 맺고 여러 transaction을 병렬로 처리할 수 있다. 클라이언트의 네트워크 대역폭(bandwidth)을 한 객체를 다운받는데에 다 쓰는 것이 아니라, 남는 대역폭은 다른 객체를 받는데 사용하는 방식이다. 앞의 글에서 3개의 이미지가 있는 웹페이지의 경우를 예를 들게 되면, HTML을 다운받으면서 동시에 3개의 이미지를 다운로드하기 때문에 각각의 지연시간이 겹치게 되어 결과적으로는 전체 지연시간이 줄어드는 효과를 낸다.
하지만 이 방법에도 단점이 있다.
- 클라이언트의 네트워크 대역폭이 좁은 경우, 제한된 대역폭에서 각 객체를 내려받는 것은 병렬 커넥션의 이점이 사라진다.
- 다수의 커넥션은 서버의 메모리를 많이 소모하고 자체적인 성능 문제를 야기한다. 예를 들어 하나의 클라이언트가 100개의 커넥션을 맺는 경우 100명이 동시 접속하면 서버는 10000개의 커넥션을 감당해야 한다.
이 때문에, 브라우저는 실제로 병렬 커넥션을 쓰긴 하지만 대부분 6-8개 정도의 커넥션만을 허용한다. 서버 역시 특정 클라이언트에서 과도한 커넥션을 맺는 경우 강제로 커넥션을 끊을 수 있다. 병렬 커넥션과 관련해서는 도메인 샤딩이라는 방법과 함께 읽어보면 좋을 것 같다.
2. 지속 커넥션 (Persistent Connection)
1. 지속 커넥션의 개념
HTTP/1.1을 지원하는 경우 요청에 대한 처리가 완료된 후에도 TCP 커넥션을 유지하여 HTTP 요청에 재사용할 수 있다. 이를 지속 커넥션
이라 하는데 일반 커넥션과 달리 클라이언트나 서버에서 커넥션을 끊기 전 까지 커넥션을 유지한다. 이는 TCP slow start와 3 way hanshake 지연를 막을 수 있는 방법이다.
지속 커넥션과 병렬 커넥션은 다음과 같은 차이점이 있다. 지속 커넥션|병렬 커넥션 -|- 이미 맺어진 커넥션을 사용하기 때문에 시간과 대역폭 소모가 없음.|각 transaction 마다 새로운 커넥션을 맺고 끊어 시간과 대역폭이 소모됨. Slow start를 하지 않고 튜닝된 커넥션을 사용.|Slow start로 인해 성능이 떨어지며 실제 병렬 커넥션 수에는 제한이 있다.
이렇게만 보면 지속 커넥션이 병렬 커넥션의 상위 기능으로 보이지만 반드시 그런것 만은 아니다. 지속 커넥션의 관리를 잘못할 경우 계속 연결된 상태로 많은 커넥션이 생기기때문에 클라이언트와 서버의 불필요한 리소스 소모를 일으키게 된다.
오히려 지속 커넥션은 병렬 커넥션과 함께 사용할 때 효과적이다. 실제로 오늘날의 많은 웹사이트는 적은 수의 병렬 커넥션을 유지하는 방식을 사용한다.
2. HTTP/1.0의 Keep-Alive
1. Keep-Alive 헤더의 옵션과 동작
Keep-Alive는 HTTP/1.1의 명세에서 제외되었다.
클라이언트가 request 헤더에 Connection: Keep-Alive
를 포함하여 보내고, 서버 역시 이 헤더를 받았을 때 response 헤더에 같은 헤더를 포함해 보내면 동작하게 된다. 동작하게 되면, transaction마다 커넥션을 맺고 끊지 않고 하나의 커넥션으로 요청을 처리하고 응답을 받게 된다. 만약 클라이언트가 받은 response 헤더에 Connection: Keep-Alive
헤더가 없으면 서버가 Keep-Alive를 지원하지 않으며 커넥션이 끊어질 것이라고 추정하게 된다.
Keep-Alive헤더의 옵션은 다음과 같다.
Connection: Keep-Alive
Keep-Alive: timeout=5, max=100
- max: 최대 몇개의 transaction을 처리하는데 커넥션을 유지할 것인가에 대한 옵션
- timeout: 커넥션을 얼마나 유지할 것인가에 대한 옵션
이 경우, 커넥션은 5초간 유지되며 100개의 transaction을 처리하는데 사용된다. 클라이언트나 서버가 Keep-Alive요청을 받았다고 그대로 따를 필요가 없으며, 이 헤더의 내용이 그대로 실행되리라는 보장을 할 수 없다.
2. Keep-Alive 커넥션 제한과 규칙
- Keep-Alive는 HTTP/1.0에서는 기본적으로 사용되지 않는다.
- 엔터티 본문의 길이를 알 수 있어야 커넥션 유지가 가능하다.
- Proxy와 gateway는 message를 전달하거나 cache에 넣기 전에 Connection에 명시된 모든 헤더들과 Connection 헤더를 제거해야 한다 (hob-by-hob).
- Dumb proxy로 인해 proxy와는 Keep-Alive 커넥션을 맺으면 안된다. 기술적으로 HTTP/1.0을 따른 기기로 부터 받는 모든 Connection 헤더를 무시해야한다.
- 클라이언트는 response 전체를 받기 전에 커넥션이 끊어질 경우 요청을 다시 보낼 준비를 해야한다.
3. Dumb proxy와 Proxy-Connection 헤더
지속 커넥션에서 Dumb proxy문제란 오래된 스펙을 따르는 proxy가 Connection헤더를 이해하지 못하고(단순한 확장 헤더로 인식) 다음 커넥션으로 전달하면서 발생하는 문제이다. 즉, Connection 헤더는 hob-by-hob으로 적용해야하는 헤더인데 이를 다음 hob으로 전달하면서 생기는 문제이다.
위의 그림에서 볼 수있는 문제를 정리하자면 다음과 같다.
- 클라이언트가 proxy에 Connection: Keep-Alive를 전달.
- Proxy가 Connection 헤더를 삭제하지 않고 서버에 전달.
- 서버는 proxy와 커넥션을 유지하고 커넥션을 끊지 않는다.
- 서버가 proxy에 Connection: Keep-Alive가 포함된 response를 전달. 하지만 proxy는 Connection: Keep-Alive헤더를 이해하지 못하고 서버로부터의 커넥션이 끊어지길 기다린다.
- Proxy가 클라이언트에 Connection: Keep-Alive를 전달.
- 클라이언트가 다음 요청을 보내면, proxy는 서버와의 커넥션 단절을 기다리고 있기 때문에 클라이언트의 요청이 무시된다.
이를 해결하기 위해 Proxy-Connection
헤더가 탄생했지만 이 역시 완벽하게 문제를 해결하지 못한다.
3. HTTP/1.1의 지속 커넥션
HTTP/1.1에서는 Keep-Alive를 지원하지 않지만 설계가 더욱 개선된 지속 커넥션을 제공한다. 이는 기본적으로 활성화 되어있고 커넥션을 끊으려면 message에 Connection: close
헤더를 포함하면 된다.
1. 지속 커넥션 제한과 규칙
- 클라이언트가 request 헤더에 close를 명시했다면, 클라이언트는 그 커넥션으로 추가 요청을 보낼 수 없다.
- 커넥션 상의 모든 message가 자신의 정확한 길이 정보를 가지고 있어야 커넥션의 지속이 가능하다.
- HTTP/1.1 application은 커넥션 헤더의 값과 상관없이 언제든 커넥션을 끊을 수 있다. 서버는 커넥션을 끊기 전에 적어도 한개의 request에 대한 response를 보내야 한다.
- HTTP/1.1 application은 커넥션이 중간에 끊어지더라도 커넥션을 복구할 수 있어야 한다.
- 클라이언트는 전체 응답을 받기전에 커넥션이 끊어지면 request를 다시 보낼 준비를 해야한다.
- 사용자는 서버의 부하를 덜기 위해 최소한의 커넥션(2개)만을 유지해야 한다.
3. 파이프라인 커넥션
HTTP/1.1은 지속커넥션을 통해 request를 pipelining할 수 있다. Request에 대한 response가 오기 전에 여러개의 request를 queue에 쌓아 순차적으로 요청하고 그에 대한 response를 받는 방법이다.
파이프 라인 커넥션에는 몇가지 규칙이 있다.
- 클라이언트는 지속 커넥션이 확인되기 전 까지 파이프라인을 만들면 안된다.
- 클라이언트는 커넥션이 끊어지더라도 완료되지 않은 request가 파이프라인에 있으면 다시 보낼 준비를 해야한다.
- POST같이 연산이 한 번 일어날때마다 결과가 바뀔수도 있는(non-idempotent) request는 파이프라인을 사용하면 안된다.
4. 커넥션 끊기
1. 마음대로 끊기
어떠한 클라이언트, 서버, proxy라도 언제든지 TCP 커넥션을 끊을 수 있다. 예를들어, 지속 커넥션의 경우에 일정 시간 동안 request가 없는 경우 서버는 그 커넥션을 끊는 것이 가능하다.
하지만 서버는 커넥션을 끊는 시점에 클라이언트가 데이터를 보내지 않을 것이라는 확신을 할 수 없고, 커넥션을 끊는 순간 클라이언트가 request를 보내게 되면 문제가 생긴다.
2. Content-Length와 truncation
Response 헤더에는 Content-Length 헤더가 명시되어 있어야 한다. 클라이언트가 response 헤더에 명시된 length와 엔터티 본문의 길이가 다르거나, Content-Length 헤더 자체가 존재하지 않으면 서버에 Content-Length를 질의해야 한다.
3. 커넥션 끊기의 허용과 재시도, idempotent
HTTP application은 예상치 못하게 커넥션이 끊어져도 적절히 대응할 준비가 되어 있어야 한다. 예를들어, transaction 수행 중 커넥션이 끊기면 클라이언트는 커넥션을 다시 맺고 한 번더 transaction을 시도해야 한다.
GET, HEAD, PUT, DELETE 등 idempotent한 메소드들은 반복요청을 자동으로 처리하거나 파이프 라인을 이용해도 상관 없지만, POST의 경우 자동으로 요청을 재전송하면 안된다.
4. 우아한 커넥션 끊기
1. 전체 끊기와 절반 끊기
TCP 커넥션에서 입력 채널과 출력채널을 동시에 끊는 것을 ‘전체 끊기’라고 하고, 입력 채널과 출력 채널중 하나만 끊는 것을 ‘절반 끊기’라고 한다.
특히 서버의 출력만을 끊는 것을 우아한 커넥션 끊기
라고 한다.
2. TCP 끊기와 리셋 에러
보통은 커넥션의 출력 채널만 끊는 것이 안전하다. 왜냐하면, 반대편의 기기는 모든 전송받은 데이터를 버퍼로 부터 읽고 나서 데이터 전송 완료됨가 동시에 커넥션을 끊었다는 것을 알게 되기 때문이다.
만약 반대편에서 더는 데이터를 보내지 않을 것이란 확신이 없는 이상 입력채널을 끊게 되면 문제가 발생한다. 클라이언트에서 끊긴 입력 채널에 데이터를 전송하면 서버에서 ‘connection reset by peer’ 에러를 발생시키는데, 대부분의 OS에서는 상당히 중요한 에러로 취급한다. 이 에러가 발생되면 버퍼에 저장된 아직 읽히지 않은 모든 request, response 데이터를 삭제하기 때문에 파이프라인 커넥션에서는 더욱 악화된다.
3. 우아하게 끊기
위의 문제를 회피하기 위해서는 application이 출력 채널을 먼저 끊고, 다른쪽 기기의 출력채널이 끊어지기를 기다려야 한다. 반대편에서 출력 채널을 끊으면 (혹은 더 이상의 데이터 전송이 없을것이라 알려주면) 커넥션을 온전히 종료한다.
하지만, 양 단의 기기가 이 기능을 구현했다는 보장이 없기 때문에 출력채널을 끊은 후에도 입력 채널에 대한 상태 검사를 주기적으로 해 주어야 한다.
5. Conclusion
두 개의 글을 통해 HTTP가 어떻게 커넥션을 관리하는지에 대해 정리해 보았다. 직접적으로 와 닿는 부분은 크지 않았지만, 정말 중요한 커넥션
의 동작을 알고 모르고의 차이는 클 것같다는 생각이 들었다. 앞으로 개발을 하면서 TCP layer를 직접 개발하지 않더라도, browser에서 구동하는 web application을 개발하는 입장에서 이번 파트 역시 정리하면서 많은 도움을 주었다고 생각한다.