更新AppEngine中每个实例的内存缓存(自动缩放)

时间:2018-06-15 09:45:46

标签: google-app-engine caching go memory-management

我正在创建一个架构,其中大多数数据非常稳定,不需要对其进行严格更新,并且不经常更新。此外,该信息的大小并不多(大约几MB)。

因此,当我在Google AppEngine中初始化一个实例(使用GoLang)时,不是使用Memcached,而是首先获取所有信息,将其作为热身缓存在内存中,并使用它。

但我需要每x次更新一次。因此,我需要一种方法来处理特定实例并更新它的内存缓存。

如果有不同的解决方案:

  • 处理完用户请求后,如果信息已过时,请在后台更新。问题?该用户请求可能需要相当多的时间才能完成。即使我尝试关闭与用户的连接并刷新数据,在该过程完成之前,用户也无法完全处理该请求。此外,如果在需要更新缓存时出现了很多请求,我还应该处理并发问题,以避免多次执行此操作。
  • 使用Cron + Pub / Sub(类似于此处所做的:https://cloud.google.com/solutions/reliable-task-scheduling-compute-engine,但有Cloud Engine)。问题是我只能通过我定义的端点URL一次“命中”一个服务的实例,因此我无法随意更新所有实例。
  • 杀死并续订实例。由于显而易见的原因,我不太喜欢这个。

使用Basic Sc​​aling可以解决特定的实例,但我找不到使用Automatic Sc​​aling的方法,如下所示:https://cloud.google.com/appengine/docs/standard/go/how-instances-are-managed

所以...,你能想象一下优雅的方式来一次更新所有实例的内存状态而不会打扰客户端吗? 如何单独点击AppEngine的所有实例来更新内存缓存?

3 个答案:

答案 0 :(得分:3)

访问所有动态实例通常很麻烦,而且你不应该依赖它。

而是重新设计并使用不同的方法。

让所有实例都使用内存缓存,但使用缓存数据的到期时间。每当需要这样的数据时,首先检查数据是否仍然有效(检查到期时间),如果是,请继续使用它。如果已过期,则从“某个”位置获取新的实际数据。这个“一些”的地方可能是Memcache或数据存储区,或者可选择两者都喜欢在Memcache中首先尝试,如果没有,那么从Datastore;或者它可能位于完全不同的地方,即使在Google Cloud Platform之外也是如此。获取新数据应包含其到期时间。

此方法不要求您访问动态实例,它们会在过期后自动刷新缓存数据。

如果从多个goroutine访问,则必须同步对缓存数据的访问。最好的方法是使用sync.RWMutex,这样就可以允许多个读取器互不阻塞(频繁操作),只有在缓存数据已过期且需要刷新时才能获取写锁定。

以下是此类内存缓存的示例实现:

func getFreshData() (data interface{}, expires time.Time, err error) {
    // Implement getting fresh data here:
    return nil, time.Now().Add(time.Minute), nil
}

type cachedData struct {
    sync.RWMutex
    data    interface{}
    expires time.Time
}

var cd = new(cachedData) // zero value is ready to use

func Get() (data interface{}, err error) {
    cd.RLock()
    if time.Now().Before(cd.expires) {
        // We're done: we can use the cached data:
        data = cd.data
        cd.RUnlock()
        return
    }

    cd.RUnlock()

    // Either we don't have cached data or it has expired.
    // Acquire write lock and get data
    cd.Lock()
    defer cd.Unlock()

    // But once we have the write lock, check again, as another competing
    // goroutine might have fetched data before us:
    if time.Now().Before(cd.expires) {
        // Another goroutine fetched fresh data:
        return cd.data, nil
    }

    // Nope, we have to do it ourselves:
    data, expires, err = getFreshData()
    if err == nil {
        // Also put fresh data into the cache:
        cd.data = data
        cd.expires = expires
    } else {
        // There was an error getting it, set a 5 sec timeout to not keep calling:
        cachedData.data = nil
        cachedData.expires = time.Now().Add(5 * time.Second)
    }

    return
}

答案 1 :(得分:0)

除了icza的答案:

仅当使用手动缩放时,才能定位特定实例:

  

如果使用的是手动扩展服务,则可以通过包含实例ID来确定目标并将请求发送到实例。实例ID是一个整数,范围是0到正在运行的实例总数,可以指定如下:

     

将请求发送到特定实例中的特定服务和版本:

https://[INSTANCE_ID]-dot-[VERSION_ID]-dot-[SERVICE_ID]-dot-[MY_PROJECT_ID].appspot.com
http://[INSTANCE_ID].[VERSION_ID].[SERVICE_ID].[MY_CUSTOM_DOMAIN]
  

注意:在配置为自动缩放或基本缩放的服务中不支持定位实例。实例ID必须是0到运行实例总数之间的整数。无论您使用的是缩放类型还是实例类,都无法在不针对该实例内的服务或版本的情况下向特定实例发送请求。

source

答案 2 :(得分:0)

我建议:

  1. 使用memcache满足您的内存需求,从而跨所有实例工作
  2. 通过数据存储区返回内存缓存:可以在不发出警告的情况下清除内存缓存,以便在内存缓存未命中,从数据存储区获取并更新内存缓存的情况。
  3. 在请求中,如果数据很旧,请创建一个TaskQueue条目。然后执行此操作时,让它更新数据存储和内存缓存。但是,至关重要的是,您应该命名该任务并在所有实例中选择相同的名称:具有给定名称的一次只能存在一个任务,因此多个实例将不会创建多个任务。

在3中,您可以返回旧数据,但会触发更新。如果这样做不好,请考虑在应用引擎中使用Cron。