是否有同时交易的每个请求限制?

时间:2014-10-19 22:22:43

标签: java google-app-engine transactions google-cloud-datastore

我在我的应用程序中使用了很多(分片)计数器。根据我目前的设计,单个请求可以导致100-200个不同的计数器递增。

因此,对于每个计数器,我正在拾取一个其值正在递增的碎片。我正在递增事务中的每个分片,这意味着我将最终在处理单个请求时执行100-200个事务。当然我打算异步这样做,这样我基本上可以并行运行所有100-200个事务。

由于这个数字感觉非常高,我不知道是否有同时事务(或数据存储请求)的每个请求或每个实例限制。我从文档中找不到相关信息。

顺便说一下,谷歌的文档说“如果你的应用程序有频繁更新的计数器,你不应该以事务方式递增它们”[1],但另一方面,他们在分片计数器上的代码示例使用一个事务递增碎片[2]。我想我可以使用事务,如果我只使用足够的分片。我更喜欢交易,因为我希望我的柜台不要错过增量。

  1. https://cloud.google.com/appengine/docs/java/datastore/transactions
  2. https://cloud.google.com/appengine/articles/sharding_counters

3 个答案:

答案 0 :(得分:2)

有三个限制可能会导致您遇到问题:

  • 每个实体组1 / sec写入限制
  • 每个XG 5个实体组
  • 每个实例10个并发“线程”

最后一个对你的用例来说很棘手。

它有点难以找到信息(实际上可能是过时的信息 - 因此值得测试),但每个实例只允许10个并发核心线程(无论大小 - F1 / F2 / F ... )。

也就是说,忽略后台线程的创建,如果你假设每个请求都需要一个线程,就像每个RPC(数据存储,内存缓存,文本搜索等)一样,你一次只能使用10个。如果调度程序认为传入请求超过10,它将把请求路由到新实例。

在你想要并行写入100个实体的场景中,我希望它只允许大约10个并发写入(其余的阻塞),而且你的实例一次只能处理一个请求。

替代方案:

  • 使用专用的内存缓存 - 您需要处理将计数器备份到持久存储上,但您可以在后端批量执行此操作。这可能会导致您因为刷新而丢失一些数据,无论是否可以,您将不得不做出决定
  • 使用CloudSQL序列或表 - 如果您不需要大规模, 但确实需要很多计数器,这可能是一个很好的方法 - 你 可以将计数存储为原始计数,也可以存储为时间序列数据和 后处理准确计数
  • 使用pull队列在后端批量更新计数器。您可以批量处理多个计数器表中的许多“事件”。缺点是计数在任何给定的时间点都不是最新的

最好的方法可能是混合方式。

例如,接受一些最终的计数一致性:

  • 当请求进入时 - 记忆库中的计数器的原子增量
  • 当请求进入时 - 排队'事件'任务
  • 从memcache服务所需的计数 - 如果不存在来自数据存储区的负载
  • 在内存缓存上使用TTL,以便最终将数据存储视为“真相来源”
  • 运行一个cron,每隔5分钟(或根据需要)将100个“事件”任务从队列中拉出,并更新数据存储区中事务中所有事件的计数器

更新:我在文档talking about controlling max number of concurrent requests中找到了此部分,它对

进行了模糊的引用
  

如果此设置太高,您可能会遇到API延迟增加。

我说这值得玩。

答案 1 :(得分:1)

我发现您使用分片计数器方法来避免争用,如:cloud.google.com/appengine/articles/sharding_counters中所述。

您可以在一个实体中收集所有计数器,以便每个分片都是一堆计数器吗?那你就不需要那么多单独的交易了。根据{{​​3}},一个实体最大可以是1MB,当然200个整数也适合这个大小限制。

可能是您事先并不知道属性名称。以下是Go使用PropertyLoadSaver接口表示的方法,该接口可以处理动态计数器名称。

const (
    counterPrefix = "COUNTER:"
)

type shard struct {
    // We manage the saving and loading of counters explicitly.
    counters map[string]int64 `datastore:"-"`
}

// NewShard construct a new shard.
func NewShard() *shard {
    return &shard{make(map[string]int64)}
}

// Save implements PropertyLoadSaver.
func (s *shard) Save(c chan<- datastore.Property) error {
    defer close(c)
    for key, value := range s.counters {
        c <- datastore.Property{
            Name:  counterPrefix + key,
            Value: value,
            NoIndex: true,
        }
    }
    return nil
}

// Load implements PropertyLoadSaver.
func (s *shard) Load(c <-chan datastore.Property) error {
    s.counters = make(map[string]int64)
    for prop := range c {
        if strings.HasPrefix(prop.Name, counterPrefix) {
            s.counters[prop.Name[len(counterPrefix):]] = prop.Value.(int64)
        }
    }
    return nil
}

关键是在保存到数据存储区时使用原始API来定义自己的属性名称。鉴于存在cloud.google.com/appengine/docs/python/ndb/#quotas

,Java API几乎肯定具有类似的访问权限

PropertyContainer中描述的其余代码将表达为操纵知道多个计数器的单个实体。因此,例如,而不是让Increment()处理单个计数器:

// // Increment increments the named counter.
func Increment(c appengine.Context, name string) error {
    ...
}

我们将其签名更改为面向批量的操作:

// // Increment increments the named counters.
func Increment(c appengine.Context, names []string) error {
    ...
}

并且实现将找到一个分片,为我们想要递增的每个计数器调用Increment(),并且在单个事务中将Save()该单个实体添加到数据存储区。 Query还涉及咨询所有分片......但读取速度很快。我们仍然保持分片架构以避免写入争用。


Go的完整示例代码是:

package sharded_counter

import (
    "fmt"
    "math/rand"
    "strings"

    "appengine"
    "appengine/datastore"
)

const (
    numShards     = 20
    shardKind     = "CounterShard"
    counterPrefix = "counter:"
)

type shard struct {
    // We manage the saving and loading of counters explicitly.
    counters map[string]int64 `datastore:"-"`
}

// NewShard constructs a new shard.
func NewShard() *shard {
    return &shard{make(map[string]int64)}
}

// Returns a list of the names stored in the shard.
func (s *shard) Names() []string {
    names := make([]string, 0, len(s.counters))
    for name, _ := range s.counters {
        names = append(names, name)
    }
    return names
}

// Lookup finds the counter's value.
func (s *shard) Lookup(name string) int64 {
    return s.counters[name]
}

// Increment adds to the counter's value.
func (s *shard) Increment(name string) {
    s.counters[name]++
}

// Save implements PropertyLoadSaver.
func (s *shard) Save(c chan<- datastore.Property) error {
    for key, value := range s.counters {
        c <- datastore.Property{
            Name:    counterPrefix + key,
            Value:   value,
            NoIndex: true,
        }
    }
    close(c)
    return nil
}

// Load implements PropertyLoadSaver.
func (s *shard) Load(c <-chan datastore.Property) error {
    s.counters = make(map[string]int64)
    for prop := range c {
        if strings.HasPrefix(prop.Name, counterPrefix) {
            s.counters[prop.Name[len(counterPrefix):]] = prop.Value.(int64)
        }
    }
    return nil
}

// AllCounters returns all counters.
func AllCounters(c appengine.Context) (map[string]int64, error) {
    var results map[string]int64
    results = make(map[string]int64)
    q := datastore.NewQuery(shardKind)
    q = q.Ancestor(ancestorKey(c))
    for t := q.Run(c); ; {
        var s shard
        _, err := t.Next(&s)
        if err == datastore.Done {
            break
        }
        if err != nil {
            return results, err
        }
        for _, name := range s.Names() {
            results[name] += s.Lookup(name)
        }
    }
    return results, nil
}

// ancestorKey returns an key that all counter shards inherit.
func ancestorKey(c appengine.Context) *datastore.Key {
    return datastore.NewKey(c, "CountersAncestor", "CountersAncestor", 0, nil)
}

// Increment increments the named counters.
func Increment(c appengine.Context, names []string) error {
    shardName := fmt.Sprintf("shard%d", rand.Intn(numShards))
    err := datastore.RunInTransaction(c, func(c appengine.Context) error {
        key := datastore.NewKey(c, shardKind, shardName, 0, ancestorKey(c))
        s := NewShard()
        err := datastore.Get(c, key, s)
        // A missing entity and a present entity will both work.
        if err != nil && err != datastore.ErrNoSuchEntity {
            return err
        }
        for _, name := range names {
            s.Increment(name)
        }
        _, err = datastore.Put(c, key, s)
        return err
    }, nil)
    return err
}

,如果你仔细观察,它几乎是一个单一的,未命名的计数器的例子,但扩展到处理多个计数器名称。我在查询端稍微改了一下,以便读取使用相同的祖先密钥,以便我们在同一个实体组中。

答案 2 :(得分:1)

感谢您的回复!我想我现在有了我需要的答案。

关于每个请求或每个实例的限制

并发线程存在每个实例的限制,这有效地限制了并发事务的数量。默认限制为10.它可以递增,但不清楚会产生哪些副作用。

关于潜在问题

我选择将计数器分组,以便计数器通常会增加&#34;在一起&#34;属于同一组。碎片携带与各个碎片相关联的组内的所有计数器的部分计数。

计数在交易中仍然增加,但由于分组,每个请求最多只需要五个交易。每个事务增加存储在单个分片中的多个部分计数,其表示为单个数据存储实体。

即使交易是按顺序运行的,处理请求的时间仍然可以接受。每个计数器组都有几百个计数器。我确保有足够的分片来避免争用。

应该注意这个解决方案是唯一可行的,因为计数器可以分成相当大的计数器组,这些计数器通常会一起递增。