강력한 일관성을 위한 데이터 구조화

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

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

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

strong consistency를 위해 데이터를 구조화하는 방법을 이해하기 위해 단순한 방명록 애플리케이션에 두 가지 다른 접근 방식을 적용한 결과를 비교해 보겠습니다. 첫 번째 방식은 생성된 각 항목에 새로운 루트 항목을 만듭니다.

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;
}

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

protected List<Entity> listGreetingEntities(DatastoreService datastore) {
  Query query = new Query("Greeting").addSort("date", Query.SortDirection.DESCENDING);
  return datastore.prepare(query).asList(FetchOptions.Builder.withLimit(10));
}

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

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

protected Entity createGreeting(
    DatastoreService datastore, User user, Date date, String content) {
  // String guestbookName = "my guestbook"; -- Set elsewhere (injected to the constructor).
  Key guestbookKey = KeyFactory.createKey("Guestbook", guestbookName);

  // Place greeting in the same entity group as guestbook.
  Entity greeting = new Entity("Greeting", guestbookKey);
  greeting.setProperty("user", user);
  greeting.setProperty("date", date);
  greeting.setProperty("content", content);

  datastore.put(greeting);
  return greeting;
}

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

protected List<Entity> listGreetingEntities(DatastoreService datastore) {
  Key guestbookKey = KeyFactory.createKey("Guestbook", guestbookName);
  Query query =
      new Query("Greeting", guestbookKey)
          .setAncestor(guestbookKey)
          .addSort("date", Query.SortDirection.DESCENDING);
  return datastore.prepare(query).asList(FetchOptions.Builder.withLimit(10));
}

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