我在我的应用程序中使用了很多(分片)计数器。根据我目前的设计,单个请求可以导致100-200个不同的计数器递增。
因此,对于每个计数器,我正在拾取一个其值正在递增的碎片。我正在递增事务中的每个分片,这意味着我将最终在处理单个请求时执行100-200个事务。当然我打算异步这样做,这样我基本上可以并行运行所有100-200个事务。
由于这个数字感觉非常高,我不知道是否有同时事务(或数据存储请求)的每个请求或每个实例限制。我从文档中找不到相关信息。
顺便说一下,谷歌的文档说“如果你的应用程序有频繁更新的计数器,你不应该以事务方式递增它们”[1],但另一方面,他们在分片计数器上的代码示例使用一个事务递增碎片[2]。我想我可以使用事务,如果我只使用足够的分片。我更喜欢交易,因为我希望我的柜台不要错过增量。
答案 0 :(得分:2)
有三个限制可能会导致您遇到问题:
最后一个对你的用例来说很棘手。
它有点难以找到信息(实际上可能是过时的信息 - 因此值得测试),但每个实例只允许10个并发核心线程(无论大小 - F1 / F2 / F ... )。
也就是说,忽略后台线程的创建,如果你假设每个请求都需要一个线程,就像每个RPC(数据存储,内存缓存,文本搜索等)一样,你一次只能使用10个。如果调度程序认为传入请求超过10,它将把请求路由到新实例。
在你想要并行写入100个实体的场景中,我希望它只允许大约10个并发写入(其余的阻塞),而且你的实例一次只能处理一个请求。
替代方案:
最好的方法可能是混合方式。
例如,接受一些最终的计数一致性:
更新:我在文档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;属于同一组。碎片携带与各个碎片相关联的组内的所有计数器的部分计数。
计数在交易中仍然增加,但由于分组,每个请求最多只需要五个交易。每个事务增加存储在单个分片中的多个部分计数,其表示为单个数据存储实体。
即使交易是按顺序运行的,处理请求的时间仍然可以接受。每个计数器组都有几百个计数器。我确保有足够的分片来避免争用。
应该注意这个解决方案是唯一可行的,因为计数器可以分成相当大的计数器组,这些计数器通常会一起递增。