目录

GAE并发竞争处理 Sharding Counters

对于GAE的性能,我一直有些怀疑。例如,它的并发访问量,最大能支持多少?

今天终于看到这篇性能优化说明了:

http://code.google.com/intl/zh-CN/appengine/articles/sharding_counters.html

原理很简单:

GAE使用的是面向对象的数据库(BigTable)。一个条目就是一个文件块。每个文件块每秒大约最多允许更新5次,这是瓶颈所在。

绕过瓶颈的方法很简单:将数据储存在多个条目上(用随机选择的方法,保证每个块的负载均衡)。

上面那篇文章里举了两个例子,都是这个意思。第一个例子是基本的方法演示,第二个例子与memcache配合使用,将结果临时储存起来,减少读的次数,进一步提高性能。

这种方法叫做Sharding Counters,理论上可以达到极高的并发处理速度——一般的应用都足够了。

分片计数器 (转载)

简介

在 Google App Engine 上开发有效的应用程序时,您需要注意上传实体的频率。在缩放 App Engine 的数据存储区以支持大量的实体时,请注意任何单个实体或实体组一秒钟只能上传约五次,这是非常重要的。这是估算值,实体的实际更新率将取决于实体的一些属性,包括实体所具有的属性数量、实体的大小以及需要更新的索引数量。单个实体或实体组对更新的速度有所限制,而 App Engine 在处理不同实体之间分布的多个平行请求方面具有优势,所以总的来说可以处理高得多的更新速度,这就是我们要通过使用分片在这篇文章中实现的目标。 问题是,如果您希望一个实体的上传速度大于一秒五次,该怎么办?例如,您可能要对投票中的票数或注释的数量进行计数等。以该简单示例为例:

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

如果您的单个实体是计数器,并且上传速率太快,那么您可能会遇到到争用的情况,因为序列化写入将会堆叠并开始超时。如果来源是关系数据库,则解决此问题的方式有些有悖常理,而该解决方案依赖于以下事实:从 App Engine 数据存储区读取是非常快速而便宜的,因为最近读取或更新的实体都缓存在内存中。缓解争用的方法是构建一个共享计数器;将该计数器分成 N 不同的计数器。当您希望增加计数器时,可以随机选择一个碎片来增加。如果您要了解计数器的值,则读取所有的计数器碎片,并将它们加到一起。碎片越多,计数器上增量的吞吐量越高。这一技术不仅仅适用于计数器,而一个需要掌握的重要技巧是通过大量的写入在应用程序中定位实体,然后找到将其分片的好方法。 以下是分片计数器的简单实现:

from google.appengine.ext import db
import random
 
class SimpleCounterShard(db.Model):
  """Shards for the counter"""
  count = db.IntegerProperty(required=True, default=0)   
 
NUM_SHARDS = 20
 
def get_count():
  """Retrieve the value for a given sharded counter."""
  total = 0
  for counter in SimpleCounterShard.all():
    total += counter.count
  return total
 
def increment():
  """Increment the value for a given sharded counter."""
  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)
 

在 get_count() 中,我们只对所有的碎片循环 (CounterShard.all()),并增加单独的碎片计数。在 increment() 中,我们需要读取、增加并写入一个随机选择的且需要在事务内部完成的计数器碎片。

请注意,我们很少创建碎片,仅在首次增加碎片的时候创建。正因为很少创建碎片,所以将来需要更多碎片的时候就可以增加它们的数量(但从不减少)。NUM_SHARDS 的值可以加倍到 20,但 get_count() 的结果不变,因为查询仅选择已经添加到数据存储区的碎片,而 increment() 很少创建不在数据存储区中的碎片。

这是一个很适于学习的示例,但计数器的更常见用途的是允许您即时创建命名的计数器,动态地增加碎片的数目,并使用 memcache 来加快对碎片的读取速度。Brett Slatkin 在 Google I/O 大会上的演讲中提到的示例代码即可完成此操作,我已在本文中包含了该代码,以及用来对特殊计数器增加碎片数目的函数。

from google.appengine.api import memcache
from google.appengine.ext import db
import random
 
class GeneralCounterShardConfig(db.Model):
  """Tracks the number of shards for each named counter."""
  name = db.StringProperty(required=True)
  num_shards = db.IntegerProperty(required=True, default=20)
 
 
class GeneralCounterShard(db.Model):
  """Shards for each named counter"""
  name = db.StringProperty(required=True)
  count = db.IntegerProperty(required=True, default=0)
 
 
def get_count(name):
  """Retrieve the value for a given sharded counter.
 
  Parameters:
    name - The name of the counter 
  """
  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):
  """Increment the value for a given sharded counter.
 
  Parameters:
    name - The name of the counter 
  """
  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, initial_value=0)
 
 
def increase_shards(name, num): 
  """Increase the number of shards for a given sharded counter.
  Will never decrease the number of shards.
 
  Parameters:
    name - The name of the counter
    num - How many shards to use
 
  """
  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)

这两个计数器的源在 Google App Engine 示例中均可用作为分片计数器示例。虽然示例的网络接口很不起眼,但它对于在几次增加两个计数器以后使用管理接口并检查数据模型很有启发性。

总结

分片是构建可扩展应用程序的许多重要技术之一,希望这些示例能够对您在应用程序中应用此技术有所启发。这些文章中的代码在 Apache 2 许可下可用,所以在构建解决方案时尽可以从这些代码开始着手。

更多信息

查看 Brett Slatkin 在 Google I/O 大会上的演讲“通过 Google App Engine 构建可扩展的网络应用程序”

参考