Thiết kế database cho serverless, thách thức lớn nhất trong đầu chúng tôi là xây dựng infrastructure hỗ trợ per-request pricing profitable. Tin Upstash đã achieve này. Sau launch, thấy thách thức major khác: Database connections!
Như bạn biết, Serverless Functions scale từ 0 đến infinity. Nghĩa là khi functions nhận nhiều traffic, cloud provider tạo containers mới (lambda functions) parallel và scale out backend. Nếu tạo new database connection trong function, nhanh chóng đạt connection limit của database.
Nếu cache connection ngoài lambda functions, vấn đề khác xảy ra. Khi AWS freeze Lambda function, nó không close connection. Có thể end up với nhiều idle/zombie connections vẫn đe dọa.
Vấn đề này không specific Redis, áp dụng cho all databases dựa trên TCP connections (MySQL, PostgreSQL, MongoDB, v.v.). Community serverless tạo solutions như serverless-mysql. Đây là client-side solutions. Upstash có advantage implement/maintain server-side. Quyết định mitigate bằng monitoring connections và evicting idle ones. Algorithm:
Connection Eviction Algorithm
if( current_connection_count < SOFT_LIMIT ) { ACCEPT_NEW_CONNECTIONS }
if( current_connection_count > SOFT_LIMIT && current_connection_count < HARD_LIMIT ) { ACCEPT_NEW_CONNECTIONS START_EVICTING_IDLE_CONNECTIONS }
if( current_connection_count > HARD_LIMIT ) { REJECT_NEW_CONNECTIONS }
Lưu ý: max concurrent connection limits trong Upstash docs là soft limits.
Ephemeral Connections
Sau deploy algorithm trên, thấy great decrease rejected connections all regions. Nhưng muốn safe side, giải quyết ở side bạn cũng được. Thay vì reuse connection, mở Redis connection trong function nhưng close khi done:
exports.handler = async (event) => {
const client = new Redis(process.env.REDIS_URL);
/* do stuff with redis */
await client.quit();
/* do other stuff */
return { response: "response" };
};
Code trên minimize concurrent connection count. Người ta hỏi latency overhead của new connections. Redis connections rất lightweight.
Redis Connections có thực sự Lightweight?
Chạy benchmark test xem Redis connections lightweight đến đâu. So sánh latency 2 approaches:
1- EPHEMERAL CONNECTIONS: Không reuse connection. Tạo new connection cho mỗi command và close ngay. Record latency client creation, ping(), client.quit() cùng lúc. Xem benchEphemeral() method.
2- REUSE CONNECTIONS: Tạo connection một lần, reuse cho all commands. Record latency ping() operation. Xem benchReuse() method.
async function benchReuse() {
const client = new Redis(options);
const hist = hdr.build();
for (let index = 0; index < total; index++) {
let start = performance.now() * 1000; // to μs
client.ping();
let end = performance.now() * 1000; // to μs
hist.recordValue(end - start);
await delay(10);
}
client.quit();
console.log(hist.outputPercentileDistribution(1, 1));
}
async function benchEphemeral() {
const hist = hdr.build();
for (let index = 0; index < total; index++) {
let start = performance.now() * 1000; // to μs
const client = new Redis(options);
client.ping();
client.quit();
let end = performance.now() * 1000; // to μs
hist.recordValue(end - start);
await delay(10);
}
console.log(hist.outputPercentileDistribution(1, 1));
}
Xem repo nếu muốn tự chạy benchmark.
Chạy benchmark code AWS EU-WEST-1 region 2 setups. Setup 1: SAME ZONE (client và database cùng AZ). Setup 2: INTER ZONE (client khác AZ với database). Dùng Upstash Standard type.
Kết quả: Overhead tạo/đóng new connection (ephemeral approach) chỉ 75 microseconds (99th percentile). Interzone setup tương tự (80 microseconds).
Test lại bên trong AWS Lambda functions. Kết quả khác. Đặc biệt khi set Lambda memory thấp (128Mb), thấy bigger overhead Redis connections. Latency overhead lên tới 6-7 msec bên trong AWS Lambda.
Kết luận về Redis connections:
- Redis connections thực sự lightweight trên hệ thống có CPU power hợp lý. Thậm chí t2.micro.
- CPU power với default AWS Lambda config rất yếu, tăng significant cost của TCP connections so với total execution time Lambda function.
- Nếu dùng Lambda functions default/minimum memory, better cache Redis connection ngoài function.
Frozen Container => Zombie Connection
Sau khi nhận ra connection có notable overhead ở một số AWS Lambda setups, quyết định test thêm reusing connection trong AWS Lambda. Phát hiện issue khác. Edge case chưa ai report.
Timeline:
STEP1 - timer-0sec: Gửi request, cache connection ngoài lambda function.
if (typeof client === "undefined") {
var client = new Redis("REDIS_URL");
}
module.exports.hello = async (event) => {
let response = await client.get("foo");
return { response: response + "-" + time };
};
STEP2 - timer-5sec: AWS freeze container sau short time.
STEP3 - time-60sec: Upstash timeout 60s cho idle connections. Kill connection nhưng không get ACK từ client vì frozen. Server connection vào state FIN_WAIT_2.
STEP4 - time-90sec: Upstash server kill connection hoàn toàn, exit FIN_WAIT_2 state.
STEP5 - time-95sec: Client gửi request cùng, get ETIMEDOUT exception. Client assume connection open nhưng không. 🤦🏻 🤦🏻 🤦🏻
STEP6 - time-396sec: 5 phút sau last request, AWS kill container hoàn toàn.
STEP7 - time-400sec: Client gửi request lần này work vì container tạo từ đầu, initialization không skip. New connection created.
Như trên, AWS thaw container và reuse connection. Nhưng connection đã closed từ server side và không communicated vì function frozen. Synchronization problem giữa Upstash evicting idle connection và AWS handling idle function. Nếu kill idle connection chỉ sau AWS terminate function thì không issue.
Thay đổi Upstash connection timeout thành 310 seconds assume AWS terminate idle function trong 300 seconds. Sau change, problem disappeared. Issue: AWS không transparent khi terminate idle functions. Cần continue testing detect nếu issue xảy ra lại.
Issue tương tự issue trên serverless-mysql library. Comments suggest retry request upon ETIMEDOUT exception. Nhưng retry có 2 drawbacks: 1) có thể retry write request đã processed mà timed out do real network problem. 2) extra latency của failed request.
GraphQL cũng giúp
Cách loại bỏ connection issues: connectionless API. Upstash hỗ trợ GraphQL API bên cạnh Redis protocol. GraphQL HTTP-based nên không có connection limit issue. Check docs cho supported commands. Lưu ý GraphQL API có latency overhead (~5msec) so với Redis protocol.
Kết luận
Chúng tôi customize Upstash databases cho smooth experience serverless apps. Server-side algorithm mới remove inactive connections AWS Lambda tạo nhiều. Bạn minimize connections bằng open/close Redis client trong Lambda function nhưng có latency overhead nếu function memory < 1GB.
Khuyến nghị cho serverless use cases:
- Use case latency sensitive (6msec lớn) -> reuse Redis client.
- Experience very high concurrent clients (>1000) -> reuse Redis client.
- Use case không latency sensitive -> open/close Redis client trong function.
- Function có ít nhất 1GB memory -> open/close Redis client trong function.