TÔI ĐÃ TỰA GẦN KỀ CÁI CHẾT KHI MARIADB SẬP VÌ NỬA TRIỆU DÒNG DỮ LIỆU… VÀ CHIẾN THẮNG TRỞ VỀ!
(Bài viết for fun dành cho ae ít kinh nghiệm giống mình : ))) )
Chào anh em dev thân mến! Hôm nay tôi xin kể câu chuyện đẫm máu và nước mắt của một thằng dev quèn đã phải vật lộn với con quái vật MariaDB như thế nào…
Một buổi sáng đẹp trời… cho đến khi…
Sau một đêm để job chạy tự động, tôi thức dậy và mỉm cười tự mãn khi thấy database đã được nhồi nhét hơn NỬA TRIỆU bản ghi. “Ê hê, ngon rồi!” – Tôi nghĩ vậy, rồi cười khẩy.
Mở Backend UI lên, trang đầu tiên còn tạm ổn. Nhưng khi tôi mạnh dạn nhấn qua trang cuối cùng thì…
BÙM!!!
MariaDB SẬP NGUỒN như ông già 80 tuổi bị trượt chân! Con server của tôi đang đau đớn gào thét trong vô vọng!
Hành trình bất đắc dĩ của gã dev tay ngang
Là một dev tự học (méo học trường lớp tử tế nào cả), mọi thứ tôi biết đều từ Google và hai người thầy tốt bụng chỉ dạy. Thế nên tôi nghĩ: “Chắc server thiếu tài nguyên rồi!” và lật đật cấp thêm RAM như người ta cho ăn xin vậy.
Nhưng hỡi ôi! Server vẫn ì ạch như con rùa bò qua đường cao tốc! Thử xem log thì phát hiện thủ phạm:
select * from `posts` where deleted_at is null order by `group_id` desc, `post_number` desc limit 15 offset 418000
Giải phẫu “bệnh nhân” MariaDB
Quan sát kỹ cấu trúc bảng của chúng ta:
– `id` (bigint unsigned) – dùng để lưu ID dạng snowflake
– `content` (longtext) – đây chính là “con quái vật” nặng ký nhất!
– `group_id` (bigint unsigned)
– `post_number` (integer unsigned)
1 SIMPLE posts index posts _group_id_post_number_unique 12 674669 Using where
Ôi hỡi ơi! 80% dòng index bị quét qua chỉ để phân trang! Chi phí tính toán cao như giá xăng hiện tại!
Cuộc tìm kiếm giải pháp bắt đầu!
Đầu tiên, tôi thêm một cái index gồm 3 trường deleted_at + group_id + post_number
như người chết đuối vớ được cọc, nhưng hiệu quả không khác gì trước. Sau đó, tôi nghe nói đến keyset pagination, nhanh như Usain Bolt vậy! Nhưng nó lại khó sử dụng như đi tìm nước giữa sa mạc nếu không có bookmark sẵn.
Cuối cùng, tôi quyết định cầu cứu anh em trong group như một người lạc trôi giữa biển khơi phát tín hiệu cầu cứu.
Và rồi phép màu đã đến!
Nhờ ý kiến của anh em cao nhân, tôi đã phát hiện ra sai lầm chết người của mình: Thiết kế database quá “ngây thơ”!
Vấn đề chính là field `content` với kiểu dữ liệu LONGTEXT – một con quái vật ngốn RAM và CPU mỗi khi SQL phải sort hàng trăm nghìn dòng. Nó như một con voi chen vào tiệm đồ sứ vậy – cồng kềnh và phá hoại mọi thứ! Ờm chỗ này ae cũng cần lưu ý chỉ chọn các field cần thiết khi select, không nên select * trong mọi trường hợp.
Giải pháp đơn giản đến không ngờ: TÁCH BẢNG! Một bảng để lưu metadata nhẹ tênh (id, group_id, post_number), một bảng khác để nhốt con quái vật LONGTEXT. Database tôi tự tin nhấc tạ, từ 50 giây thực thi xuống còn 1.5 giây!
Uhm một kết quả chưa quá tối ưu, nhưng cũng đã là một khởi đầu tốt đẹp hơn nhiều rồi.
Bài học xương máu rút ra:
1. Thiết kế DB là nghệ thuật, không phải trò đùa: Trước khi xây dựng ứng dụng, hãy dành thời gian thiết kế schema một cách cẩn thận. Phân tích luồng truy vấn, khối lượng dữ liệu và cách dữ liệu sẽ được truy cập. (Có thể tìm hiểu thêm về thứ tự thực thi của mệnh đề nữa, ngoài ra thì việc chỉ select các field cần thiết cũng khá là quan trọng quá trình build query)
2. Normalization là bạn đồng hành: Những fields lớn (như LONGTEXT, BLOB) nên được tách ra bảng riêng để tránh truy vấn nặng nề khi không cần thiết. Hãy tuân thủ các nguyên tắc của database normalization để tránh lặp dữ liệu và tối ưu hiệu suất.
Uhm chỗ này từng có anh em bảo lưu trữ text vào file thay vì db, tôi cũng sẽ cân nhắc thử nghiệm trong tương lai gần.
3. Hiểu rõ về data types: Mỗi loại dữ liệu có những đặc tính riêng ảnh hưởng đến hiệu năng. TEXT và BLOB nặng nề hơn nhiều so với VARCHAR hay INT, đặc biệt khi cần sắp xếp và tìm kiếm.
4. Index không phải là thần dược: Không phải cứ thêm index là mọi thứ sẽ nhanh hơn. Hiểu cách database engine hoạt động để tối ưu đúng chỗ. Index cũng có thể gây chậm khi thực hiện INSERT hoặc UPDATE.
5. Hiểu rõ pagination và tác động của nó: OFFSET truyền thống rất tệ với dữ liệu lớn. Keyset pagination, cursor-based pagination là những giải pháp thay thế tốt hơn.
6. Tìm hiểu về database partitioning: Với dữ liệu lớn, việc phân vùng có thể giúp cải thiện hiệu năng đáng kể. Partitioning cho phép database engine chỉ quét qua các phân vùng cần thiết thay vì toàn bộ bảng.
Tôi cũng nghĩ tới điều này nhưng với quy mô dự án nhỏ nên hơi lười, và cũng khá là khó để nhận định xem nên phân vùng như thế nào cho hợp lý.
7. Tìm hiểu về các công cụ monitoring: Sử dụng các công cụ như Prometheus, Grafana để giám sát hiệu năng database từ sớm.
8. Đừng ngại hỏi cộng đồng: Nhiều khi, những vấn đề bạn gặp phải đã được ai đó giải quyết rồi. Cộng đồng là nguồn tài nguyên vô giá!
9. Caching là người hùng thầm lặng: Với dữ liệu static hoặc ít thay đổi, implement Redis hoặc các giải pháp caching khác có thể giúp giảm tải cho DB.
10. Học hỏi từ sai lầm: Mỗi lần “đâm đầu vào tường” là một bài học quý giá. Hãy ghi nhớ những kinh nghiệm này để lần sau không lặp lại.
Cuối cùng, tôi muốn gửi lời cảm ơn đến anh em trong group đã giúp một thằng dev quèn như tôi thoát khỏi cảnh “ngồi nhìn query chạy và cầu nguyện”!
Các bạn có từng gặp tình huống tương tự chưa? Chia sẻ kinh nghiệm của mình dưới comment nhé!
Ầu lúc đầu tôi viết khác lắm nhưng để con AI nó biên lại thấy thiếu thiếu gì đó, kết thêm 1 cái là ko nên lạm dụng AI quá kkk