Trong bài viết blog này, chúng ta sẽ triển khai trang waiting room (phòng chờ) cho trang web của bạn.
Tại sao?
Số lượng người truy cập lớn vào trang web thường là điều tốt nhưng không phải lúc nào cũng vậy. Tăng trưởng đột ngột có thể dễ dàng làm quá tải ứng dụng, làm gián đoạn dịch vụ hoàn toàn. Waiting room là giải pháp giúp kiểm soát lưu lượng và bảo vệ tài nguyên trong lúc tăng trưởng truy cập. Cloudflare Waiting Room là giải pháp tốt nhưng chỉ dành cho tài khoản business và enterprise. Đừng lo, trong blog này chúng ta sẽ xây dựng waiting room cho bất kỳ loại trang web nào dùng Cloudflare Workers và Upstash Redis.
Cách thức?
Có hai tham số kiểm soát waiting room.
- Max session duration (Thời gian phiên tối đa): Khách truy cập có thể ở nhàn rỗi trên trang web bao lâu?
- Max website capacity (Sức chứa tối đa trang web): Trang web cho phép bao nhiêu người truy cập cùng lúc?
Khi khách truy cập vào trang web, chúng ta tạo khóa duy nhất (như session key) và lưu trong Redis. Chúng ta đặt khóa này vào Redis với thời gian hết hạn là max session duration. Trước khi cho khách vào trang web, kiểm tra dbsize của Redis. Nếu kích thước lớn hơn max website capacity, chuyển khách đến waiting room. Waiting room là HTML tĩnh nhưng refresh mỗi 30 giây. Nghĩa là, mỗi 30 giây, khách có thể được phép vào trang web nếu có chỗ trống.
Khóa duy nhất tạo ra cũng được ghi vào request của khách dưới dạng cookie. Vì vậy request của cùng một khách sẽ có cùng khóa. Điều này cần thiết để kiểm tra xem khách đã có phiên trong trang web khi sức chứa đầy. Với tư cách khách, bạn được phép vào trang web ngay cả khi sức chứa đầy miễn là khóa của bạn tồn tại trong Redis keyspace. Nếu bạn ở nhàn rỗi lâu hơn max session duration, khóa sẽ bị xóa khỏi Redis. Và nếu sức chứa đầy khi bạn gửi request, bạn sẽ được chuyển đến waiting room.
Chúng ta sẽ triển khai logic này tại Cloudflare Workers. Sử dụng Upstash làm Redis store. Bây giờ giải thích bối cảnh quyết định công nghệ này.
Tại sao Cloudflare Workers?
Triển khai waiting room sẽ chặn tất cả request đến trang web. Vì vậy chi phí hiệu suất nên tối thiểu. Cloudflare Workers tận dụng cơ sở hạ tầng edge của Cloudflare, cho độ trễ tối thiểu toàn cầu. Khác với AWS Lambda, nó không có vấn đề cold start. Là công nghệ serverless nên khả năng mở rộng cũng không thành vấn đề.
Tại sao Upstash Redis?
Bạn cần kiểm tra kích thước trang web hiện tại trước khi cho mỗi khách mới vào. Cloudflare Workers là stateless nên cần lưu thông tin này bên ngoài. Redis là lựa chọn tốt nhất với độ trễ thấp. Nhưng các sản phẩm Redis yêu cầu kết nối TCP không được Cloudflare Workers hỗ trợ. Upstash là sản phẩm Redis duy nhất có REST API tích hợp sẵn. Cũng nhờ Global replication, nó cho độ trễ thấp khắp thế giới.
Triển khai từng bước
Chúng ta sẽ triển khai dự án từng bước dưới đây. Nếu muốn clone dự án và thiết lập waiting room cho trang web trực tiếp, làm theo các bước trong readme của mã nguồn.
1 Thiết lập Dự án
Tạo dự án dùng wrangler.
wrangler generate waiting-room
Sau đó cài đặt dependency:
npm install cookie @upstash/redis
2 Cập nhật wrangler.toml
Cập nhật type:
type = "webpack"
Đặt Cloudflare account id. Xem đây để tìm account id.
account_id = "REPLACE_HERE"
Thêm các biến này:
[vars]
UPSTASH_REDIS_REST_TOKEN = "REPLACE_HERE"
UPSTASH_REDIS_REST_URL = "REPLACE_HERE"
TOTAL_ACTIVE_USERS = 10
SESSION_DURATION_SECONDS = 30
Bạn cần tạo Global database từ bảng điều khiển Upstash. Chỉ cần copy paste REST token và URL từ console. Cơ sở dữ liệu Redis nên trống ban đầu và chỉ dùng cho ứng dụng này.
Đặt TOTAL_ACTIVE_USERS và SESSION_DURATION_SECONDS tùy theo yêu cầu của bạn.
3 index.js
Index.js là file triển khai Cloudflare Workers. Vậy chúng ta sẽ đặt toàn bộ logic vào đây. Copy paste mã dưới vào:
import { parse } from "cookie";
import { Redis } from "@upstash/redis/cloudflare";
const redis = Redis.fromEnv();
addEventListener("fetch", (event) => {
event.respondWith(
handleRequest(event.request).catch(
(err) => new Response(err.stack, { status: 500 })
)
);
});
const COOKIE_NAME_ID = "__waiting_room_id";
const COOKIE_NAME_TIME = "__waiting_room_last_update_time";
const init = {
headers: {
Authorization: "Bearer " + UPSTASH_REDIS_REST_TOKEN,
},
};
async function handleRequest(request) {
const { pathname } = new URL(request.url);
if (!pathname.startsWith("/favicon")) {
const cookie = parse(request.headers.get("Cookie") || "");
let userId;
if (cookie[COOKIE_NAME_ID] != null) {
userId = cookie[COOKIE_NAME_ID];
} else {
userId = makeid(8);
}
const size = await redis.dbsize();
console.log("current capacity:" + size);
// there is enough capacity
if (size < TOTAL_ACTIVE_USERS) {
return getDefaultResponse(request, cookie, userId);
} else {
// site capacity is full
const user = await redis.get(userId);
if (user === "1") {
// the user has already active session
return getDefaultResponse(request, cookie, userId);
} else {
// capacity is full so the user is forwarded to waiting room
return getWaitingRoomResponse(userId);
}
}
} else {
return fetch(request);
}
}
async function getDefaultResponse(request, cookie, userId) {
// uncomment below to test the function with a static html content
// const newResponse = new Response(default_html)
// newResponse.headers.append('content-type', 'text/html;charset=UTF-8')
const response = await fetch(request);
const newResponse = new Response(response.body, response);
const now = Date.now();
let lastUpdate = cookie[COOKIE_NAME_TIME];
if (!lastUpdate) lastUpdate = 0;
const diff = now - lastUpdate;
const updateInterval = (SESSION_DURATION_SECONDS * 1000) / 2;
if (diff > updateInterval) {
await redis.setex(userId, SESSION_DURATION_SECONDS, 1);
newResponse.headers.append(
"Set-Cookie",
`${COOKIE_NAME_TIME}=${now}; path=/`
);
}
newResponse.headers.append(
"Set-Cookie",
`${COOKIE_NAME_ID}=${userId}; path=/`
);
return newResponse;
}
async function getWaitingRoomResponse(userId) {
const newResponse = new Response(waiting_room_html);
newResponse.headers.set("content-type", "text/html;charset=UTF-8");
return newResponse;
}
function makeid(length) {
let result = "";
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
const waiting_room_html = `
Waiting Room
You are now in line.
Thanks for your patience.
We are experiencing a high volume of traffic. Please sit tight and we will let you in soon.
This page will automatically refresh, please do not close your browser.
`;
const default_html = `
Waiting Room Demo
Waiting Room Demo
Visit this site from a different browser, you will be forwarded to the waiting room when the capacity is full.
Check this project to set up a waiting room for your website.
`;
Trong mã trên, bạn có thể chỉnh sửa biến waiting_room_html. Đó là HTML tĩnh của trang waiting room.
Phương thức handleRequest trả về getDefaultResponse hoặc getWaitingRoomResponse tùy thuộc vào khả năng dùng của trang web.
4 Chạy Cục bộ
Để test waiting room cục bộ, có thể đặt capacity = 1, session duration = 30 giây. Sau đó chạy:
wrangler dev
Mở https://127.0.0.1:8787/ trên Chrome, bạn sẽ thấy:

Sau đó mở cùng URL trên Safari (hoặc Chrome ẩn danh), bạn sẽ thấy:

Đợi hơn 30 giây. Bạn sẽ thấy trang waiting room refresh và vào trang web.
Cục bộ không chuyển đến trang web thật nên thấy trang 404 của Cloudflare, điều đó ổn.
5 Xuất bản (Publish)
Triển khai hàm CF Workers với:
wrangler publish
Sẽ cho URL như: https://waiting-room.upsdev.workers.dev/
Bây giờ hãy định tuyến worker cho trang web. Đầu tiên nameserver domain phải trỏ đến Cloudflare. Xem đây. Sau đó, thêm domain làm route và chọn hàm Workers trong dashboard CF Workers.

Kết luận
Chúng ta đã xây dựng thành công waiting room mà không chạm vào mã ứng dụng nhờ Cloudflare Workers và Upstash Redis. Chúng tôi tin đây là minh chứng khác cho thấy các edge function trở nên mạnh mẽ khi kết hợp với Upstash.
Có những điều cần cải thiện:
- Thời gian chờ ước tính: Có thể tính và hiển thị thời gian chờ trung bình.
- Hàng đợi công bằng và có thứ tự: Hiện tại khách chờ vào trang ngẫu nhiên khi có chỗ. Có thể giữ hàng đợi và cho vào theo thứ tự.
Cả hai cải tiến trên đều yêu cầu lưu trữ nhiều state hơn và gọi remote nhiều hơn. Đó là lý do chúng ta bỏ qua. Nếu trường hợp sử dụng yêu cầu chặt chẽ, tham khảo blog này để lấy cảm hứng từ công việc của team Cloudflare trên giải pháp enterprise.
Xem mã nguồn.