RkBlog

Hardware, programming and astronomy tutorials and reviews.

Sharding w Google App Engine

Opis zastosowania shardingu licznika w celu zwiększenia przepustowości aktualizacji encji.

Tworząc wydajne aplikacje na Google App Engine trzeba uważać na częstotliwość aktualizacji encji. Datastore jest dostosowane do obsługi ogromnych ilości encji, lecz należy pamiętać że można zaktualizować pojedynczą encję lub grupę encji około 5 razy na sekundę. Jest to szacunkowa szybkość aktualizacji encji, zależy ona od wielu czynników jak ilość właściwości, rozmiaru encji, ilości indeksów do przebudowania. Mimo ograniczeń szybkości aktualizacji jednej encji lub grupy encji datastore świetnie radzi sobie z wieloma współbieżnymi żądaniami na różnych encjach. Agregując aktualizację po różnych encjach oferuje znacząco lepszą szybkość aktualizacji.

Powiedzmy że mamy gdzieś encję zawierającą sumę komentarzy, ilości oddanych głosów w ankiecie itp.:

class Counter(db.Model):
     count = db.IntergerProperty()

Mając pojedynczą encję będącą licznikiem w takim przypadku może dojść do sytuacji, gdy aktualizacje będą napływały zbyt szybko, spiętrzą się i zaczną zawodzić (timeout). Rozwiązaniem takiego problemu będzie sharding licznika - odczytywanie encji jest bardzo szybkie i efektywne (bo ostatnio stworzone, zmodyfikowane encje trzymane są w pamięci), więc można zastosować kilka encji do przetrzymywania sumy. Będzie to szybsze rozwiązanie bo inkrementując losowo wybrany shard rozproszymy żądania aktualizacji na kilka encji, którą mogą być aktualizowane niezależnie i współbieżnie. By otrzymać sumę - pobieramy wszystkie encji i sumujemy ich wartości, oto przykład:

from google.appengine.ext import db
import random

class SimpleCounterShard(db.Model):
  """Shardy licznika"""
  count = db.IntegerProperty(required=True, default=0)

# licznik dzielimy na 20 shardów (20 niezależnych encji)
NUM_SHARDS = 20

def get_count():
  """Pobranie sumy z danego licznika"""
  total = 0
  for counter in SimpleCounterShard.all():
    total += counter.count
  return total
   
def increment():
  """Inkrementacja licznika poprzez aktualizację losowej encji-sharda"""
  def txn():
    index = random.randint(0, NUM_SHARDS - 1)
    shard_name = "shard" + str(index)
    counter = SimpleCounterShard.get_by_key_name(shard_name)
    if counter is None:
      counter = SimpleCounterShard(key_name=shard_name)
    counter.count += 1
    counter.put()
  db.run_in_transaction(txn)
W get_count iterujemy po wszystkich encjach-shardach i sumujemy indywidualne wartości razem - otrzymując interesującą nas sumę. W increment() musimy odczytać, zinkrementować i zapisać jedną z encji-sharda wybraną losowo. Musi to także być wykonane w transakcji żeby inkrementacje się nie nadpisywały.

Powyższy przykład jest dobrym punktem zaczepienia do nauki i zrozumienia tego typu rozwiązań. Bardziej przydatny byłby algorytm shardów pozwalający tworzyć nazwane shardy w locie, jak i zwiększać ilość shardów także dynamicznie. Jak wiele rzeczy w GAE - powinien także używać memcache. Brett Slatkin w prezentacji "Building Scalable Web Applications with Google AppEngine" zaprezentował właśnie taki przykład shardów. Oto on:

from google.appengine.api import memcache
from google.appengine.ext import db
import random

class GeneralCounterShardConfig(db.Model):
  """Śledzenie ilości shardów dla każdego nazwanego sharda."""
  name = db.StringProperty(required=True)
  num_shards = db.IntegerProperty(required=True, default=20)


class GeneralCounterShard(db.Model):
  """Shardy dla każdego nazwanego sharda"""
  name = db.StringProperty(required=True)
  count = db.IntegerProperty(required=True, default=0)
 
           
def get_count(name):
  """Pobranie wartości dla danego nazwanego sharda.
 
  Parametry:
    name - Nazwa licznika
  """
  total = memcache.get(name)
  if total is None:
    total = 0
    for counter in GeneralCounterShard.all().filter('name = ', name):
      total += counter.count
    memcache.add(name, str(total), 60)
  return total

 
def increment(name):
  """Inkrementacja wartości dla podanego licznika
 
  Parametry:
    name - Nazwa licznika
  """
  config = GeneralCounterShardConfig.get_or_insert(name, name=name)
  def txn():
    index = random.randint(0, config.num_shards - 1)
    shard_name = name + str(index)
    counter = GeneralCounterShard.get_by_key_name(shard_name)
    if counter is None:
      counter = GeneralCounterShard(key_name=shard_name, name=name)
    counter.count += 1
    counter.put()
  db.run_in_transaction(txn)
  memcache.incr(name)

 
def increase_shards(name, num): 
  """Zwiększa ilość shardów dla danego licznika
  Nie może zmniejszyć ilości shardów
 
  Parametry:
    name - Nazwa licznika
    num - Ile shardów użyć
   
  """
  config = GeneralCounterShardConfig.get_or_insert(name, name=name)
  def txn():
    if config.num_shards < num:
      config.num_shards = num
      config.put()   
  db.run_in_transaction(txn)

Sharding to jedna z wielu ważnych technik budowania skalowalnych aplikacji (nie tylko na GAE) i warto o niej pamiętać projektując aplikacje na GAE.

Na podstawie Sharding counters, zobacz także prezentację Building Scalable Web Applications with Google AppEngine.
RkBlog

11 August 2009;

Comment article