nodejs 實踐:express 最佳實踐(四) express-session 解析
nodejs 發展很快,從 npm 上面的包托管數量就可以看出來。不過從另一方面來看,也是反映了 nodejs 的基礎不穩固,需要開發者創造大量的輪子來解決現實的問題。
知其然,並知其所以然這是程序員的天性。所以把常用的模塊拿出來看看,看看高手怎么寫的,學習其想法,讓自己的技術能更近一步。
引言
最近 ‘雙十一‘ 快到了,領導安排我們給網站做性能優化。其中最要的方向是保證網站的穩定性。我主要是負責用戶登錄入口這一塊的工作。
優化的目標是:在高峰下,如果系統服務器端 session 的存儲(memcached)出現了問題,用戶還能正常登錄和使用我們的網站。
並已經給出了技術思路:對 session, 進行服務器端 session(memcached) 和 瀏覽器端 session(cookie) 雙備份,一但連接發現服務器端 session 出現了問題,就啟用瀏覽器端 session, 實現自動降級處理。
借此機會,正好比較深入的了解了一下 session 等相關知識實現。
session
session 是什么
注意這里說的都是網站相關技術環境下。
session 是一種標識對話的技術說法。通過 session ,我們能快速識別用戶的信息,針對用戶提供不一樣的信息。
session 的技術實現上:會對一次對話產生一個唯一的標識進行標識。
session 生命周期
session 標識產生的時機和清除時機:
- 用戶已經登錄:這個唯一標識會在用戶登錄時產生,用戶點擊退出時或者關閉瀏覽器時清除。
- 用戶未登錄: 這個唯一標識會用用戶進入網站時產生,用戶關閉所有網站相關頁面時清除。
session 生命周期: 在生成和清除之間,在網站內的頁面任意跳轉,session 標識不會發生變化。
從 session 開始到清除,我們叫一次會話,也就是生成 session。
session 特點
每次對話, session 的 id 是不一樣的。
session id 需要每次請求都由客戶端帶過來,用來標識本次會話。這樣就要求客戶端有能用保存的 sesssionId。
session 技術方案
當前業界通用的方案是:cookie 。當然還有無 cookie 的方案,對每個鏈接都加上 sessionId 參數。
session 使用流程
- 用戶登錄后,將 sessionId 存到 cookie 中。
- 用戶在請求的網站別的服務時,由瀏覽器請求帶上 cookie,發送到服務器。
- 服務器拿到 sessionId 后,通過該 Id 找到保存到在服務器的用戶信息。
- 然后再跟據用戶信息,進行相應的處理。
從流程有幾個點要關注:
- 什么時候根據 sessionId 去拿 session
- 確保 session 可用性
下面就結合 express-session 來講講具體 session 的實現。
express-session 的分析
主要關注問題:
- 怎樣產生 session
- 怎樣去拿到 session
- 怎樣去保存 session
- 怎樣去清除 session
express-session 位置
這一一張更詳細的 session 流程圖,同時也說明了 express-session 的基本的工作模塊。
express-session 有四個部分:
- request, response 與 session 的交互的部分
- session 數據結構
- session 中數據存儲的接口 store
- store 默認實現 memory(cookie 實現已被廢)
這張是 express-session 的流程圖,從圖中可以看到, express-session 的工作流程。
具體的情況只能去看代碼了。
因為我們的網站是 session store 是基於 memcached 的。所以我把 connect-memcached 和 memcached 都看了一遍。
connect-memcached 是基於 memcached 實現 session store 接口。
memcached 是基於連接池的應用,下面是我畫的結構圖:
問題解決方案
上面把 session 和 express-session + connect-memcached 都仔細看過了。
回到前面引言中的方案,我們需要解決以下的問題:
- 基於 memcached 的 session 怎么把數據同步到基於 cookie 的 session 中。
- 怎么把 cookie 的 session 數據恢復到 session 中。
- 怎樣判斷 memcached 已經失去連接。
解決1,2兩個問題,可以讓用戶在一次對話中,在 mecached 和 cookie 中切換,數據還一直存在,不會丟掉。
第3個問題,就是在 memcached 斷開時,程序能知道 memcached 已斷,然后數據從 cookie 中拿。
庫選擇
已經有 express-session 的方案,要有一個在客戶端找一個基於 cookie 的 session 方案:cookie-session 和 client-session 這兩個都可以。我選了第二個,主要是加密第二個更好。
方案
我前前后后,考慮了多個方案,方案如下:
首先方案一:主要思路是通過一個基礎的監控程序去按時間定時(比如5分鍾)去ping memcached 服務器,去判斷是否可用,然后把結果寫入到 zookeeper 中,通過 zookeeper 的變量去控制數據從 memcached 的session 中讀取,還是從 cookie session 中讀數據。
方案二:在兩個 session 之上,再建一個 session,就是對從哪里讀數據通過這個 session 來實現,也就是代理模式。
方案三:在 store 層上做一層 common-store , 然后由他負責從哪個store 中讀取數據,就是 store 的代理。
方案四:不做中間層,直接使用進行處理,只用 express-session 進行處理數據。
這四個方案都在選擇,區別只是實現上的難度:
- memcached 的不可連接是否可以在框架層感知道
- 業務代碼盡量不用調整
- session 同步方案是否有效
其中第一個問題最重要,如果框架層不可感知,那就要有一個外部程序進行處理,或者寫一個中間件去主動連接一下,看看是否可連接。
再一次閱讀 express-session 重點查看 session 中 store 連接這塊。發現如果 memcached 不可連接,req.session 是 undefined 的。
這樣,就可以通過判斷 req.session 是否是直來判斷是否可連接。
第二個問題:因為業務代碼中使用都是 req.session 的形式, 從 cookie 中恢復數據的時候,就要成初始化成 express-session 的接口。
這個問題也通過閱讀代碼解決:
req.sessionID = uuid.v4();
req.session = new expresssession.Session(req, data);
req.session.cookie = new memcachedSession.Cookie({
domain: config.cookieDomain,
httpOnly: false
});
通過以上的代碼就可以從數據中恢復 session。
第三個問題: 要保證 session 一致,就讓數據指向同一個對象
req.session2.sessionBack = req.session;
因此方案1,方案2, 方案3 都扔掉,直接方案4。
完整的代碼如下:
const config = global.config;
const session = require('express-session');
/**
* 該中間件主要把 express-session 和 client-session 集中起來處理,如果 memcached 出錯了,使用 cookie session
* @param backSession cookeSession 的鍵名
* @returns {function(*=, *=, *)}
*/
module.exports = (backSession) => {
return (req, res, next) => {
let notUseMemcached = _.get(req.app.locals.pc, 'session.removeMemcached', false);
if (req.session && !notUseMemcached) { // memcached 可用
req.memcachedSessionError = false;
} else { // memcached 不可用
// 重建 session
res.emit('sessionError');
req.memcachedSessionError = true;
req.session = new session.Session(req);
req.session.cookie = new session.Cookie({
domain: config.cookieDomain,
httpOnly: false
});
req.session = Object.assign(req.session, req[backSession].sessionBack);
}
Object.defineProperty(req.session, 'reset', {
configurable: true,
enumerable: false,
value: function() {
req.session.destory();
req[backSession].reset();
},
writable: false
});
// 備份數據
req[backSession].sessionBack = req.session;
next();
};
};
這里就不貼 express-session 和 client-session 初始化代碼,需要注意的是:這個中間件要放到初始化的后面。
app.use(memcachedSession({
// ... options
}));
app.use(cookieSession({
// ... options
}));
app.use(yohoSession({
backSession: 'session2'
}));
總結
網站穩定性一直是一個重要的話題。這次通過 session 的改造,讓我復習了很多的知識,學無止盡。