Strong Consistency를 위한 데이터 구조화

참고: 새로운 애플리케이션을 빌드하는 개발자는 이 클라이언트 라이브러리보다 Memcache API를 통한 자동 항목 캐싱과 같은 여러 이점이 있는 NDB 클라이언트 라이브러리를 사용할 것을 적극 권장합니다. 현재 기존 DB 클라이언트 라이브러리를 사용하는 중이라면 DB에서 NDB로의 이전 가이드를 참조하세요.

Cloud Datastore는 여러 머신에 데이터를 배포하고 광범위한 지리적 영역에 걸쳐 마스터 없는 동기 복제를 통해 고가용성, 확장성, 내구성을 제공합니다. 하지만 이러한 설계에서는 단일 항목 그룹의 쓰기 처리량이 초당 커밋 약 1회로 제한되고 여러 항목 그룹에 걸쳐 있는 쿼리나 트랜잭션에 제한이 있다는 단점이 있습니다. 이 페이지에서는 이러한 제한 사항을 자세히 설명하고 애플리케이션의 쓰기 처리량 요구사항을 충족하면서도 강력한 일관성을 지원하도록 데이터를 구조화하기 위한 권장사항을 설명합니다.

강력한 일관성을 가진 읽기는 항상 최신 데이터를 반환하며, 트랜잭션 내에서 수행되면 일관된 단일 스냅샷에서 가져온 것처럼 보입니다. 하지만 강력한 일관성을 갖거나 트랜잭션에 참여하려면 쿼리가 상위 필터를 지정해야 하며, 트랜잭션에는 항목 그룹이 최대 25개까지 포함될 수 있습니다. 최종 일관성을 가진 읽기는 이러한 제한에 적용되지 않고, 대부분의 경우 그 자체로 충분합니다. 최종 일관성을 가진 읽기를 사용하면 많은 수의 항목 그룹들에 데이터를 배포할 수 있으므로, 여러 항목 그룹에서 커밋을 동시에 실행하여 쓰기 처리량을 더 높일 수 있습니다. 하지만 애플리케이션에 적합한지 여부를 결정하려면 최종 일관성을 가진 읽기의 특성을 이해해야 합니다.

  • 이러한 읽기의 결과는 최신 트랜잭션을 반영하지 않을 수도 있습니다. 이러한 경우가 발생하는 이유는 읽기에서 실행 중인 복제본이 최신 상태인지 확인하지 않기 때문입니다. 그 대신 쿼리 실행 시점에 해당 복제본에 사용 가능한 데이터를 무조건 사용합니다. 복제 지연 시간은 거의 항상 몇 초 미만입니다.
  • 여러 항목에 걸쳐 있는 커밋된 트랜잭션은 일부 항목에만 적용된 것처럼 보일 수 있습니다. 하지만 단일 항목 내에서 하나의 트랜잭션이 부분적으로 적용된 것처럼 보이는 일은 절대 없습니다.
  • 쿼리 결과에 필터 기준에 따라 포함되지 않아야 하는 항목이 포함되고 포함되어야 하는 항목이 제외될 수도 있습니다. 이러한 경우가 발생하는 이유는 항목 자체를 읽을 때의 버전과 다른 버전에서 색인을 읽을 수 있기 때문입니다.

Strong Consistency를 위해 데이터를 구조화하는 방법을 이해하려면 App Engine 방명록 가이드 연습에 나와 있는 두 가지 방식을 비교해 보세요. 첫 번째 접근 방식은 인사말마다 새로운 루트 항목을 만듭니다.

자바 7

protected Entity createGreeting(
    DatastoreService datastore, User user, Date date, String content) {
  // No parent key specified, so Greeting is a root entity.
  Entity greeting = new Entity("Greeting");
  greeting.setProperty("user", user);
  greeting.setProperty("date", date);
  greeting.setProperty("content", content);

  datastore.put(greeting);
  return greeting;
}
import webapp2
from google.appengine.ext import db

class Guestbook(webapp2.RequestHandler):
  def post(self):
    greeting = Greeting()
    ...

그런 다음 최근 10개 인사말의 Greeting 항목 종류를 쿼리합니다.

import webapp2
from google.appengine.ext import db

class MainPage(webapp2.RequestHandler):
  def get(self):
    self.response.out.write('<html><body>')
    greetings = db.GqlQuery("SELECT * "
                            "FROM Greeting "
                            "ORDER BY date DESC LIMIT 10")

하지만 비상위 쿼리를 사용하고 있으므로 이 스키마에서 쿼리를 수행하는 데 사용된 복제본이 쿼리 실행 시점에 새 인사말을 보지 못할 수 있습니다. 그럼에도 불구하고 커밋한 후 몇 초 이내에 비상위 쿼리에 거의 모든 쓰기를 사용할 수 있습니다. 많은 애플리케이션의 경우 현재 사용자의 자체 변경사항이라는 컨텍스트에서 비상위 쿼리의 결과를 제공하는 솔루션을 활용하는 것만으로도 일반적으로 이러한 복제 지연 시간을 완전히 허용 가능한 수준으로 유지하는 데 충분합니다.

강력한 일관성이 애플리케이션에 중요한 경우, 다른 접근 방식은 강력한 일관성을 가진 단일 상위 쿼리에서 읽어야 하는 모든 항목에 동일한 루트 항목을 식별하는 상위 경로로 항목을 쓰는 것입니다.

import webapp2
from google.appengine.ext import db

class Guestbook(webapp2.RequestHandler):
  def post(self):
    guestbook_name=self.request.get('guestbook_name')
    greeting = Greeting(parent=guestbook_key(guestbook_name))
    ...

그러면 공통된 루트 항목으로 식별된 항목 그룹 내에서 강력한 일관성을 가진 상위 쿼리를 수행할 수 있습니다.

import webapp2
from google.appengine.ext import db

class MainPage(webapp2.RequestHandler):
  def get(self):
    self.response.out.write('<html><body>')
    guestbook_name=self.request.get('guestbook_name')

    greetings = db.GqlQuery("SELECT * "
                            "FROM Greeting "
                            "WHERE ANCESTOR IS :1 "
                            "ORDER BY date DESC LIMIT 10",
                            guestbook_key(guestbook_name))

이 접근 방식은 방명록별로 단일 항목 그룹에 쓰기를 수행하여 강력한 일관성을 달성하지만 방명록에 대한 변경이 초당 쓰기 1회 이하로 제한됩니다(항목 그룹에 지원되는 제한). 애플리케이션에서 쓰기 작업이 많이 수행될 가능성이 높은 경우에는 다른 방법을 사용하는 것이 좋을 수도 있습니다. 예를 들어, Memcache에 만료 시간이 있는 최신 게시물을 넣고 Memcache와 Cloud Datastore의 최신 게시물을 섞어서 표시하거나, 쿠키로 최근 게시물을 캐시하고, URL에 상태를 설정하거나, 기타 전혀 다른 방법을 사용할 수도 있습니다. 목표는 사용자가 애플리케이션에 게시물을 게시하는 기간 동안 현재 사용자에게 데이터를 제공하는 캐싱 솔루션을 찾는 것입니다. get, 상위 쿼리, 트랜잭션 내 작업을 수행할 때는 항상 가장 최근에 작성된 데이터가 표시됩니다.