BigPipe가 웹 페이지를 페이지릿으로 분해하고 서버와 브라우저의 처리 단계를 겹쳐 사용자 체감 지연 시간을 크게 줄이는 방식을 설명합니다.
Facebook에서 사이트 속도는 가장 중요한 회사 목표 가운데 하나입니다. 2009년에 우리는 Facebook 사이트를 두 배 더 빠르게 만드는 데 성공했고, 이는 이 게시물에서 소개되었습니다. 우리 엔지니어링 팀의 몇 가지 핵심 혁신이 이를 가능하게 했습니다. 이 블로그 게시물에서는 이러한 뛰어난 기술적 성과를 뒷받침한 비밀 병기 중 하나인 BigPipe를 설명하겠습니다. BigPipe는 동적 웹 페이지 제공 시스템을 근본적으로 다시 설계한 것입니다. 일반적인 아이디어는 웹 페이지를 페이지릿(pagelet)이라고 부르는 작은 조각으로 분해하고, 이를 웹 서버와 브라우저 내부의 여러 실행 단계에 걸쳐 파이프라인으로 처리하는 것입니다. 이는 대부분의 현대식 마이크로프로세서가 수행하는 파이프라이닝과 유사합니다. 여러 명령어를 프로세서의 서로 다른 실행 유닛을 통해 파이프라인 처리하여 최고의 성능을 달성하는 방식입니다. BigPipe는 기존 웹 제공 프로세스를 근본적으로 재설계한 것이지만, 기존 웹 브라우저나 서버를 변경할 필요는 없습니다. 전적으로 PHP와 JavaScript로 구현됩니다.
BigPipe를 이해하려면, 기존의 동적 웹 페이지 제공 시스템이 가진 문제를 살펴보는 것이 도움이 됩니다. 이 시스템은 World Wide Web 초창기로 거슬러 올라가며, 그 이후로 크게 바뀌지 않았습니다. 현대 웹사이트는 10년 전보다 훨씬 더 동적이고 상호작용적으로 변했지만, 전통적인 페이지 제공 모델은 오늘날 인터넷의 속도 요구 사항을 따라가지 못했습니다. 전통적인 모델에서 사용자 요청의 생명 주기는 다음과 같습니다:
전통적인 모델은 현대 웹사이트에 매우 비효율적입니다. 시스템의 많은 작업이 순차적이어서 서로 겹쳐서 수행할 수 없기 때문입니다. JavaScript 다운로드 지연, 리소스 다운로드 병렬화 등과 같은 몇몇 최적화 기법은 이러한 한계를 일부 극복하기 위해 웹 커뮤니티에서 널리 채택되어 왔습니다. 그러나 이러한 최적화 가운데 웹 서버와 브라우저가 순차적으로 실행되면서 발생하는 병목을 건드리는 경우는 거의 없습니다. 웹 서버가 페이지를 생성하느라 바쁠 때 브라우저는 아무 일도 하지 않은 채 유휴 상태로 사이클을 낭비합니다. 웹 서버가 페이지 생성을 마치고 이를 브라우저로 보내면, 이번에는 브라우저가 성능 병목이 되고 웹 서버는 더 이상 도울 수 없습니다. 웹 서버의 생성 시간과 브라우저의 렌더링 시간을 겹치게 하면 종단 간 지연 시간을 줄일 수 있을 뿐만 아니라, 웹 페이지의 초기 응답을 사용자가 훨씬 더 일찍 볼 수 있게 되어 사용자 체감 지연 시간을 크게 줄일 수 있습니다. 웹 서버 생성 시간과 브라우저 렌더링 시간을 겹치는 것은 Facebook과 같은 콘텐츠 중심 웹사이트에서 특히 유용합니다. 일반적인 Facebook 페이지에는 친구 목록, 새 피드, 광고 등 다양한 데이터 소스의 데이터가 포함됩니다. 전통적인 페이지 렌더링 모델에서는 최종 문서를 구성하고 사용자의 컴퓨터로 보내기 전에 이러한 질의가 모두 데이터를 반환할 때까지 사용자가 기다려야 합니다. 하나의 느린 질의가 나머지 모든 것을 붙잡아 두는 셈입니다.
웹 서버와 브라우저 사이의 병렬성을 활용하기 위해, BigPipe는 먼저 웹 페이지를 페이지릿이라고 하는 여러 조각으로 나눕니다. 파이프라이닝 마이크로프로세서가 명령어의 생명 주기를 여러 단계(예: “instruction fetch”, “instruction decode”, “execution”, “register write back” 등)로 나누는 것처럼, BigPipe는 페이지 생성 과정을 여러 단계로 나눕니다:
처음 세 단계는 웹 서버에서 실행되고, 마지막 네 단계는 브라우저에서 실행됩니다. 각 페이지릿은 이 모든 단계를 순차적으로 거쳐야 하지만, BigPipe는 여러 페이지릿이 서로 다른 단계에서 동시에 실행되도록 합니다.
위 그림은 Facebook의 홈 페이지를 예로 들어 웹 페이지가 어떻게 페이지릿으로 분해되는지 보여줍니다. 홈 페이지는 “composer pagelet”, “navigation pagelet”, “news feed pagelet”, “request box pagelet”, “ads pagelet”, “friend suggestion box”, “connection box” 등 여러 페이지릿으로 구성됩니다. 각각은 서로 독립적입니다. “navigation pagelet”이 사용자에게 표시될 때도 “news feed pagelet”은 여전히 서버에서 생성 중일 수 있습니다. BigPipe에서 사용자 요청의 생명 주기는 다음과 같습니다: 브라우저가 웹 서버에 HTTP 요청을 보냅니다. HTTP 요청을 받고 이에 대해 몇 가지 기본적인 유효성 검사를 수행한 후, 웹 서버는 즉시 HTML 태그와 태그의 첫 부분을 포함하는 닫히지 않은 HTML 문서를 되돌려 보냅니다. 이 태그에는 나중에 수신될 페이지릿 응답을 해석하기 위한 BigPipe의 JavaScript 라이브러리가 포함됩니다. 이 태그 안에는 페이지의 논리적 구조와 페이지릿용 플레이스홀더를 지정하는 템플릿이 있습니다. 예를 들면 다음과 같습니다: <p><div id=”left_column”> <div id=”pagelet_navigation”></p></p><div id=”middle_column”> <div id=”pagelet_composer”></p><div id=”pagelet_stream”></p></p><div id=”right_column”> <div id=”pagelet_pymk”></p><div id=”pagelet_ads”></p><div id=”pagelet_connect”></p></p></p> 첫 번째 응답을 클라이언트로 플러시한 후, 웹 서버는 계속해서 페이지릿을 하나씩 생성합니다. 페이지릿이 생성되는 즉시, 해당 응답은 그 페이지릿에 필요한 모든 CSS, JavaScript 리소스와 HTML 콘텐츠, 그리고 일부 메타데이터를 포함한 JSON 인코딩 객체 형태로 즉시 클라이언트에 플러시됩니다. 예를 들면 다음과 같습니다: <script type="text/javascript"> big_pipe.onPageletArrive({id: “pagelet_composer”, content=<HTML>, css=[..], js=[..], …}) </script> 클라이언트 측에서는 “onPageletArrive” 호출을 통해 페이지릿 응답을 받으면, BigPipe의 JavaScript 라이브러리가 먼저 해당 CSS 리소스를 다운로드합니다. CSS 리소스 다운로드가 끝나면, BigPipe는 해당 플레이스홀더 div의 innerHTML을 페이지릿의 HTML 마크업으로 설정하여 페이지릿을 표시합니다. 여러 페이지릿의 CSS는 동시에 다운로드될 수 있으며, 어느 쪽 CSS 다운로드가 더 빨리 끝나는지에 따라 순서와 상관없이 표시될 수 있습니다. BigPipe에서는 JavaScript 리소스의 우선순위가 CSS와 페이지 콘텐츠보다 낮게 설정됩니다. 따라서 BigPipe는 페이지의 모든 페이지릿이 표시될 때까지 어떤 페이지릿에 대해서도 JavaScript 다운로드를 시작하지 않습니다. 그 이후에는 모든 페이지릿의 JavaScript가 비동기적으로 다운로드됩니다. 그런 다음 페이지릿의 JavaScript 초기화 코드는 어느 쪽 JavaScript 다운로드가 더 빨리 끝나는지에 따라 순서와 상관없이 실행됩니다. 이처럼 고도로 병렬화된 시스템의 최종 결과는 여러 페이지릿이 서로 다른 단계에서 동시에 실행된다는 것입니다. 예를 들어, 브라우저는 세 개의 페이지릿에 대한 CSS 리소스를 다운로드하는 동시에 다른 페이지릿의 콘텐츠를 렌더링할 수 있으며, 그 사이 서버는 또 다른 페이지릿의 응답을 계속 생성하고 있을 수 있습니다. 사용자 관점에서 보면 페이지는 점진적으로 렌더링됩니다. 초기 페이지 콘텐츠가 훨씬 더 일찍 보이게 되므로 페이지의 사용자 체감 지연 시간이 극적으로 개선됩니다. 직접 차이를 확인하려면 다음 링크를 사용해 볼 수 있습니다: Traditional model 및 BigPipe. 첫 번째 링크는 전통적인 단일 플러시 모델로 페이지를 렌더링합니다. 두 번째 링크는 BigPipe의 파이프라인 모델로 페이지를 렌더링합니다. 브라우저 버전이 오래되었거나, 네트워크 속도가 느리거나, 브라우저 캐시가 워밍되지 않은 상태라면 두 페이지의 로드 시간 차이는 훨씬 더 크게 나타날 것입니다.
아래 그래프는 전통적인 모델과 BigPipe에서 페이지의 가장 중요한 콘텐츠(예: Facebook 홈 페이지에서는 뉴스 피드가 가장 중요한 콘텐츠로 간주됨)를 보는 데 걸리는 75백분위 사용자 체감 지연 시간을 비교한 성능 데이터를 보여줍니다. 이 데이터는 차가운 브라우저 캐시 상태의 브라우저를 사용해 Facebook 홈 페이지를 50번 로드하여 수집되었습니다. 그래프는 BigPipe가 대부분의 브라우저에서 사용자 체감 지연 시간을 절반으로 줄인다는 것을 보여줍니다.
주목할 만한 점은 BigPipe가 파이프라이닝 마이크로프로세서에서 영감을 받았다는 것입니다. 그러나 이들이 수행하는 파이프라이닝 사이에는 몇 가지 차이점이 있습니다. 예를 들어 BigPipe의 대부분 단계는 한 번에 하나의 페이지릿만 처리할 수 있지만, CSS 다운로드나 JavaScript 다운로드와 같은 일부 단계는 여러 페이지릿을 동시에 처리할 수 있으며, 이는 슈퍼스칼라 마이크로프로세서와 유사합니다. 또 다른 중요한 차이점은 BigPipe에서 병렬 프로그래밍에서 빌려온 ‘배리어’ 개념을 구현했다는 점입니다. 여기서는 어떤 페이지릿 하나라도 JavaScript를 다운로드하고 실행하는 다음 단계로 나아가기 전에 모든 페이지릿이 특정 단계, 예를 들어 페이지릿 표시 단계를 마쳐야 합니다. Facebook에서는 고정관념을 벗어난 사고를 장려합니다. 우리는 사이트를 더 빠르게 만들기 위한 새로운 기술을 끊임없이 혁신하고 있습니다.
Changhao Jiang은 Facebook의 Research Scientist이며, 혁신적인 방법으로 사이트를 더 빠르게 만드는 일을 즐깁니다.