시작 전에

“The Log: What every software engineer should know about real-time data’s unifying abstraction”[1]라는 제목의 글을 요약하였습니다.

이 글의 저자인 Jay Kreps[2]는 아파치 카프카의 창시자이자 컨플루언트의 co-founder 입니다. 아파치 카프카는 저자가 링크드인에 재직 당시 고안 되었습니다.

이 글을 10년 정도 이전에 쓰인 글입니다. 지금이야 event log를 중앙화된 kafka에 보내고 이중 실시간 처리는 flink나 카프카 streams, 배치 처리는 spark job을 실행하는 것이 보편화 되었지만, 당시에는 kafka가 이제 막 나온 시기이기 때문에 여러 분산된 시스템에서 데이터를 하나로 모으는 것에 대한 합의가 이루어지지 않았던 것으로 보입니다.

이 글에서 저자는 현대 어플리케이션에서 로그의 중요성과 분산 시스템 및 실시간 스트림 처리에서 로그가 어떤 역할을 할 수 있는지 제시해 줍니다. 시간이 되시는 분은 전문을 읽어주시기 바랍니다.


The Log

part 1. 그래서 로그가 뭔데?

로그는 가장 단순한 형태의 저장소입니다. 로그 데이터는 추가만 가능하며 시간의 흐름에 따라 정렬이 됩니다. log_arch 새롭게 추가되는 값(이하 레코드)은 로그의 가장 마지막에 저장되고 각각의 값은 로그 번호을 부여받습니다. 로그는 시간순으로 자동 정렬되어 있기 때문에 로그 번호는 하나의 타임스탬프로써의 역할을 합니다(이후 로그의 타임스탬프라 함은 로그 번호를 의미합니다). 나중에 생성된 로그는 이전에 생성된 로그보다 무조건 더 높은 로그 번호를 부여받는 특성이 있기 때문에 물리적인 시계가 없다고 생각하면 충분히 타임스탬프로써의 가치가 있습니다. 그리고 이 타임스탬프는 분산시스템에서 중요한 역할을 합니다.

로그는 파일이나 데이터베이스의 테이블과 유사합니다. 파일은 바이트의 배열이며 테이블은 레코드들의 배열입니다. 로그는 시간순으로 정렬된 특별한 형태의 배열이며 파일 혹은 테이블의 부분집합이 될 수 있습니다.

로그는 ‘어떤’사건이 ‘언제’ 벌어졌는지에 대한 정보를 제공하므로 단순한 자료 구조임에도 불구하고 여러 분산 시스템에서 특별히 여겨집니다.

다음 발화를 이어가기 전에 혼동 할 수 있는 개념을 짚고 넘어가겠습니다. 프로그래머가 흔히 접하는 에러메시지, 애플리케이션의 시스템 정보, 그 밖에 log4j등으로 가져오는 구조화되지 않는 텍스트 로그들을 “애플리케이션 로그"라 칭하겠습니다. 이런 텍스트 형태로 된 로그들은 가장 퇴행적인 개념이라 생각합니다. 이러한 로그들은 주로 사람이 읽을 수 있는 형태입니다. 이와 달리, 이번 포스트에서는 ‘journal’ 이나 ‘data logs’라 칭하는 프로그래밍적 접근을 위한 로그를 설명합니다. (사람이 읽는 형태의 로그는 퇴행적입니다. 이는 여러 분산시스템에서 특히 더 오작동을 하며 그래프나 쿼리등으로 쉽게 가져올 수 있는 로그가 더 중시 되어야 합니다)

분산 시스템에서의 로그

분산시스템에서 로그를 적절히 이용하려면 우선 레플리카간 로그의 순서가 섞이고 로그들이 여러군데에 저장되는 문제를 해결해야 합니다. 이를 위해 로그 중심 접근법을 소개합니다. 해당 접근법은 저자가 State Machine Replication 원칙이라고 하는 간단한 이론을 통해 비롯됩니다.

만약 두개의 동일하고 결정론적인(deterministic) 프로세스가 같은 상태(state)를 공유하며 같은 순서로 입력 값을 받을 때, 그 둘은 같은 결과값을 같은 상태에서 리턴한다.

여기서 결정론적(deterministic)이라는 것은 해당 프로세스가 시간에 독립적이고 입력값을 제외한 다른 값들에는 영향을 받지 않음을 의미합니다. 또한 프로세스의 상태(state)는 프로세스 종료 시점에 컴퓨터에 남아있는 메모리나 디스크 등을 의미합니다.

이 이론을 분산 시스템에 적용해 보겠습니다. 만약 여러 레플리카들이 모두 같은 일을 한다고 할때(결정론 적이라 할때) 동일한 입력값에 대해 모두 동일한 결과값을 산출할 것입니다. 여기서 로그의 목적은 입력 스트림에서 모든 비결정성을 제거하여 입력을 처리하는 각 레플리카들이 동기화 상태(결정론적 상태)를 유지하는데 있습니다. 이를 처리하기 위해 로그 번호, 즉 로그의 타임스탬프가 필요합니다. 각각의 레플리카에서 로그의 타임스탬프는 상태에 대한 하나의 시계가 될 수 있습니다. 레플리카의 각 레코드를 하나의 숫자로 칭할 수 있으며 레플리카 마다 가장 높은 타임스탬프가 해당 프로세스에서 처리된 레코드의 양임을 알 수 있습니다.

데이터 베이스로 예시를 들어보겠습니다. 다음의 두가지 아키텍처가 있습니다. log_two_way 변경된 값을 로깅하는 Primary-Backup 모델은 하나의 레플리카를 리더로 선정하고 해당 리더가 들어오는 요청을 처리합니다. 그리고 그 변화를 로깅합니다. 다른 레플리카들은 해당 상태변화를 동기화 합니다.(비결정성을 로그를 통해 결정성으로 동기화) 리더의 장애 발생 시 다른 레플리카가 새롭게 리더로 선출됩니다.

이와 달리 sql 을 직접 로깅하는 State-Machine Replication 모델의 경우에는 들어오는 요청(sql 쿼리문)을 로깅하고 레플리카들이 하나씩 이 로그를 할당받아 요청들을 처리합니다. 언뜻보면 간단해보이지만 다음의 예시를 통해 발생가능한 문제점에 대해 이야기 해보겠습니다.

간단한 사칙연산을 생각해 봅시다. 사칙연산에서 계산의 순서는 중요합니다 초기값이 0인 값에 1을 더하고 3를 곱해야 한다 했을 때, primary backup모델은 리더가 모든 연산을 수행하고 로그에는 0, 1, 3이 남습니다. 반면 state machine model에서는 +1, *3 이 로그로 남는다.그리고 이를 여러 레플리카들이 처리합다. 만약 이전 로그가 수행 되기 전 다음 로그가 수행된다면 결과는 아예 잘못되게 되며 State-Machine Replication에서는 분산시스템의 일관성을 유지하기 위해 처리 순서를 신경써야 합니다.

changelog 기본 : 테이블과 이벤트는 불가분(dual) 이다.

은행을 예시로 든다면, 로그는 통장의 이체내역과 비슷하고 테이블은 현재 남아있는 잔고와 비슷합니다. 로그의 변화가 발생하면 이를 가지고 현재 테이블의 상태를 변경할 수 있습니다. 이는 로그가 테이블보다 더 근본적인 구조라는 것을 의미합니다. 반대의 과정도 마찬가지입니다. 테이블이 업데이트를 실행하면 해당 작업을 changelog 형태로 발행합니다. 이를 가지고 다른 레플리카들의 동일한 테이블의 상태를 동기화 할 수 있습니다. 따라서 테이블과 이벤트는 불가분이라 할 수 있습니다.테이블은 가장 최근의 데이터를 지원하고 로그는 변화를 기록합니다. 만약 변화에 대해 완벽하게 기록한 로그가 있다면 해당 로그를 가지고 최종 버전의 테이블을 나타낼 수 있을 뿐 아니라 새롭게 현재 테이블과 동일한 테이블을 재생산할 수 있습니다. 사실산 로그는 테이블의 모든 상태에 대한 백업을 의미할 지도 모릅니다.

changelog에 대한 이런 설명으로 git과 같은 버전관리 시스템을 떠올릴 수도 있습니다. 버전 관리 시스템과 데이터베이스 같은 여러 분산 시스템의 레플리카들은 로그를 통해 존재합니다. 새로운 업데이트가 생기면, 이를 다운받아 현재 스냅샷에 적용시킬 것입니다(상태 동기화)

앞으로 서술하는 모든 경우에 대해 로그는 직관적인 기능을 수행합니다. 로그는 일관적이고, 재실행 가능한 역사의 기록들을 만들어 냅니다.

Part 2. 데이터 통합(data integration)

data integration이 무엇인지, 왜 중요한지 그리고 이게 로그와 어떤 관련이 있는지 알아보겠습니다.

data integration은 회사가 소유하는 모든 데이터를 회사의 서비스들, 시스템들에서 사용가능하도록 만들어 내는 것입니다.

이 정의를 듣고 ETL 을 떠올릴 수 있습니다. ETL은 data integration의 역할의 일부 기능인 data warehouse를 채우는 행위를 표현할 뿐이지 data integration 그 자체를 의미하는 것은 아닙니다. data integration은 ETL기능과 더불어 실시간 시스템 처리 및 데이터 처리 흐름을 포함합니다.

효과적인 데이터 처리 흐름은 ‘매슬로의 욕구단계설’로 은유할 수 있습니다. 매슬로의 욕구 단계설에 따르면 욕구의 종류가 계층화 되어 있으며, 하위 욕구를 실현해야만 상의 욕구가 가치 있음을 나타냅니다. 이를 데이터 처리 흐름으로 대입해 보겠습니다. 피라미드의 기저에는 모든 관련 데이터를 수집합니다. 해당 데이터는 쉽게 읽고 처리할 수 있어야 합니다. 이후 map reduce, 실시간 쿼리 시스템 등 다양한 방식으로 이 데이터를 처리하기 위한 인프라 작업이 수행됩니다. 신뢰가능한 데이터 수집 시스템이 존재하지 않으면 map reduce를 수행하는 hadoop cluster는 쓸모 없어집니다. 기반부터 올바르게 다져져야 잘 정제된 데이터를 가지고 시각화 / 보고 / 알고리즘 처리 등 비즈니스 문제에 데이터를 적용시킬 수 있습니다.

대부분의 조직은 데이터 수집 즉 data integration과정에 가장 큰 구멍을 가지고 있습니다. 신뢰할 수 있는 data integration시스템의 부재에도 불구하고 고급 데이터 모델링 기술을 통해 비즈니스 문제를 해결하길 원합니다. 문제의 앞뒤가 바뀐 셈이죠.

주장하는 바는, 고급 비즈니스 문제를 해결하기 전에 어떻게 하면 조직의 모든 데이터 시스템에 효과적인 data integration 시스템을 구축할 수 있어야 한다는 것입니다.

data integration을 방해하는 두가지 요인

첫번째는 이벤트 데이터의 탄생 입니다. 이벤트 데이터는 데이터의 존재가 아닌 발생한 사건에 대해 기록합니다. 예컨데, 웹 애플리케이션에서는 유저의 activity log를 포함하여 machine-level의 이벤트를 이벤트 로그로 기록합니다. 이러한 데이터들은 현대 웹 비즈니스의 심장부로 은유할 수 있을 만큼 가치가 큰 데이터입니다. google은 클릭과 노출, 즉 이벤트에 기반한 비즈니스를 통해 수익을 벌어드립니다. 이러한 종류의 이벤트 데이터는 무엇이 일어났는지 기록하며 전통적인 데이터베이스보다 훨씬 많은 양을 산출합니다. 이는 상당한 규모의 데이터 처리가 요구되는 문제로 이어집니다.

두번째는 전문화된 데이터 시스템의 증가입니다. elastic search, redis, druid 등 수많은 오픈소스 데이터 시스템이 등장하였습니다. 이렇게 파편화된 데이터 시스템들의 등장은 data integration의 문제로 이어지게 됩니다.

로그 기반의 data integration

로그는 시스템간의 데이터 flow를 처리하기 위한 최적의 자료구조 입니다. 방법은 간단합니다. 회사의 모든 데이터를 가져와서 실시간 subscribe를 할 수 있는 중앙 집중식으로 구성된 로그에 데이터를 집어넣습니다.

data_flow

data source에서 로그를 write 합니다. data source는 log 를 발생시키는 이벤트(클릭이나 페이지를 보는행위)나 수정이 되는 데이터베이스 테이블 같은 애플리케이션이 될 수 있습니다. 각각의 subscribe 시스템은 해당 로그들을 자신의 속도로 수집합니다. 각각의 새로운 레코드 들을 자신의 시스템에 새로운 변경사항으로 추가하고 로그 포지션을 다음으로 이동시킵니다. subscirbe 시스템은 무엇이든 될 수 있습니다. 캐시, 하둡, 혹은 다른 종류의 데이터베이스나 elastic search 같은 것들을 포함합니다.

subscribe 시스템은 로그의 읽는 시점(로그의 타임스탬프)를 가지고 있기 때문에, 스스로 속도를 제어할 수 있습니다. 따라서 같은 log로 어떤 시스템에서는 배치 처리, 어떤 시스템에서는 실시간 처리가 가능하게 됩니다.

한가지 중요한 사실은 destination 시스템이 로그에 대해서만 알지 이 로그가 어디서 발원되었는지에 대해서는 모른다는 것입니다.

ETL과 데이터 웨어하우스의 관계

데이터 웨어하우스는 데이터 분석을 위해 정제되고 통합된 데이터를 저장한 레포지토리 입니다. source database에서 데이터를 꺼내와 이리저리 핸들링 한 후 정제된 형태로 데이터 웨어하우스에 저장합니다. 이렇게 중앙화되게 데이터들을 한곳으로 모으면 비즈니스 문제 해결을 위한 데이터 분석 및 리포트 생성 등 다양한 문제를 해결할 수 있습니다.

몇몇 회사에서는 잘 정제된 데이터를 데이터 웨어하우스와 강하게 결합시키는 문제를 가지고 있습니다. 데이터 웨어하우스는 배치 쿼리를 통해 데이터를 특정 도메인 문제를 분석하기 위해 저장됩니다. 즉 데이터 웨어하우스만을 이용한다면 잘 정제된 데이터는 배치 시스템을 통해서만 생성될 수 밖에 없음을 의미하고 따라서 이러한 배치시스템들은 실시간 처리에는 사용될 수 없음을 의미합니다.

ETL에는 두가지 역할이 있다고 생각합니다. 첫째로, 데이터추출 및 종속성 제거입니다. 조직의 비즈니스에 맞게 다양하게 분산되어 시스템에 종속되어있는 데이터를 꺼내와서 시스템의 종속성을 제거합니다. 두번째로, 데이터 웨어하우징 쿼리를 위한 재구성입니다. 종속성이 제거된 데이터들은 데이터 웨어하우스에 저장되기 위한 스타 스키마 등으로 재구성됩니다. 이 두가지 역할이 분리되어야 정제되고 종속성이 제거되는 데이터가 실시간 처리 시스템에도 사용될 수 있습니다.

아파치 카프카와 같은 중앙 집중식 로그 시스템을 이용하면 이 두가지 과정을 분리할 수 있습니다. 만약 과거 처럼 ETL 처리를 한다면 데이터를 통합하고 정제하는 책임은 데이터 웨어하우스팀에 있습니다. 데이터 producer는 종종 데이터 consumer가 데이터를 어떻게 사용할 지 잘 알지 못하며 때로는 변환이 어려운 데이터를 생산합니다. 하지만 중앙 로그 시스템을 사용한다면 이 파이프라인과 통합하여 잘 구성된 데이터를 제공하는 책임은 data producer로 넘어갑니다. data producer의 시스템 설계 및 구현의 일부로 파이프라인으로 데이터를 잘 넘기기 위한 고려를 하게 됩니다. 또한 새로운 source db의 추가는 데이터 웨어하우스 팀의 작업에 영향을 주지 않습니다. 데이터 웨어하우스팀은 중앙 로그에서 구조화된 데이터 피드를 로드하고 특정한 변환을 수행하는 단순한 문제만 처리하게 됩니다

log_flow_3

이렇게 하게 된다면 새로운 시스템 추가시 단순히 파이프라인을 신규로 만들어 연결하면 됩니다.

로그 파일과 이벤트

이벤트 기반 아키텍처는 위에서 설명한 아키텍처의 좋은 예시가 됩니다. 우선 it 산업에서 전통적으로 유저의 activity data를 처리하는 방식에 대해 설명하겠습니다. activity data를 이벤트 로그로 만들어 파일로 저장합니다. 그 다음 이를 데이터 웨어하우스나 hadoop으로 집어넣습니다. 해당 아키텍처는 batch etl과 같은 문제점을 야기합니다. 즉 데이터 흐름을 데이터 웨어하우스의 처리 스케줄과 처리 능력에 의존한다는 것입니다.

링크드인에서는 kafka를 중앙 로그 처리 시스템으로 두고 많은 subscriber가 이를 수신합니다. 이는 페이지를 보고, 광고를 누르고, 검색하고, 앱에서 발생하는 에러를 캐치하는 등 다양한 이벤트를 수집합니다. 만약 전통적인 etl 모델을 따를 경우, 검색 시스템, 추천 시스템, 보안 시스템 등 시스템들이 추가될 때마다 새로운 etl 잡이 추가될 가능성이 있습니다. 만약 이벤트 기반 아키텍처를 차용한다면 단순히 kafka에서 내려오는 로그를 subscribe하는 파이프라인만 하나 추가하면 됩니다.

확장 가능한 로그 시스템 구축하기

수많은 source 시스템에서 충분히 빠른 속도로 데이터를 수집하고 이를 subscribe시스템에서 문제없이 데이터를 수신하려면 로그 시스템이 충분히 빠르고 확장 용이한 구조를 가져야 합니다.

이를 위해 아파치 카프카에서는 수직 확장을 가능하게 하기 위해 로그를 파티션들로 나눈다는 것입니다. 각각의 파티션은 모두 정렬이된 로그들로 구성됩니다. 그러나 global 하게 정렬 하지는 않습니다. 메시지를 특정 파티션에 할당하는 것은 producer가 선택할 수 있으며 대부분의 producer는 사용자 id와 같은 key를 기준으로 파티션을 선택합니다. 파티셔닝은 샤드간의 조정이 필요 없이 수직으로 확장이 가능하도록 설계할 수 있습니다. 각각의 파티션은 몇몇의 레플리카들로 복제되어 있습니다. 언제든 그들 중 하나가 leader가 될 수 있습니다. 하나의 leader가 장애를 일으키면 다른 레플리카 중 하나가 leader로 선출됩니다. 파티션간 글로벌하게 정렬되지 않는다는 점을 의아해 할 수 있습니다. 하지만 로그들은 보통 수백/ 수천개의 distinct된 프로세스들에 의해 생산되기 때문에 global order는 무의미합니다. 대신 파티션 내의 정렬은 보장되며 카프카는 하나의 시스템에서 보낸 로그가 그 순서대로 소비된다는 것을 보장합니다. (결정론적이기 때문이다) 또한 아파치 카프카는 간단한 바이너리 format을 사용하고 읽기 및 쓰기 패턴에 맞게 쉽게 최적화를 진행하여 불필요한 데이터 copy를 줄이고 효율적으로 데이터 전송을 가능케 합니다.

Part 3. 로그 시스템과 실시간 스트림 처리에 관하여

지금까지 많은 이야기를 나누었지만 아직 이야기하지 않은 것 있습니다. 로그는 스트림의 다른 이름이며 스트림 처리의 핵심입니다. 스트림 처리는 sql과 관련없으며 실시간 처리에만 국한되지 않습니다. (당연히 이전에 생성된 데이터 역시 처리 가능합니다)

스트림처리 : 지속적인 데이터 처리를 위한 인프라 구조

스트림 처리를 위해 필요한 모델은 맵리듀스나 여타 다른 분산처리 프레임워크처럼 보편적이면서도 low-latency로 결과를 만들어내는 능력이 있어야 합니다. 스트림 처리는 데이터를 수집하는 방법에 의존합니다. 배치로 수집되는 데이터는 배치로 처리됩니다. 지속적으로 수집되는 데이터는 자연스레 스트림 처리가 됩니다. 많은 회사들이 데이터들이 실제로 배치 처리되는데, 이를 지속적으로 수집하는 방식으로만 바꾸어도 스트림 처리가 가능해집니다.

스트림처리는 처리중인 데이터에 시간 개념을 포함한 처리일 뿐이며 데이터에 대한 정적인 스냅샷(테이블을 의미)이 필요하지 않기 때문에 데이터가 모두 저장되어 배치를 기다리기 전에 유저(consumer)의 통제에 맞추어 데이터를 처리할 수 있습니다. 이러한 관점에서 볼때 배치 처리는 스트림 처리의 일부라고 볼 수 있습니다.

로그는 데이터를 실시간으로 불러오기 때문에 스트림 처리의 하나의 대안이 될 수 있습니다.

Data flow graphs

우리는 이전에 다양한 source로 부터 온 log가 어떻게 처리되는지에 대해 보았습니다. 이에 더해 스트림처리는 다른 피드에서 온 로그들을 새롭게 처리할 수 있도록 만들어줍니다. 즉 source 시스템으로부터 온 레코드를 하나의 job에서 처리하고 이를 다시 다른 job의 입력값으로 만들 수 있습니다.

graph

로그 시스템은 이를 차용하면서 프로세스에 일종의 버퍼링 역할을 제공합니다. 상위 시스템에서 완료되지 않은채 하위 시스템에서 레코드를 consume하면 안됩니다. 만약 로그를 쓰지않는다면 버퍼나 데이터를 drop 해야 하는데, drop의 경우는 전체 시스템에 영향을 줄 수 있습니다. log시스템은 매우 큰 버퍼를 제공합니다.

상태를 가지고 있는 실시간 처리 (Stateful Real-Time processing)

많은 경우 실시간 스트림처리에서는 stateful한 연산을 수행해야 합니다. 예컨대 클릭에 대한 이벤트 스트림이 주어졌을 때 이 정보와 해당 유저 정보를 유저 DB에서 꺼내와 결합하고 싶을 때, 이러한 종류의 처리는 프로세서가 일정시간동안 상태를 저장하고 있어야 합니다. 프로세서가 단순히 이 상태를 메모리에 저장할 수도 있지만, 속된 말로 프로세서가 날라가면 끝납니다. 한가지 대안으로는 이 상태를 원격 스토리지에 저장하는 방법도 있지만, 이 역시 네트워크 트래픽이 과도하게 많아지고 지역성이라는 특성이 없어집니다.

우리는 이전에 논의하였던 로그와 테이블이 불가분관계라는 것을 기억해야 합니다. 스트림 프로세서는 상태를 로컬 테이블이나 인덱스 시스템(key-value store)등에 저장합니다. 처음에 이야기했듯 테이블은 로그의 마지막 캡처본 즉, change log를 통해 테이블을 재구성할 수 있으므로 해당 changelog를 input stream으로 만들어 계속해서 local storage 의 key/value를 업데이트 합니다. 이렇게 하면 프로세서가 직접 데이터베이스를 쿼리하지 않아도 되고 만약 프로세서가 실패하더라도 changelog 스트림을 통해 복구할 수 있습니다. (결정론적 시스템이고, 해당 정보는 다른 레플리카에 저장되기 때문)

Part 4. 시스템 만들기

데이터는 결국 현실에 있는 무언가를 추상화한 것이라고 생각합니다. 따라서 은탄환은 없습니다. 추상화란 결국 사람의 의도가 들어가기 때문입니다. 관계형 데이터베이스의 전성기때에도 수많은 종류의 데이터 시스템이 있었고 현재까지 정말 다양한 데이터 시스템들이 존재합니다. 따라서 진정한 의미의 data integration은 불가능할 수도 있습니다. 하둡과 같은 기술은 이러한 다양한 데이터 시스템들을 하나의 분산 시스템으로 옮길 수 있는 좋은 시스템입니다. 하지만 구축의 어려움이 있기에 지금의 다양한 데이터시스템은 각자 존재합니다.

시스템 설계에서 로그의 역할

로그 시스템을 위에서 언급한 것과 같이 중앙 집중식으로 설계하면 다음과 같은 이점을 얻을 수 있습니다.

  • 데이터 일관성 유지
  • 노드간 데이터 복제본 제공
  • 여러 시스템에서 동일한 데이터 subscribe 가능
  • 레플리카의 오류와 실패 복구 가능
  • 노드간 balancing 조절 가능

위 역할은 분산 시스템에서 제공해야 하는 역할중 일부이기도 합니다. 따라서 분산 시스템에서 중앙 집중식 로그 시스템을 차용하면 좋습니다. 분산 시스템에서는 이와 더불어 final client-facing query API 와 indexing 전략을 고려해야 합니다. 예컨대 모든 문자열을 검색하는 쿼리의 경우 모든 파티션들을 쿼리해야합니다. 반면 primary key를 이용한 쿼리의 경우 키의 데이터가 존재하는 하나의 노드만 쿼리하면 됩니다.

layer

이것이 어떻게 동작하는지 살펴보겠습니다. 위 사진과 같이 시스템은 두가지 논리적인 영역으로 구분됩니다. 로그시스템과 서빙 레이어 입니다. 로그 시스템은 상태 변화를 시간의 순서대로 저장합니다. 서빙 레이어에서는 쿼리를 수행하는데 필요한 모든 인덱스를 저장합니다(key-value 저장소는 btree, 검색 시스템은 inverted index) 클라이언트의 쓰기 작업은 로그에 직접 할 수도 있고 서빙 레이어 프록시를 통해 수행될 수 있습니다. 로그시스템에 쓰는 작업은 하나의 논리적인 로그 타임스템프를 만들어냅니다. 만약 시스템이 파티셔닝되어있다면, 로그 시스템과 서빙 노드들은 같은 수의 파티션들이 존재할 것입니다. 서빙 노드들은 로그를 subscribe하고 로그가 저장한 순서대로 가능한 한 빨리 로컬 인덱스에 subscribe한 데이터를 업데이트합니다. 클라이언트는 쿼리 작성 시 타임스탬프를 제공함으로써 분산시스템에서 해결되어야 하는 read-your-write consistency를 유지할 수 있습니다. 서빙 노드들로부터 데이터를 읽어올 때(쿼리 시) 받은 timestamp를 index point와 비교하여 인덱스가 업데이트 될 때까지 읽기 작업을 지연시킬 수 있습니다. 이렇게 되면 값을 업데이트 한 후 해당 값을 읽어올 때 변경된 값을 받아올 수 있음을 보장받습니다.

분산시스템에서 가장 까다로운점 중 하나는 오류가 발생한 노드를 복구하거나 노드간 파티션을 옮기는 것입니다. 로그 시스템을 사용하면, ETL을 다른 시스템에 제공하는 데이터 저장소의 컨텐츠에 대해 완전히 사용 가능한 subscription api를 얻을 수 있습니다. 실제로 많은 시스템이 서로 다른 인덱스를 제공하면서 동일한 로그를 공유할 수 있습니다.

arch

다음과 같이 로그 집중식 시스템은 실시간 처리를 위한 데이터 스트림의 provider가 될수 있습니다. 또한 스트림 프로세서는 많은 입력 스트림을 받아 다른 시스템으로 결과를 보낼 수도 있습니다.

위 아키텍처를 통해 실시간 쿼리 시스템을 구축할 수 있습니다. 이러한 시스템은 데이터베이스에서 데이터를 공급하고 해당 데이터 스트림 위에 특정 파티셔닝, 인덱싱 및 쿼리 기능을 제공합니다. 이러한 시스템 중 어느 것도 외부에서 액세스 가능한 쓰기 API를 가질 필요가 없으며 로그 시스템(Kafka) 및 데이터베이스는 기록 시스템으로 사용되며 변경 사항은 해당 로그를 통해 적절한 쿼리 시스템으로 흐릅니다. 쓰기는 특정 파티션을 호스팅하는 노드에서 로컬로 처리됩니다. 이러한 노드는 로그에서 제공하는 피드를 자신의 저장소에 기록합니다. 실패한 노드는 업스트림 로그를 재생하여 복원할 수 있습니다.

Reference

[1] The Log: What every software engineer should know about real-time data’s unifying abstraction

[2] Jap Kreps