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 時就表明同步完成。