CHAPTER3 - 웹을 파헤치는 이유와 방법 -1

2019. 2. 25. 15:35Learning HTTP2


(비교적) 오래된 프로토콜을 사용해 현대 웹 페이지를 빠르게 전송하는 일은 줄타기 곡예를 하는 것과 비슷하다. 웹 성능 기술자의 전문 역량으로만 이러한 웹 페이지의 성능을 유지해오고 있는 셈이다. 오라일리의 벨로시티 콘퍼런스 시리즈가 생겨난 것도 부분적으로는 이 낡은 프로토콜을 퇴대한 활용하는 다양한 기법과 팁을 공유하려는 사람들 때문이라고 할 수 있다. 우리가 가고 있는 곳(즉, HTTP/2)을 이해하려면, 우리가 현재 있는 곳, 우리가 직면한 문제, 우리가 현재 그 문제를 다루고 있는 방법을 먼저 이해하는 것이 중요하다.


3.1 오늘날의 성능 문제

현대의 웹 페이지나 웹 애플리케이션을 전송하는 일은 결코 간단한 문제가 아니다. 페이지 내 수백 개의 개체, 수천 개의 도메인, 변동이 심한 네트워크, 광범위한 디바이스 기능이 존재하는 환경에서 일관되고 빠른 웹 경험을 만들어 내는 것은 쉬운 일이 아니다. 웹 페이지를 가져와 렌더링하는 데 필요한 여러 단계뿐 아니라 단계마다 내재된 문제를 이해하는 것은 웹사이트와 상호 작용하는 사용자를 불편하지 않게 하는 데 가장 중요한 부분이다. 또한 HTTP/2를 만든 이유를 이해하는 데 필요한 통찰력을 가장 주용한 부분이다. 또한 HTTP/2를 만든 이유를 이해하는 데 필요한 통찰력을 얻고, HTTP/2의 상대적 장점을 평가할 수 있을 것이다.


3.1.1 웹페이지 요청의 구조

깊이 들어가기 전에 먼저, 우리가 최적화하려는 대상, 특히 사용자가 웹 브라우저에서 링크를 클릭한 때부터 화면에 웹 페이지가 표시될 때까지 일어나는 일을 기본적으로 이해하는 것이 중요하다. 브라누저는 웹 페이지를 요청할 때, 화면에 페이지를 표시하는 데 필요한 모든 정보를 가져오기 위해 반복적인 절차를 거친다. 이는 개체를 반입하는 로직과 페이지를 파싱/렌더링하는 로직, 두 부분으로 나누어 생각하는 게 더 쉽다. 반입 로직 먼저 살펴보자.

[그림 3-1]은 이 절차의 구성요소를 보여준다.


그림 3-1 개체 요청/반입 흐름도


위 흐름도를 단계별로 살펴보자.

 1 가져올 URL을 대기열에 넣는다.

 2 URL 내의 호스트이름의 IP 주소를 조회한다(A).

 3 호스트로 TCP 연결을 연다(B).

 4 요청이 HTTPS라면, TLS 핸드셰이크를 완료한다(C).

 5 기준 페이지 URL에 대한 요청을 전송한다.


[그림 3-2]는 응답을 수신하여 페이지를 렌더링하는 절차를 보여준다.


그림 3-2 개체 응답/페이지 렌더링 흐름도


계속해서 흐름도를 살펴보자.

 6 응답을 수신한다.

 7 기준 HTML이라면, HTML을 파싱하여 우선순위에 따라 페이지의 개체들의 반입을 시작한다(A).

 8 페이지의 필수 개체를 수신했다면, 화면 렌더링을 시작한다(B).

 9 추가 개체를 수신하면, 끝날 때까지 파싱과 렌더링을 계속한다(C).


앞의 절차는 페이지를 클릭할 때마다 반복되어야 한다. 이 반복적인 절차는 네트워크와 디바이스 자원에 부담을 준다. 이 중 어느 단계든 최적화하거나 제거하는 일이 웹 성능 튜닝이라는 예술에 핵심적인 부분이다.


3.1.2 중요 성능

앞의 두 그림에서 웹 성능에 중요한 부분과 앞으로 집중적으로 살펴봐야 할 부분을 이끌어낼 수 있다. 먼저 웹페이지를 불러오는 데 전반적인 영향을 미치는 네트워크 수준의 지표부터 시작해보다.

  • 지연 시간
    지연 시간이란 IP 패킷이 한 지점에서 다른 지점으로 이동하는 데 걸리는 시간을 말한다. 이와 관련된 것으로 왕복 시간 이라는 것이 있으며, 지연 시간의 2배를 의미한다. 지연 시간은 성능의 주요 병목점이며, 서버까지 많은 왕복이 이루어지는 HTTP와 같은 프로토콜에서 특히 더 그러하다.

  • 대역폭
    두 지점 사이의 연결은 포화 상태가 되기 직전까지의 데이터양만 동시에 처리할 수 있다. 웹 페이지의 데이터양과 연결의 용량에 따라 대역폭이 성능의 병목점이 될 수도 있다.

  • DNS 조회
    클라이언트가 웹 페이지를 가져올 수 있으려면 인터넷의 전화번호부(이 비유를 이해하는 독자들이 있을 것이다)인 DNS를 사용해 호스트이름을 IP 주소로 변환해야 한다. 이 절차는 가져온 HTML 페이지에 있는 모든 고유한 호스트 이름에 대해 이루어져야 하며, 다행히 호스트이름당 한 번만 하면 된다.

  • 연결 시간
    연결을 수립하려면 클라이언트와 서버 사이에 '3방향 핸드셰이크'라는 메시지 주고받기(왕복)이 필요하다. 이 핸드셰이크 시간은 보통 클라이언트와 서버 사이의 지연 시간과 관련이 있다. 핸드셰이크를 위해서는, 클라이언트가 서버로 SYN 패킷을 전송하고, 서버는 그 SYN에 대한 서버의 ACK와 SYN 패킷을 클라이언트로 전송하며, 클라이언트는 다시 SYN에 대한 ACK를 서버로 전송한다.[그림 3-3]을 보라.

  • TLS 협상 시간
    클라이언트가 HTTPS 연결을 하고 있다면, SSL의 후속 프로토콜인 TLS 협상이 필요하다. 이 때문에 서버와 클라이언트의 처리 시간에 왕복 시간이 더 추가된다.


그림 3-3 TCP 3방향 핸드셰이크


이 시점에 클라이언트는 아직 요청을 보내지도 않았으며, DNS 왕복 시간과 TCP와 TLS를 위한 추가 시간이 이미 소요되었다. 다음으로, 네트워크보다는 서버 자체의 내용이나 성능에 좀 더 의존적인 지표를 살펴보자.

  • TTFB
    TTFB는 클라이언트가 웹 페이지 탐색을 시작한 때부터 기준 페이지 응답의 첫 번째 바이트를 수신한 때까지 걸린 시간을 측정한 것이다. 이것은 서버의 처리 시간뿐만 아니라 앞서 소개한 여러 지표를 합한 값이다. 한 페이지에 여러 개체가 있는 경우, TTFB는 브라우저가 요청을 전송한 시점부터 첫 번째 바이트가 되돌아온 시점까지의 시간을 측정한다.

  • 콘텐츠 다운로드 시간
    이것은 요청한 개체에 대한 TTLB다.

  • 렌더링 시작 시간
    클라이언트가 사용자를 위해 얼마나 빨리 화면에 무언가를 표시할 수 있는가? 이것은 사용자가 얼마나 오랫동안 빈 페이지를 바라보았는지를 측정한 것이다.

  • 문서 완성 시간(또는 페이지 로딩 시간)
    이것은 클라이언트가 페이지 표시를 완료한 시간이다.


웹 성능을 들여다 볼 때, 특히 더 빠르게 동작하는 새로운 프로토콜을 만드는 것이 목적이라면 이 지표들을 반드시 염두에 두어야 한다. HTTP/1.1이 직면한 문제들과 다른 무언가가 필요한 이유를 논의할 때 이들을 다시 참조할 것이다.

이 지표들 외에도, 인터넷은 성능상의 병목을 유발하는 요소들이 갈수록 증가하고 있다. 다음은 이러한 증가하는 요소들 중 기억해두어야 할 몇가지다.

  • 바이트 수의 증가
    매년 페이지 크기, 이미지 크기, 자바스크립트와 CSS의 크기가 증가하고 있음은 지명한 사실이다. 크기가 커진다는 것은 내려 받을 바이트 수가 더 많아지고 페이지 로딩 시간이 더 오래 걸린다는 것을 의한다.

  • 개체 수의 증가
    개체는 크기가 커지기보다는 수가 더 많아지고 있다. 더 많은 개체 수는 모든 것을 가져와 처리하는 데 전체적으로 오랜 시간이 걸리게 한다.

  • 복잡도의 증가
    더 많고 풍부한 기능을 추가할수록 페이지와 그 종속 개체들은 점점 더 복잡해진다. 복잡해질수록 페이지를 계산하고 렌저링하는 시간이 늘어나며, 처리 능력이 떨어지는 모바일 디바이스에서 측히 더 그러하다.

  • 호스트 수의 증가
    웹 페이지는 개별 호스트에서 가져온 것이 아닐뿐더러, 대부분의 페이지는 수많은 참조 호스트가 있다. 각 호스트이름은 추가적인 DNS 조회 시간, 연결 시간, TLS 협상 시간을 의미한다.

  • TCP 소켓 수의 증가
    이러한 증가하는 요소들을 해결하려고, 클라이언트는 호스트마다 여러 개의 소켓을 연다. 이는 호스트당 연결 협상 오버헤드를 증가시키고, 디바이스이ㅡ 부하를 가중시키며, 잠재적으로 네트워크 연결 과부하를 일으켜, 재전송과 버퍼블로트로 인한 실효 대역폭 저하를 유발한다.

3.1.3 HTTP/1의 문제점
HTTP/1은 우리를 현재의 휍 환경으로 이끌어주었지만 그 설계상의 한계로 현대 웹의 요구를 충분히 충족시키지 못하고 있다. 다음은 HTTP/1 프로토콜이 가진 중요한 문제이자 결국 HTTP/2가 설계적으로 해결한 핵심 문제들이다.

 
NOTE_ HTTP/1이라는 것은 없다. 여기서는 HTTP/(h1)을 HTTP/1.0(RFC 1945)과 HTTP/1.1(RFC 2616)의 통칭으로 사용한다.


HOL 블로킹

브라우저는 특정 호스트에서 단 하나의 개체만 가져오라고 하지 않는다. 브라우저는 대게 한 번에 많은 개체를 가져오라고 한다. 특정 도메인에 모든 이미지를 넣어둔 웹사이트 하나를 생각해보자. HTTP/1은 그 이미지들을 동시에 요청하는 어떠한 매커니즘도 제공하지 않는다. 단일 연결상에서 브라우저는 요청 하나를 보내고 그 응답을 수신한 후에야 또 다른 요청을 보낼 수 있다. h1 은 브라우저가 많은 요청을 한 번에 보낼 수 있게 해주는 파이프라이닝이라는 기능이 있지만, 브라우저는 여전히 전송된 순서대로 하나씩 응답을 수신한다. 추가로, 파이프라이닝은 상호 운용성과 배포 측면에서 사용하기 어렵게 하는 여러 문제가 있다.

이러한 여러 요청이나 응답 중 어디엔가 문제가 발생하면 그 요청/응답을 뒤따르는 다은 모든 것들은 막혀버린다. 이 현상을 HOL 블로킹이라고 한다. 이 때문에 웹페이지에 전송광 렌더링이 중단될 수 있다. 요즘 브라우저는 특정 호스트에 최대 6개의 연결을 열고 각 연결로 요청을 전송해 어느 정도 병렬 처리가 가능하다. 하지만 각 연결은 여전히 HOL 블로킹의 영향을 받을 수 있다. 게다가 이는 제한된 디바이스 자원을 적절히 사용하는 방법이 아니다. 다음 절에서 그 이유를 설명한다.


TCP의 비효율적 사용

TCP는 보수적인 환경을 가정하고 네트워크상의 다양한 트래픽 용도에 공평하게 동작하도록 설계되었다. TCP의 혼잡 회피 메커니즘은 최악의 네트워크 상태에서 동작하도록 만들어졌고, 경쟁적인 요구가 있는 환경에서 비교적 공평하게 동작한다. 이것은 TCP가 성공적이었던 이유 중 하나로, TCP가 데이터를 전송하는 가장 빠른 방법이라기보다는 가상 신뢰성 있는 방법이기 때문이다. 여기에서 핵심은 혼잡 윈도우라는 개념이다. 혼잡 윈도우는 수신자가 확인(ACK)하기 전까지 송신자가 전송할 수 있는 TCP 패킷의 수를 의미한다. 예를 들어, 혼잡 윈도우가 1로 설정되어 있다면, 송신자는 단 하나의 패킷만 전송하며, 그 패킷에 대한 수신자의 확인을 받아야만 또 다른 패킷을 전송할 것이다.


패킷이란 무엇인가?

패킷, 더 구체적으로 IP 패킷은 패킷의 길이, 전송 방법(출발지와 목적지), TCP 통신에 필요한 여러 항목을 정의하고 있는 구조(프레임)로 캡슐화된 bytes의 모음(페이로드)이다. 패킷의 페이로드 하나에 넣을 수 있는 가장 큰 데이터의 크기는 1,460 bytes다. 14,600 bytes의 이미지가 있는가? 그렇다면 그 이미지는 패킷 10개로 나누어질 것이다. 패킷(그리고 이 절에서 소개하는 다른 정보)을 이해하고 나면 인터넷 성능 수치를 들여다 볼 수 있다.
 


한 번에 하나의 패킷만 전송하는 것은 매우 비효율적이다. TCP 현재 연결에 알맞은 혼잡 윈도우의 크기를 결정하기 위한 느린 시작이라는 개념이 있다. 느린 시작의 설계 목적은 새로운 연결이 네트워크의 상태를 감지해 이미 혼잡한 네트워크를 악화시키지 않게 하는 것이다. 으린 시작을 통해, 송신자는 ACK를 수신할 때마다 패킷 수를 늘려서 전송할 수 있다. 이는 새로운 연결에서 첫 번째 ACK를 순신한 다음, 송신자는 두 새의 패킷을 전송할 수 있으며, 스 두 개의 패킷이 확인되면 다시 네 개의 패킷을 전송할 수 있음을 의미한다. 이 기하급수적인 증가는 얼마 되지 않아 프로토콜에 정의된 상한선에 도달하며, 그 시점에 이 연결은 이른바 혼잡 회피 단계로 들어갈 것이다. [그림 3-4]를 보라.


그림 3-4 TCP 혼잡제어(리노 알고리즘)

출처 - https://jwprogramming.tistory.com/36


최적의 혼잡 윈도우 크기를 얻는 데 몇번의 왕복이 필요하다. 또한 성능 문제를 해결하는 데 그 몇 번의 왕복은 중요한 시간이다. 현대의 운영체제는 보통 4에서 10의 초기 혼잡 윈도우 크기를 사용한다. 패킷의 크기가 최대 크기인 약 1,460 bytes라면, 송신자는 5,860 bytes만 전송한 후 ACK를 기다려야 한다. 요즘의 웹 페이지는 HTML과 모든 종속 개체를 포함해 평균 2MB의 데이터가 있다. 이상적인 환경에서 운이 따라 준다면, 이는 페이지를 전송하는데 약 9번의 왕복 시간이 소요될 것임을 의미한다.

게다가 브라우저는 특정 호스트에 대해 보통 6개의 연결을 열고  있으므로, 각 연결마다 이러한 혼잡 제어를 해야 한다.


패킷 계산

앞의 숫자들은 어떻게 나온 것일까? 계산 방법을 이해하면 더 많거나 더 적은 바이트를 전송할 때의 성능 영향도를 추정하는 데 도움이 된다. 왕복 때마다 혼잡 위도우는 2배가 되고 모든 패킷의 크기는 1,460 bytes라고 가정한다. 이상적인 시나리오에서는 다음 표와 유사하게 예측할 수 있다.

왕복 

전송된 패킷 수 

전송된 최대 바이트 수 

전송된 총 바이트 수 

5,840 

5,840 

11,680 

17,520 

16 

23,360 

40,880 

46,720 

87,600 

64 

93,440 

181,040 

128 

186,880 

3,967,920 

256 

373,760 

741,680 

512 

747,520 

1,489,200 

1,024 

1,495,040 

2,984,240 

9번째 왕복 후에야 2MB 데이터가 전송 완료될 것이다. 안탑깝게도 이는 실제 상황을 지나치게 단순화한 것이다. 윈도우 크기가 1,024에 도달하기도 전에 ssthresh라는 임계값에 도달하거나 패킷 손실이 생기게 되며, 둘 중 어느 경우든 이 기하급수적인 증가는 중단될 것이다. 하지만 이 책의 목적에서는 이러한 단순한 접근법이면 충분하다.
 



NOTE_ 전통적인 TCP 구현은 패킷 손실을 피드백 매커니즘으로 사용하는 혼잡 제어 알고리즘을 채택하고 있다. 패킷이 손실되었다고 판단되면 이 알고리즘은 혼잡 윈도우를 줄이도록 반응한다. 이는 어두운 방을 돌아다니면서 정강이가 커피 테이블에 부딪히면 방향을 바꾸는 것과 유사하다. 예상 응답이 제시간 안에 오지 않아 시간이 초과되면 이 알고리즘은 혼잡 윈도우를 완전히 재설정하고 다시 느린 시작에 들어갈 것이다. 최근 알고리즘은 더 나은 피드백 메커니즘을 제동하려고 지연 시간과 같은 다른 요소들도 고려한다.
 

앞서 언급한 바와 같이, h1 다중화를 지원하지 않기 때문에, 브라우저는 특정 호스트에 보통 6개의 연결을 연다. 이는 혼잡 윈도우 널뛰기가 동시에 6번 일어나야 함을 의미한다. TCP는 이 연결들이 함께 잘 동작하게는 해주지만 이들이 최적의 성능을 발휘하도록 보장하지는 못한다.


비대한 메시지 헤더

h1은 요청된 개체를 압축하는 메커니즘을 제공하긴 하지만 메시지 헤더를 압축하는 방법은 없다. 실제로 헤더는 크기가 증가할 수 있다. 응답 패킷에서는 개체 크디 대비 헤더 크디의 비율이 매우 낮지만, 요청 패킷에서는 헤더가 대부분(전체는 아닐지라도)의 바이트를 차지한다. 쿠키가 있는 경우, 요청 헤더의 합이 수 킬로바이트 크기로 커지는 것은 이상한 일이 아니다.

HTTP 아카이브에 따르면, 2016년 말 현재, 요청 헤더의 평균 크기는 약 460 bytes다. 140개의 개체가 있는 일반적인 웹 페이지의 경우, 요청 헤더의 전체 크기는 약 63KB가 된다. TCP 혼잡 윈도우 제어에 관한 논의를 다시 떠올려보면, TCP가 해당 개체에 대한 요청을 보내는 데만 3~4번의 왕복이 필요할 수 있다. 네트워크 지연으로 인한 손해는 빠르게 누적되기 시작한다. 또한, 업로드 대역촉은 보통 네트워크의 제약을 받기 때문에(특히, 모바일 환경인 경우), 혼잡 윈도우의 크기가 처음부터 충분히 커지지 않아 더 많은 왕복을 유발할 수 있다.

헤더 압축 기능이 없기 때문에, 클라이언트가 대역촉 제한에 걸릴 수도 있따. 이는 저대역촉 또는 과밀 링크에서 특히 더 그러하다. 대표적인 예가 바로 '경기장 효과'다 수만 명의 사람들이 동시에 같은 장소에 있을 때(주요 스포츠 경기 등), 모바일 통신 대역폭은 빠르게 소진된다. 헤더를 압축해 요청 크기를 줄이면 이와 같은 상왕에서 도음이 되며 시스템 부하도 전반적으로 줄어들 것이다.


제한적인 운선순위

브라우저가 하나의 호스트에 다수의 소켓(이 소켓들은 각각 HOL 블로킹을 겪는다)을 열고 개체를 요청하기 시작할 때, 그 요청들의 우선순위를 지정하기 위한 옵션은 매우 제한적인데, 그것은 바로 요청을 보내거나 보내지 않거나 둘 중 하나다. 페이지에서 어떤 개체는 다른 개체보다 훨씬 더 중요하다. 브라우저는 높은 우선순위의 개체를 먼저 가져오려고 다른 개체에 대한 요청을 보류하는데, 그러는 동안 우선순위가 낮은 개체들은 줄줄이 대기 상태로 빠지게 된다. 이로 인해 브라우저가 높은 우선순위 항목을 기다리는 동안 서버는 낮은 운선순위 항목을 처리할 기회를 엊디 못해 페이지 다운로드 시간이 전체적으로 길어질 수 있다. 또는, 브라우저가 페이지를 처리하는 방식 때문에, 브라우저가 높은 우선순위 개체를 발견했어도 이미 반입된 낮은 우선순위 항목 뒤에서 높은 우선순위 개체가 막혀버리는 경우도 있다.


서드파티 개체

특별히 HTTP/1의 문제점은 아니지만, 성능 문제로 대두되고 있는 것을 설명할 것이다. 현대 웹 페이지에서 요청되는 것 중에는 웹 서버의 제어 범위에서 완전히 벗어나 있는 것들이 많으며, 이를 서드파티 개체라고 한다. 서드파티 개체의 탐색과 처리는 보통 요즘의 웹 페이지를 불러오는 데 소요되는 시간의 절반을 차지한다. 서드파티 개체가 페이지 성능에 미치는 영향을 최소화하려는 기법이 많이 있다. 하지만 웹 개발자가 직접 제어할 수 있는 범위를 벗어난 콘텐츠가 많은 경우, 그 개체들 중 일부는 성능을 저하 시키고 페이지 렌더링을 지연 또는 중단 시킬 가능성이 있다. 웹 선능에 관한 어떠한 논의도 이 문제를 언급하지 않고는 끝나지 않을 것이다.(미리 알려주는데, h2도 이 문제를 처리할 마법의 해결책은 없다).


서드파티 개체에 치르는 대가

서드파티 개체가 웹 페이지를 얼마나 느리게 할까? 아카마이의 파운드리팀이 수행한 한 연구에 따르면, 서드파티 개체는 보통 전체 페이지 로딩 시간의 절반을 차지할 정도로 그 영향이 매우 크다. 이 연구는 서드파티 개체의 영향도를 추적하기 위한 '3rd party trailing ratio'라는 새로운 지표를 제안했는데, 이 지표는 서드파티 콘텐츠의 반입과 표시가 페이지 렌더링 시간에 영향을 미치는 비율을 측정한 것이다.
 


'Learning HTTP2' 카테고리의 다른 글

CHAPTER5 - HTTP/2 프로토콜-1  (0) 2019.02.28
CHAPTER4 - HTTP/2로의 전환  (0) 2019.02.26
CHAPTER3 - 웹을 파헤치는 이유와 방법 -2  (0) 2019.02.26
CHAPTER2 - HTTP/2 맛보기  (0) 2019.02.25
CHAPTER1 - HTTP의 진화  (0) 2019.02.25