# 🍪 Server stateless và session lưu ở đâu

By Better Wise

## Điểm xuất phát: thêm server thứ hai và cái logout bất ngờ

Bài trước chúng ta đã chọn hướng scale: nhồi thêm CPU/RAM cho một máy (scale dọc) hay thêm máy (scale ngang). Quyết định xong, bạn dựng instance thứ hai sau load balancer. Rồi 2 giờ sáng điện thoại rung: user phản ánh "cứ bấm vài lần là bị đá ra trang đăng nhập".

Nguyên nhân không nằm ở database hay logic nghiệp vụ. Nó nằm ở một `Map<string, Session>` trong bộ nhớ của mỗi process.

Trong bài này bạn sẽ:
- Phát biểu được "stateless" nghĩa là gì và vì sao nó là điều kiện bắt buộc để scale ngang.
- Chỉ ra ba chỗ lưu session: in-memory, Redis, JWT.
- Nêu đánh đổi của từng cách bằng con số cụ thể.
- Nhận ra nút thắt tiếp theo ngay sau khi tách session ra khỏi server.

## Stateless: mỗi request tự mang theo mọi thứ nó cần

Stateless không có nghĩa là server không lưu gì. Nó có nghĩa là **server không giữ ký ức riêng giữa hai request liên tiếp của cùng một user**. Mọi thứ cần cho request đó (ai đang gọi, quyền gì, giỏ hàng ra sao) phải nằm trong chính request, hoặc trong một store dùng chung mà mọi instance đều đọc được.

Sơ đồ dưới đây là trạng thái bạn muốn có: hai server giống hệt nhau, không server nào "sở hữu" user nào, cả hai cùng nhìn vào một session store.

```mermaid
flowchart TD
  U["👤<br/>Người dùng"] --> LB["⚖️<br/>Load balancer"]
  LB --> S1["🖥️<br/>Server 1"]
  LB --> S2["🖥️<br/>Server 2"]
  S1 --> R["🗄️<br/>Session store dùng chung"]
  S2 --> R
  S1 --> DB["🗃️<br/>Database"]
  S2 --> DB
```

### Ví dụ có số: 500 user, một Map, hai server

Giả sử có 500 user online, session lưu trong `Map` của process, mỗi session khoảng 2KB. Một server thì mọi thứ ổn. Thêm server thứ hai, load balancer chia round-robin: mỗi user có khoảng 50% khả năng rơi vào server không giữ session của mình. Với 500 user, chỉ vài chục cú click là đủ để hàng loạt người bị đăng xuất ngẫu nhiên, dù CPU còn thừa rất nhiều.

### Sticky session: chữa triệu chứng, không chữa bệnh

Load balancer gắn mỗi user với một server cố định, thường qua cookie hoặc hash IP. User không bị đá nữa. Nhưng bạn vừa đánh đổi ba thứ:

- Mất cân bằng tải: toàn bộ nhân viên một công ty đi qua cùng một IP NAT có thể dồn hết vào một server.
- Server đó chết là mất sạch session của những người đang bám vào nó.
- Autoscale gần như vô nghĩa: instance mới chỉ nhận user mới, instance cũ vẫn gánh nguyên đám user quen.

Sticky session là giải pháp tình thế, không phải đích đến.

## Ba chỗ lưu session và cái giá của từng chỗ

### 1. In-memory (trong process)

Nhanh nhất: không network hop, truy cập tính bằng microsecond. Nhưng nó chỉ đúng khi bạn có đúng một instance, hoặc đang ở máy dev. Restart hay deploy là mất sạch session. Đây là lựa chọn mặc định của gần như mọi framework, và cũng là lỗi mặc định khiến trải nghiệm user tệ đi đúng lúc bạn scale ngang.

Phù hợp: dev, prototype, hệ thống một instance mà bạn chấp nhận logout hàng loạt khi deploy.

### 2. Redis (session store tập trung)

Đẩy session ra một store mà mọi instance cùng đọc. Key dạng `session:<id>`, value là hash các trường, kèm TTL trượt: mỗi request chạm vào thì được gia hạn thêm.

Đánh đổi: mỗi request thêm khoảng 0.5 đến 2ms cho một lần đọc, trong cùng datacenter. Với 600 RPS và mỗi request đọc session một lần, đó là 600 ops/s, Redis đơn lẻ còn xử lý được rất xa mức này. Nhưng hãy đếm số lần đọc: nếu code gọi session 4 lần rải rác trong một request, con số thành 2.400 ops/s, và đó là lúc nên gom lại còn một lần đọc mỗi request.

Rủi ro lớn nhất: Redis chết là toàn bộ user bị logout cùng lúc. Vì vậy cần replica, và cần chấp nhận rằng session store giờ là thành phần không được phép sập. TTL cũng bắt buộc: không có TTL thì key rác tích tụ, bộ nhớ phình dần cho tới khi Redis bị đẩy khỏi RAM.

### 3. JWT (stateless token)

Server không lưu gì. Token được ký, mỗi request mang theo, server chỉ cần xác thực chữ ký. Scale ngang trở nên dễ vì không còn state dùng chung.

Đánh đổi ngược lại với Redis: bạn không thu hồi token ngay được. User bị khóa tài khoản vẫn dùng được token tới khi hết hạn, nên nhiều nơi phải đặt access token ngắn (5 đến 15 phút) và thêm refresh token. Token cũng làm phình request: một JWT 800 byte trong header, nhân với 1 triệu request mỗi ngày là khoảng 800MB băng thông thêm. Và payload chỉ là base64, ai có token cũng đọc được, tuyệt đối không đặt dữ liệu nhạy cảm trong đó.

### Bảng chọn nhanh

| Cách | State nằm ở đâu | Scale ngang | Server chết | Thu hồi ngay | Thêm độ trễ |
| --- | --- | --- | --- | --- | --- |
| In-memory | Process | Không | Mất session | Có | 0ms |
| Redis | Store dùng chung | Có | Không mất | Có | 0.5-2ms |
| JWT | Trong token phía client | Có | Không mất | Không, phải chờ hết hạn | ~0ms, chỉ verify |

### Nút thắt tiếp theo

Tách session sang Redis xong, nút thắt thường chuyển sang chính Redis và network tới nó, không còn ở app server nữa. Vì vậy trước mỗi thay đổi tôi luôn hỏi: "sau bước này, thứ đầu tiên gục sẽ là gì?". Phần lớn các đợt sập tôi từng trực không phải do thiếu tối ưu, mà do tối ưu một chỗ rồi dồn tải sang chỗ chưa chuẩn bị.

## Những điều tôi ước biết trước lần đầu tách session ra ngoài

- **Đừng đặt session store khác region** với app server. 0.5ms thành 40ms và mỗi request đều trả giá.
- **Luôn có TTL**, kể cả với session "không bao giờ hết hạn". Không có TTL nghĩa là bạn đang giữ rác vĩnh viễn.
- **Cookie session cần `HttpOnly`, `Secure`, `SameSite`**. Ba thuộc tính này là mức tối thiểu, không phải tùy chọn nâng cao.
- **Đừng nhồi cả profile vào session.** Session càng nhỏ, đọc càng nhanh, và dữ liệu cũ trong session càng ít gây lỗi.
- **Chỉ tối ưu đúng một nút thắt mỗi lần.** Sửa cache, session và database cùng lúc thì bạn không biết cái nào thực sự có tác dụng.
- **Đo trước khi đổi.** Chưa biết RPS và p95 hiện tại thì chưa có cách chứng minh thay đổi session làm hệ thống tốt hơn.

## Mang gì sang bài checkpoint

- Stateless nghĩa là mỗi request tự mang đủ ngữ cảnh; server không giữ ký ức riêng. Đây là điều kiện để scale ngang có ý nghĩa.
- Ba chỗ lưu session: in-memory (nhanh, không scale), Redis (scale được, thêm 0.5-2ms, cần replica và TTL), JWT (không state, không thu hồi ngay).
- Sticky session là giải pháp tình thế, không phải kiến trúc.
- Sau mỗi lần tách state ra ngoài, hỏi ngay: nút thắt tiếp theo là gì?

Bài tiếp theo là checkpoint của module: chúng ta ghép nút thắt cổ chai, chọn hướng scale và chỗ lưu session thành một bộ câu hỏi tình huống, rồi mới bước sang load balancer ở module 2.

Một server được gọi là _____ khi mỗi request mang đủ thông tin để xử lý và server không giữ ký ức riêng giữa các request.
Cách chữa tạm thời là _____, gắn mỗi user vào một server cố định, đổi lại là mất cân bằng tải và mất session khi server đó chết.
Session lưu trong _____ thì nhiều instance cùng đọc được, đổi lại mỗi request thêm khoảng 0.5 đến 2ms.
Với _____, server không lưu session nhưng cũng không thu hồi token ngay được.
Session trong Redis cần có _____ để key cũ tự hết hạn và user không hoạt động bị đăng xuất.

Front: Server stateless nghĩa là gì?
Back: Mỗi request mang đủ dữ liệu để xử lý; server không giữ ký ức riêng giữa các request.

Front: Vì sao session in-memory chặn scale ngang?
Back: User đăng nhập ở instance A, request sau rơi vào instance B thì B không có session đó nên bị đăng xuất.

Front: Sticky session giải quyết gì và trả giá bằng gì?
Back: Giữ user bám một server cố định; đổi lại là mất cân bằng tải và mất session khi server đó chết.

Front: Ba chỗ lưu session phổ biến?
Back: In-memory trong process, Redis (session store tập trung), và JWT (token phía client).

Front: Đọc session từ Redis thêm bao nhiêu latency mỗi request?
Back: Khoảng 0.5 đến 2ms nếu Redis cùng datacenter.

Front: Vì sao session trong Redis phải có TTL?
Back: Để key cũ tự hết hạn, tránh bộ nhớ phình dần và tự đăng xuất user không hoạt động.

Front: Nhược điểm lớn nhất của JWT là gì?
Back: Không thu hồi token ngay được, phải chờ hết hạn, nên thường dùng access token ngắn kèm refresh token.

Front: Payload của JWT có bí mật không?
Back: Không, chỉ là base64; ai có token cũng đọc được nên không đặt dữ liệu nhạy cảm trong đó.

Front: Redis session store chết thì chuyện gì xảy ra?
Back: Toàn bộ user bị logout cùng lúc, nên cần replica để giảm rủi ro.

Front: Stateless giúp gì khi deploy?
Back: Rolling deploy không đá user ra, instance mới lên là nhận traffic được ngay.

Front: Nút thắt tiếp theo sau khi chuyển session sang Redis là gì?
Back: Số connection mỗi instance mở tới Redis và database, tức là connection pool và cách load balancer chia request.

## Ba vết xe đổ khi làm server stateless

Đây là những lỗi tôi gặp nhiều nhất khi đội chuyển app từ một server sang nhiều server.

- **Để session trong memory rồi bật autoscale.** Instance thứ hai được sinh ra với bộ nhớ session rỗng. User quay lại, rơi vào instance mới, bị đá ra ngoài. Luôn kiểm tra: tắt một instance bất kỳ, user có bị logout không?
- **Quên TTL cho session trong Redis.** Key không bao giờ hết hạn thì bộ nhớ phình dần qua nhiều tháng, và user không hoạt động cũng không bao giờ bị đăng xuất. Đặt TTL bằng đúng thời gian idle cho phép, thường 30 phút đến vài giờ.
- **Đặt dữ liệu nhạy cảm trong JWT.** Payload chỉ là base64, ai có token cũng đọc được. Không đặt role chi tiết, email, số điện thoại hay bất cứ thứ gì bạn không muốn thấy trên màn hình.
- **Một Redis, không replica.** Redis chết là toàn bộ user bay session cùng lúc. Với hệ thống thật, thêm replica và theo dõi độ trễ đọc.
- **Hiểu sai "stateless".** App stateless không có nghĩa là không lưu gì. Database vẫn lưu state nghiệp vụ. Stateless nghĩa là mỗi request tự mang đủ ngữ cảnh để instance nào cũng xử lý được như nhau.

Quy tắc một dòng để tự kiểm tra: **request vào instance nào cũng phải cho ra cùng một câu trả lời.** Nếu câu đó sai, bạn chưa stateless.

## Mang theo gì sang bài checkpoint

- Stateless là điều kiện để scale ngang có ý nghĩa: instance nào cũng giống instance nào, nên thêm hay bớt đều an toàn.
- Session in-memory là nút thắt đầu tiên khi mở rộng ngang. Sticky session chỉ là giải pháp tạm, trả giá bằng mất cân bằng tải và mất session khi server chết.
- Redis session store là lựa chọn mặc định cho đa số hệ thống: đọc khoảng 0.5 đến 2ms, phải có TTL và replica.
- JWT bỏ được lượt đọc session mỗi request nhưng đánh đổi bằng việc không thu hồi token ngay được. Dùng access token ngắn kèm refresh token.

Bài tiếp theo là checkpoint của module, nơi bạn tự kiểm tra lại toàn bộ chuỗi lập luận: nút thắt cổ chai, chọn hướng scale, và chỗ đặt session, trước khi bước sang load balancer ở module 2.

---

## Contents

Part of [🧯 App chịu tải: load balancer, stateless và rate limiting](https://everwise.study/courses/app-chiu-tai-load-balancer-stateless-va-rate-limiting.md)

- **📈 Vì sao app sập và cách scale**
  - [🔍 Nút thắt cổ chai: vì sao app sập khi đông người dùng](https://everwise.study/courses/app-chiu-tai-load-balancer-stateless-va-rate-limiting/nut-that-co-chai-khi-dong-nguoi-dung.md)
  - [↔️ Scale dọc và scale ngang: chọn hướng nào](https://everwise.study/courses/app-chiu-tai-load-balancer-stateless-va-rate-limiting/scale-doc-va-scale-ngang.md) (sign-in required)
  - [🍪 Server stateless và session lưu ở đâu](https://everwise.study/courses/app-chiu-tai-load-balancer-stateless-va-rate-limiting/server-stateless-va-noi-luu-session.md) — this lesson
  - [✅ Checkpoint: nền tảng scale và session](https://everwise.study/courses/app-chiu-tai-load-balancer-stateless-va-rate-limiting/checkpoint-nen-tang-scale-va-session.md)
- **⚖️ Load balancer và connection pool**
  - [🔀 Load balancer: chia tải và health check](https://everwise.study/courses/app-chiu-tai-load-balancer-stateless-va-rate-limiting/load-balancer-chia-tai-va-health-check.md)
  - [🗄️ Connection pool database: vì sao cạn connection](https://everwise.study/courses/app-chiu-tai-load-balancer-stateless-va-rate-limiting/connection-pool-database-can-connection.md)
  - [✅ Checkpoint: load balancer và database](https://everwise.study/courses/app-chiu-tai-load-balancer-stateless-va-rate-limiting/checkpoint-load-balancer-va-database.md)
- **🛡️ Rate limiting, hàng đợi và đọc số tải**
  - [🚧 Rate limiting: chặn tải xấu trước khi vào hệ thống](https://everwise.study/courses/app-chiu-tai-load-balancer-stateless-va-rate-limiting/rate-limiting-chan-tai-xau-truoc-khi-vao-he-thong.md)
  - [📮 Hàng đợi cho tác vụ nặng: đẩy việc dài ra background](https://everwise.study/courses/app-chiu-tai-load-balancer-stateless-va-rate-limiting/hang-doi-cho-tac-vu-nang.md)
  - [📊 Đọc con số tải cơ bản: RPS, latency, p95, error rate](https://everwise.study/courses/app-chiu-tai-load-balancer-stateless-va-rate-limiting/doc-con-so-tai-co-ban-rps-latency-p95.md) (premium content)
  - [✅ Checkpoint: phòng thủ và đọc số tải](https://everwise.study/courses/app-chiu-tai-load-balancer-stateless-va-rate-limiting/checkpoint-phong-thu-va-doc-so-tai.md) (premium content)
- [Course cheatsheet](https://everwise.study/courses/app-chiu-tai-load-balancer-stateless-va-rate-limiting/cheatsheet.md) (premium content)
