데이터 중심 애플리케이션 설계
‘데이터 중심 애플리케이션 설계’ 온오프라인 스터디 내용과 기타 참고자료를 정리한 내용입니다.
Intro
엄청난 양의 트래픽, 개발주기 단축, 자유 오픈소스 소프트웨어, 병렬처리의 증가, 서비스형 인프라(IaaS), 고가용성 요구 등으로 기술은 발전했습니다. 이러한 기술의 발전과 더불어 단순히 계산 중심적인 애플리케이션이 아닌 데이터의 양, 복잡성, 데이터의 변화 속도 등을 고려하는 데이터 중심적 애플리케이션이 가능해졌습니다.
- 하둡(Hadoop): 대규모 데이터 처리를 위한 오픈 소스 프레임워크로, 분산 저장과 처리를 지원합니다.
- 스파크(Spark): 대규모 데이터 처리 및 분석을 위한 클러스터 컴퓨팅 프레임워크로, 빠른 속도의 데이터 처리를 제공합니다.
- NoSQL: 관계형 데이터베이스 모델을 따르지 않는 다양한 형태의 데이터 저장소로, 확장성과 유연성이 강조됩니다.
- 빅데이터: 기존 데이터베이스 도구로 처리하기 어려운 대규모 데이터 세트로, 다양한 유형과 소스에서 생성될 수 있습니다.
- 샤딩(Sharding): 데이터를 여러 서버에 분산 저장하여 데이터베이스의 성능을 향상시키는 기술로, 데이터를 조각내어 저장합니다.
- 최종적 일관성(Eventual Consistency): 분산 시스템에서 일정 시간이 지난 후에는 모든 복제본이 동일한 상태가 되는 일관성 모델입니다.
- ACID: 트랜잭션의 속성을 나타내는 네 가지 속성인 원자성(Atomicity), 일관성(Consistency), 고립성(Isolation), 지속성(Durability)을 의미합니다.
- CAP 정리(CAP Theorem): 분산 시스템에서 일관성(Consistency), 가용성(Availability), 분할 내성(Partition Tolerance) 중에서 두 가지만 보장할 수 있다는 이론입니다.
- 맵리듀스(MapReduce): 대규모 데이터 세트를 병렬 처리하기 위한 프로그래밍 모델로, 분산 컴퓨팅 환경에서 사용됩니다.
이번 스터디에서 개인적으로 데이터 중심 애플리케이션과 실제로 대형 웹사이트와 서비스들이 어떻게 돌아가는지에 대한 부분을 중점적으로 이해하고 넘어가는 것을 목표로 하고 있습니다.
Part 1. 데이터 시스템의 기초
01. 신뢰할 수 있고 확장 가능하며 유지보수하기 쉬운 애플리케이션
데이터 중심 애플리케이션은 데이터를 보관하는 데이터베이스, 한 번 불러온 데이터를 기억해서 다시 읽기 없이 수행결과를 가지고 오는 캐시, 사용자가 키워드로 검색하거나 필터링을 가능하게 하는 검색 색인, 비동기 처리를 위해 다른 프로세스로 메시지를 보내는 스트림 처리, 주기적으로 다량의 데이터를 분석하는 일괄 처리(batch processing)를 필요로 합니다.
요즘은 메시지 큐로 사용하는 데이터 스토어인 레디스, 데이터베이스처럼 지속성을 보장하는 아파치 카프카와 같이 그 경계가 흐려지고 있기 때문에 데이터 시스템이라는 포괄적 용어를 사용합니다. 또한 애플리케이션의 task가 많아짐에 따라 여러 도구를 사용하게 됩니다. 애플리케이션 관리 캐시 계층과 full-text 검색 서버의 캐시나 색인을 유지하는 것은 모두 애플리케이션 코드로 연결되어 있습니다. 즉, 개발자는 이제 애플리케이션 개발자뿐만 아니라 데이터 시스템 설계자이기도 합니다.
- 엘라스틱서치(Elasticsearch): 분산형 검색 및 분석 엔진으로, 대용량의 데이터를 신속하게 검색하고 분석하는 데 사용됩니다. 오픈 소스이며, 실시간 데이터 검색과 분석을 지원합니다.
- 솔라(Solr): 엘라스틱서치와 유사한 오픈 소스 검색 플랫폼으로, Apache Lucene 기반으로 만들어졌습니다. 대용량의 데이터를 색인하고 검색하는 데 사용되며, 고성능의 검색 기능을 제공합니다.
신뢰성(Reliability)
하드웨어나 소프트웨어의 결함, 심지어 인적 오류와 같은 역경에도 시스템은 지속적으로 올바르게 동작해야 합니다. 즉, 결함이 있어도 정상적으로 동작해야 한다는 것인데 여기서 결함은 장애와 동일하지 않습니다. 장애는 사용자에게 필요한 서비스를 제공하지 못하고 시스템 전체가 멈추게 되는 경우를 말합니다. 결함으로 인한 장애가 발생하지 않도록 내결함성 구조를 설계하는 것이 좋은데 넷플릭스 카오스 몽키가 이런 접근 방식의 한 예입니다.
- 넷플릭스 카오스 몽키(Chaos Monkey)
- 카오스 멍키는 어떤 장애가 발생할지 모르는 상황을 위해 서비스를 공급하는 인스턴스를 무작위로 셧다운시켜버리는 것으로 지금은 운영 이슈 테스트를 위한 개발 원칙으로 자리잡았습니다. 넷플릭스는 자사의 내결함성 전략을 ‘실패를 피하는 가장 좋은 방법은 지속적으로 실패하는 것’으로 삼고 카오스 엔지니어링을 수행한다고 합니다.
- 스프링부트 카오스 멍키 라이브러리
하드웨어 오류
하드디스크의 평균 장애 시간은 약 10~50년으로 보고됐으며, 10,000개의 디스크로 구성된 저장 클러스터는 평균적으로 하루에 한 개의 디스크가 죽는다고 예상해야 합니다. 시스템 장애율을 줄이기 위해서는 중복을 추가해 구성할 수 있었으며 일부 고가용성이 필요한 소수의 애플리케이션에 한해 다중 장비 중복으로 만들었습니다.
- 중복을 추가한다는 것은 디스크 RAID 구성, 서버의 이중 전원 디바이스, 핫 스왑이 가능한 CPU, 데이터센터의 건전지와 예비 전원용 디젤 발전기와 같은 것을 의미합니다. 최근에는 클라우드 플랫폼과 같이 다중 장비를 구성하는 것이 쉬워졌지만 각 장비 인스턴스의 신뢰성보다 유연성과 탄력성을 우선적으로 할 경우에 적합한 방식입니다.
- 어느 정도 규모가 있는 시스템에서는 단일 서버 시스템보다는 순회식 업그레이드가 가능하도록 장비 장애를 견딜 수 있는 설계가 필요합니다.
소프트웨어 오류
다수의 하드웨어 구성요소에 장애가 발생하지 않는 경우도 있고, 시스템 내 체계적 오류로 일부 프로세스의 공유 자원을 과도하게 사용하므로 다른 구성요소의 장애가 발생하는 경우도 있습니다. 이런 경우는 예상도 어렵기 때문에 테스트, 프로세스 격리, 모니터링 등으로 지속적으로 확인할 필요가 있습니다.
- 리눅스 커널의 버그로 많은 애플리케이션이 일제히 멈춰버린 원인이 된 2012년 6월 30일 윤초
인적 오류
운영자의 설정 오류 등과 같은 것으로 다음과 같은 방식으로 신뢰성을 높일 수 있습니다. (버그는 생산성 저하의 원인이 되고, 사이트 중단은 매출 손실과 명성에 타격을 주기 때문)
- 인터페이스, API와 같은 추상화
- 실수로 인한 장애가 발생할 가능성이 있는 부분을 분리해 실제 사용자에게 비 프로덕션 샌드박스 제공
- 단위테스트와 통합테스트
- 정상적인 동작에서 발생하지 않는 코너 케이스를 다루는데 용이
- 데이터 재계산 도구를 제공
- 성능지표와 명확한 모니터링 대책 마련
확장성(Scalability)
시스템의 데이터 양, 트래픽 양, 복잡도가 증가하면서 이를 처리할 수 있는 적절한 방법이 있어야 합니다. 규모가 커짐에 따라 부하가 증가되기 때문인데, 단순히 확장가능한지 여부보다는 특정 방식으로 시스템이 커졌을 때 대처방안, 추가 부하를 다루기 위한 계산 자원의 투입 등과 같은 질문을 할 필요가 있습니다. 부하는 부하 매개변수로 나타낼 수 있는데 예를 들면, 웹 서버의 초당 요청 수, 데이터베이스의 읽기 대 쓰기 비율, 대화방의 동시 활성 사용자, 캐시 적중률 등이 있습니다.
####### 트위터 사례 트위터의 홈 타임라인을 생각해봅시다. 한 유저가 트윗을 작성했을 때, 이 유저의 팔로워들은 홈 타임라인에서 유저가 작성한 새로운 트윗을 볼 수 있어야 합니다.
- 각 팔로워들이 홈 타임라인을 여는 시점에 자기가 팔로우 하는 사람들의 트윗을 찾고 시간순으로 나열해서 보여주는 방식을 택할 수 있을 것 입니다.
- 캐시를 이용하는 방법도 있습니다. 빠르게 홈 타임라인을 보여주는 것이 중요하고 그 데이터를 반복해서 질의해 올 필요가 없다면, 홈 타임라인의 내용을 캐시에 두고 반복적인 요청을 기억해 두었다가 처리할 수 있습니다.
- 실제로 트위터에서 트윗 게시 요청량이 홈 타임라인 읽기 요청량에 비해 훨씬 적기 때문에, 트윗을 작성하면 그 유저를 팔로우하는 사람들의 각각의 홈 타임라인 캐시에 최신 트윗을 삽입하는 방식으로 쓰기 시점에 더 많은 일을 하도록 했습니다.
하지만 인플루언스와 같이 팔로우가 많은 사람이 트윗을 쓴다면 어떨까요?
트윗 작성시점에 쓰기 요청이 많아지고 적시에 트윗을 전송해야한다는 목표를 달성하기 어려울 수 있습니다. 따라서 트위터는 최종적으로 대부분 2의 방법대로 트윗의 읽기 작업을 캐시화하되, 특정 사용자에 한하여 팬 아웃에서 제외하고 유명인의 트윗을 팔로우 하는 경우는 다시 1번의 방법처럼 읽는 시점에 홈 타임라인에 합쳐서 제공하는 방식을 택했습니다.
####### 지연시간과 응답시간 응답시간은 클라이언트 과전에서 본 시간으로 요청을 처리하는데 실제 시간 외에도 네트워크 지연과 큐 지연도 포함된 개념입니다. 반면, 지연시간은 요청이 처리되길 기다리는 시간으로 서비스를 기다리며 휴지 상태인 시간을 말합니다.
응답시간을 측정 가능한 값의 분포로 평균 값으로 생각하지만 일반적으로는 백분위를 사용하는 것이 좋습니다. 사용자가 얼마나 오랫동안 기다려야하는지 알려면 중앙값을, 특이 값이 얼마나 좋지 않은지 알아보려면 상위 백분위를 살펴보는 것도 좋습니다. 상위 백분위는 꼬리 지연 시간(tail latency)이라고도 하며, 95분위 응답시간이 1.5초라고 하면 1.5초 미만인 요청이 100개 중에 95개인 것을 말합니다.
- 응답시간은 대체로 요청마다 다양한 값을 얻게 되는데 백그라운드 프로세스의 컨텍스트 스위치, 네트워크 패킷 손실과 TCP 재전송, 가비지 컬렌션 휴지, 디스크에서 읽기를 강제하는 페이지 폴트, 서버 렉의 기계적인 진동 등이 원인이 될 수 있기 때문입니다.
- 서비스 수준 목표(SLO), 서비스 수준 협약서(SLA)
- 선두 차단(head-of-line blocking): 서버에서 후속 요청이 빠르게 처리되더라도 이전 요청이 완료되길 기다리는 시간 때문에 전체적인 응답시간이 느리다고 생각할 수 있습니다.
- 꼬리 지연 증폭: 여러 번의 백엔드 호출로 느린 호출이 되고 응답시간이 느려지게 되는 효과
####### 부하 대응 접근 방식 아키텍처를 결정하는 요소는 읽기의 양, 쓰기의 양, 저장할 데이터의 양, 데이터의 복잡도, 응답 시간 요구사항, 접근 패턴 등이 있습니다.
- 용량 확장(scaling up) » 수직 확장(vertical scaling)
- 규모 확장(scaling out) » 수평 확장(horizontal scaling)
- 탄력적인 시스템은 부하를 예측할 수 없을 만큼 높은 경우에 유용하지만 수동적으로 확장하는 시스템이 더 간단하고 운영상 예상치 못한 일이 더 적습니다.
- 스타트업 초기 단계나 검증되지 않은 제품의 경우에 미래를 갖어한 부하에 대비해 확장하기보다는 빠르게 반복해서 제품 기능을 개선하는 작업이 좀 더 중요합니다.
유지보수성(Maintainability)
모든 사용자가 시스템 상에서 생산적으로 작업할 수 있게 해야 합니다.
- 운영성: 운영팀이 시스템을 원활하게 운영할 수 있게 쉽게 만드는 것
- 단순성: 복잡도를 최대한 제거해 새로운 엔지니어가 이해하기 쉽게 만드는 것
- 우발적 복잡도를 제거하기 위해 추상화를 이용해서 세부 구현 사항을 숨길 수 있습니다.
- 발전성: 엔지니어가 이후에 쉽게 면경할 수 있게 하는 것
02. 데이터 모델과 질의 언어
관계형 모델과 문서 모델
데이터 모델은 데이터의 관계, 흐름에 필요한 처리 과정에 대한 추상화된 모형입니다. 데이터 모델을 통해 이 프로그램이 어떻게 작성되었는지 문제에 대한 접근방법에 대해 알 수 있습니다. 이렇게 데이터 모델을 사용하므로써 계층 간의 복잡성을 숨길 수 있습니다. 즉, 추상화를 통해 데이터를 재사용한다거나 단순화하여 애플리케이션, 데이터베이스, 하드웨어 개발자들이 효율적으로 일할 수 있게 합니다.
관계형 데이터베이스와 NoSQL 관계형 데이터베이스는 관계(relation)을 바탕으로 튜플과 로우를 가지는 데이터베이스입니다. 객체, XML, 네트워크 모델들에도 불구하고 관계형 모델이 여전히 비즈니스 데이터 처리에 우위를 가지고 있습니다. NoSQL은 Not Only SQL로 재해석되어 쓰기 처리량이 높은 경우에 관계형 데이터베이스를 보완할 수 있는 이점을 가지고 있습니다. 현재는 애플리케이션에 따라 관계형 데이터베이스와 NoSQL을 함께 사용해서 다중 저장소 지속성을 지닌 데이터베이스 설계 방식으로 구축하는 방식이 많이 적용되고 있습니다.
객체에 의해 모델 사이의 임피던스 불일치가 발생하는데, 애플리케이션 코드와 질의에 의한 데이터베이스 저장시의 문제를 말합니다. ORM의 지원에도 한계가 있고, 관계형 데이터베이스에서 복잡한 참조 처리로 불필요한 테이블이 다량 생성되기도 합니다. 이 경우, 하나의 결과를 가지고 오기 위해 많은 조인 처리로 성능 또한 저하될 수 있습니다.
문서 지향 데이터베이스 문서 지향 데이터베이스(몽고, 리싱크, 카우치 등)에서는 JSON, XML을 지원해서 스키마보다 더 나은 지역성을 갖을 수 있습니다. 스키마가 없다는 것은 문서에 포함된 필드의 존재를 보장하지 않는 것을 의미합니다. 일반적으로 문서 지향 데이터베이스는 조인에 대한 지원이 부족합니다. 상위 레코드 내에 중첩된 레코드를 저장하는 방식입니다. 관계형의 외래 키와 달리 문서형 모델에서는 문서 참조(document reference)를 사용합니다.
데이터를 위한 질의 언어
SQL은 선언형 질의어인 반면 IMS와 코다실은 명령형 코드를 이용한 질의입니다. 명령형 질의는 특정 순서로 특정 연산을 수행하게 지시하고, 선언형 질의는 종종 병렬 실행에 적합하며 데이터베이스에만 국한되는 것이 아니라 다른 환경에서도 사용될 수 있습니다.
맵듀리스 질의는 대량의 데이터를 처리하기 위한 모델로 일부 NoSQL에서 지원하고 있습니다. 주로 많은 문서를 대상으로 read-only 질의 수행시 사용됩니다. 저수준의 프로그래밍 모델로 주로 분산 시스템에서 사용되며 맵(map) 단계에서는 입력 데이터를 키-값 쌍으로 변환하고 병렬로 동시 처리하며 중간 결과를 생성합니다. 리듀스(reduce) 단계에서는 맵 단계의 중간 결과를 키를 기반으로 그룹화하고 집계합니다. 리듀스 함수를 사용해서 중간 결과를 집계하고 최종 결과를 생성합니다.
그래프형 데이터 모델
그래프형 데이터 모델은 다대다 관계가 일반적인 모델에서 정점과 간선이라는 객체로 이뤄진 데이터 모델을 말합니다. 이는 동종 데이터에 국한되지 않습니다.
속성 그래프 모델
속성 그래프 모델은 고유한 식별자 / 유출 간선 집합 / 유입 간선 집합 / 속성 컬렉션의 정점으로 가지며, 정점은 다른 정점과 간선으로 연결됩니다. 일련의 정점을 따라 앞뒤 방향으로 순회하기에 많은 유연성을 제공합니다. 구조가 다른 경우나 데이터 입도가 다양한 경우도 가능합니다.
사이퍼 질의 언어
속성 그래프를 위한 선언형 질의 언어로 Neo4j 그래프 데이터베이스용으로 제작되었습니다. 정점은 id로 지정하고, 간선을 (꼬리노드 id) -[:간선 label]=>(머리노드 id)
의 형태로 생성합니다. 선언형 언어이기 때문에 질의 최적화기가 가장 효율적인 전략을 자동적으로 선택합니다.
트리플 저장소와 스파클 트리플 저장소는 속성 그래프 모델과 거의 유사하며, 모든 정보를 (주어, 서술서, 목적어)와 같은 구문의 형식으로 나누어 저장합니다. 여기서 트리플의 주어는 그래프의 정점과 동등하며 목적어는 문자열이나 숫자 같은 원시 데이터 타입이거나 그래프의 다른 정점 중 하나를 의미합니다.
1
_:lucy a :Person; :name "Lucy"; :bornIn _:idaho._
스파클 질의 언어는 RDF(Resource Description Framework) 데이터 모델을 사용한 트리플 저장소 질의 언어입니다. 사이퍼보다 먼저 만들어졌고 사이퍼 패턴 매칭으로 유사한 형태를 띕니다.
데이터 로그
스파클이나 사이퍼보다 더 오래된 언어로 서술어로 작성하며 select로 바로 질의하는 것이 아니라 단계를 나눠서 조금씩 질의하는 방식입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
// 데이터 정의
name(namerica, 'North America').
type(namerica, continent).
// rule 정의
within_recursive(Location, name) :- name(Location, name).
within_recursive(Location, name) :- within(Location, Via),
within_recursive(Via, Name).
migrated(Name, BorIn, LivingIn) :- name(Person, Name),
born_in(Person, BornLoc),
...
within_recursive(LivingLoc, LivingIn).
다른 데이터 모델과의 차이
그래프 데이터 모델은 네트워크 데이터 모델과 달리, vertex는 edge로 다른 vertex와 자유롭게 연결되고 고유 id를 사용해 빠르게 탐색이 가능하다는 장점이 있습니다. 정렬 시점 또한 다른데 그래프 모델은 읽기 질의시 유연하게 정렬됩니다. 또한 문서 데이터베이스와 달리 그래프 데이터베이스는 모든 것이 잠재적으로 관련이 있다는 사용 사례를 대상으로 합니다.
03. 저장소와 검색
어떤 데이터베이스를 사용해야 할까?
애플리케이션에 적합한 데이터 엔진을 사용하는 데이터베이스를 고르는 것이 중요합니다. 데이터 엔진에 따라 트랙젹션 처리에 적합하거나 분석에 적합한 것이 다르기 때문입니다. 로그는 연속된 추가 레코드를 말하며 일반적으로 거의 대부분의 데이터베이스에서 사용되는 추가작업 방식입니다. 로그의 형태는 사람이 읽을 수 있는 형태 뿐만 아니라 바이니리 형식의 경우도 포함됩니다.
데이터베이스에는 검색을 용이하게 하는 색인이라는 것도 있는데 부가적인 메타데이터로 기존 데이터와는 별개로 구성되어 있습니다. 색인을 이용하면 모든 데이터를 읽을 필요없이 색인을 이용한 검색으로 해당 위치를 더 빠르게 찾을 수 있습니다. 하지만 쓰기 작업에서 색인도 같이 갱신해야하므로 필요 이상의 오버헤드를 만들 수도 있습니다. 따라서 적절히 트레이드 오프를 고려한 사용이 요구됩니다.
해시 색인
키-값 쌍으로 이뤄져 해시 맵 또는 해시 테이블을 이용해 데이터 파일에서 오프셋을 찾아 해당 값의 위치를 찾을 때 사용합니다. 대표적으로 비트캐스크(Bitcask)가 사용하는 방식으로 값이 자주 갱신되는 경우에 유리합니다. 책에서는 동영상의 url이 키이고, 동영상 클릭 수와 같이 계속 자주 업데이트되어야 하는 경우를 값으로 갖는 경우를 말하고 있습니다.
파일에 추가만 하면 디스크 용량이 커질 수 있어 가장 최근의 값만 저장하는 세그먼트를 사용합니다. 세그먼트를 병합해서 더 작은 컴팩션화를 거친 세그먼트로 만들기도 합니다. 세그먼트는 덮어 쓸 수 없기 때문에 이전 세그먼트는 삭제하고 새로운 세그먼트를 생성합니다. 구현시에는 아래 사항들을 고려해야 합니다.
- 파일 형식 자체는 바이너리 형식을 사용하는 것이 빠르고 간편합니다.
- 키와 관련된 값을 삭제하기 위해서는 특수한 삭제 레코드를 추가해야 합니다,
- 데이테베이스가 재시작되면 인메모리 해시 맵은 손실되므로 다시 처음부터 읽고 각 키 대한 최신 값의 오프셋을 확인해서 복원해야 합니다.
- 데이터베이스는 로그에 레코드를 쓰는 동안에도 죽을 수 있습니다.
- 쓰기에 대한 다중 스레드로 관리할 것인지 순차적인 하나의 쓰기 스레드로 동시성을 관리할지에 대한 부분을 고려해야 합니다.
해시테이블은 키가 너무 많은 경우 디스크 성능을 떨어뜨릴 수 있고, 모든 개별 키를 조회해야 한다는 점에서 효율적이지 않습니다.
SS 테이블과 LSM 색인
SS 테이블은 정렬된 문자 테이블이라고도 하며, 키로 정렬된 형식을 말합니다. 따라서 각 키는 각 세그먼트에서 한 번씩만 나타나야 합니다. 특정 키를 찾고자 하는 경우에 메모리에 있는 색인의 키 오프셋을 확인해서 그 오프셋에 해당하는 디스크에 세그먼트를 확인해서 빠르게 스캔할 수 있습니다. 새로운 키-값 쌍이 들어오는 경우는 기존의 디스크에 있는 정렬된 세그먼트 외에 인메모리에 멤테이블(memtable)을 두고 관리합니다. 멤테이블이 특정 임계값을 넘는 경우 새로운 세그먼트로 디스크에 저장하는 방식으로 저장소 엔진을 만들 수 있습니다. 이 경우 검색은 멤테이블 검색 후 디스크의 최신순 세그먼트를 조회하는 순서로 이뤄지게 됩니다. 데이터베이스 고장으로 메모리에 있는 멤테이블이 손실되는 것을 우려해 디스크에 로그를 두어 복원용으로 사용할 수 있습니다.
LSM 트리는 Log-Structured Merge Tree로 정렬될 파일 병합과 컴팩션 원리를 기반으로 하는 저장소 엔진을 말합니다. 멤테이블에 없는 키를 찾기 위해 오래된 세그먼트까지 조회하는 과정이 필요하기 때문에 최적화를 위해서는 블룸 필터를 추가적으로 사용합니다. 블룸 필터는 키가 데이터베이스에 존재하지 않음을 알려주기 때문에 불필요한 디스크 읽기를 절약할 수 있습니다. SS 테이블의 컴팩션 전략은 크기 계층 컴팩션과 레벨 컴팩션으로 나눌 수 있습니다. 크기 계층 컴팩션은 새롭고 작은 SS 테이블을 상대적으로 오래된 SS 테이블에 병합하고, 레벨 컴팩션은 SS 테이블을 더 작게 나누고 오래된 데이터는 개별 레벨로 이동하기 때문에 디스크 공간을 덜 사용한다는 장점이 있습니다.
B 트리
가장 일반적인 색인 유형으로 고정 크기 블록이나 페이지로 나누고 한 번에 하나의 페이지 읽기 또는 쓰기를 하는 것을 말합니다. 색인에서 키를 찾기 위해서는 root에서 시작하고 키가 해당하는 범위의 leaf page에 도달하는 방식으로 이동하게 됩니다. 하위 페이지를 참조하는 수를 분기 계수라고 합니다. 키 값을 갱신하기 위해서는 해당 키가 속하는 페이지를 찾고 거기에 키를 추가하는 방식으로 하거나, 페이지 공간이 없다면 페이지를 나누고 상위 페이지에 연결하는 방식으로 갱신할 수 있습니다. B 트리의 쓰기 작업은 쓰기 전 로그를 비상시에 대비해 두고, 새로운 데이터를 디스크 상의 페이지 덮어쓰는 방식으로 진행합니다. 동시성 제어를 위해 래치(latch, 가벼운 잠금)로 트리의 데이터 구조를 보호하고 다른 스레드의 영향을 받지 않고 백그라운드에서 처리할 수 있습니다. B 트리는 포인터를 추가하거나 페이지에 키를 축약해서 쓴다거나 하는 방식으로 최적화를 할 수 있습니다.
LSM 트리와 B 트리의 차이
- 일반적으로 LSM 트리가 보통 쓰기에서 빠르고 B 트리가 읽기에서 빠른 편입니다. LSM 트리는 각 컴팩션 단계의 데이터 구조와 SS 테이블을 확인해야 하기 때문입니다.
- LSM 트리는 B 트리보다 쓰기 처리량을 높게 유지할 수 있습니다. LSM 트리가 상대적으로 증폭이 더 낮고 트리에서 여러 페이지를 덮어 쓰는 것이 아니라 순차적으로 컴팩션된 SS 테이블 파일을 쓰기 때문입니다.
- LSM 트리는 압축률이 좋습니다.
- B 트리는 장점은 각 키가 색인의 한 곳에만 정확하게 존재합니다. 로그화 구조화 저장소 엔진은 다른 세그먼트에 같은 키의 다중 복사본이 존재할 수 있습니다.
04. 부호화와 발전
읽기 스키마(스키마리스) 데이터베이스는 스키마를 강요하지 않으므로 다른 시점에 쓰여진 이진 데이터 타입과 새로운 데이터 타입이 섞여 포함될 수 있습니다. 대규모 시스템에서는 노드가 많아서 순회식 업그레이드를 하므로 데이터 타입이나 스키마 변경에 대한 내용을 즉시 반영할 수 없습니다.
즉, 이전 코드와 데이터 타입의 공존으로 인해 호환성이라는 부분이 중요합니다. 상위 호환성은 새로운 코드가 기록한 데이터를 이전 코드도 읽을 수 있어야 하는 부분으로 더 어려운 달성하기 어렵습니다. 반대로 하위 호환성은 기존 코드와 데이터 타입을 알고 있기 때문에 이를 새로운 코드가 이해하고 적용하는 것은 쉽습니다.
이를 해결하기 위한 데이터 부호화 형식들을 살펴봅시다.
데이터 부호화 형식
여기서 말하는 부호화란 인메모리 표현에서 바이트열로의 전환을 말하며, 직렬화 또는 마샬링이라고도 합니다. (자바의 Serializable, 루비의 Marshal) 반대는 복호화(파싱, 역직렬화, 언마셜화)입니다. (책에서는 직렬화라는 말이 트랜잭션 단위와의 혼동을 피하기 위해 부호화로 표현)
XML과 JSON
표준화된 부호화로는 JSON과 XML이 있습니다. 가장 보편적이지만 수를 다룰 때 JSON은 부동소수점 수를 구별하지 않고, XML과 CSV는 수와 숫자의 문자열을 구분할 수 없다는 문제가 있습니다. 둘 다 이진 문자열을 지원하지 않으며 Base64로 부호화 되어 데이터 크기가 증가하는 특징을 갖습니다. 그리고 둘 다스키마를 지원합니다.
크기를 줄이기 위해 이진 부호화는 하는 것이 맞는가에 대한 이야기가 나오는데, 크기가 현저하게 JSON 부호화와 차이 나지 않는다면 가독성을 해칠 정도로 가치가 있지는 않습니다. 메시지팩을 이용한 이진 부호화와 JSON 부호화(그림 4-1.)을 보면 부호화 가독성에 대해 생각했을 때 (예시에 한해서) JSON이 더 적합하게 느껴졌습니다. 현재 회사에서도 이진 부호화를 이용한 경우, 스키마로 이진 부호화됨을 알려주는 형태로 저장되는 데이터와 그냥 의료 표준에 따라 XML 형태로 저장되는 데이터들이 있습니다.
프로토콜 버퍼와 스리프트, 아브로는 스키마를 이용해 이진 부호화 형식을 기술합니다.
프로토콜 버퍼와 스리프트
프로코톨 버퍼는 구글에서 개발했고, 스리프트는 페이스북에서 개발되었습니다. 둘 다 스키마를 정의하고 스키마 구현 클래스를 생헝합니다. 이를 이용해서 스키마의 레코드를 부호화, 복호화할 수 있습니다. 스리프트는 두 가지 이진 부호화 형식을 가지고 있으며, 바이너리프로토콜보다 컴팩트프로토콜을 사용했을 때 필드 이름을 필드 태그로 표기하기 때문에 바이트를 더 줄여서 34바이트로 부호화가 가능해집니다. 프로토콜 버터는 33바이트로 스리프트의 컴팩트프로토콜과 유사하게 부호화됩니다.
스키마 발전을 지원하기 위해 필드 태그의 경우 데이터 인식을 불가능하게 할 수 있어 변경할 수 없습니다. 호환성 측면에서 하위 호환성은 이전 데이터에 새로운 필드를 required 할 수 없기 때문에 optional 처리해야 합니다. 그리고 상위 호환성 측면에서 보면 optional 필드만 삭제가 가능하고 required 필드는 삭제할 수 없습니다. 생각해보면 호환성 부분은 당연하기 때문에 이해만 하고 넘어가면 될 것 같습니다.
프로토콜 버퍼에는 repeatable이라는 표시자가 있어서 이전 데이터를 읽을 때 optional 필드를 마지막 엘리먼트만 보게 됩니다. 스리프트는 전용 목록 데이터타입을 두고 있어 중첩된 목록을 지원합니다.
아브로
스리프트가 하둡에 적합하지 않아 개발되었습니다. 스키마를 사용한다는 것은 동일하지만 JSON 기반 언어를 이용하고 태그 번호가 없으며 가장 짧은 32바이트 부호화가 됩니다. 필드가 데이터타입을 위한 식별 정보가 없으며, 읽기를 위해서는 코드가 정확히 같은 스키마를 사용하는 경우에만 올바르게 복호화가 가능합니다. 아브로의 상위호환성은 새로운 버전의 쓰기 스키마와 예전 버전의 읽기 스키마를 가질 수 있음을 의미합니다. 반대로 하위 호환성은 새로운 버전의 읽기 스키마와 이전 버전의 쓰기 스키마를 가질 수 있음을 의미합니다.
이외에도 특정 데이터베이스 벤더에 특화된 인메모리 데이터 구조로 복호화하는 드라이버(ODBC, JDBC)들이 있습니다.
데이터플로 모드
새로운 버전의 애플리케이션이 기록한 데이터를 이전 애플리케이션이 갱신하는 경우 데이터 유실 가능성이 있습니다. 따라서 마이그레이션 하는 방법도 있지만 비용이 크기 때문에 자주 일어나지는 않고, 대부분 관계형 데이터베이스에서의 null을 기본값으로 하는 스키마의 추가를 허용하고 있습니다. 아니면 스냅샷과 같이 데이터 웨어하우스와 백업으로 데이터 덤프를 만들 수 있습니다.
서비스를 통한 데이터플로
서버와 클라이언트가 사용하는 데이터 부호화는 서비스 API 버전 간 호환이 가능하게 합니다. 웹 서비스에서 REST는 HTTP 원칙을 토대로 한 설계철학이고, SOAP는 네트워크 API 요청을 위한 XML 기반 프로토콜입니다. SOAP는 WSDL 언어로 기술하며 클라이언트가 로컬 클래스와 메소드 호출로 원격 서비스에 접근하는 코드 생성이 가능합니다. 원격 프로시저 호출(RPC)은 네트워크 요청으로 예측이 어렵기 때문에 실패 처리에 대한 대책이 필요합니다.
Part 2. 분산 데이터
Part 3. 파생
11. 스트림 처리
간단하지만 잘 작동하는 시스템을 만들자
일괄 처리는 사전에 입력을 유한한 크기로 한정하지만 스트림 처리는 입력의 크기가 한정되어 있지 않고 입력하는 순서대로 처리가 이뤄지는 것을 의미합니다. 이벤트 발생할 때마다 처리되기 때문에 반영이 빠르고 점진적으로 처리됩니다. 이벤트는 일반적으로 타임스탬프를 포함하며, 예를 들어 사용자의 행동, 로그 한 줄, 장비에 발생한 데이터가 이벤트가 될 수 있습니다. 일괄처리의 파일을 여러 사람이 읽을 수 있듯이 스트리밍에서도 생산자가 생산한 이벤트를 복수의 소비자가 처리할 수 있습니다.
메시징 시스템
메시징 시스템을 두어 다수의 생산자 노드가 같은 토픽으로 메시지를 전달할 수 있고 다수의 소비자 노드가 토픽 하나에서 메시지를 받아 갈 수 있습니다. 발행/구독 모델은 다양한 접근법을 사용합니다. 생산자가 더 빠르게 메시지를 전송한다면 흐름제어 방식으로 큐에 메시지를 버퍼링하거나 메시지를 더 보내지 못하게 할 수도 있습니다.
그럼 스트림 처리에서 이슈를 어떻게 처리해야 할까?
많은 메시지 시스템은 중간 노드를 통하지 않고 생산자와 소비자를 네트워크로 직접 통신합니다. 이 경우 메시지 유식을 고려해야 하고 메시지 누실을 대비해 생산자가 재시도를 하거나 소실되는 메시지가 없도록 디스크에 기록하거나 복제본을 생성해야 합니다. 하지만 재시도에 대한 메시지 버퍼를 잃어버릴 가능성도 고려해야 합니다.
메시지 브로커
메시지 브로커
메시지 큐를 이용하는 대안이 있는데 메시지 스트림을 처리하는데 최적화된 데이터베이스의 일종으로 서버로 구동되고 브로커 서버에 접속해 생산자와 소비자는 메시지를 주고 받게 됩니다. 그 과정에서 생산자는 소비자의 메시지 처리시점을 기다리지 않고, 큐의 상태에 따라 소비자를 많은 시간이 지난 후 이벤트 처리를 하게 될 수도 있습니다. 하지만 직접 스트림 처리와 달리 메시지 브로커를 이용하면 생산자, 소비자의 메시지 전달에 대한 데이터를 한 곳에서 관리할 수 있게 됩니다. 메시지 브로커는 데이터베이스와 유사하지만 영구 데이터 저장을 하는 경우에 적합하지 않습니다. 그리고 작업 큐가 작으며, 특정 패턴과 토픽을 구독해서 소비자가 처리한다는 부분과 임의 질의를 지원하지 않지만 데이터 변화 발생 즉시 클라이언트에 알려준다는 차이가 있습니다. 복수의 소비자가 있는 경우 생산자를 메시지를 로드밸린싱으로 메시지 처리 작업을 병렬화 해서 비용을 분산할 수 있습니다. 또는 팬 아웃 방식처럼 모든 소비자에 메시지를 전달할 수도 있습니다. 소비자가 메시지를 처리할 때 장애가 발생할 수 있기 때문에 확인 응답을 통해 명시적으로 브로커의 메시지 큐를 지울 수 있도록 해야 합니다. 확인 응답을 받지 못한 경우 브로커는 다른 소비자에게 메시지를 전송해서 재시도 하는데 이 때 해당 소비자는 이미 처리하고 있던 작업이 있기 때문에 생산자가 의도한 순서대로 메시지가 처리되지 않을 수 있습니다. 소비자마다 독립된 큐를 두어 독립으로 메시지 순서를 바꾸지 않도록 할 수 있는데 메시지 간 인과성을 고려해야 하는 경우 순서가 매우 중요하기 때문입니다.
로그 기반 메시지 브로커
는 데이터베이스의 지속성과 메시징 시스템의 짧은 지연시간을 가진 알림 기능의 장점을 결합한 조합에서 시작된 개념입니다. 아파치 카프카, 아마존 키네시스 스트림, 트위터의 분산 로그가 이런 방식으로 동작합니다. 생산자가 메시지를 전송하면 메시지는 토픽 파티션 파일에 추가되고 소비자는 순서대로 파티션 파일을 읽습니다. 이 방식은 모든 메시지를 저장하고 여러 장비에 메시지를 파티셔닝해 초당 수백만 개의 메시지를 처리할 수 있고 메시지를 복제해 장애에 대비할 수 있습니다. 파티션은 메시지 오프셋으로 순차적으로 되어 있기 때문에 소비자가 메시지 오프셋을 처리할 때 현재 오프셋 보다 작은 오프셋은 처리된 것으로 알 수 있습니다. 단일 리더 데이터베이스 복제에 쓰이는 로그 순차 번호 방식처럼 사용된다고 생각하면 됩니다. (리더와 팔로우의 데이터 복제시 누락방지)
메시지 처리 비용이 비싸고 메시지 단위로 병렬화 처리하고 싶지만 메시지 순서는 중요하지 않다면 JMS/AMQP 방식의 메시지 브로커가 적합하고, 처리량이 많고 메시지를 처리하는 속도가 빠르지만 메시지 순서가 중요한 경우 로그 기반 접근법이 효과적입니다.
원형 버퍼 상태의 로그는 제한된 크기기 때문에 버퍼가 다 차게 되면 가장 오래된 조각을 버리고 새로운 로그로 채우기 때문에 소비자는 메시지 일부를 잃어버릴 수 있습니다. 그리고 소비자 처리속도가 늦어지는 경우에는 디스크 공간을 추가로 더 사용하게 되고 이는 메모리에서만 유지하는 것과 달리 디스크를 사용하면서 처리 속도가 매우 느려지게 됩니다. 하지만 다른 소비자가 처리하는 파티션과 독립적이기 때문에 영향을 미치지 않습니다.
오래된 메시지를 재생하는 경우에도 메시지 오프셋 정보가 있기 때문에 소비자 클라이언트에서만 현재 오프셋 설정을 변경하는 단순한 방법으로 반복처리를 할 수 있으며 이는 일괄처리와 유사하게 입력 데이터에 영향을 주지 않고 처리할 수 있다는 장점이 있습니다.
데이터베이스와 스트림
메시지와 스트림 아이디어를 데이터베이스에 적용하는 것도 가능합니다. 로그 자체를 데이터베이스에 기록하면 데이터에 변경이 발생했다는 사실을 알 수 있습니다.
데이터베이스에 아이템을 갱신하면 색인, 데이터 웨어하우스도 갱신해야 합니다. 데이터베이스 전체 덤프가 어려운 경우 이중 기록으로 데이터 변화시 애플리케이션 코드에서 각 시스템에 기록하는 방식입니다. 이렇게 하게 되면 경쟁 조건으로 요청이 교차해 데이터베이스와 검색 색인의 기록이 일치하지 않는 문제가 발생할 수 있습니다.
변경 데이터 캡쳐(change data capture, CDC)
변경 데이터 캡처는 이용해 모든 데이터 변화를 관찰하고 이를 다른 시스템으로 복제할 수 있는 형태로 추출하는 과정을 말합니다. 변경 데이터 로그대로 색인, 데이터 웨어하우스에 순서대로 적용하기 때문에 위에서 말한 경쟁 위험을 방지할 수 있습니다. 변경 데이터 캡쳐 역시 또 하나의 뷰로 파생 데이터 시스템입니다. 이렇게 캡쳐를 구현하는 것은 데이터베이스 트리거를 사용하기도 하지만 고장 나기도 쉽고 성능 오버헤드가 크기 때문에 복제 로그 파싱 방식을 사용하는 것이 더 견고한 방식입니다. 로그 캠팩션을 수행하므로써 실제 값을 제거하고 가장 최신 값을 유지하기 때문에 색인과 같은 파생 데이터 시스템을 재구축할 때 컴팩션된 로그 토픽의 오프셋 순서대로 모든 키를 스캔해서 전체 복사본을 얻을 수 있는 방법도 있습니다. (아파치 카프카에서 제공하는 기능)
이벤트 로그
이벤트 소싱은 이벤트 로그를 모두 저장하는 개발 아키텍처 패턴을 말합니다. (이벤트 소싱에서 애플리케이션 상태는 이벤트 로그를 적용하므로써 유지됩니다.) DDD 커뮤니티에서 개발한 기법인 이벤트 소싱은 이벤트 로그를 저장하지만 변경 데이터 캡쳐와 달리 애플리케이션 수준에서 발생한 일을 반영하고 로그 데이터를 추가할 뿐 삭제, 갱신 등은 권장하지 않습니다. 그리고 재현하면 현재 시스템 상태를 재구성하지만 이벤트 전체 히스토리를 필요로 합니다.
이벤트 자체는 생성 시점의 사실이며 사용자 요청 자체인 명령과는 구별됩니다. 이벤트가 발생되면 소비자는 거절할 수 없으므로 명령이 왔을 때 이를 동기적으로 유효성 검증을 하고 직렬성 트랜잭션으로 명령을 검증해 이벤트를 발생시킵니다.
일련의 이벤트들은 모든 변경 가능 상태를 이벤트 로그로부터 파생됐다고 생각할 수 있으며 각 캐시의 부분집합은 로그로부터 가져온 각 레코드와 색인의 최신 값을 말합니다. 이는 불변 이벤트는 현재 상태보다 많은 정보를 제공합니다. 예를 들어 금융거래에서의 잘못된 거래 내역들을 원장에 기록하고 환급하는 등 모든 내역을 기록하는 것과 유사합니다. 따라서 현재 상태의 이벤트 로그는 동시성 제어 측면에서 단순하고 한 장소에서 한 번의 쓰기로 원자적으로 만드는 것이 가능해집니다.
하지만 상황에 따라 이렇게 모두 기록된 이벤트 로그를 이용해서 최신 상태만을 가지고 오는 것은 비효율적일 수 있습니다. 데이터를 쓰는 형식과 읽는 형식을 분리해 다양한 읽기 뷰를 허용한다는 개념을 명령과 질의 책임의 분리(CORS)라고 합니다. 데이터 쓰기 최적화된 이벤트 로그에서 읽기 최적화된 애플리케이션 상태로 전환 가능하면 정규화와 비정규화에 관한 논쟁은 의미가 거의 없습니다. 읽기 최적화된 뷰는 데이터를 비정규화하는 것이 전적으로 합리적입니다.
- 특히 읽기 수가 쓰기 수보다 훨씬 큰 경우
- 개발자 중 한 팀은 쓰기 모델에 포함되는 복잡한 도메인 모델에 집중하고 또 한 팀은 읽기 모델과 사용자 인터페이스에 집중할 수 있는 시나리오.
- 시스템이 시간이 지나면서 진화할 것으로 예상되어 여러 버전의 모델을 포함할 수 있거나 비즈니스 규칙이 정기적으로 변하는 시나리오.
스트림 처리
스트림 처리는 이벤트에서 데이터를 꺼내서 데이터베이스, 캐시 등에 기록하고 질의하는 방식으로 이용하기도 하고 이벤트 자체를 사용자에게 직접 보내거나, 하나 이상의 출력 스트림을 생산할 수도 있습니다. 이렇게 파생 스트림을 생성하는 경우 코드 조각을 연산자나 작업이라고 합니다.
스트림의 입력은 일괄처리와 달리 제한이 없으므로 장애 발생에 대한 모니터링이 필수적입니다. 모니터링 시스템은 금융 시장의 가격변화나 기계 상태의 오작동을 모니터링하는 등 상황에서 유용하게 사용됩니다. 복잡한 이벤트 처리를 위해 CEP를 이용해 특정 이벤트 패턴을 검색할 수 있습니다. 이벤트가 매치하는 경우 이벤트 패턴의 세부사항을 포함하는 복잡한 이벤트를 방출합니다. 이벤트 질의를 오랫동안 저장하고 이벤트 질의를 찾는 다는 점에서 일반적인 데이터베이스 데이터 질의와는 상반됩니다.
분석을 통해 대량 이벤트를 집계하고 통계적 지표를 얻을 수 있습니다. 일반적으로 고정된 시간 간격을 기준으로 계산하며 확률적 알고리즘으로 블룸 필터, 하이퍼로그로그 등을 사용해 근사 결과를 제공합니다.