brunch

You can make anything
by writing

C.S.Lewis

by 손주식 Jun 02. 2019

Airbnb의 이벤트 로그 처리

Tech Review of Airbnb #5

세계 최대 규모의 숙박 공유 서비스인 Airbnb의 기술 리뷰 다섯 번째 글입니다. Spark Streaming 기반으로 대용량 이벤트 로그를 처리하는 방법에 대한 Airbnb의 기술 블로그 포스트를 번역 및 의역하였습니다. 검은색 글은 원문의 번역 및 의역이고, 파란색 글은 번역자의 의견입니다.

원문 : https://medium.com/airbnb-engineering/scaling-spark-streaming-for-logging-event-ingestion-4a03141d135d 


Airbnb의 이벤트 로그 수집

이벤트 로그는 모바일 앱과 웹 브라우저와 같은 클라이언트와 온라인 서비스에서 발생하는데, 행동과 운영에 대한 핵심 정보들을 포함하고 있다. 각 이벤트는 구체적인 정보의 조각들을 말한다. 예를 들어, 어떤 고객이 Airbnb.com에서 말리부의 해변 집을 검색했다면, 위치 정보와 체크인, 체크아웃 날짜 등을 포함한 검색 이벤트가 발생한다. (그리고 개인정보 보호를 위해 익명화된다.)


 이벤트 로그는 Airbnb에서 게스트와 호스트를 이해하고 그들에게 더 나은 경험을 제공하기 위해서 매우 중요하다. 이벤트 로그는 사업 관점에서의 의사 결정, 그리고 검색, 실험, 결제 등의 기술 제품 개발 방향 결정을 위한 정보를 제공해준다. 한 예로, 이벤트 로그는 검색 결과 랭킹을 만들기 위한 머신 러닝 모델 학습을 위한 주요 재료가 된다. 


이벤트 로그는 거의 실시간으로 데이터 웨어하우스로 수집되고, 많은 ETL과 분석 작업의 재료로 제공된다. 이벤트들은 먼저 클라이언트와 서비스에서 Kafka로 발행된다. Airbnb의 Streaming 처리 프레임워크인 Airstream 기반으로 생성된 하나의 Spark Streaming 작업은 계속해서 Kafka로부터 데이터를 읽고, 중복제거를 위해 HBase에 기록한다. 마지막으로, 이벤트들은 매 시간 HBase에서 Hive table로 옮겨진다. 이벤트 로그들은 수많은 파이프라인들의 입력 정보이자, 회사 전체의 다양한 대시보드의 동력 역할이기 때문에, 정확한 시간에 데이터 웨어하우스에 도착하고 SLA를 만족시키는 것이 정말로 중요하다.



생성된 이벤트 로그의 규모는 거대하고, 빠르게 증가하고 있다. 그래서 기존 수집 인프라에 여러 어려움들을 안겨주었는데, 특히 Kafka에서 HBase로 이벤트를 밀어 넣는 Spark Streaming 작업이 문제였다. 이 아티클에서는 인프라 규모 확장의 어려움과, 급증하는 throughput을 더 효율적으로 처리하기 위한 해결책에 대해 논의할 예정이다. 


Airbnb의 사업이 성장할수록 매주 발생하는 이벤트의 숫자도 함께 성장한다.


데이터 기반의 사업을 하기 위해서는 사업을 운영하면서 발생하는 여러 종류의 데이터들을 잘 기록하고 관리하는 것이 중요한데요, Airbnb에서는 이런 데이터들을 '이벤트'라는 단위로 묶어서 처리하고 있습니다. 당연히 사업이 커질수록 다루는 데이터의 양이 많아질 텐데 결국 이런 대용량 데이터를 빠르고 정확하게 처리하는 역량이 데이터 기반 사업의 핵심 역량이 됩니다. 이 아티클에서는 Airbnb에서 대용량의 이벤트 데이터를 처리하면서 마주한 몇 가지 어려운 문제들을 소개하고 나름대로 해결한 방법을 이야기하고 있습니다. 이 이야기는 Airbnb가 성공할 수밖에 없었던 몇 가지 중요한 요소들 중 하나로 보입니다.


어려운 점


1. Spark의 병렬성은 Kafka 파티션 수에 의해 결정된다.

현재의 Spark Kafka Connector에서, Kafka 파티션과 Spark Task 사이에는 1대 1 관계가 성립한다. 기본적으로 이벤트가 Spark에서 처리될 때 순서를 보장하기 위해서 하나의 Spark Task는 하나의 Kafka 파티션에서 데이터를 읽도록 만들어져 있다. 그러나, 이 설계에서는 단순히 자원을 더 많이 할당하여 병렬 처리를 많이 하는 방식으로는 Spark Streaming 작업의 규모 확장을 할 수 없다. 


Spark 병렬성과 Throughput을 증가시키기 위해서는, 크기가 크거나 QPS가 높은 이벤트를 가지고 있는 Kafka Topic 하나에 파티션 여러 개를 할당해야 한다. 불행히도, 이것은 수작업으로 처리해야 하며, Topic 개수가 많고 점점 증가하는 경우에 대응할 수 없다.


또 다른 문제는, Topic 하나에 여러 파티션을 할당하는 것은 이미 Kafka에 저장된 과거 데이터들에 대해서는 적용되지 않는다는 점이다. 추가 파티션들은 새로운 이벤트에만 사용된다. Kafka Topic이 영향을 받기 전에 이벤트 Spike를 예상하고 추가 파티션을 할당하는 것은 현실적이지가 않다. Spike는 언제든지, 새로운 기능이 추가되거나 휴일인 경우 등 다양한 이유로 발생이 가능하기 때문이다. 


이벤트 규모가 특정 수준에 도달하면 큰 Kafka Topic들은 데이터 웨어하우스로 충분히 빠르게 옮겨질 수 없게 된다. 그리고 이 문제는 다음에 논의할 이벤트 데이터 쏠림 현상에 의해서 더 악화될 수 있다.


2. 이벤트 용량과 크기 쏠림 현상

다양한 이벤트 종류들은 저마다 제 각각의 용량과 크기로 기록된다. 어떤 이벤트들은 매우 드물게 발생하는 반면 어떤 이벤트들은 규모 자체가 다른 QPS를 가지기도 한다. 이벤트 타입들의 크기는 수백 바이트에서 수백 킬로바이트까지 다양하다. 아래의 박스 플롯은 KafKa Topic 별 평균 이벤트 크기의 다양성을 보여준다. (Y 축이 로그 스케일인 것에 주목) 큰 이벤트일수록 더 많은 파티션을 할당해줬음에도 불구하고, 여전히 심각한 파티션 쏠림 현상이 존재한다.

쏠림 현상은 데이터 분야에서 보통 심각한 문제로 취급된다. 이 경우에는, 몇몇 Spark Task가 다른 것들에 비해 훨씬 빨리 끝나게 된다. Spark에서는 모든 Task가 종료되어야 다음 단계로 넘어가기 때문에, 많은 실행기(executor)들이 공회전(idle) 하면서 자원 낭비를 하게 된다. 큰 이벤트를 가지고 있는 Kafka 파티션들은 파티션의 수가 충분하지 않을 경우 데이터를 읽기 위해 말도 안 되게 많은 시간을 소모하게 된다. 그리고 일괄 처리 작업은 순차적으로 처리되기 때문에 Spark Streaming 작업에 렉을 유발한다. 


3. 실시간 수집과 여유 공간

위에서 설명한 어려움들 때문에, Spark Streaming 작업에 약간의 Throughput 격차가 발생한다. 데이터 노드 문제나 Hive Metastore 장애 등과 같은 다양한 이유로 한번 작업이 밀리게 되면, 실시간 작업의 속도를 따라잡기 위해 상당히 긴 시간이 걸린다.


예를 들어 2분 주기로 실행되는 배치 작업이 평균적으로 1분 안에 끝난다고 하자. 만약 작업이 4시간 지연될 경우, 이를 따라잡기 위해 다시 추가로 4시간이 걸리게 된다. 만약 우리가 1시간 안에 따라잡고 싶으면, 4배의 여유공간이 필요하다. (즉, 각 배치를 24초 만에 처리하게 만들어야 한다.) 장애에서 회복할 때뿐만 아니라, 주기적으로 발생하는 Spike를 다루기 위해서도 큰 여유공간이 필요하다. 


엔지니어링 관점에서 크고 복잡한 어려움을 마주했을 때, 모든 문제를 하나의 방법으로 한 방에 해결하기는 어렵습니다. 일단 상황을 정확히 분석하여 작은 문제들로 분할하여야 합니다. 그리고 이렇게 문제를 분할하는 것보다 더 중요한 것은 병목 포인트를 찾는 것입니다. 특히 빠르게 성장하는 스타트업에서는 항상 리소스가 충분하지 않기 때문에 최소한의 노력을 투자하여 최대한의 효과를 얻어야 합니다. 그러기 위해서는 이런 장애물을 극복할 때 가장 심한 병목이 되는 지점을 정확히 찾아내서 공략하는 수밖에 없습니다. Airbnb에서는 이벤트 로그를 실시간으로 처리하는 문제를 풀기 위해 위와 같은 세 가지 병목 포인트를 찾아냈습니다. 아마 여러 시행착오를 겪었겠지만, 그래도 이렇게 "반드시 해결해야 하는 문제"를 잘 찾아내는 것을 보면 Airbnb의 뛰어난 기술적 역량을 짐작할 수 있습니다. 


해결책

이상적인 시스템이라면 Spark Streaming 작업들을 수평적으로 확장할 수 있을 것이다. (병렬성을 높이고 더 많은 자원을 할당하여 더 높은 Throughput을 획득) 또한, 부하를 잘 분산시켜서 각 Task가 Kafka에서 데이터를 읽어오는 시간이 거의 비슷하게 걸릴 것이다. 


이 두 가지 목표를 달성하기 위해, Airbnb 데이터 플랫폼 팀에서는 두 요구사항을 모두 만족시키는 균형 잡힌 Spark Kafka Reader를 개발하였다.


균형 잡힌 Spark Kafka Reader

Streaming 데이터 수집에 있어서, 각 이벤트는 최소한으로 처리되어 HBase에 개별적으로 저장될 것이기 때문에 순서가 보장될 필요가 없다. 이로 인해 우리는 모델을 다시 생각하고 확장성 문제를 해결할 새로운 방법을 찾아볼 수 있게 되었다. 그 결과, 우리는 Spark의 새로운 균형 잡힌 Kafka Reader를 만들게 되었는데, 첫째, 임의의 숫자로 분할할 수 있어서 Throughput을 증가시키기 위해 병렬성을 높일 수 있고, 둘째, 이벤트의 용량과 크기 기반으로 얼마나 분할해야 할지 계산이 가능하다. 


요약하자면, 균형 잡힌 Kafka Reader는 아래와 같이 동작한다.


1. 각 Topic의 평균 이벤트 크기를 먼저 계산하고 CSV 파일에 저장한다.

2. Spark Streaming 작업이 균형 잡힌 Kafka Reader를 생성할 때, 요구되는 병렬성을 표현한 numberOfSplits라는 추가 parameter를 전달한다. 

3. 각 Kafka 파티션에 대해서, 데이터를 읽을 offset 범위(현재 offset부터 최근 offset까지)를 계산하고 maxRatePerPartition 제약이 설정된 경우 적용한다.

4. 다음 섹션에서 다룰 균형 잡힌 파티션 알고리즘을 사용하여 offset 범위의 부분 집합들을 균등하게 분할한다.

5. 각 Spark Task들은 분할된 만큼 Kafka로부터 하나 혹은 여러 offset 범위의 데이터를 읽어온다.


아래 그림은 두 개의 Kafka Topic을 3개의 Split으로 분할한 예제이다. Topic A는 더 높은 QPS를 가지고 있지만 크기가 Topic B에 비해 작다. 균형 잡힌 Kafka Reader는 이 이벤트들의 부분집합들을 묶어서 각 Split이 Kafka로부터 데이터의 1/3 씩 읽어올 수 있도록 만든다. 예를 들어 Split 2의 경우 Topic A에서 4개의 이벤트와 Topic B에서 하나의 이벤트를 포함하여 총 8kb 크기를 가지게 된다.  


Step 1은 평균 이벤트 크기를 동적으로 계산하는 방식으로 추후에 더 향상될 여지가 있다. 그러면 새로운 Topic들 또는 자주 이벤트 크기가 변경되는 Topic들에 대해서 더 정확한 계산을 할 수 있게 된다. 


균형 잡힌 파티션 알고리즘

offset 범위를 균등하게 분할하고 할당하는 문제는 NP-hard인 Bin Packing 문제와 매우 유사하다. 최적의 해를 찾거나 빠른 속도로 동작하는 선형 시간에 수행되지 않는 복잡한 알고리즘들이 존재한다. 하지만 우리의 문제는 약간 다르기 때문에 이런 것들을 바로 적용할 수 없었다. 첫째, Split의 개수가 고정이어야 한다. 둘째, offset 범위가 더 작은 조각으로 쪼개질 수 있다.


우리는 기존의 복잡한 알고리즘을 적용하는 것이 아니라, 아래와 같이 간단하면서도 효율적인 알고리즘을 개발하였다.

1. 위 공식에 따라 이상적인 weight-per-split을 계산한다. 미리 계산된 목록에 존재하지 않는 새로운 이벤트 타입의 경우, 모든 이벤트 타입들의 평균값을 사용한다.

2. Split 0부터 시작하고, 모든 offset 범위에 대해서...

3. 만약 전체 weight가 weight-per-split보다 작으면 현재 Split에 할당한다.

4. 만약 더 크면, 한번 쪼개고 이 Split에 할당할 수 있는 부분 집합을 할당한다.

5. 현재 Split이 weight-per-split보다 커지면, 다음 Split으로 넘어간다.


이 알고리즘은 O(Split 개수)의 시간 복잡도로 매우 빠르게 동작한다. 단순히 Split과 Kafka 파티션들을 한 번씩 순차적으로 처리하기만 하면 된다. 그 결과로, 마지막 Split을 제외한 대부분의 Split에 대해 극단적으로 균형 잡힌 가중치를 주게 된다. 마지막 Split의 경우 약간 적은 가중치를 받게 되지만 최대 하나의 Task의 리소스만 낭비하는 것이기 때문에 큰 문제는 되지 않는다. 한 테스트에서, 2만 개의 Split에 대해 계산된 weight-per-split은 489,541,767이었다. 실제로 가장 작은 weight는 278,068,116, 가장 큰 weight는 489,725, 277이었다. 두 번째로 작은 weight는 489,541,772였다. 가장 작은 Split을 제외하면 가장 큰 Split과 두 번째로 작은 Split의 차이가 183,505(가장 큰 weight의 0.04%)에 불과했다. 


이 균형 잡힌 파티션 알고리즘은 테스트와 실제 환경 양쪽에서 잘 동작했다. 아래 그림에서 볼 수 있듯이 Spark 수행 시간의 편차는 기존 Spark Kafka Reader에 비해 훨씬 균등하게 분배되었다. 대부분의 Task들은 2분 안에 끝났다. 오직 적은 비율의 작업들만 2분에서 3분 정도 소요되었다. 이벤트의 QPS와 크기가 다양하게 분포됨에도 불구하고 Task 수행 시간의 편차가 적다는 것은 균형 잡힌 파티션 알고리즘의 효과가 아주 훌륭하다는 점을 증명한다. 이벤트의 용량과 크기를 고려하면서, 수집 작업의 부하가 실행기들에게 골고루 분산되었다는 것을 알 수 있다.


상단과 하단 시스템의 발전

균형 잡힌 Kafka Reader는 이벤트 로그를 Streaming으로 수집하고 규모를 확장하는 데 있어서 매우 핵심적인 조각이다. 그런데, 상단과 하단 시스템에 또 다른 병목이 존재하지 않는지 확인하는 것 또한 중요하다. 우리는 Kafka와 HBase의 Throughput과 신뢰성을 높이기 위한 노력을 기울였다. Kafka의 경우, 브로커들을 4배의 Throughput을 가지는 VPC로 이전하였다. Kafka의 파티션 별 QPS를 모니터링하기 위한 Streaming 작업이 세팅되었고, 이벤트 용량이 증가되는 경우 더 많은 파티션이 제시간에 추가되도록 하였다. 하단의 HBase의 경우, HBase Table을 위한 Region 수를 200에서 1,000으로 올렸고, 이에 따라 HBase로 이벤트를 대량 적재하는 작업의 병렬성이 높아졌다. 


Spark Streaming 작업의 경우, 예측 실행(Speculative execution)이 인프라에서 발생하는 신뢰성 이슈를 해결하기 위해 활용되었다. 예를 들어, 한 Spark Task가 Disk에 문제가 있는 불량 데이터 노드에서 데이터를 읽다가 멈춰있을 수 있다. 예측 실행을 적용한 경우, 이 작업은 이런 종류의 이슈에 훨씬 적게 영향을 받을 수 있다.


어떻게 보면 대용량 분산 시스템이라면 공통적으로 겪게 되는 어려움을 깔끔하게 잘 해결한 케이스로 보입니다. 결국 수평적인 확장을 용이하게 하기 위해서 어떻게 partition을 구성해야 하는가 하는 문제였는데, 이를 Kafka Reader를 개선함으로써 완벽하게 극복하였습니다. 이벤트 로그 처리 시간의 불균형이 병목 지점이니까 이런 불균형을 없애기 위해 처리 시간을 균등하게 분배해야 한다는, 어쩌면 꽤 단순한 논리인데 쉽게 도전할 수 있는 영역은 아닌 것 같습니다. 글에서는 단순하게 소개되어 있지만 상단, 하단 시스템들과 연계되어 있고 실시간 처리를 지원해야 하는 입장이기 때문에 많은 복잡한 커뮤니케이션과 운영 상 어려움을 겪었을 것으로 짐작됩니다. 그런 장애물들을 다 극복하면서도 이렇게 기술적인 문제 해결까지 달성할 수 있었다는 점에서 Airbnb는 정말 뛰어난 엔지니어들을 많이 보유하고 있을 것이라는 확신이 들었습니다. 


마지막 생각

균형 잡힌 Kafka Reader 덕분에, Kafka에서 데이터를 가져가서 소비하는 Spark 응용 시스템들은 이제 임의의 병렬성으로 수평 확장이 가능해졌다. 이 균형 잡힌 파티션 알고리즘은 간단하지만 매우 효과적인 것으로 증명되었다. 이런 향상 덕분에, 이벤트 로그를 수집하는 Spark Streaming 작업은 이전에 비해 훨씬 큰 규모의 이벤트도 잘 처리하게 되었다. 시스템의 안정성도 아주 많이 향상되어서 이제는 변경 사항이 반영될 때도 특별히 큰 렉이 발생하지 않는다.


이벤트 수집을 위한 Spark Streaming 작업은 미래의 트래픽 증가와 Spike에 대해서도 부드럽고 효율적으로 잘 처리할 것이다. 더 이상 이벤트 쏠림 현상을 걱정할 필요가 없기 때문이다. 만약 인프라에서 발생한 이슈로 인해 어떤 작업이 좀 밀린다고 하더라도 금방 회복될 것이다.


여기서 우리가 해결한 문제들은 일반적인 대용량 Spark 응용 시스템이나 데이터 시스템에서 흔히 발생하는 것은 아니다. 데이터 그 자체뿐 아니라 각 단계에서 어떻게 처리되는지, 어느 부분이 병목이 될 가능성이 있는지, 데이터 쏠림 현상은 없는지, 최적화할 수 있는지 등을 아주 자세하게 이해하는 것이 중요하다. 예를 들어, Spark는 각 작업의 DAG를 보여주는 훌륭한 UI를 제공한다. 이를 통해 어떻게 각 작업이 수행되고, cache나 다시 파티셔닝 하는 것을 통해 성능이 나아질 수 있을지 확인할 수 있다.


Airbnb의 기술 블로그 글들을 읽으면 읽을수록 단순히 숙박 공유에 대한 아이디어로, 아니면 단순히 투자를 많이 받거나 운이 좋아서 성공한 회사가 아니라는 생각을 하게 됩니다. 데이터 플랫폼에 대한 뛰어난 기술력을 축적하고 있고, 계속해서 마주하는 새로운 기술적 한계점과 문제들을 하나씩 극복해나가면서 사업의 확장을 튼튼히 뒷받침해주고 있습니다. 장기적인 안목으로 확장성 있는 설계에 대한 고민을 하고, 예상하기 힘든 인프라 장애 등과 같은 이슈에도 유연하게 대처할 수 있는 플랫폼을 만들기 위해 얼마나 노력하는지를 보면, 이 회사는 잘될 수밖에 없었겠구나 싶습니다. 기술 부채를 외면하고 눈 앞의 단기적인 성과에만 치중하거나 확장성 없는 시스템을 계속 끌고 가는 등 성공의 길과 반대되는 문화가 만연한 가운데 이 아티클을 통해 많은 반성을 하게 됩니다.



지난 리뷰 보기

https://brunch.co.kr/@sonjoosik/2

https://brunch.co.kr/@andrewhwan/51

https://brunch.co.kr/@sonjoosik/4

https://brunch.co.kr/@andrewhwan/54




브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari