GCP-appengine通過version管理應用,你可以在appengine上部署多個version(dev、qa等),而每個version可以有多個instance,一個instance可簡單理解為一個基于Spring Boot實現(xiàn)的微服務,當有請求到達時appengine會根據(jù)一定策略選擇由哪一個instance處理該請求,如果現(xiàn)有的instance處理的流量已經(jīng)很多,那么appengine會啟動新的instance來處理這個請求,這個行為主要由instance的三種擴縮策略決定:手動、自動和基礎。
背景
應用數(shù)據(jù)保存在GCP的數(shù)據(jù)存儲組件datastore中,datastore是一個NoSQL數(shù)據(jù)庫。假設我們的應用對其中一部分數(shù)據(jù)M的操作的QPS要求很高,如果每次都從datastore查詢則不能滿足需求。
//更新M數(shù)據(jù)
POST/m
//查詢M數(shù)據(jù)
GET/m
解決方案
為了加快請求的處理速度,我們在應用啟動時(即instance啟動時)先將這部分數(shù)據(jù)全部加載到內(nèi)存,之后直接從內(nèi)存中讀取,而不是每次都從datastore中查詢。這種方式有幾個問題需要解決:
1.每次請求到達時不確定appengine會將請求路由給哪一個instance,所以當這部分數(shù)據(jù)有更新(POST/m)時需要通知該version的所有instance進行數(shù)據(jù)同步
2.在POST/m請求中要確保所有instance都成功同步了數(shù)據(jù)(所有instance中M數(shù)據(jù)保持一致),才能以請求處理成功的狀態(tài)返回。
數(shù)據(jù)同步
instance間通信是一個棘手的問題,因為appengine沒有直接提供API或外部組件來完成這件事情,甚至你想將請求發(fā)給指定的instance都需要犧牲很多靈活性才能實現(xiàn)。當然大多數(shù)情況下請求的路由應該由appengine控制,只有在實現(xiàn)某些特殊需求時才可以考慮一些特殊做法。
數(shù)據(jù)同步的基本思路是在POST/m時先在datastore transactions中更新M數(shù)據(jù),事務成功提交后再通過pubsub通知所有instance從datastore更新最新數(shù)據(jù),所有instance都確認成功更新數(shù)據(jù)后,請求成功。
通過查閱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的消息組件,消息發(fā)送后消費者有兩種方式消費消息:pull和push
1.pull:通過拉取的方式消費消息,我們的目的是將“數(shù)據(jù)同步”這個消息立刻通知到每一個instance,pull的方式需要每一個instance以輪詢的方式檢查并拉取消息,這樣在資源占用和響應速度(POST/m)上都不能滿足要求。
2.push:這種方式在消息發(fā)到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的創(chuàng)建只需要執(zhí)行一次,可以在version啟動后通過appengine-taskqueue創(chuàng)建n個subscriber,n為這個version的實例數(shù)。這意味著實例數(shù)量n是個"常數(shù)"(只有手動擴縮模式才能確保n為“常數(shù)”)。
到這里,在`POST/m`里通知所有instance進行“數(shù)據(jù)同步”這個消息可以正確發(fā)送并最終通知到所有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就會收到請求,并攜帶著消息內(nèi)容。在這個請求中,我們可以從消息中知道需要怎樣更新數(shù)據(jù),進而完成數(shù)據(jù)同步的任務。
一致性
上面介紹了數(shù)據(jù)同步的具體流程,在這個過程中一致性的保證是很重要的,主要體現(xiàn)在數(shù)據(jù)同步時需要確保所有的instance都成功消費“數(shù)據(jù)同步”消息POST/m`才能以請求成功處理的狀態(tài)返回。
在這里version的instance數(shù)量n是“常量”,那么我們只需在一個公共的地方維護一個標識A,標識當前已經(jīng)成功同步的instance數(shù)量,當這個A==n時也就意味著所有instance都成功同步了數(shù)據(jù)。
appengine-memcache是主要應用于appengine上的分布式緩存服務,我們可以在上面存儲這個唯一標識,POST/m請求中成功發(fā)送“數(shù)據(jù)同步”消息后,就以輪詢的方式從memcache中查詢A的值,同時在POST/_ah/push-handlers/your-topic-name中要遞增A的值,A==n時就表明同步完成