데이터 파이프라인 기본 원리와 원칙은 시간이 지나도 유효해야 한다(1/2)

| 2021-09-14

안녕하세요, 넷마블 빅데이터실 데이터엔지니어링팀 이재호입니다.

게임은 서비스를 이어가면서 다양한 콘텐츠와 이벤트 요소를 지속적으로 업데이트합니다. 이에 따라 유저 유입이나 이탈 이외에도, 매출 등락이 시시각각 일어납니다. 이런 흐름을 분석하고 예측할 수 있어야, 서비스의 현 상태를 진단하고 정확한 의사결정을 할 수 있습니다. 현재 넷마블에서는 여러 분석/예측 애플리케이션을 개발하고 있고, 데이터 파이프라인은 여러 애플리케이션에 데이터를 공급하는 역할을 담당합니다. 이 글에서 데이터 파이프라인을 구축하기 위해 검토했던 환경적인 요인과 시스템 디자인 과정을 소개합니다.

넷마블의 데이터 환경

넷마블은 온라인 게임을 퍼블리싱하는 회사입니다. 100개가 넘는 게임을 서비스하며, 각기 다른 스튜디오에서 개발한 여러 애플리케이션으로부터 데이터를 수집하고 있습니다. 데이터 생산자가 다양하므로 생성 단계에서 자유도가 높을 수밖에 없습니다. 데이터 파이프라인은 이런 데이터를 수집해 일관된 형태로 사용자에게 제공할 수 있어야 합니다. 또한, 그 과정에서 사용자가 신뢰할 수 있는 데이터 품질을 보장해야 합니다.

게임 퍼블리싱은 흥행 사업입니다. “리니지2 레볼루션”, “제2의나라”와 같은 대작 게임이 크게 흥행할 경우, 기존에 서비스하고 있던 모든 게임을 합친 것보다 더 많은 데이터 트래픽이 한순간에 유입될 수 있습니다. 또한 신규 게임 오픈 시점,  대규모 업데이트, 이벤트 등에 따라서도 데이터 트래픽이 급증할 수 있는 환경입니다.  그래서 데이터 파이프라인을 구성하는 각 컴포넌트들의 성능을 선형적으로 향상시킬 수 있어야 합니다.

넷마블은 속도와 협업을 중요 시 하는 회사입니다. 마켓 동향과 플랫폼 변화에 따라, 다양한 의사결정을 신속하게 하고 있습니다. 그리고 각 조직과 여러 시스템은 유기적으로 연결돼 변화하는 요구사항에 적절히 대응하고 있습니다. 데이터 파이프라인 구조와 아웃풋 또한 그 흐름에 맞춰 변화해야 합니다.

기본적인 요구사항

앞서 넷마블의 데이터 환경과 데이터 파이프라인이 갖춰야 하는 특성을 간략하게 훑었습니다. 이런 특성에 대응하는 데이터 파이프라인을 설계하기 위해서는 많은 유스 케이스와 이슈를 고려해야 합니다. 예를 들면 다음과 같은 질문에 답할 수 있어야 합니다.

  • 내부적인 문제가 발생하면 데이터 무결성과 완전한 처리를 어떻게 보장할 것인가?
  • 부분적인 성능 저하가 발생하면 일관성 있게 좋은 성능을 어떻게 보장할 것인가?
  • 증가한 부하를 처리하기 위해 어떻게 규모를 키울 것인가? 
  • 좋은 컴포넌트 추상화(Abstraction)는 어떤 형태인가?
  • 좋은 서비스 API는 어떤 형태인가?
  • 고가용성이 고려된 환경인가?

이를 좀더 일반적인 소프트웨어 요구사항으로 표현하면 Reliability(신뢰성), Scalability(확장성), Maintainability(유지보수성)라는 세 가지 디자인 원칙과 Maintainability의 세부 항목인 Simplicity(단순성), Evolvability(발전성), Operability(운영성)로 정리할 수 있습니다. (“System Design Principles”에 대한 자세한 내용은 2번째 글 맨 뒤에 첨부한 내용을 참고하시면 됩니다.) 

이런 디자인 원칙은 단순한 구호가 아니라, 실제 서비스 환경에서 발생하는 다양한 이슈와 밀접하게 연관돼 있습니다. 

분산 환경 이슈

넷마블의 데이터 파이프라인은 여러 서비스를 조합해 구성하며, 각 서비스는 증가하는 트래픽을 효과적으로 처리하기 위해 수평 확장할 수 있는 구조를 갖추고 있습니다. 분산 환경 기반 시스템이 기본 전제가 되며, 그 특징은 아래와 같습니다.

  • 네트워크로 연결된 여러 노드로 구성하며 상호 공유자원이 없는 시스템(shared-nothing systems).
  • 각 노드는 각자 메모리와 디스크를 갖고 있으며, 다른 노드에 있는 메모리나 디스크에 직접 접근할 수 없음.
  • 각 노드는 오로지 네트워크를 통해서만 커뮤니케이션 할 수 있음.

위와 같은 특징으로 인해, 아래와 같은 문제가 발생할 수 있습니다.

  • 인터넷과 대부분 데이터 센터 내부 네트워크는 비동기 패킷 네트워크이므로(asynchronous packet networks) 노드 간 메시지 전송 시 메시지가 언제 도착할지, 또는 완전하게 도착할 수 있을지 보장하지 않음. 
  • 요청(request)을 보낸 후 응답(response)을 기대하지만, 그 과정에서 많은 것이 잘못될 수 있음.

이런 문제는 분산 시스템을 설계할 때 가장 처리하기 까다로운 부분입니다. 특히, 요청에 대한 응답이 없을 때 우리가 얻을 수 있는 유일한 정보는 “응답 메시지를 아직까지 받지 못하고 있다”라는 사실 뿐입니다. 보통 이런 이슈를 다루는 가장 실용적인 방법은 타임아웃(timeout: 일정한 시간이 지난 후에 기다리기를 포기하고 응답이 도착하지 않을 것이라고 추정하는 것) 처리이며, 타임아웃을 사용할 때는 아래와 같은 이슈를 검토해야 합니다.

  • 긴 타임아웃 사용 시: 실제로 문제가 발생했을 때 이를 감지하기 위한 시간이 오래 걸리고 서비스가 지연됨.
  • 짧은 타임아웃 사용 시: 실제로 원격 노드가 아직 작업을 수행 중인 경우, 동일 작업을 중복 실행할 위험성이 있음.

특히, 시스템 과부하로 원격 노드와 커넥션이 불안정할 때 타임아웃 발생 확률은 올라갑니다. 이런 상황에서 중복 실행은 네트워크와 노드 전반에 부하를 추가하기 때문에 문제를 악화시킬 수 있습니다. 또한, 재시도하는 오퍼레이션이 멱등하지(idempotent: 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질) 않는다면 산출하는 아웃풋이 달라질 수 있기 때문에 데이터 정합성 관점에서도 매우 주의 깊게 처리해야 합니다.

시스템 복잡도 이슈

데이터 파이프라인은 여러 데이터 기반 애플리케이션을 지원하는 공용 시스템입니다. 다양한 소스 데이터를 수집해 중앙 저장소에 적재한 후, 불특정 다수 사용자를 위한 여러 형태의 ETL을 처리합니다. 소스 타입, 원본 데이터 형태, 데이터 가공 방식, 전달 채널이 각각 다르게 적용될 수 있기 때문에 서비스가 지속될수록 시스템 복잡도가 쉽게 증가할 수밖에 없습니다. 

시스템 복잡도가 증가할수록 운영 모델의 직관성도 나빠집니다. 이로 인해 운영 비용이 증가할 뿐만 아니라, 휴먼 에러에 의한 장애 발생 확률도 높아집니다. 또한 시스템이 비대해지고, 코드가 복잡해질 수록 새로운 요구사항을 반영하기가 더욱 어려워지고, 여러 예외처리를 추가할 수록 시스템 복잡도가 더욱 증가하는 악순환에 빠질 수 있습니다.

분산 환경에 대한 미숙한 처리와 시스템 복잡도 증가는 각각 Scalability와 Maintainability를 악화시킬 뿐만 아니라, 두 가지 요인이 복합적으로 작용하면서 수많은 휴먼 에러를 유발할 수 있습니다. 이는 데이터 파이프라인의 Reliability를 악화시키는 주요한 원인이 됩니다.

고가용성 시스템 구축

넷마블에서는 로그를 준실시간으로 마케팅과 경영 지표에서 사용합니다. 나아가서는 AI 시스템에서 사용하므로 외부 장애 또는 내부 장애로부터 높은 가용성을 요구합니다. 고가용성을 보장하기 위해서는 물리적인 네트워크 부터 응용 프로그램 설계까지 적용할 수 있어야 합니다. BigPi 로그 처리 시스템에서는 고가용성을 위해 아래와 같은 설계로 구축했습니다.

  • HDFS Namenode 이중화로 고가용성 보장.
  • 대체 클러스터를 항상 Ready 상태로 셋팅(사용자 개입 필요).
  • 클라우드 환경에서 시스템을 구축해 파이프라인 이중화.

데이터 파이프라인 설계

데이터 파이프라인 설계는 앞서 정리한 분산 환경 이슈와 시스템 복잡도 이슈를 해소하는 방법을 기반으로 진행했고, 그 과정에서 선택했던 몇 가지 전략을 소개합니다. 설계 전략에 대한 이해를 돕기 위해 데이터 파이프라인 구성을 간단히 살펴보겠습니다.

넷마블 데이터 파이프라인 구성도

Collect: Apache + Tomcat

Message Queue: Kafka

Data Lake: HDFS

Ingest, ETL: Spark on YARN

Scheduler: Airflow

위 그림은 넷마블 데이터 파이프라인을 단순화해서 표현한 구성도입니다. 가장 좌측의 아이콘은 넷마블 서비스 애플리케이션이 실행되고 있는 클라이언트와 서버를 의미합니다. 각 게임에서 수집한 데이터는 화살표 방향으로 흘러가며, Message Queue를 거쳐 Data Lake에 적재하고, 이후 사용자들의 목적에 맞게 10분 간격으로 ETL하고 있습니다.

프로세싱 컴포넌트의 기능

아래는 프로세싱 컴포넌트 각각의 기능 명세입니다.

  • Collect: 수집한 원본 데이터에 수집 시간 기록 후 Message Queue에 저장합니다.
  • Ingest: Message Queue에서 데이터를 추출한 후, 게임과 수집 시간 기준으로 파티셔닝해 Data Lake에 적재합니다.
  • ETL: Message Queue 또는 Data Lake에서 파티션 단위로 데이터를 추출한 후 사용자 별 요구사항에 맞춰 가공해 전달합니다.

신뢰성 높은 데이터 파이프라인

이처럼, 여러 컴포넌트를 분산 처리 기반으로 구성하고 네트워크에 연결해 파이프라인을 구성합니다. 컴포넌트 자체가 무결하더라도 메시지 전달 단계에서 문제가 발생할 수 있으며, 이 부분을 잘못 처리한다면 데이터가 유실되거나 오염될 수 있습니다. 결국, 데이터 파이프라인 전체를 reliable(신뢰)할 수 있으려면 최종 아웃풋이 정상 처리됐을 때 항상 동일한 결과가 나오는 것을 보장할 수 있어야 합니다.

데이터 파이프라인에서 Reliability(신뢰성)은 사용자와의 약속이며 시스템의 근간입니다. 따라서, 시스템을 reliable하기 위해서는 몇 가지 정책을 협의해야 합니다. 아래 내용은 넷마블 데이터 파이프라인 구축 시 고려했던 사안이며, 이는 시스템 환경과 조직 운영 방식에 따라 달라질 수 있습니다.

  • 데이터 유실이나 오염은 허용할 수 없음(불가피할 경우 중복 적재나 지연 적재 감수).
  • 데이터 유실이나 오염 발생에 대한 대응 로직이 없는 경우 crash를 낼 수 있으며, 재실행할 수 있음
  • 재실행으로 해소할 수 없다면 장애 확산을 막고, 관리자가 개입할 수 있음

컴포넌트 구성 방식

위 정책을 준수하는 방향으로 데이터 파이프라인 각 단계를 설계해야 합니다. 구체적인 내용은 아래와 같습니다.

Collect

수집 후 Message Queue에 저장하는 단계까지가 가장 불안정하며, 별도의 전달 보장 장치가 없습니다. 그래서 데이터 전송 실패나 타임아웃이 발생하면 클라이언트에서 재전송하는 방식을 통해 at-least-once semantic 보장해야 합니다.

Ingest

Message Queue에서 데이터를 추출한 후 수집 시간 기준으로 파티셔닝 해 Data Lake에 적재합니다. 프로세스 실행 중 추출 및 적재시 microbatching과 checkpointing 방식을 통해 exactly-once semantic을 보장할 수 있습니다. 프로세스가 갑자기 셧다운돼 재시작하는 경우(microbatching 적재 후 checkpointing을 실패한 케이스)도 있으므로, 중복 발생을 허용하고 있습니다.

ETL

데이터 파티션마다 Deduplication, Transform, Transfer/Load 단계를 순서대로 실행합니다. 데이터 인풋은 손상시키지 않고, 아웃풋을 별도 경로에 저장하는 방식으로 단계별 데이터를 생성해야 합니다. idempotent한 단계를 chaining하는 방식을 적용하면 fault가 발생해도 재시도를 통해 복구할 수 있으므로, exactly-once semantic을 보장할 수 있습니다.

Deduplication

앞서 Collect와 Ingest 단계에서 발생한 중복 데이터를 제거합니다. 각 데이터 파티션에서 logkey(로그 고유성 식별자)를 별도로 저장한 후, 이를 기반으로 중복 제거합니다. 각 데이터 파티션별로 logkey 수집과 중복 제거 처리 간 의존성을 제어해 idempotent execution 보장할 수 있습니다.

Transform

사용자별로 설정한 규칙에 따라 데이터를 가공합니다. 가공 규칙 버전은 datetime 형태로 관리하며, 데이터 파티션마다 확정적으로 매핑됩니다. 각 가공 규칙은 실행 연산에 대한 순열로 구성하며, 순차 처리를 통해 idempotent execution을 보장할 수 있습니다.

Transfer/Load

사용자 접점이 되는 분석 시스템 또는 데이터베이스로 데이터를 전송한 후 타겟 테이블에 불러옵니다. 데이터 전송 단위마다 확정적으로 타겟 경로를 할당하고 삭제 후 전송하는 방식으로 idempotent execution을 보장할 수 있습니다. 사용자 시스템 자체에 장애가 있는 경우에는 제한된 횟수만큼 재시도한 후 관리자에게 알려줍니다.

시스템 견고성 향상

이처럼 명확한 역할 단위로 작은 서비스를 만들고, 한 아웃풋이 다음 인풋이 되는 방식으로 연결해 파이프라인을 구성하면, 결과적으로 시스템 robustness(견고성)를 향상하는 효과를 얻을 수 있습니다. 이 방식을 통해 얻는 이점은 아래와 같습니다.

  • 처리 단계가 작아지므로 crash 발생에 대응하는 재시도 비용 감소.
  • 처리 단계 로직을 약하게 coupling해, 한 영역에서 발생한 문제가 다른 영역으로 퍼지는 것을 막음.
  • 별도 분산 트랜잭션이나 coordination 없이 단순 반복(replay)을 통해 내고장성(fault-tolerance) 메커니즘을 제공.

장애 상황 극복을 위한 직관적인 운영 모델

물론, 이와 같은 노력에도 불구하고 여전히 알려지지 않은 이슈가 발생할 수 있습니다. 이 시점에 중요한 것은 우리가 시스템 상태를 정확하게 진단하는 것과 각 컴포넌트에서 발생한 fault를 우리의 예상 범위 안에 두는 것입니다. 이를 기반으로 직관적인 운영 모델을 제시해 관리자가 장애 상황을 쉽게 극복할 수 있게 해야 합니다. 직관적인 운영 모델은 아래와 같은 루틴으로 표현할 수 있습니다.

  • 스키마 관리자의 실수로 일부 누락이 발생한 경우 스키마 갱신 단계부터 재처리한다.
  • 사용자가 데이터 가공 방식 변경을 요청한 경우 Transform 단계부터 재처리한다.
  • 파일 전송 중 네트워크 장애로 실패한 경우 Transfer 단계부터 재처리한다.

이런 관점은 시스템 아키텍처와 컴포넌트별로 인터페이스를 설계하는 과정에서부터 고민해야 하며, 일관성 있는 운영 모델을 유지하기 위해서는 시스템 복잡도 관리가 무엇보다도 중요합니다.

여기까지 고가용성 시스템을 구축하기 위해, 신뢰성 높은 데이터 파이프라인을 설계하고 구성하는 방식에 대해 살펴봤습니다. 다음 글에서 복잡도를 관리하기 위한 요소에 대해 이어가겠습니다.