GCP - appengine 通过 version 管理应用,你可以在 appengine 上部署多个 version(dev、qa等),而每个 version 可以有多个 instance,一个 instance 可简单理解为一个基于 Spring Boot 实现的微服务,当有请求到达时 appengine 会根据一定策略选择由哪一个 instance 处理该请求,如果现有的 instance 处理的流量已经很多,那么 appengine 会启动新的 instance 来处理这个请求,这个行为主要由 instance 的三种扩缩策略决定:手动、自动和基础。
背景
应用数据保存在 GCP 的数据存储组件 datastore 中,datastore 是一个 NoSQL 数据库。假设我们的应用对其中一部分数据 M 的操作的 QPS 要求很高,如果每次都从 datastore 查询则不能满足需求。
// 更新 M 数据
POST /m
// 查询 M 数据
GET /m
解决方案
为了加快请求的处理速度,我们在应用启动时(即 instance 启动时)先将这部分数据全部加载到内存,之后直接从内存中读取,而不是每次都从 datastore 中查询。这种方式有几个问题需要解决:
1. 每次请求到达时不确定 appengine 会将请求路由给哪一个 instance,所以当这部分数据有更新(POST /m)时需要通知该 version 的所有 instance 进行数据同步
2. 在 POST /m 请求中要确保所有 instance 都成功同步了数据(所有 instance 中 M 数据保持一致),才能以请求处理成功的状态返回。
数据同步
instance 间通信是一个棘手的问题,因为 appengine 没有直接提供 API 或外部组件来完成这件事情,甚至你想将请求发给指定的 instance 都需要牺牲很多灵活性才能实现。当然大多数情况下请求的路由应该由 appengine 控制,只有在实现某些特殊需求时才可以考虑一些特殊做法。
数据同步的基本思路是在 POST /m 时先在 datastore transactions中更新 M 数据,事务成功提交后再通过 pubsub 通知所有 instance 从 datastore 更新最新数据,所有 instance 都确认成功更新数据后,请求成功。
通过查阅 appengine 请求路由说明可以知道通过以下格式的地址可以将请求路由给指定的 instance。
https://[INSTANCE_ID]-dot-[VERSION_ID]-dot-[SERVICE_ID]-dot-[MY_PROJECT_ID].appspot.com
但这种方式需要将扩缩方式设置为手动扩缩,而且 INSTANCE_ID并不是 instance 的唯一 id,而是一个下标索引,比如 version test 有 3 个 instance,分别为 A、B、C,那么可以保证的是通过 test[0]、test[1]、test[2] 可以成功访问(遍历)三个 instance,但你无法知道 test[0] 究竟是指向 A、B 还是 C。
pubsub 是 GCP 的消息组件,消息发送后消费者有两种方式消费消息:pull 和 push
1. pull: 通过拉取的方式消费消息,我们的目的是将“数据同步”这个消息立刻通知到每一个 instance,pull 的方式需要每一个 instance 以轮询的方式检查并拉取消息,这样在资源占用和响应速度(POST /m)上都不能满足要求。
2. push: 这种方式在消息发到 pubsub 的指定 topic 后,pubsub 会立刻把这个消息 push 到订阅了这个 topic 的所有 subscriber(subscriber 指定的 endpoint 处,这里我们的域需要设置为:https://[INSTANCE_ID]-dot-[VERSION_ID]-dot-[SERVICE_ID]-dot-[MY_PROJECT_ID].appspot.com)。
需要注意的是 pubsub 的 tpoic 和 subscriber 的创建只需要执行一次,可以在 version 启动后通过 appengine-taskqueue 创建 n 个 subscriber,n 为这个 version 的实例数。这意味着实例数量 n 是个"常数"(只有手动扩缩模式才能确保 n 为“常数”)。
到这里,在 `POST /m` 里通知所有 instance 进行 “数据同步”这个消息可以正确发送并最终通知到所有 instance 了。使用 appengine 预留的 /_ah/push-handlers/.* 路径可以简化 endpoint 的认证和授权,最终的 endpoint 是下面的形式:
https://[INSTANCE_ID]-dot-[VERSION_ID]-dot-[SERVICE_ID]-dot-[MY_PROJECT_ID].appspot.com/_ah/push-handlers/your-topic-name
pubsub 进行 push 时每个 instance 的 `POST /_ah/push-handlers/your-topic-name` controller/handler 就会收到请求,并携带着消息内容。在这个请求中,我们可以从消息中知道需要怎样更新数据,进而完成数据同步的任务。
一致性
上面介绍了数据同步的具体流程,在这个过程中一致性的保证是很重要的,主要体现在数据同步时需要确保所有的 instance 都成功消费“数据同步”消息POST /m` 才能以请求成功处理的状态返回。
在这里 version 的 instance 数量 n 是“常量”,那么我们只需在一个公共的地方维护一个标识 A,标识当前已经成功同步的 instance 数量,当这个 A == n 时也就意味着所有 instance 都成功同步了数据。
appengine-memcache是主要应用于 appengine 上的分布式缓存服务,我们可以在上面存储这个唯一标识,POST /m 请求中成功发送“数据同步”消息后,就以轮询的方式从 memcache 中查询 A 的值,同时在 POST /_ah/push-handlers/your-topic-name 中要递增 A 的值,A == n 时就表明同步完成。