4장 커넥션 관리
목차
- HTTP는 어떻게 TCP 커넥션을 사용하는가?
- TCP 커넥션의 지연, 병목, 막힘
- 병렬 커넥션, keep-alive 커넥션, 커넥션 파이프라인을 활용한 HTTP의 최적화
- 커넥션 관리를 위해 따라야 할 규칙들
TCP 커넥션
TCP 커넥션이 맺어지면 주고받는 데이터들은 손실, 손상되지 않고 순서대로 전달
우리가 아는 3-Way-Handshake는 바로 (4)에서 TCP 커넥션을 맺으며 수행
이 맺어진 커넥션을 통해 통신(Request와 Response)을 하게되고 통신이 완료된 후 커넥션은 close
이 때, 흔히 얘기하는 데이터는 IP 패킷, IP 데이터그램을 통해 전송
- 여기서 A, B, C가 각각 하나의 패킷이다.
하나의 큰 덩어리 데이터를 작은 패킷단위로 나누어 쪼개는 이유는 뭘까?
효율적인 라우팅을 위해서!!
순서 보장을 위해 각 패킷에 일련의 순서를 배정한 뒤 발신지에서 발송을 하게되고, 수신지에서는 이 패킷을 재조립하게 되는데 이 일련의 순서를 보고 재조립을 하여 순서를 보장할 수 있게 된다.
TCP 패킷(세그먼트)
- Source Port, Destination Port : 송, 수신 포트 번호
- Sequence Number : 데이터의 순서번호를 표시 등등
TCP 커넥션 생성
<발신지 IP 주소, 발신지 포트, 수신지 IP 주소, 수신지 포트>
이 4가지 값으로 TCP 커넥션을 생성하는데 모든 값이 같은 커넥션은 존재할 수 없다.(유일무이)
TCP 소켓 프로그래밍
Java의 Socket API를 사용해 간단히 구현해보자.
- 서버
- 클라이언트
TCP의 성능
HTTP 트랜잭션의 성능은 TCP 에 영향을 받음
- 도메인으로부터 IP 주소를 알아내는 과정
- 서버와 클라이언트간에 TCP 커넥션을 맺는 과정
- 클라이언트가 요청을 전송하는 과정
- 서버에서 응답을 전송하는 과정
대부분의 HTTP 지연은 TCP 네트워크 지연으로 인해 발생!!
TCP 커넥션 핸드셰이크 지연
- 클라이언트가 서버에게 SYN전달(커넥션 요청)
- 서버가 클라이언트에게 SYN과 ACK전달(커넥션 요청이 받아들여졌고 반대로 커넥션 요청)
- 클라이언트가 서버로 'ACK`와 전송할 데이터를 전송
- 서버가 응답을 전송
TCP의 ACK 패킷은 크고, 무겁다.(생성 비용이 비싸다)
즉, 전송할 데이터의 크기가 작은데 이런 비싼 커넥션을 생성한다면 지연이 발생한다.(크기가 작은 HTTP 트랜잭션은 50% 이상의 시간을 TCP 구성에 쓴다.)
Connection Pool, Thread Pool처럼 재사용할 수는 없을까??
확인응답 지연
TCP의 특징은 데이터의 손실, 손상을 방지하고 순서를 보장하는 신뢰성있는 전송 프로토콜이다.
이를 위해 Sequence Number와 Checksum을 통해 순번과 데이터의 무결성을 체크한다.
- Checksum을 통해 해싱과 같은 역할
데이터를 체크하고 만약 제대로 받았다면 수신측에서 송신측으로 확인응답패킷를 반환한다.
송신측에서 이를 받지 못했다면 패킷이 손상, 파기되었거나 문제가 있는 것으로 판단하여 다시 전송하는데, 확인응답패킷은 크기가 작다.
효율적인 전송을 위해 같은 방향으로 송출되는 데이터 패킷에 편승시켜 반환해준다.
한번에 더 많은 확인응답패킷을 편승시키기위해 확인응답지연 알고리즘을 사용하는데, 일정 시간동안 편승시킬 패킷을 찾고 찾지못하면 새로 패킷을 만들어 전송한다.
HTTP 통신은 요청과 응답 두가지로만 이루어지기 때문에 편승될 기회가 많이 없으므로 이로 인한 지연이 발생한다.
송신측에서는 수신측의 확인응답패킷을 기다리게되고 수신측에서는 데이터를 정상적으로 수신했지만 편승시킬 패킷을 찾기위해 대기 ==> 지연 발생
TCP 느린 시작
TCP 커넥션은 시간이 자나며 자체적으로 튜닝이 된다!!!
인터넷의 갑작스러운 부하와 혼잡을 방지하기 위한 방법으로 초기에 TCP가 전송할 수 있는 패킷의 수를 제한하고 정상적으로 전송을 했을 시에 더 많은 패킷을 전송할 수 있게 해준다.
따라서, 방금 막 생성된 TCP 커넥션은 여러번 데이터를 주고 받은 TCP 커넥션보다 속도가 느리다.
4개의 패킷을 보내게 되는 것을 혼잡 윈도를 연다라고 표현한다.
무조건 재사용을 해야겠네 그럼??
네이글(Nagle) 알고리즘과 TCP_NODELAY
네이글 알고리즘은 확인응답지연 알고리즘과 비슷한 메커니즘이다.
데이터가 작아도 TCP 세그먼트(패킷)의 크기가 크기 때문에 작은 데이터를 여러개 보내면 네트워크의 성능은 떨어질 수 밖에 없다.
따라서, 세그먼트가 최대 크기가 될 때 까지는 전송을 하지 않고 버퍼에 저장해둔다.
다만, 다른 모든 패킷이 확인 응답을 받았을 경우에는 최대 크기보다 작더라도 전송을 허용해준다.
책에서 말하고 있듯이 네이글 알고리즘이 확인응답지연과 함께 쓰일 경우 형편없이 동작한다.
네이글 알고리즘은 패킷이 최대 크기가 되거나 모든 확인응답을 기다리는데 확인응답지연이 이를 지연시키기 때문이다.
(지연+지연=?)
TCP_NODELAY를 HTTP 스택에 설정해 네이글 알고리즘을 비활성화 할 수 있다.(주의가 필요!)
TIME_WAIT의 누적과 포트 고갈
TCP 커넥션이 끊어지면 종단에서 커넥션의 IP주소, 포트번호를 기록해놓는다.
이는 동일한 커넥션이 생성되어 패킷의 충돌을 막기위한 조치이지만, 최근에는 빠른 라우터 덕분에 중복되는 패킷이나 충돌이 발생할 가능성이 없어졌다고 한다.
하지만 성능 측성을 위한 서버에서는 이러한 TIME_WAIT로 인한 문제가 발생할 수 있다.
클라이언트 IP, 서버 IP, 서버 포트가 고정되어있고 클라이언트의 포트만을 변경하여 유일한 커넥션을 생성해야하기 때문!!
HTTP 커넥션 관리
HTTP는 클라이언트와 서버 사이에 중개 서버(프락시 서버, 캐시 서버)가 놓이는 것을 허용하므로 HTTP 메서지는 중개 서버들을 거치며 전달
두 개의 인접한 HTTP 애플리케이션의 커넥션에만 적용될 옵션을 지정해야할 때는 HTTP Connection 헤더 필드를 사용하자!!
- 다른 커넥션으로 전달되지 않는다.
- ex : Connection:close, Connection:keep-alive
- 비표준 옵션 : keep-alive
Connection헤더에 있는 모든 헤더 필드는 메시지를 다른 곳으로 전달하는 시점에 삭제되어야함!!(hop-by-hop)
- Connection 헤더와 메시지 수신
- 송신자의 요청에 있는 모든 옵션 적용
- 다음 hop으로 전달하기 전에 Connection 헤더에 기술되어 있는 모든 헤더 삭제
- 다음 hop으로 전달
hop-by-hop 헤더
- Proxy-Authenticate : 프록시 서버 뒤에 있는 리소스에 엑세스하는데 사용해야하는 인증 방법을 정의(6장 프락시에서 자세하게 다룰 예정)
- Proxy-Connection : 클라이언트와 프락시 사이의 커넥션 옵션을 명시하기 위해 사용
- Transfer-Encoding : 메시지 본문에 적용된 인코딩의 목록
- Upgrade : 메시지를 발송하는 사람이 다른 프로토콜을 사용하는 의사를 전달하기 위해 사용
- ex: http/1.1을 사용하는 클라이언트가 http/1.0을 사용하는 서버에게 요청을 보낼 수 있도록
순차적인 트랜잭션 처리에 의한 지연
각각의 트랜잭션이 순차적으로 처리되고 트랜잭션 당 커넥션이 생성된다면 커넥션을 맺는데 발생하는 지연(3-way handshake), 느린 시작 지연이 발생
HTTP Connection의 성능을 향상시킬 수 있는 기술들
- 병렬 커넥션
- 지속 커넥션
- 파이프라인 커넥션
- 다중 커넥션
병렬 커넥션
- 각 커넥션의 지연 시간을 겹치게 하여 총 지연 시간을 줄일 수 있음
- 대역폭 : 주어진 경로를 통한 데이터 전송의 최대속도(쉽게 말해 인터넷 속도)
- 따라서, 대역폭이 좁으면 병렬 커넥션의 장점이 사라진다.
- 다수의 커넥션은 메모리를 많이 소모, 자체적인 성능 문제
- 각 클라이언트마다 제한된 병렬 커넥션을 허용
- 실제로 빠르게 내려받지 않더라도 한번에 여러 객체가 보이기 때문에 더 빠르게 느껴질 수도 있다
지속 커넥션
한 웹페이지에 첨부된 이미지, 하이버 링크는 같은 사이트에 있으며 같은 사이트를 가르킨다. 이를 사이트 지역성이라고 한다.
앞서 살펴본 커넥션들은 트랜잭션 이후에 커넥션을 close하지만 지속 커넥션은 종단(서버 or 클라이언트)에서 끊기 전까지는 커넥션을 유지한다!!
재사용을 통한 비용 절약, 튜닝된 TCP 커넥션 사용(느린 시작)하여 빠른 전송
지속 커넥션의 장점
- 커넥션을 맺기 위한 비용과 지연을 줄임
- 튜닝된 커넥션 사용
- 커넥션의 수를 줄임(메모리 소모 줄임)
하지만, 잘못 관리하면 연결된 상태로 커넥션이 쌓임
병렬 커넥션과 지속 커넥션을 함께 사용하면 가장 효율적 ==> 제한된 수의 병렬 커넥션을 맺고 그것을 유지하며 재사용
HTTP/1.0+의 Keep-Alive 커넥션
Keep-Alive, 지속 커넥션을 사용하게 되면 커넥션을 맺고 끊는 작업이 없기 때문에 시간이 단축
HTTP/1.1에서는 keep-alive를 사용하지 않기로 하였지만 여러 곳에서 사용되고 있기 때문에 HTTP 애플리케이션은 이를 처리할 수 있어야한다.
Keep-Alive 동작
커넥션을 유지하기 위해 요청에 Connection : Keep-Alive를 포함시켜 전달하고, 요청을 받은 서버는 같은 헤더를 포함시켜 응답을 반환한다.
만약 요청에 keep-alive를 보냈으나 응답에 keep-alive가 반환되지 않으면 서버가 지원하지 않는 것이다.
Keep-Alive 옵션
항상 keep-alive 요청을 따를 필요는 없다. 언제든 요청을 close할 수 있음
- timeout : 커넥션이 얼마나 유지될 것인지를 의미
- max : 커넥션이 몇 개의 트랜잭션을 처리할 때까지 유지될 것인지를 의미
Connection: Keep-Alive Keep-Alive: max=5, timeout=120 # 5개의 트랜잭션이 처리될 때까지 or 120초 동안 유지
Keep-Alive 커넥션 제한과 규칙
- keep-alive 커넥션을 사용하기 위해서는 Connection: Keep-Alive 요청 헤더를 전송해야함
- Connection: Keep-Alive 헤더를 모든 메시지에 포함시켜야 계속 유지됨
- Connection: Keep-Alive 헤더가 없으면 트랜잭션 후 커넥션을 종료
- 엔티티 본문이 정확한 Content-Length 값과 함께 multipart media type을 가지거나 chunked transfer encoding으로 인코드 되어야함
- 실제보다 짧은 Content-Length가 전달된다면 데이터가 다 전달되지도 않았는데 커넥션이 끊어지는 문제가 발생하지 않을까?
- 중개 서버(프락스, 게이트웨이)는 전달하거나 캐싱하기 전 Connection 헤더에 명시된 모든 헤더 필드와 Connection 헤더를 제거해야함
- keep-alive 커넥션은 Connection 헤더를 인식하지 못하는 프락시 서버와는 맺어지면 안됨
- 기술적으로 HTTP/1.0을 따르는 기기로부터 받는 모든 Connection 헤더 필드는 무시해야함 ==> 뒤에 나오는 멍청한 프락시와 연결되는 내용
- hang : 프로그램이 수행 중 멈추게 된 상황을 의미
- 클라이언트는 응답을 모두 받기 전에 커넥션이 끊어지면 요청을 다시 보낼 수 있게 준비되어 있어야함
Keep-Alive와 멍청한 프락시
Proxy는 Connection 헤더를 이해하지 못한다. 따라서 이에 대한 처리 없이 요청 그대로를 다음으로 전달한다.
클라이언트-프락시-서버로 구성되어 있다고 가정해보자.
먼저, 클라이언트가 keep-alive 커넥션을 사용하기 위해 Connection 헤더를 프락시로 전달한다.
Connection: Keep-Alive
프락시 : 이게 뭐야?? 모르니까 다음 Hop으로 넘기자
여기서 문제가 발생하는데 서버는 프락시로부터 Connection 헤더를 받고 프락시가 커넥션을 유지하기를 요청했다고 판단하여 프락시와의 커넥션을 유지한다.
하지만, 프락시는 keep-alive에 대해 알지못하므로 응답으로 반환된 Connection 헤더를 클라이언트에게 전달하고 서버와의 커넥션이 끊어지기를 대기한다.
서버는 프락시와의 커넥션을 끊지 않기 때문에 여기서 Hang이 발생한다. 커넥션 종료를 대기하는 동안 프락시는 클라이언트의 요청을 모두 무시한다.
결국 Timeout이 발생할 것이다!
홉별 헤더는 절대로 전달해선 안된다!!
Proxy-Connection
모든 헤더를 무조건 전달하는 멍청한 프락시의 문제를 해결하기 위해 Proxy-Connection헤더를 사용한다.
- 이는 확장 헤더로써 비표준
멍청한 프락시가 Proxy-Connection 헤더를 그대로 서버로 전달하더라도 서버는 이를 무시한다.
영리한 프락시라면 Proxy-Connection 헤더를 보고 클라이언트의 keep-alive 요청은 인지하여 이를 Connection: Keep-Alive로 변환하여 서버로 전달한다.
- 서버와 영리한 프락시 사이에 Keep-Alive 커넥션이 유지
그러나 영리한 프락시 옆에 멍청한 프락시가 존재한다면 다시 문제가 발생
- 앞선 멍청한 프락시에서의 문제와 동일한 문제가 발생한다.
문제를 발생하는 프락시는 기저에 있어 보이지 않는 경우가 많기 때문에 애플리케이션들이 지속 커넥션을 명확히 구현하는 것이 중요하다.
HTTP/1.1의 지속 커넥션
HTTP/1.1에서 별도 설정을 하지 않으면 모든 커넥션을 지속 커넥션으로 취급한다. keep-alive 커넥션을 지원하지 않음
트랜잭션 이후 커넥션을 종료하려면 헤더에 Connection: close를 명시해야한다!!!
응답에 Connection: close 헤더가 없다면 커넥션을 유지하자는 것으로 인지하면 된다.
지속 커넥션의 제한과 규칙
- 클라이언트가 요청에 Connection: close 헤더를 포함해 보냈으면 그 커넥션으로 추가적인 요청을 보낼 수 없다.
- 클라이언트가 해당 커넥션으로 추가적인 요청을 보내지 않을 것이라면 마지막 요청에 Connection: close 헤더를 보내야한다.
- 기본이 지속 커넥션이기 때문에 커넥션을 유지하고 있어 메모리 낭비
- 커넥션의 모든 메시지가 자신의 길이 정보를 정확히 가지고 있을 때만 커넥션을 지속시킬 수 있다.
- HTTP/1.1 프락시는 클라이언트와 서버 각각에 별도의 지속 커넥션을 맺고 관리해야한다.
- 오래된 프락시가 Connection 헤더를 전달하는 문제가 발생할 수 있기 때문에 HTTP/1.1 프락시는 커넥션 관련 기능에 대한 지원 범위를 알고 있을 때만 지속 커넥션을 맺어야한다.
- 지원하지 않은 Connection : Keep-Alive를 전달하는 문제
- HTTP/1.1 기기는 Connection 헤더의 값과 상관없이 언제든지 커넥션을 끊을 수 있다.
- HTTP/1.1 애플리케이션은 중간에 끊어지는 커넥션을 복구할 수 있어야한다. 다시 보내도 문제가 없는 요청이라면 가능한 다시 보내야한다.
- 클라이언트가 응답을 받기 전에 커넥션이 끊어질 상황을 대비해 요청을 다시 보낼 준비가 되어있어야한다.
- 클라이언트는 서버의 과부하 방지를 위해 넉넉잡아 두개의 지속 커넥션을 유지해야한다. N명의 사용자가 서버로 접근하려한다면, 프락시는 약 2N개의 커넥션을 유지해야한다.
- 커넥션을 생성할 때의 서버 부하를 막기위해 2개 정도의 지속 커넥션을 잡아두고 유지시킨다는 의미라고 생각함
파이프라인 커넥션
여러 개의 요청은 큐에 쌓이고 하나의 요청이 전달되면 바로 다음 요청이 전송된다. 하나의 요청에 대한 응답을 대기하지 않아 성능을 높일 수 있다.
즉, 커넥션에 파이프라인을 설치하여 여러번 호출이 가능하게 만드는 것!!
제약 사항
- HTTP 클라이언트는 커넥션이 지속 커넥션인지를 확인한 후에 파이프라인 커넥션을 사용해야함
- HTTP 응답은 요청 순서와 동일하게 반환되어야함
- TCP 패킷처럼 순서를 보장할 수 있는 장치가 없기 때문
- HTTP 클라이언트는 커넥션이 끊어지더라도 완료되지 않은 요청을 바로 다시 전달할 준비가 되어 있어야한다.
- HTTP 클라이언트는 서버에 변화가 생기는 POST같은 요청을 파이프라인을 통해 보내선 안된다.
- GET과 같이 단순 조회를 하는 요청은 서버에 변화를 주지 않으므로 반복해서 보내도 문제가 생기지 않음
커넥션 끊기에 대한 미스테리
커넥션 관리에 대한 명확한 기준은 없다.
마음대로 커넥션 끊기
앞서 언제든지 커넥션을 끊을 수 있다고 언급하였다.
대부분은 메시지 전송이 완료된 후에 끊지만 에러가 있으면 중간에서 끊길 수 있다.
또한 HTTP 애플리케이션에서 임의로 지속 커넥션을 끊을 수 있다.
만약 커넥션을 끊는 시점에 클라이언트가 요청을 전송하고 있다면? 문제가 발생
Content-Length와 Truncation
이 부분도 앞서 언급되었던 부분인데 반복되어서 나온다.
본문에 대한 정확한 Content-Length를 가져야한다.
실제 엔티티의 길이와 Content-Length의 값이 일치하지 않거나 존재하지 않으면 다시 서버에게 물어봐야한다.
이 경우 수신자가 캐시 프락시라면 응답을 캐싱하면 안된다. ==> 잠재적으로 오류가 발생할 수 있기 때문에
캐시 프록시
캐시 프록시 서버를 서버 앞단에 두어 자주 찾는 데이터를 빠르게 반환할 수 있다.
예시로 설명을 해보자면
- 앞서 HTTP 통신의 성능은 물리적인 거리에 의해서도 차이가 난다고 언급하였었다.
- 만약 위와 같은 상황에서 서버가 지구 반대편 어딘가에 있을 때와 집 근처에 있는 것 중 어느 것이 더 빠르게 응답할까?
- 당연히 서버가 가까운 경우에 더 빠르게 응답한다.
- 이러한 성질을 사용해 적용시킨 것이 캐시 프록시 서버이다.
- 핵심 아이디어는 캐시 프록시 서버를 우리집 근처에 두어 데이터를 캐싱하여 속도를 높인다는 것!!
- 클라이언트의 요청은 먼저 캐시 프록시 서버로 전달된다.
- 만약 캐시 프록시 서버에 클라이언트가 찾는 데이터가 존재한다면 바로 반환된다.
- 존재하지 않는다면 원서버로 요청이 전달되는 것이다.
실제로 넷플릭스, 유튜브가 이런 방식으로 캐시 프록시 서버를 우리나라에 설치해두고 원서버는 미국에 위치한 상태에서 서비스를 제공한다고 한다.
커넥션 끊기의 허용, 재시도, 멱등성
이전에 내용과 동일하게 반복되는 내용이다.
커넥션은 언제든지 끊을 수 있으며 예상치 못하게 끊어졌을 시에는 적절한 대응(다시 요청을 전송한다든지?)을 할 준비가 되어 있어야한다.
멱등성(GET, HEAD, PUT, DELETE, TRACE, OPTIONS) 요청은 반복적으로 아무런 영향을 미치지 않기 때문에 파이프라인을 통해 요청해도 괜찮다.
하지만 비멱등성(POST, PATCH, CONNECT) 요청은 파이프라인을 통해 요청하면 안된다.
- 파이프라인은 응답을 기다리지 않고 요청하기 때문이다.
비멱등성 요청은 이전 요청에 대한 응답을 받을 때까지 기다린 후에 다시 요청을 보내야한다.
- 캐시된 POST 요청 페이지를 다시 로드하려할 때 이와 같은 대화상자를 띄운다.
우아한 커넥션 끊기
TCP 커넥션은 양방향!!
close()는 입력, 출력 채널을 모두 끊는다.(전체 끊기)
shutdown()은 입력, 출력 채널 중 하나를 개별적으로 끊는다.(절반 끊기)
커넥션의 출력채널(서버에서 클라이언트로 가는 채널)을 끊는 것이 가장 안전하다.
입력채널을 끊은 상태에서 클라이언트가 서버로 요청을 보내면 서버의 OS는 connection reset by peer 메시지를 클라이언트로 전송한다.
- 더 자세히 말해보자면, 연결이 끊어졌다는 RST 패킷을 원격 서버에서 클라이언트로 전송한다.
이는 심각한 에러로 취급되어 버퍼에 저장된 아직 읽히지 않은 데이터 모두를 삭제한다.
책에서 언급한 바와 같이 파이프라인을 사용할 때 10개의 요청이 정상 처리되어 버퍼에 담겨있고 11번째 요청을 보낼 때 서
버에서 입력 채널을 끊어 RST 패킷이 반환되었다면 버퍼에 담긴 모든 데이터는 지워진다.
우아하게 커넥션을 끊어라!
애플리케이션(서버)의 출력 채널을 먼저 끊고 반대쪽의 출력채널이 끊기기를 기다리자!
양쪽에서 더는 데이터를 전송하지 않을 것이라고 알려주면 커넥션은 온전히 종료된다.
이 방식을 사용하면 리셋(RST 패킷 반환)의 위험이 없어진다.
양쪽에서 절반 끊기를 구현했다는 것과 이를 검사해준다는 보장이 없기 때문에 애플리케이션은 출력 채널을 끊은 후 입력 채널에 대한 상태 검사를 주기적으로 해야한다.
Timeout 내에 끊어지지 않으면 강제로 끊을 수도 있다.
참고 및 출처: https://minchul-son.tistory.com/541
'공부' 카테고리의 다른 글
Edge Computing (FeConf 2022) (0) | 2022.09.02 |
---|---|
HTTP 2.0 (0) | 2022.08.15 |
HTTP 리다이렉션과 부하균형 (0) | 2022.07.31 |
HTTP 쿠키 (0) | 2022.07.17 |
웹 서버 (0) | 2022.07.10 |