Xử lý Traffic gấp 5 lần khi chuyển đổi từ SQL sang NoSQL
27 Feb, 2025
Hướng nội
AuthorChỉ khoảng ba tháng sau khi chuyển đổi từ SQL sang NoSQL, ứng dụng của họ xử lý được lượng truy cập gấp 5 lần, query chạy nhanh hơn 10 lần

Mục Lục
Ứng dụng chính của họ đã từng sập khi gặp lượng truy cập cao điểm, khiến nhiều người dùng bị khóa không thể truy cập. Thời gian phản hồi query tăng vọt lên 2,5 giây, đơn hàng liên tục bị lỗi và log lỗi thì tràn ngập các deadlock errors.
"Chuyển sang NoSQL sẽ làm mất tính toàn vẹn dữ liệu của bạn."
"Chỉ cần tối ưu các SQL query là được."
"NoSQL chỉ là một từ khóa thời thượng thôi."
Bạn và mình có thể đã từng nghe những lời này và cũng giống như họ chúng ta bỏ qua NoSQL, vì nghĩ "NoSQL không thể xử lý tốt transactions".
Họ có hai lựa chọn: tiếp tục scale database SQL theo chiều dọc (như đã từng làm nhiều lần trước đó) hoặc suy nghĩ lại từ đầu về toàn bộ kiến trúc dữ liệu, chuyển sang thử nghiệm NoSQL Database.
Kết quả là gì? Chỉ khoảng ba tháng sau, ứng dụng của họ xử lý được lượng truy cập gấp 5 lần, query chạy nhanh hơn 10 lần, và thời gian downtime hoàn toàn bằng 0. Đây chính là cách họ đã làm được điều đó chúng ta hãy cùng khám phá nhé.
1. Tổng quan hệ thống hiện tại
Ban đầu, kiến trúc của họ gần như hoàn hảo với:
- PostgreSQL RDS cho transactional data
- Redis dùng làm cache
- Elasticsearch cho search
- Multiple read replicas để giảm tải đọc
- SQL queries được tối ưu cẩn thận
- DataDog để monitoring mọi thứ
Tất cả đều được làm "đúng chuẩn", Queries được index đầy đủ, Schema được normalize. Đội ngũ dev am hiểu PostgreSQL đến mức có thể đọc thuộc document trong giấc ngủ.
2. Họ đã gặp vấn đề gì?
Họ nhận ra hệ thống PostgreSQL của mình đã chạm đến giới hạn:
- Các query JOIN phức tạp mất hơn 1,5 giây để chạy khi hệ thống chịu tải cao
- Row-level locking liên tục gây deadlocks
- Chi phí scale theo chiều dọc ngày càng leo thang, không kiểm soát được
- Thường xuyên downtime mỗi khi lưu lượng truy cập tăng đột biến
- Đội kỹ sư thì dành nhiều thời gian xử lý sự cố hơn là xây dựng các tính năng mới
Họ đã thử mọi cách mà các chuyên gia SQL khuyên:
- Thêm nhiều indexes, nhưng chỉ cải thiện được chút ít
- Partition dữ liệu, nhưng vẫn gặp vấn đề tranh chấp khi ghi (write contention)
- Nghĩ đến việc scale theo chiều dọc, nhưng $50,000 cho một server database rõ ràng sẽ vượt ngân sách
Nhưng dù đã làm mọi thứ, workload đọc lớn vẫn "đè bẹp" hệ thống. Các query JOIN phức tạp trở thành cơn ác mộng hiệu năng khi hệ thống scale lên, và mọi nỗ lực tối ưu không thể giải quyết triệt để vấn đề này.
Bên dưới là một trong nhữg câu query bị chậm và query plan của nó:
-- Our most problematic query (2.5s+ execution time)
SELECT
o.id, o.status, o.created_at,
c.name, c.email,
p.title, p.price,
i.quantity,
a.street, a.city, a.country,
(SELECT COUNT(*) FROM order_items WHERE order_id = o.id) as items_count
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN order_items i ON o.id = i.order_id
JOIN products p ON i.product_id = p.id
JOIN addresses a ON o.shipping_address_id = a.id
WHERE o.status = 'processing'
AND o.created_at > NOW() - INTERVAL '24 HOURS'
ORDER BY o.created_at DESC;
Nested Loop (cost=1.13..2947.32 rows=89 width=325)
-> Index Scan using orders_created_at on orders (cost=0.42..1234.56 rows=1000)
-> Materialize (cost=0.71..1701.23 rows=89 width=285)
-> Nested Loop (cost=0.71..1698.12 rows=89 width=285)
-> Index Scan using customers_pkey on customers
-> Index Scan using order_items_pkey on order_items
Hệ thống của họ đang vượt ngưỡng giới hạn thông qua các chỉ số cảnh báo:
- Thời gian query trung bình: Tăng lên hơn 1,5 giây (trước đây chỉ 200ms)
- CPU sử dụng: 89%
- IOPS: Đã chạm giới hạn
- Buffer cache hit ratio: Giảm xuống chỉ còn 65% (trước đây là 87%)
- Tỷ lệ deadlock: 6–7 lần mỗi phút
Nhìn những con số này, họ biết hệ thống đã vượt quá khả năng chịu tải. Nếu không thay đổi nhanh chóng, mọi thứ chắc chắn sẽ sụp đổ.
3. Những giải pháp thất bại
3.1 Tối ưu Query
Bước đầu tiên họ thử là tối ưu hóa query, DBA đã đưa ra một số giải pháp:
- Thêm composite indexes để cải thiện hiệu suất lookup
- Sử dụng materialized views cho các query thường xuyên
- Viết lại các query phức tạp để giảm tải xử lý
-- Added composite indexes
CREATE INDEX idx_orders_status_created ON orders(status, created_at);
CREATE INDEX idx_order_items_order_product ON order_items(order_id, product_id);
-- Materialized views for common queries
CREATE MATERIALIZED VIEW order_summaries AS
SELECT
o.id,
COUNT(i.id) as items_count,
SUM(p.price * i.quantity) as total_amount
FROM orders o
JOIN order_items i ON o.id = i.order_id
JOIN products p ON i.product_id = p.id
GROUP BY o.id;
-- Query rewrite
WITH order_data AS (
SELECT
o.id, o.status, o.created_at,
c.name, c.email
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.status = 'processing'
AND o.created_at > NOW() - INTERVAL '24 HOURS'
)
SELECT
od.*,
os.items_count,
os.total_amount
FROM order_data od
JOIN order_summaries os ON od.id = os.id;
Kết quả: Thời gian query giảm xuống còn 800ms, tốt hơn trước, nhưng vẫn không đủ để đáp ứng nhu cầu thực tế khi tải cao.
3.2 Caching với Redis
Họ đã triển khai caching mạnh tay với Redis để cải thiện hiệu suất:
// Redis caching layer
const getOrderDetails = async (orderId) => {
const cacheKey = `order:${orderId}:details`;
// Try cache first
let orderDetails = await redis.get(cacheKey);
if (orderDetails) {
return JSON.parse(orderDetails);
}
// Cache miss - query database
orderDetails = await db.query(ORDER_DETAILS_QUERY, [orderId]);
// Cache for 5 minutes
await redis.setex(cacheKey, 300, JSON.stringify(orderDetails));
return orderDetails;
};
// Cache invalidation on updates
const updateOrder = async (orderId, data) => {
await db.query(UPDATE_ORDER_QUERY, [data, orderId]);
await redis.del(`order:${orderId}:details`);
};
Ngoài ra, họ còn thêm một bước cache warming để đẩy trước dữ liệu phổ biến vào cache:
// Warm cache for active orders
const warmOrderCache = async () => {
const activeOrders = await db.query(`
SELECT id FROM orders
WHERE status IN ('processing', 'shipped')
AND created_at > NOW() - INTERVAL '24 HOURS'
`);
await Promise.all(
activeOrders.map(order => getOrderDetails(order.id))
);
};
// Run every 5 minutes
cron.schedule('*/5 * * * *', warmOrderCache);
Kết quả: Hiệu suất được cải thiện đáng kể, nhưng việc cache invalidation (xóa cache để đảm bảo đồng bộ với dữ liệu mới) đã trở thành cơn ác mộng, đặc biệt trong lúc tải cao.
3.3 Thêm Read Replica
Họ đã mở rộng hệ thống bằng cách thêm 5 read replicas (cơ sở dữ liệu chỉ đọc) và thiết lập load balancing (cân bằng tải) để chia bớt tải cho cơ sở dữ liệu chính:
// Database connection pool with read-write split
const pool = {
write: new Pool({
host: 'master.database.aws',
max: 20,
min: 5
}),
read: new Pool({
hosts: [
'replica1.database.aws',
'replica2.database.aws',
'replica3.database.aws',
'replica4.database.aws',
'replica5.database.aws'
],
max: 50,
min: 10
})
};
// Load balancer for read replicas
const getReadConnection = () => {
const replicaIndex = Math.floor(Math.random() * 5);
return pool.read.connect(replicaIndex);
};
// Query router
const executeQuery = async (query, params, queryType = 'read') => {
const connection = queryType === 'write'
? await pool.write.connect()
: await getReadConnection();
try {
return await connection.query(query, params);
} finally {
connection.release();
}
};
Việc thêm read replicas đã giảm tải đáng kể cho cơ sở dữ liệu chính khi xử lý các thao tác đọc. Tuy nhiên, trong những giai đoạn tải cao, độ trễ đồng bộ (replication lag) giữa cơ sở dữ liệu chính và các read replicas trở nên nghiêm trọng. Điều này làm hệ thống không thể xử lý dữ liệu thời gian thực một cách chính xác, gây ra nhiều vấn đề khi dữ liệu không đồng nhất giữa master và replicas.
4. Quyết định chuyển sang NoSQL
Sau ba tháng cố gắng tối ưu hóa, hệ thống của họ vẫn gặp phải những vấn đề lớn:
- Mất doanh thu: $110,000 mỗi tháng do hệ thống ngừng hoạt động
- Đội ngũ kỹ thuật: Dành hơn 38% thời gian để xử lý các vấn đề liên quan đến cơ sở dữ liệu
- Sự hài lòng của khách hàng: Đang giảm sút
- Phát triển tính năng: Bị đình trệ hoàn toàn
- Chi phí AWS RDS: Đang gần chạm mức $5,750/tháng
Trong các cuộc thảo luận nội bộ , MongoDB liên tục được nhắc đến, nhưng họ vẫn còn một số nghi ngại như:
- Liệu nó có xử lý được khối lượng giao dịch lớn không?
- Liệu nó có làm ảnh hưởng đến tính nhất quán dữ liệu không?
Những nghi ngại này là hoàn toàn có cơ sở, nhưng vào thời điểm đó, họ không còn lựa chọn nào khác.
Họ bắt đầu bằng việc thử nghiệm một giải pháp nhỏ (POC), tập trung vào việc di chuyển dịch vụ có vấn đề lớn nhất: xử lý đơn hàng (order processing).
// MongoDB order document model
{
_id: ObjectId("507f1f77bcf86cd799439011"),
status: "processing",
created_at: ISODate("2024-02-07T10:00:00Z"),
customer: {
_id: ObjectId("507f1f77bcf86cd799439012"),
name: "John Doe",
email: "john@example.com",
shipping_address: {
street: "123 Main St",
city: "San Francisco",
country: "USA"
}
},
items: [{
product_id: ObjectId("507f1f77bcf86cd799439013"),
title: "Gaming Laptop",
price: 1299.99,
quantity: 1,
variants: {
color: "black",
size: "15-inch"
}
}],
payment: {
method: "credit_card",
status: "completed",
amount: 1299.99
},
shipping: {
method: "express",
tracking_number: "1Z999AA1234567890",
estimated_delivery: ISODate("2024-02-10T10:00:00Z")
},
metadata: {
user_agent: "Mozilla/5.0...",
ip_address: "192.168.1.1"
}
}
Các truy vấn trước đây mất 2.3 giây trên PostgreSQL giờ chỉ mất dưới 200ms trên MongoDB, như các bạn thấy trong document ở trên, họ loại bỏ hoàn toàn các bước join không cần thiết, vì tất cả thông tin đã có sẵn trong order.
Nhờ vậy mà hệ thống của họ đã cải thiện rất nhiều:
- Không có downtime trong dịp Black Friday: Hệ thống xử lý lượng truy cập gấp 3 lần so với bình thường mà không gặp bất kỳ sự cố nào.
- Tăng tốc độ phát triển: Tốc độ phát triển sản phẩm tăng 57%.
- Sự hài lòng của khách hàng tăng: Điểm hài lòng của khách hàng tăng 42%.
- Loại bỏ mất mát doanh thu: Khoản lỗ $110,000 mỗi tháng đã được loại bỏ, đồng thời tạo thêm dòng doanh thu mới trị giá $75,000 mỗi tháng.
- Tinh thần đội ngũ kỹ thuật cải thiện đáng kể: Đội ngũ kỹ thuật không còn phải vật lộn với các vấn đề cơ sở dữ liệu liên tục, giúp họ làm việc hiệu quả hơn và vui vẻ hơn.
5. Kết luận
Sau khi đọc đến đây có lẽ bạn cũng có cảm giác giống như mình là tác giả đã không nói hết tất cả những gì bạn ấy đã làm có thể do bài viết khá dài, vì khi từ bỏ SQL hỗ trợ tính ACID cực kì tốt để đánh đổi lại performance thì họ cũng cần phải sửa đổi kiến trúc phần mềm, làm sao để một chuỗi các hành động bên trong một transaction (trừ tồn kho, cập nhật order status, ....) có thể đồng thời commit hay rollback như áp dụng Saga, ... chẳng hạn.
Tuy nhiên bài viết này đã cho mình thấy rất rõ việc từ bỏ phép JOIN đã khiến hệ thống của họ trở nên nhanh như thế nào khi đối mặt với lượng truy cập cao, nếu bạn đang ở trong tình huống tương tự, thì đừng ngại thử nhé.
Nguồn: Link
Bài viết liên quan
Lập trình backend expressjs
xây dựng hệ thống microservices- Kiến trúc Hexagonal và ứng dụng
- TypeScript: OOP và nguyên lý SOLID
- Event-Driven Architecture, Queue & PubSub
- Basic scalable System Design