Xử lý lưu lượng gấp 5 lần sau khi chuyển từ SQL sang NoSQL

Trước đó, ứng dụng chính nhiều lần bị “nghẽn” vào giờ cao điểm: người dùng không truy cập được, truy vấn mất 2,5 giây, đơn hàng lỗi liên tục và log đầy deadlock errors.

 

SQL, NoSQL, Traffic

 

Không ít ý kiến phản đối:

 

  • “Chuyển sang NoSQL sẽ hy sinh tính toàn vẹn dữ liệu.”

 

  • “Chỉ cần tối ưu query SQL là đủ.”

 

  • “NoSQL chỉ là trào lưu.”

 

Họ từng bỏ qua NoSQL vì nghĩ rằng không thể xử lý tốt transaction. Nhưng đến khi buộc phải chọn giữa scale SQL theo chiều dọc hoặc tái thiết kế kiến trúc dữ liệu với NoSQL, họ đã chọn hướng thứ hai.

 

Kết quả: sau ba tháng, hệ thống chịu tải gấp 5 lần, truy vấn nhanh hơn 10 lần, downtime bằng 0.

 

1. Kiến trúc ban đầu

 

  • PostgreSQL RDS cho transactional data

 

  • Redis cache

 

  • Elasticsearch search

 

  • Multiple read replicas

 

  • Query tối ưu, index đầy đủ

 

  • DataDog giám sát

 

 

2. Vấn đề khi SQL chạm trần hiệu năng

 

  • Query JOIN phức tạp > 1,5 giây khi tải cao

 

  • Row-level locking gây deadlock

 

  • Chi phí scale dọc cao

 

  • Downtime khi traffic spike

 

  • Dev tốn thời gian chữa cháy thay vì phát triển

 

Ví dụ truy vấn chậm

 

-- 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 báo:

 

  • Thời gian query TB: > 1,5 giây

 

  • CPU: 89%

 

  • IOPS: chạm giới hạn

 

  • Cache hit ratio: 65% (trước 87%)

 

  • Deadlock: 6–7 lần/phút

 

3. Những giải pháp không thành công

 

3.1 Tối ưu Query

 

Thêm composite indexes, dùng materialized views, viết lại query 

 
-- 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;


→ Giảm còn 800ms, nhưng chưa đủ khi tải cao.

 

3.2 Cache với Redis

 

Triển khai caching + cache warming

 

// 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);


→ Nhanh hơn, nhưng invalidation phức tạp, dễ lỗi khi tải lớn.

 

3.3 Thêm Read Replica

 

Tăng 5 read replicas + load balancing

 

// 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();
 
  }
 
};
 
→ Giảm tải đọc, nhưng replication lag gây mất đồng bộ dữ liệu thời gian thực.

 

4. Quyết định chuyển sang NoSQL

 

Ba tháng cố gắng không cứu nổi:

 

  • Mất $110,000/tháng do downtime

 

  • 38% thời gian dev xử lý sự cố DB

 

  • AWS RDS gần $5,750/tháng

 

  • Khách hàng giảm hài lòng

 

  • Phát triển tính năng đình trệ

 

MongoDB được chọn thử nghiệm với 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: "[email protected]",
 
    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"
 
  }
 
}
 


Kết quả: query 2,3 giây trên PostgreSQL → còn <200ms trên MongoDB.

 

5. Thành quả

 

  • Black Friday không downtime, chịu tải gấp 3 lần

 

  • Dev speed +57%

 

  • Khách hàng hài lòng +42%

 

  • Xóa lỗ $110k/tháng, thêm $75k/tháng doanh thu mới

 

  • Dev team bớt stress, tập trung sáng tạo

 

 HỖ TRỢ TRỰC TUYẾN