系統設計系列文6:Session 要放哪?
昨天我們把 Application Server 從一台擴展成多台,靠 Load Balancer 分攤流量:

這解決了單台 Server 的容量問題,但當時刻意跳過了一個細節:
如果同一個使用者的 Request,被分配到不同的 Server,這些 Server 還認得出他是誰嗎?
回想 Day 2,我們是把 Session 直接存在 Server 的 Memory 裡:
Server
├── Application
└── Session(存在 Memory 裡)
├── Pikachu
├── Charmander
└── Squirtle
這種把使用者狀態保存在 Server 本機的做法,稱為 Stateful Server。在只有一台 Server 的時候完全沒問題,但現在情況不一樣了。
Horizontal Scaling 之後的裂縫
假設 Pikachu 送出第一個 Request,被 Load Balancer 分配到 Server A,Server A 在自己的 Memory 裡建立了 Pikachu 的 Session。
沒過多久,Pikachu 又送出第二個 Request,這次被分配到 Server C,但 Server C 的 Memory 裡根本沒有 Pikachu 的 Session,於是 Server C 完全不知道「這個 Request 是 Pikachu 發的」,可能就把他當成未登入使用者。
這就是 Stateful Server 在 Horizontal Scaling 之下最典型的問題。
第一個解法:Sticky Session
一個很直覺的方法:既然 Pikachu 的 Session 在 Server A,那就讓 Load Balancer 永遠把 Pikachu 的 Request 導回 Server A,這就是 Sticky Session。
若沿用前一天銀行櫃檯來比喻,Sticky Session 有點像是專屬負責櫃檯的概念。
這個做法的好處是完全不用改 Application,只要在 Load Balancer 上設定「同一個使用者固定導向同一台 Server」即可。但它也帶來幾個新麻煩:
問題一:Server 掛掉怎麼辦?
如果 Server A 突然壞掉,Load Balancer 只好把 Pikachu 轉去 Server B,但 Server B 沒有他的 Session,Pikachu 的登入狀態就這樣憑空消失了。
問題二:負載會變得不平均
假設剛好很多活躍使用者都黏在 Server A 上:
Server A → 非常忙
Server B → 很閒
Server C → 很閒
即使有三台 Server,實際負載可能還是分配不均。
問題三:Scale In / Scale Out 變得更麻煩
因為 Session 散落在各台 Server 的 Memory 裡,要新增或移除 Server 時,都得先想清楚「原本那些 Session 該怎麼辦」,讓 Scaling 這件事變得綁手綁腳。
Stateless:讓任何一台 Server 都能處理任何 Request
既然問題出在「Session 跟特定 Server 綁在一起」,那反過來想:如果任何一台 Server 都能處理任何使用者的 Request 呢?
Pikachu
↓
Load Balancer
↓
┌────────┬────────┬────────┐
│ Server │ Server │ Server │
└────────┴────────┴────────┘
任何一台都能處理 Pikachu 的 Request
這就是 Stateless Server 的概念:Application Server 不再依賴自己本機保存的使用者狀態來處理 Request。
要注意的是,Stateless 不代表「系統完全沒有 State」,而是把 State 從 Server 內部,搬到 Server 外部一個大家都能存取的地方。
方法一:把 Session 放進 Distributed Cache
最直覺的做法是把 Session 存進 Database,這樣不管 Request 被分配到哪一台 Server,都能查到同一份資料。但每次都查 Database 太慢,於是更常見的做法是把 Session 放進 Distributed Cache(例如 Redis)。
Session 本身也可以定義出簡單的欄位與型別:
- Session
- session_id:VARCHAR(64)(Primary Key)
- user_id:BIGINT(Foreign Key → User.id)
- payload:JSON
- expires_at:TIMESTAMP
任何一台 Server 收到 Request 後,都拿著 session_id 去 Distributed Cache 查一次,就能知道這是誰的 Request。
這也代表 Distributed Cache 除了能存放 Application Data(後面 Day 8 會談到的 Cache 就是這種用途),也能拿來保存需要跨 Server 共享的 Runtime State。
方法二:Token-based Authentication
另一種思路是乾脆不讓 Server 保存 Session,改讓 Client 自己帶著能證明身份的 Token,這就是 Token-based Authentication。
Server 驗證身份成功後,發一個經過簽章的 Token(例如 JWT)給 Client,把「使用者是誰、有什麼權限」等資訊直接封裝進 Token 本身,並用密碼學簽章確保內容不能被竄改。
之後 Client 每次 Request 都帶著這個 Token,Server 只要驗證簽章就能確認身份,完全不用查 Database 或 Cache。
整個流程畫成示意圖大概像這樣:
Client Server
│ │
│──── 1. 帳號密碼登入 ───────────────────→ │
│ │ 驗證身份成功
│ │ 簽發 JWT(簽章)
│←──── 2. 回傳 JWT ─────────────────────── │
│ │
│ Client 自行保存 JWT,之後不再需要密碼 │
│ │
│──── 3. Request + JWT ──────────────────→│
│ Authorization: Bearer <JWT> │ 只驗證簽章是否正確
│ │ 不用查 DB / Cache
│←──── 4. Response ───────────────────────│
因為身份資訊已經編碼在 Token 裡,Server 收到 Request 後不需要像方法一那樣再問一次 Distributed Cache,單靠驗證簽章就能確認「這確實是 Server 自己發出去的 Token,內容沒被竄改」。
這個做法很方便,卻有一個明顯代價:Server 失去了即時控制權。
Token 一旦發出去,在過期之前它就是有效的,如果 Pikachu 的帳號被停權,或他自己改了密碼,Server 沒辦法主動讓已經發出去的 Token 失效。
實務上通常會搭配方法一來補這個洞:
- Refresh Token:JWT 有效期通常設得很短(例如 15 分鐘),過期後 Client 要拿一個長效的 Refresh Token 換新的 JWT,而這個 Refresh Token 會存在 DB 或 Redis 裡,方便隨時被撤銷。
- Token Revocation:使用者登出或改密碼時,理論上舊的 JWT 應該立刻失效,這需要一個黑名單(或白名單)機制,通常存在 Redis。Server 收到 JWT 時,除了驗證簽章,還要多查一次黑名單,確認這個 Token 沒有被提前作廢。
Stateless 帶來的優勢
整理一下 Stateless 的好處:
- 更容易 Auto Scaling:流量增加時直接加 Server 就好,任何一台都能處理任何 Request;流量降低時要移除 Server 也不用先搬 Session。
- 更容易 Failover:某台 Server 掛掉,Load Balancer 直接把流量導去其他台即可,不會有 Pikachu 突然變成「未登入」的情況。
在 Stateless 架構下,Server 本機還是可以保留一些東西,但僅限於跟特定使用者無關、也不影響水平擴充的資料,例如:
- 全域密鑰、環境變數、System Config
- 熱門公告這類靜態資料的本機快取
- Logs、Metrics 這類監控用途的資料
現在的架構
加上共用的 Session Store 之後,架構長這樣:

小結
今天解決的問題是:Server 從一台變成很多台之後,怎麼讓任何一台都能處理任何使用者的 Request?
解法是採用 Stateless Architecture,把原本放在 Server Memory 裡的 Session,搬到 Server 外部的 Distributed Cache(或改用 Token-based Authentication)。這樣一來,Server 才真正具備自由增加、刪除、汰換的能力,而不會被 Session 卡住。
不過眼尖的話會發現,架構圖裡的 Database 從頭到尾都還是只有一台。Server 這一層已經能自由水平擴展了,如果所有 Server 最後都要打同一個 Database,它會不會變成下一個瓶頸?這是明天要處理的問題。
系列導覽
- 查看全系列:系統設計系列文總覽
