Datastore로 strong consistency와 eventual consistency 간에 균형 유지
일관된 사용자 환경을 제공하고 eventual consistency 모델을 활용하여 대규모 데이터세트에 맞게 확장
이 문서에서는 만족스러운 사용자 경험을 위해 strong consistency를 달성하는 동시에 대규모의 데이터와 사용자를 처리할 수 있도록Datastore의 eventual consistency 모델을 채용하는 방법을 설명합니다.
이 문서는Datastore에서 솔루션을 빌드하려 하는 소프트웨어 설계자와 엔지니어를 위해 마련되었습니다. Datastore와 같은 비관계형 시스템보다 관계형 데이터베이스에 좀 더 익숙한 개발자에게 도움을 주기 위해 이 문서는 관계형 데이터베이스와 유사한 개념에 대해서도 언급합니다. 이 문서에서는 Datastore에 대한 기본적인 지식이 있다고 가정합니다. Datastore를 시작하는 가장 쉬운 방법은 지원 언어 중 하나를 사용하여 Google App Engine을 사용하는 것입니다. App Engine을 사용해 본 적이 없으면 이러한 언어 중 하나에 해당하는 시작 가이드 및 데이터 저장 섹션을 먼저 읽어보는 것이 좋습니다. 예시 코드 조각에 Python이 사용되었지만 이 문서를 이해하는 데 Python에 대한 전문 지식은 필요하지 않습니다.
참고: 이 문서의 코드 스니펫은 Datastore용 Python DB 클라이언트 라이브러리를 사용하지만 더 이상 권장되지 않습니다. 새로운 애플리케이션을 빌드하는 개발자에게는 NDB Client Library 사용을 적극 권장합니다. 기존 클라이언트 라이브러리에 비해 Memcache API를 통한 자동 항목 캐싱과 같은 여러 이점이 있습니다. 현재 기존 DB 클라이언트 라이브러리를 사용하는 중이라면 DB에서 NDB로의 마이그레이션 가이드를 참조하세요.
목차
NoSQL 및 Eventual Consistency
Datastore의 Eventual Consistency
상위 쿼리 및 항목 그룹
항목 그룹 및 상위 쿼리의 제한사항
상위 쿼리 대안
Full Consistency 달성을 위한 시간 최소화
결론
추가 리소스
NoSQL 및 Eventual Consistency
NoSQL 데이터베이스라고도 하는 비관계형 데이터베이스는 최근에 관계형 데이터베이스의 대안으로 각광받고 있습니다. Datastore는 업계에서 가장 널리 사용되는 비관계형 데이터베이스 중 하나입니다. 2013년 기준Datastore의 월별 트랜잭션 처리량은 4조 5천억 개에 달합니다(Google Cloud Platform 블로그 게시물). Cloud Datastore는 개발자가 데이터를 저장하고 데이터에 액세스할 수 있는 더욱 단순화된 방법을 제공합니다. 유연한 스키마는 객체 지향 스크립트 언어에 자연스럽게 매핑됩니다. 또한 Datastore는 초대형 규모에서 보장되는 고성능과 높은 확장성을 비롯하여, 관계형 데이터베이스에서 최적으로 제공할 수 없는 여러 가지 기능을 제공합니다.
관계형 데이터베이스에 좀 더 익숙한 개발자의 경우, 비관계형 데이터베이스의 일부 특성과 사례가 상대적으로 익숙하지 않아 비관계형 데이터베이스를 활용하는 시스템을 설계하는 것이 어려울 수도 있습니다. Datastore 프로그래밍 모델은 단순하지만 어떤 특성이 있는지 숙지하고 있어야 합니다. Eventual consistency가 바로 이러한 특성 중 하나에 해당하며, eventual consistency를 위한 프로그래밍이 이 문서의 기본 주제입니다.
Eventual consistency란?
Eventual consistency는 항목이 새롭게 업데이트되지 않는다는 전제하에 항목의 모든 읽기 작업이 최종적으로는 마지막으로 업데이트된 값을 반환한다는 것을 이론적으로 보장합니다. 인터넷 DNS(도메인 이름 시스템)는 eventual consistency 모델이 사용된 시스템의 예로 잘 알려져 있습니다. DNS 서버가 항상 최신의 값을 반영하는 것은 아니며, 이러한 값들은 인터넷상의 수많은 디렉터리에서 캐싱되고 복제됩니다. 수정된 값을 모든 DNS 클라이언트와 서버에 복제하려면 어느 정도의 시간이 소요됩니다. 하지만 DNS 시스템은 인터넷의 근간을 이루는 요소로 자리잡은 매우 성공적인 시스템입니다. DNS는 가용성이 매우 높으며 엄청난 확장성이 증명되었고, 인터넷 전체에서 수천만 대 기기의 이름 조회를 가능하게 하고 있습니다.
그림 1은 eventual consistency를 고려한 복제의 개념을 보여줍니다. 이 다이어그램에서 복제본을 읽는 것은 언제든 가능하지만 일부 복제본은 특정 시점에 상위 노드에 쓰여진 값과 일치하지 않을 수 있음을 볼 수 있습니다. 다이어그램에서 노드 A는 상위 노드이며 노드 B와 C는 복제본입니다.
반대로 기존의 관계형 데이터베이스는 즉각적 일관성이라도 하는 strong consistency 개념을 토대로 설계되었습니다. 즉, 업데이트 즉시 조회된 데이터는 항목을 보는 모든 사용자에게 일관성 있게 표시됩니다. 이러한 특성은 관계형 데이터를 사용하는 많은 개발자에게 기본적인 전제가 되어 왔습니다. 하지만 strong consistency를 얻기 위해서는 개발자가 애플리케이션의 확장성과 성능을 어느 정도 포기해야 합니다. 간단히 말해 업데이트 또는 복제 프로세스 도중에는 데이터를 잠금 설정하여 다른 프로세스에서 동일한 데이터를 업데이트하지 않도록 해야 합니다.
그림 2에는 strong consistency에서의 배포 토폴로지 및 복제 프로세스에 대한 개념적 묘사가 나와 있습니다. 이 다이어그램에서는 복제본이 항상 상위 노드와 일치하는 값을 가지지만 업데이트가 완료되기 전까지 이 값에 액세스할 수 없다는 점을 확인할 수 있습니다.
strong consistency와 eventual consistency 간 균형 유지
비관계형 데이터베이스는 최근에 와서 주목받고 있으며, 특히 높은 확장성과 높은 가용성을 갖춘 성능을 요구하는 웹 애플리케이션에서 자주 사용되고 있습니다. 비관계형 데이터베이스는 개발자가 각 애플리케이션에서 strong consistency와 eventual consistency 사이에서 최적의 균형을 맞출 수 있도록 해줍니다. 개발자는 이를 통해 두 일관성의 장점을 결합하여 활용할 수 있습니다. 예를 들어 '친구 목록에서 특정 시간에 온라인 상태인 사용자 확인하기' 또는 '게시물에서 몇 명의 사용자가 +1을 눌렀는지 확인하기'는 strong consistency를 필요로 하지 않는 사용 사례에 해당합니다. 이러한 사용 사례에서 eventual consistency를 활용하면 확장성과 성능을 확보할 수 있습니다. strong consistency를 필요로 하는 경우로는 '사용자가 결제 프로세스를 완료했는지' 또는 '게임 플레이어가 한 전투 세션 동안 획득한 포인트' 정보 등의 사용 사례를 예시로 들 수 있습니다.
앞선 예를 일반화하면, 매우 많은 항목이 포함된 사용 사례에서는 보통 eventual consistency가 최적의 모델임을 알 수 있습니다. 쿼리가 대량의 결과를 반환하는 경우에는 특정 항목을 포함하거나 제외해도 사용자 경험에 영향을 미치지 않을 가능성이 높습니다. 반면에 항목 수가 적고 맥락이 제한된 사용 사례에서는 strong consistency가 필요하다는 것을 알 수 있습니다. 사용자가 맥락을 통해 포함하거나 제외할 항목을 알 수 있으므로 사용자 환경이 영향을 받게 됩니다.
따라서 개발자는 Datastore의 비관계적 특성을 이해해야 합니다. 다음 섹션에서는 eventual consistency와 strong consistency 모델을 조합하여 확장 가능하고 가용성과 성능이 뛰어난 애플리케이션을 제작하는 방법을 설명합니다. 이 과정에서 만족스러운 사용자 환경을 위한 일관성 요구사항 역시 충족시킬 수 있습니다.
Datastore의 eventual consistency
strong consistency로 데이터를 보려는 경우 올바른 API를 선택해야 합니다. 표 1에는 다양한 Datastore 쿼리 API와 각 API에 해당하는 일관성 모델이 나와 있습니다.
Datastore API |
항목 값 읽기 |
색인 읽기 |
---|---|---|
eventual consistency |
eventual consistency |
|
해당 사항 없음 |
eventual consistency |
|
strong consistency |
strong consistency |
|
키별 조회(get()) |
strong consistency |
해당 사항 없음 |
상위 쿼리가 없는 Datastore 쿼리를 전역 쿼리라고 합니다. 이 쿼리는 eventual consistency 모델에서 작동하도록 설계되었으며, strong consistency를 보장하지 않습니다. 키 전용 전역 쿼리는 항목의 속성 값이 아닌 쿼리와 일치하는 항목의 키만 반환하는 전역 쿼리입니다. 상위 쿼리는 상위 항목을 기준으로 쿼리를 찾습니다. 다음 섹션에서는 각 일관성 동작을 자세하게 설명합니다.
항목 값을 읽는 경우의 eventual consistency
상위 쿼리를 제외하고, 업데이트된 항목 값은 쿼리 실행 시 즉각적으로 표시되지 않을 수 있습니다. 항목 값을 읽는 경우 eventual consistency가 미치는 영향을 이해하려면 플레이어 항목에 점수 속성이 있는 시나리오를 생각해 보세요. 예를 들어 초기 점수 값이 100이라고 가정해 보겠습니다. 시간이 어느 정도 지난 후에는 점수 값이 200으로 업데이트됩니다. 전역 쿼리가 실행되고 결과에 동일한 플레이어 항목이 포함된 경우 반환된 항목의 점수 속성 값은 변경되지 않고 100으로 표시될 수 있습니다.
이러한 동작은 Datastore 서버 간 복제에 의해 발생합니다. 복제는 Datastore의 기반 기술인 Bigtable과 Megastore에서 관리됩니다. Bigtable 및 Megastore에 대한 자세한 내용은 추가 리소스를 참조하세요. 복제는 Paxos 알고리즘으로 실행되며, 이 알고리즘에서는 복제본 대다수가 업데이트 요청을 인식할 때까지 동기식으로 대기합니다. 복제본은 일정 기간이 지난 후에 요청의 데이터로 업데이트됩니다. 일반적으로 이 기간은 짧지만 실제 어느 정도인지 알 수 없습니다. 업데이트가 완료되기 전에 쿼리가 실행되는 경우 쿼리에서 오래된 데이터를 읽어 올 수 있습니다.
대부분의 경우 모든 복제본에서 업데이트가 매우 빠르게 적용됩니다. 하지만 복합적으로 작용하는 경우 일관성을 확보하는 시간을 늘리는 몇 가지 요인이 있습니다. 이러한 요인에는 데이터 센터 간에 다수의 서버 전환 작업과 관련한 데이터 센터 전반의 이슈가 포함됩니다. 이러한 요인이 다양하다는 점을 고려한다면 완전한 일관성을 얻는 데 요구되는 확정적인 시간을 제공하는 것은 불가능하다고 할 수 있습니다.
쿼리가 최신 값을 반환하는 데 필요한 시간은 일반적으로 매우 짧습니다. 다만, 드문 경우이긴 하지만 복제 지연 시간이 증가하게 되면 소요 시간이 더 길어질 수 있습니다. Datastore 전역 쿼리를 사용하는 애플리케이션은 이러한 경우를 원활하게 처리할 수 있도록 유의해서 설계해야 합니다.
항목 값 읽기에서의 eventual consistency는 키 전용 쿼리, 상위 쿼리 또는 키별 조회(get() 메서드)를 사용하여 피할 수 있습니다. 이러한 여러 유형의 쿼리는 아래에서 더욱 자세하게 다루도록 하겠습니다.
색인 읽기에서의 eventual consistency
전역 쿼리 실행 시 색인이 업데이트된 상태가 아닐 수 있습니다. 즉, 항목의 최신 속성 값을 읽을 수 있다 하더라도 쿼리 결과에 포함된 '항목 목록'이 기존 색인 값에 따라 필터링될 수 있습니다.
색인을 읽는 경우에서 eventual consistency의 영향을 이해하려면 새 항목 플레이어가 Datastore에 삽입되는 경우를 가정해 보세요. 항목에 초기 값이 300인 점수 속성이 있습니다. 항목을 삽입한 직후, 키 전용 쿼리를 실행하여 0보다 큰 점수 값이 포함된 모든 항목을 가져옵니다. 이때 최근에 삽입된 플레이어 항목이 쿼리 결과에 표시되리라 예상할 것입니다. 하지만 예상 외로 플레이어 항목이 결과에 표시되지 않는 것을 확인할 수 있습니다. 이러한 상황은 쿼리 실행 시 점수 속성의 색인 표가 새로 삽입한 값으로 업데이트되지 않았을 경우에 발생할 수 있습니다.
Datastore의 모든 쿼리는 색인 테이블을 기준으로 실행되지만 색인 테이블의 업데이트는 비동기식으로 진행됩니다. 모든 항목 업데이트는 기본적으로 2단계로 진행됩니다. 첫 단계는 커밋 단계로, 이 단계에서는 트랜잭션 로그에 쓰기가 수행됩니다. 두 번째 단계에서는 데이터가 기록되고 색인이 업데이트됩니다. 커밋 단계가 성공하면 쓰기 단계가 성공하지만 즉시 수행되지는 않습니다. 색인이 업데이트되기 전 항목을 쿼리하면 일관성이 충족되지 않은 데이터가 표시될 수 있습니다.
이러한 2단계 프로세스로 인해 항목의 최신 업데이트가 전역 쿼리에 표시되기까지 시간 지연이 있게 됩니다. 항목 값 eventual consistency와 마찬가지로 지연 시간은 보통 짧지만 좀 더 오래 걸릴 수도 있습니다(예외적인 상황에서는 몇 분 이상이 소요될 수도 있음).
업데이트 후에도 동일한 상황이 발생할 수 있습니다. 예를 들어 기존 항목 플레이어를 새 점수 속성 값인 0으로 업데이트한 다음 동일한 쿼리를 즉시 실행했다고 가정해 보겠습니다. 새 점수 값인 0으로 인해 이 항목이 제외되므로 쿼리 결과에 나타나지 않으리라고 예상할 것입니다. 하지만 동일한 비동기 색인 업데이트 동작으로 인해 항목이 결과에 계속 포함될 수도 있습니다.
색인 읽기에서의 eventual consistency는 상위 쿼리 또는 키별 조회 메서드를 사용하는 경우에만 피할 수 있습니다. 키 전용 쿼리로는 이 동작을 피할 수 없습니다.
항목 값 및 색인을 읽는 경우의 strong consistency
Datastore에서 항목 값 및 색인 읽기에 strong consistency로 보기를 제공하는 API는 (1) 키별 조회 메서드 및 (2) 상위 쿼리 두 가지가 있습니다. 애플리케이션 로직에서 strong consistency를 요구하는 경우 개발자는 두 메서드 중 하나를 사용하여 Datastore의 항목을 읽어야 합니다.
Datastore는 특별히 이러한 API에 strong consistency를 제공하도록 설계되었습니다. 두 API 중 하나를 호출할 때 Datastore는 복제본과 색인 테이블 중 하나에서 대기 중인 모든 업데이트 내용을 지운 후 조회 또는 상위 쿼리를 실행합니다. 그러면 업데이트된 색인 테이블에 따라 최신 항목 값이 항상 최신 업데이트를 따른 값으로 반환됩니다.
키별 조회 호출은 쿼리와 반대로, 키 또는 키 모음에 의해 지정된 단일 항목이나 항목 모음만을 반환합니다. 즉, Datastore에서는 상위 쿼리만이 필터링 요구사항과 함께 strong consistency 요구사항을 충족시킬 수 있는 유일한 방법이 됩니다. 하지만 항목 그룹을 지정하지 않으면 상위 쿼리가 작동하지 않습니다.
상위 쿼리 및 항목 그룹
문서 서두에서 설명한 것처럼 Datastore의 이점 중 하나는 개발자가 strong consistency와 eventual consistency 간의 균형을 최적으로 맞출 수 있다는 점입니다. Datastore에서 항목 그룹은 strong consistency, 트랜잭션 특성 및 집약성을 갖춘 단위입니다. 개발자는 항목 그룹을 활용하여 애플리케이션 내 항목에서 strong consistency의 범위를 정의할 수 있습니다. 이러한 방식을 통해 애플리케이션은 항목 그룹 내에서 일관성을 유지할 수 있는 한편, 동시에 완전한 시스템으로서의 높은 확장성, 가용성 및 성능을 확보할 수 있습니다.
항목 그룹은 루트 항목과 하위 항목 또는 후속 항목에 의해 형성되는 계층구조입니다.[1] 항목 그룹을 생성하려면 개발자는 상위 경로를 지정해야 하며, 이는 기본적으로 하위 키 앞에 붙는 일련의 상위 키입니다. 그림 3은 항목 그룹의 개념을 보여줍니다. 이 경우에서 'ateam' 키가 있는 루트 항목에는 'ateam/098745' 및 'ateam/098746' 키가 있는 하위 항목 두 개가 있습니다.
항목 그룹 내에서는 다음과 같은 특성이 보장됩니다.
-
Strong Consistency
- 항목 그룹의 상위 쿼리는 strong consistency를 가지는 결과를 반환합니다. 이러한 방식으로, 항목 그룹은 최신 색인 상태에 의해 필터링된 최신 항목 값을 반영합니다.
-
트랜잭션 특성
- 프로그래매틱 방식으로 트랜잭션을 구분하면 항목 그룹은 트랜잭션에 ACID(Atomicity(개별성), Consistency(일관성), Isolation(격리성) 및 Durability(내구성)) 특성을 제공합니다.
-
지역
- 항목 그룹의 항목은 물리적으로 Datastore 서버와 가까운 위치에 저장됩니다. 이는 모든 항목이 키의 사전식 순서에 따라 정렬 및 저장되기 때문입니다. 따라서 상위 쿼리는 최소한의 I/O로 항목 그룹을 빠르게 검사할 수 있습니다.
상위 쿼리는 지정된 항목 그룹에 대해서만 실행되는 특수한 형식의 쿼리이며, strong consistency로 실행됩니다. 백그라운드에서 Datastore는 대기 중인 모든 복제 및 색인 업데이트가 쿼리를 실행하기 전에 적용된다고 가정합니다.
상위 쿼리 예시
이 섹션에서는 항목 그룹과 상위 쿼리를 실제로 사용하는 방법을 설명합니다. 다음 예제에서는 사용자의 데이터 기록을 관리하는 문제를 고민해 볼 것입니다. 특정한 종류의 항목을 추가하고 바로 뒤에 이 종류에 대한 쿼리가 나오는 코드가 있다고 가정해 보겠습니다. 아래의 예제 Python 코드가 이러한 개념을 보여줍니다.
# Define the Person entity class Person(db.Model): given_name = db.StringProperty() surname = db.StringProperty() organization = db.StringProperty() # Add a person and retrieve the list of all people class MainPage(webapp2.RequestHandler): def post(self): person = Person(given_name='GI', surname='Joe', organization='ATeam') person.put() q = db.GqlQuery("SELECT * FROM Person") people = [] for p in q.run(): people.append({'given_name': p.given_name, 'surname': p.surname, 'organization': p.organization})
대부분의 경우 이 코드의 문제는 위의 구문에서 추가된 항목이 쿼리에 의해 반환되지 않는 데 있습니다. 삽입 바로 뒤에 오는 행에 쿼리가 있으므로 쿼리가 실행될 때 색인이 업데이트되지 않습니다. 또한 이 사용 사례의 유효성에도 문제가 있습니다. 굳이 맥락 없이 한 페이지에 모든 사용자 목록을 반환해야 할 이유가 있을까요? 사용자 수가 백만 명일 경우에는 어떻게 될까요? 페이지를 반환하는 데 너무 많은 시간이 소요될 것입니다.
사용 사례의 특성을 통해 맥락을 어느 정도 제공하여 쿼리의 범위를 좁혀야 한다는 것을 알 수 있습니다. 다음 예제에서는 조직을 맥락으로 사용해 보겠습니다. 그러면 조직을 항목 그룹으로 사용하고 상위 쿼리를 실행하여 일관성 문제를 해결할 수 있습니다. 아래의 Python 코드가 이러한 사례를 보여줍니다.
class Organization(db.Model): name = db.StringProperty() class Person(db.Model): given_name = db.StringProperty() surname = db.StringProperty() class MainPage(webapp2.RequestHandler): def post(self): org = Organization.get_or_insert('ateam', name='ATeam') person = Person(parent=org) person.given_name='GI' person.surname='Joe' person.put() q = db.GqlQuery("SELECT * FROM Person WHERE ANCESTOR IS :1 ", org) people = [] for p in q.run(): people.append({'given_name': p.given_name, 'surname': p.surname})
이번에는 GqlQuery에 상위 조직이 지정되었고, 쿼리에서 방금 삽입된 항목을 반환합니다. 이 예제를 확대 적용해 보면, 쿼리의 일부로 상위 항목과 함께 사용자 이름을 쿼리하여 개별 사용자까지 상세하게 찾을 수도 있습니다. 또는 항목 키를 저장한 다음 키별 조회로 상세하게 찾는 데 이를 사용하여 동일한 작업을 수행할 수도 있습니다.
Memcache와 Datastore 간의 일관성 유지
항목 그룹은 Memcache 및 Datastore 항목 간에 일관성을 유지하기 위한 단위로 사용될 수도 있습니다. 예를 들어 각 팀의 사용자 수를 계산하여 Memcache에 저장한다고 가정해 보겠습니다. 캐싱된 데이터가 Datastore의 최신 값과 일치하도록 하려면 항목 그룹 메타데이터를 사용합니다. 메타데이터에서 지정된 항목 그룹의 수를 최신 버전으로 반환하므로 이 버전의 수와 Memcache에 저장된 수를 비교할 수 있습니다. 이 방식을 사용하면 그룹의 모든 개별 항목을 스캔하는 대신, 메타데이터 모음 하나를 읽어 전체 항목 그룹에 있는 모든 항목의 변경사항을 감지할 수 있습니다.
항목 그룹 및 상위 쿼리 제한사항
항목 그룹과 상위 쿼리를 사용한다고 해서 모든 것이 해결되지는 않습니다. 아래에 나와 있는 것처럼, 실제로 이 기술을 일반적으로 적용하기 어렵게 하는 두 가지 문제가 있습니다.
- 각 항목 그룹에서 업데이트 쓰기 횟수가 초당 1회로 제한됩니다.
- 항목 생성 후에는 항목 그룹 관계를 변경할 수 없습니다.
쓰기 한도
한 가지 중요한 문제는 각 항목 그룹에 업데이트 또는 트랜잭션 횟수가 포함되도록 시스템을 설계해야 한다는 것입니다. 제한으로 인해 업데이트는 항목 그룹별로 초당 1회만 지원됩니다.[2] 업데이트 수가 한도를 초과해야 하는 경우에는 항목 그룹에서 성능 병목 현상이 발생할 수 있습니다.
위의 예제에서 각 조직은 조직 내 사용자의 기록을 업데이트해야 할 수도 있습니다. 'ateam'에 1,000명의 사용자가 있고, 각 사용자가 모든 속성에서 초당 1회의 업데이트를 수행한다고 가정해 보겠습니다. 그러면 항목 그룹에 초당 최대 1,000회의 업데이트가 발생할 수 있습니다. 하지만 업데이트 제한이 있기 때문에 이러한 결과를 얻을 수 없습니다. 이는 성능 요구사항을 고려하여 적절한 항목 그룹 디자인을 선택하는 것이 중요하다는 것을 알려줍니다. 이러한 점이 eventual consistency와 strong consistency 간에 최적의 균형을 찾기가 어려운 이유 중 하나입니다.
항목 그룹 관계의 불변성
두 번째 문제는 항목 그룹 관계를 변경할 수 없다는 데 있습니다. 항목 그룹 관계는 키 이름 지정 방식에 따라 정적으로 형성되며, 항목을 생성한 후에는 변경할 수 없습니다. 관계를 변경할 수 있는 유일한 방법은 항목 그룹의 항목을 삭제하고 다시 생성하는 것입니다. 이러한 문제로 인해 항목 그룹을 사용하여 일관성의 범위를 즉석에서 정의하거나 트랜잭션 특성을 동적으로 정의할 수 없습니다. 대신 일관성 및 트랜잭션 특성 범위는 설계 시 정의된 정적 항목 그룹과 밀접하게 연결됩니다.
예를 들어 두 은행 계좌 간에 송금을 구현하고자 하는 시나리오를 가정해 보겠습니다. 이 비즈니스 시나리오에서는 strong consistency와 트랜잭션 특성이 요구됩니다. 하지만 두 계좌를 마지막 순간에 한 항목 그룹으로 그룹화하거나, 전역적 상위 항목에 기반할 수 없습니다. 이러한 항목 그룹이 있는 경우 전체 시스템에 병목 현상을 야기해 다른 송금 요청의 실행에 지장을 주기 때문입니다. 따라서 이러한 방식으로는 항목 그룹을 사용할 수 없습니다.
확장성과 가용성이 높은 방식으로 송금을 구현할 수 있는 대안이 있습니다. 모든 계좌를 하나의 항목 그룹에 배치하는 대신 각 계좌별로 항목 그룹을 생성하는 것입니다. 이렇게 하면 두 은행 계좌 모두에 ACID 업데이트를 보장하는 트랜잭션을 사용할 수 있습니다. 트랜잭션은 최대 25개의 항목 그룹에 ACID 특성을 갖춘 작업 집합을 생성하는 Datastore 기능입니다. 트랜잭션 중에는 키별 조회 및 상위 쿼리처럼 strong consistency를 가진 쿼리를 사용해야 합니다. 트랜잭션 제한 사항에 대한 자세한 내용은 트랜잭션 및 항목 그룹을 참조하세요.
상위 쿼리 대안
Datastore에 저장된 다수의 항목이 포함된 기존 애플리케이션을 보유한 경우 이후에 리팩토링 단계에서 항목 그룹을 통합하는 것이 어려울 수도 있습니다. 이를 수행하려면 모든 항목을 삭제한 후 항목 그룹 관계 내에 추가해야 합니다. 따라서 Datastore의 데이터 모델링에서는 애플리케이션 디자인 초기 단계에서 항목 그룹 디자인을 결정하는 것이 중요합니다. 그렇지 않으면 리팩토링에서 특정 수준의 일관성을 확보하는 데 다른 대안(예: 키별 조회 이후의 키 전용 쿼리 또는 Memcache 사용)으로 제한될 수 있습니다.
키 전용 전역 쿼리 이후의 키별 조회
키 전용 전역 쿼리는 항목의 속성 값이 없는 키만 반환하는 특수한 유형의 전역 쿼리입니다. 값으로 키만 반환되기 때문에 쿼리는 일관성 문제의 소지가 있는 항목 값을 포함하지 않습니다. 키 전용, 전역 쿼리를 조회 메서드와 조합하면 최신 항목 값을 읽을 수 있습니다. 하지만 키 전용 전역 쿼리는 쿼리 시 색인에서 일관성을 확보할 수 없어 항목이 전혀 검색되지 않을 가능성이 있다는 점에 유의하세요. 쿼리 결과는 이전 색인 값의 필터링을 기반으로 생성될 수도 있습니다. 요약하자면 애플리케이션 요구사항에서 쿼리 시 색인 값의 일관성이 확보되지 않아도 된다는 점을 허용하는 경우에만 개발자는 키 전용 전역 쿼리 이후의 키별 조회를 사용할 수 있습니다.
Memcache 사용
Memcache 서비스는 변수가 많지만 strong consistency를 가집니다. 따라서 Memcache 조회와 Datastore 쿼리를 조합하면 대부분의 상황에서 일관성 문제를 최소화할 수 있는 시스템을 구축할 수 있습니다.
예를 들어 각각 점수가 0보다 큰 플레이어 항목 목록을 관리하는 게임 애플리케이션 시나리오를 가정합니다.
- 삽입 또는 업데이트 요청의 경우 Memcache뿐만 아니라 Datastore의 플레이어 항목 목록에도 요청을 적용합니다.
- 쿼리 요청의 경우 Memcache에서 플레이어 항목 목록을 읽고 Memcache에 목록이 없는 경우에는 Datastore에서 키 전용 쿼리를 실행합니다.
Memcache에 캐싱된 목록이 있으면 반환된 목록이 항상 일관적입니다. 항목이 삭제되었거나 Memcache 서비스를 일시적으로 사용할 수 없는 경우 시스템에서는 일관성이 없는 결과를 반환할 수도 있는 Datastore 쿼리에서 값을 읽어야 합니다. 이 기술은 일관성이 없는 것을 조금 허용하는 애플리케이션에 적용할 수 있습니다.
Memcache를 Datastore의 캐싱 레이어로 사용하는 경우 몇 가지 권장사항이 있습니다.
- Memcache 예외 및 오류를 감지하여 Memcache 값과 Datastore 값의 일관성을 유지합니다. Memcache에서 항목을 업데이트할 때 예외가 표시되면 Memcache의 이전 항목을 무효화해야 합니다. 그렇지 않으면 한 항목이 다른 값을 가질 수 있습니다(Memcache에는 이전 값, Datastore에는 새로운 값).
- Memcache 항목에 만료 기간을 설정합니다. 각 항목의 만료 기간을 짧게 설정하여 Memcache 예외 발생 시 일관성 문제의 가능성을 최소화하는 것이 좋습니다.
- 동시 실행 제어를 위해 항목을 업데이트하는 경우 비교 및 설정 기능을 사용합니다. 그러면 동일한 항목을 동시에 업데이트하는 경우 서로 간에 충돌이 발생하지 않도록 하는 데 도움이 됩니다.
항목 그룹으로의 점진적인 마이그레이션
이전 섹션의 제안사항은 일관성이 없는 동작의 가능성을 완화할 뿐입니다. strong consistency가 필요한 경우에는 항목 그룹과 상위 쿼리를 기반으로 애플리케이션을 설계하는 것이 좋습니다. 하지만 기존 데이터 모델과 애플리케이션 로직을 전역 쿼리에서 상위 쿼리로 변경해야 할 수도 있는 기존 애플리케이션의 경우, 마이그레이션하기에 적합하지 않을 수도 있습니다. 마이그레이션을 달성하기 위한 한 가지 방법은 다음과 같은 점진적인 전환 과정을 거치는 것입니다.
- 애플리케이션에서 strong consistency를 요구하는 기능을 파악하고 우선순위를 정합니다.
- 기존 로직을 대체하는 대신, 항목 그룹을 사용하여 기존 로직에 insert() 또는 update() 함수의 새 로직을 작성합니다. 그러면 새 항목 그룹과 이전 항목의 새로운 삽입 또는 업데이트가 적절한 함수에 의해 처리될 수 있습니다.
- 읽기를 위한 기존 로직을 수정하지 않으면, 요청에 새 항목 그룹이 있는 경우 쿼리 함수 상위 쿼리가 먼저 실행됩니다. 항목 그룹이 없는 경우에는 이전 전역 쿼리를 대체 로직으로 실행합니다.
이러한 방식을 사용하면 eventual consistency로 인한 문제 발생 위험을 최소화하는 항목 그룹을 기준으로 기존 데이터 모델에서 새 데이터 모델로 점진적인 마이그레이션을 수행할 수 있습니다. 실질적으로 이러한 접근 방식을 실제 시스템에 적용하는 것은 특정한 사용 사례와 요구사항에 달려 있습니다.
디그레이드 모드로 대체
현재로서는 애플리케이션에서 일관성이 저하되는 경우 이러한 상황을 프로그래매틱 방식으로 감지하는 것이 어렵습니다. 그러나 애플리케이션에서 일관성이 저하된 사실을 다른 방법으로 알게 된 경우, 켜거나 끄는 방식으로 strong consistency를 요구하는 애플리케이션 로직의 일부 영역을 사용 중지하는 디그레이드 모드를 구현할 수도 있습니다. 예를 들어 일관성이 없는 쿼리 결과를 결제 보고서 화면에 표시하는 대신 화면에 유지보수 메시지를 대신 표시할 수 있습니다. 그러면 애플리케이션의 다른 서비스를 계속해서 제공할 수 있으며, 사용자 경험에 미치는 영향을 줄일 수 있게 됩니다.
완전한 일관성을 확보하기 위한 시간 최소화
수백만 명의 사용자 또는 테라바이트 단위의 Datastore 항목이 있는 대규모 애플리케이션에서는 Datastore를 잘못 사용할 경우 일관성 저하가 발생할 수 있습니다. 다음과 같은 경우가 잘못 사용하는 습관에 해당합니다.
- 항목 키의 순차적 번호 지정
- 과도한 색인
이러한 습관은 소규모 애플리케이션에 영향을 미치지 않습니다. 하지만 애플리케이션이 대규모로 확장되면 이러한 습관이 일관성 확보를 위해 필요한 시간을 더 길어지게 할 수 있습니다. 따라서 애플리케이션 설계 초기 단계에 이러한 습관을 버리는 것이 좋습니다.
피해야 할 패턴 1: 항목 키의 순차적 번호 지정
App Engine SDK 1.8.1 출시 이전에는 Datastore에서 일반적으로 연속적인 패턴을 가지는 일련의 작은 정수 ID를 자동 생성된 기본 키 이름으로 사용했습니다. 몇몇 문서에서는 이를 애플리케이션에서 지정한 키 이름이 없는 항목을 생성하기 위한 '기존 정책'이라고 불렀습니다. 이 기존 정책은 순차적인 번호 지정 방식(예: 1000, 1001, 1002)으로 항목의 키 이름을 생성했습니다. 하지만 앞서 설명한 것처럼 Datastore에서는 키 이름의 사전순으로 항목을 저장하므로 이러한 항목이 동일한 Cloud Datastore 서버에 저장될 가능성이 높습니다. 애플리케이션에 매우 많은 트래픽이 몰리면 이러한 순차적인 번호 지정 방식으로 인해 작업이 특정 서버에 집중될 수 있으며, 그 결과 일관성에서 지연 시간이 더욱 길어질 수 있습니다.
App Engine SDK 1.8.1에서는 분산형 ID를 사용하는 기본 정책이 포함된 새로운 ID 번호 지정 방식이 Datastore에 도입되었습니다. 참조 문서를 확인하세요. 이 기본 정책은 거의 균일하게 분산되는 최대 16자리 ID를 임의의 순서로 생성합니다. 이 정책을 사용하면 대규모 애플리케이션의 트래픽이 Cloud Datastore 서버 집합에서 더 원활히 분산되므로 일관성에 소요되는 시간이 줄어들 가능성이 높아집니다. 애플리케이션에서 특별히 기존 정책과 호환되어야 하지 않는 이상 기본 정책을 사용하는 것이 좋습니다.
항목에 키 이름을 명시적으로 설정하는 경우 전체 키 네임스페이스에서 항목에 균등하게 액세스할 수 있도록 이름 지정 방식을 정해야 합니다. 즉, 키 이름의 사전적 순서에 따라 항목 액세스가 정렬되므로 특정 범위에 액세스를 집중해서는 안 됩니다. 그렇지 않으면 순차적 번호 지정과 동일한 문제가 발생할 수 있습니다.
키스페이스에서의 균등하지 않은 액세스 분배를 이해하려면 다음 코드에 나와 있는 것처럼 항목이 순차적 키 이름으로 생성된 경우를 생각해 보시기 바랍니다.
p1 = Person(key_name='0001') p2 = Person(key_name='0002') p3 = Person(key_name='0003') ...
애플리케이션 액세스 패턴은 키 이름의 특정 범위에서 '핫스팟'을 생성할 수 있습니다. 예를 들어 최근 생성된 Person 항목에 액세스가 몰리는 경우가 이에 해당합니다. 이 경우 자주 액세스되는 키는 모두 높은 ID 값을 가지게 되고, 그러면 로드가 특정 Datastore 서버에 집중됩니다.
또는 키스페이스에서의 균등한 분배를 이해하려면 키 이름에 긴 임의 문자열을 사용하는 경우를 생각해 보시기 바랍니다. 이는 다음 예제에서 확인할 수 있습니다.
p1 = Person(key_name='t9P776g5kAecChuKW4JKCnh44uRvBDhU') p2 = Person(key_name='hCdVjL2jCzLqRnPdNNcPCAN8Rinug9kq') p3 = Person(key_name='PaV9fsXCdra7zCMkt7UX3THvFmu6xsUd') ...
이제 최근에 생성한 Person 항목이 키스페이스와 여러 서버에 분산됩니다. 이는 충분한 수의 Person 항목이 존재한다는 가정 하에 이루어집니다.
피해야 할 패턴 2: 과도한 색인
Datastore에서 한 항목을 업데이트하면 이 항목 종류에 대해 정의된 모든 색인이 업데이트됩니다. 애플리케이션에서 많은 커스텀 색인을 사용하면 1회의 업데이트가 색인 표에서는 수십, 수백 또는 수천 회의 업데이트로 이어질 수 있습니다. 대규모 애플리케이션에서 과도한 수의 커스텀 색인을 사용하면 서버 부하가 증가할 수 있으며, 일관성 확보를 위한 지연 시간이 늘어날 수 있습니다.
대부분의 경우 커스텀 색인은 고객지원, 문제해결 또는 데이터 분석 작업과 같은 지원 요구사항에 추가됩니다. BigQuery는 사전 작성된 색인 없이 대규모 데이터세트에서 임시 쿼리를 실행할 수 있는 대규모로 확장 가능한 쿼리 엔진으로서, Datastore보다 복잡한 쿼리가 필요한 고객지원, 문제해결 또는 데이터 분석과 같은 사용 사례에 더 적합합니다.
두 서비스를 모두 활용하는 한 가지 방식은 Datastore와 BigQuery를 조합하여 다양한 비즈니스 요구사항을 충족시키는 것입니다. 핵심 애플리케이션 로직에 필요한 OLTP(온라인 트랜잭션 처리)에는 Datastore를, 백엔드 작업을 위한 OLAP(온라인 분석 처리)에는 BigQuery를 사용하세요. 쿼리에 필요한 데이터를 이동하도록 Datastore에서 BigQuery로 데이터를 지속적으로 내보내는 흐름을 구현해야 할 수도 있습니다.
커스텀 색인용 대안 구현 외에도 색인화되지 않은 속성을 명시적으로 지정하는 방법을 사용할 수도 있습니다. 자세한 내용은 속성 및 값 유형을 참조하세요. 기본적으로 Datastore에서는 항목 종류의 색인할 수 있는 각 속성에 대해 서로 다른 색인 테이블을 생성합니다. 종류에 100개의 속성이 있는 경우 이 종류에 대해 100개의 색인 테이블이 생성되며, 항목 하나가 업데이트될 때마다 100개의 추가 업데이트가 생성됩니다. 따라서 쿼리 조건에 필요 없는 경우 가능한 속성을 색인화하지 않은 상태로 설정하는 것이 좋습니다.
이렇게 색인을 최적화하면 일관성 확보 시간이 늘어날 가능성 감소 이외에도 색인을 아주 많이 사용하는 대규모 애플리케이션에서 발생하는 Datastore 스토리지 비용을 크게 줄일 수 있습니다.
결론
eventual consistency는 개발자가 확장성, 성능 및 일관성 사이에서 최적의 균형을 찾을 수 있게 해주는 비관계형 데이터베이스의 핵심 요소입니다. 따라서 애플리케이션에 최적화된 데이터 모델을 설계하기 위해서는 eventual consistency와 strong consistency 간의 균형을 처리하는 방법을 이해하는 것이 중요합니다. Datastore에서는 항목 그룹과 상위 쿼리를 사용하는 것이 항목의 범위에서 strong consistency를 보장할 수 있는 가장 좋은 방법입니다. 앞서 설명한 제한사항으로 인해 애플리케이션에 항목 그룹을 통합할 수 없는 경우 키 전용 쿼리나 Memcache 사용과 같은 다른 옵션을 고려해 볼 수 있습니다. 대규모 애플리케이션의 경우, 분산된 ID 및 색인화 축소 사용과 같은 권장사항을 적용하여 일관성에 필요한 시간을 줄일 수 있습니다. 또한 Datastore와 BigQuery를 조합하여 복잡한 쿼리를 위한 비즈니스 요구사항을 충족시키고 Datastore 색인의 사용을 최소화하는 것도 중요합니다.
추가 리소스
다음 리소스는 이 문서에서 다룬 주제에 대한 자세한 정보를 제공합니다.
- Google App Engine: 데이터 저장
- Datastore 개요
- Google Cloud Platform 블로그
- Cloud SQL
- Cloud SQL에 Python App Engine 사용
- Bigtable: 구조화된 데이터를 위한 분산 스토리지 시스템
- App Engine 1.5.2 SDK 출시
- Megastore: 대화형 서비스에 확장 가능하고 가용성이 높은 스토리지 제공
[1] 모든 항목 그룹 함수가 키 사이의 관계를 기반하여 구현되므로 루트 또는 상위 항목의 실제 항목을 저장하지 않고 루트 또는 상위 항목의 키 하나만 지정하여 항목 그룹을 생성할 수도 있습니다.
[2] 제한으로 인해 항목 그룹별로 초당 업데이트 1회(트랜잭션 이외) 또는 항목 그룹별로 초당 트랜잭션 1건만 지원됩니다. 한 트랜잭션으로 여러 업데이트를 합하는 경우 최대 트랜잭션 크기가 10MB로, 속도는 Datastore 서버의 최대 쓰기 속도로 제한됩니다.