LÀM VIỆC VỚI BẢNG CHỨA HÀNG TỶ BẢN GHI
1. Đặt vấn đề
Trong bài viết này, mình muốn thảo luận về ý tưởng làm việc với các bảng có hàng tỷ bản ghi. Mình nghĩ đây là một chủ đề rất hay vì khi anh em chúng ta thiết kế hệ thống, nó sẽ khiến chúng ta luôn luôn phải tự đặt câu hỏi. Thiết kế bảng như thế nào? Thiết kế tài liệu ra sao? hay lựa chọn loại cơ sở dữ liệu nào? Chúng ta có suy nghĩ rằng bảng này sẽ phát triển như thế nào trong tương lai sau 1, 3, 5 năm? Chúng ta có đưa ra dự đoán rằng những bảng này sẽ phát triển lớn đến mức nào và có thể đạt đến cấp độ hàng tỷ bảng ghi không?
Mình muốn đặt vấn đề và cùng anh em thảo luận về cách chúng ta làm việc với lượng dữ liệu khổng lồ như vậy. Có khá nhiều cách để giải quyết vấn đề này và mình muốn chia sẻ một số cách trong số đó. Tất nhiên, nếu mình có thể thiếu hay bỏ lỡ điều gì đó thì anh em hãy cho mình biết ở phần bình luận bên dưới nhé.
Ý tưởng về vấn đề này xuất phát từ cuộc thảo luận giữa mình và một người đồng nghiệp về thiết kế hệ thống mạng xã hội cụ thể là cho tính năng follow. Kết quả thiết kế mình đưa ra trao đổi là, thiết kế đó đã tạo ra một bảng rất, rất lớn. Mình đặt toàn bộ dữ liệu vào một bảng có cấu trúc như sau (user_id, follow_id). Người này theo dõi người kia, các mối quan hệ chằng chịt với nhau dẫn tới bảng ngày càng phình to, vậy chúng ta nên làm gì?
2. Xây dựng ý tưởng
Hãy cùng nhau bắt đầu từ zero, chưa biết gi nhé.
Nếu anh em có một bảng lớn, đầu tiên lóe lên trong đầu khi chưa biết gi là sử dụng brute force để xử lý hoặc làm việc với bảng. Vì vậy, nếu chúng ta đang cố gắng tìm một row trong bảng này, đúng không, chúng ta có thể làm gì nếu không có khái niệm về indexing, không có gì khác ngoài cách tiếp cận brute force, đó là sử dụng đa luồng, đa tiến trình và chia nhỏ bảng thành nhiều phân đoạn để tìm kiếm song song (Đây cơ bản là cách mà big data và Hadoop hoạt động, đúng không?). Ý tưởng là chia nhỏ bảng thành các phần nhỏ hơn để có thể chạy song song, sử dụng brute force để tìm những gì chúng ta cần và tự mình thực hiện quy trình. Vậy đó là ý tưởng. Chúng ta có thể chia bảng này thành hàng trăm phần và tìm kiếm các phần này đồng thời bằng cách sử dụng cụm máy 100 máy tính; điều này có thể hoạt động trong một số trường hợp. Tuy nhiên, điều mình muốn thảo luận là liệu có thể tránh xử lý toàn bộ bảng không? Thay vào đó, liệu có thể chỉ xử lý một tập dữ liệu con của bảng này và làm thế nào để làm được điều đó? Phương pháp tốt nhất là sử dụng indexing nhỉ, đúng không?
Nếu chúng ta tạo chỉ mục cho một cột trong bảng, lúc này cũng sẽ tạo ra một cấu trúc trên ổ đĩa, giúp chúng ta giảm và hình thành tập dữ liệu con mà chúng ta cần tìm kiếm. Thay vì tìm kiếm toàn bộ bảng, chúng ta chỉ tìm kiếm trong một tập con nhỏ, chính là chỉ mục, và dù là vậy, chúng ta vẫn cần quét để tìm kiếm. Điều này giống như một thư viện, nơi anh em có sổ mục lục và các tài liệu phân loại theo chữ cái. Hoặc giả sử chúng ta có hợp đồng cần tìm, nếu tên công ty bắt đầu bằng chữ Z, chúng ta sẽ trực tiếp đến phần màu tương ứng với chữ Z và bắt đầu tìm kiếm từ đó. Điều này giúp giảm thiểu phạm vi tìm kiếm. Đó là sức mạnh của indexing – giảm tập dữ liệu cần làm việc. Thay vì phải làm việc với hàng tỷ dòng, chúng ta có thể chỉ cần làm việc với vài triệu dòng trong trường hợp này.
Với cách sử dụng index cũng đã giảm được khớ khớ dữ liệu cần phải làm việc rồi nhưng liệu chúng ta có thể giảm tập dữ liệu đó xuống thậm chí còn nhỏ hơn không? Nếu giảm được nhỏ hơn nữa thì ngon, may mắn là vẫn có cách anh em à, đây là lúc những người làm cơ sở dữ liệu dùng các kỹ thuật như partition. Partition là việc chia nhỏ bảng lớn này ra, và ở đây mình đang nói về horizontal partition, chứ không phải vertical partition. Horizontal partition nghĩa là chia bảng thành hai hoặc nhiều phần dựa trên hàng. Chúng ta sẽ phân bổ các row từ khoảng này đến khoảng khác vào một vị trí cụ thể trên ổ đĩa. Sau đó, các row từ khoảng này đến khoảng khác sẽ được đặt ở một vị trí khác. Điều này khác với indexing. Cả bảng vẫn có chỉ mục, nhưng bây giờ chúng ta phân chia bảng thành nhiều phần nhỏ hơn. Vậy làm sao để biết cần tìm trong partition nào? Chúng ta cần một khái niệm khác giúp xác định partition cần tìm kiếm, và nếu may mắn, chúng ta chỉ cần tìm kiếm một hoặc hai partition. Đây là khái niệm khóa phân đoạn (partition key). Chúng ta luôn partition dựa trên một key nào đó. Tương tự như indexing, nhưng indexing làm việc trên toàn bộ bảng, trong khi partition chia bảng thành các phần nhỏ hơn. Thường thì các cơ sở dữ liệu sẽ lo việc này cho anh em, nên từ góc nhìn của người truy vấn, việc làm việc với index hay partition hoàn toàn trong suốt. Điều này giúp truy vấn cực kỳ nhanh, đúng không? Nếu biết nơi cần tìm, chúng ta có thể truy cập đúng partition mong muốn, và chỉ cần tìm kiếm trong đó, còn indexing sẽ thu nhỏ tập dữ liệu cần tìm kiếm hơn nữa. Điều này rất rất hay. Và chúng ta vẫn chỉ có một cơ sở dữ liệu, nhưng đã chia nhỏ bảng thành nhiều partition, và giờ đây chúng ta có thể tìm kiếm chính xác dữ liệu chúng ta muốn. Đến đoạn này đã thực sự giúp anh em thỏa mãn và dừng lại chưa? Mình nghĩ là chưa bởi vì mặc dù phân tách dữ liệu cần làm việc ra khá nhỏ rồi nhưng dữ liệu vẫn tồn tại trên một database nên gần như sẽ hạn chế khả năng mở rộng và tính sẵn sàng khi dữ liệu lên tới nhiều tỷ dòng.
Chạy trên một máy chủ rủi ro thì mình nghĩ ngay đến việc tìm cách có thể tổ chức trên nhiều máy chủ hơn nữa, chúng ta có thể phân tán dữ liệu xa hơn nữa qua nhiều máy chủ bằng cách sử dụng kĩ thuật sharding. Tương tự với partition, chúng ta vẫn có thể áp dụng partition và thêm ý tưởng về sharding vào đó, nhưng mà tự nhiên có cảm giác thiết kế kiểu này sẽ làm hệ thống phức tạp hơn vờ cờ đờ. Ví dụ với sharding, chúng ta có thể đặt 100,000 khách hàng đầu tiên vào một cơ sở dữ liệu và 100,000 khách hàng tiếp theo vào một cơ sở dữ liệu khác, và chúng sẽ không liên kết, ràng buộc với nhau. Tuy nhiên, điều này gây ra vấn đề về giao dịch, vì chúng là hai cơ sở dữ liệu khác nhau. Chúng ta chỉ cần giảm kích thước của bảng, nhưng đồng thời chúng ta cũng làm phức tạp thêm phía client vì giờ đây client phải biết về shard. Ví dụ như, nếu chúng ta đang tìm kiếm khách hàng số 500, thì chúng ta phải biết là shard nào chứa dữ liệu đó kiểu như “À, nó nằm ở shard 1”, vì đó là nơi chứa dữ liệu cần tìm. Khi chúng ta tìm sâu hơn vào shard đó, bảng sẽ được chia thành các partition, và trong mỗi partition lại có các index. Nhờ đó, chúng ta đã thu hẹp từ hàng tỷ dòng xuống chỉ còn vài nghìn hoặc vài trăm nghìn dòng, điều này có vẻ cũng ô sờ kê đấy chứ nhỉ. Đó là ý tưởng của việc sử dụng shard, partition và sau đó là index để tìm đúng dòng mà chúng ta đang cần. Đây là cách mà chúng ta giới hạn phạm vi dữ liệu cần làm việc.
Và cuối cùng, sau khi suy nghĩ nát óc anh em à, tại sao lại phải có một bảng hàng tỷ bản ghi ngay từ đầu? Vậy chúng ta hoàn toàn có thể tránh được vấn đề này không? Ví dụ như trường hợp của mạng xã hội, có lẽ chúng ta có thể giải quyết vấn đề này để không cần phải có bảng chứa hàng tỷ bảng ghi. Ví dụ, nếu có một bảng hồ sơ cá nhân (user info) với các thông tin như: ID, tên, ảnh đại diện, chúng ta có thể thêm một trường “số lượng người theo dõi” dạng số nguyên. Hơn nữa, đa phần các cơ sở dữ liệu quan hệ hiện nay đều hỗ trợ JSON. Chúng ta có thể thêm một trường JSON hoặc danh sách chứa thông tin người theo dõi trong hồ sơ cá nhân. Như vậy, thay vì có một bảng quan hệ cho biết ai đang theo dõi ai, ta có thể gói tất cả thông tin vào một hồ sơ duy nhất. Khi muốn lấy thông tin người theo dõi, chỉ cần truy vấn vào hồ sơ cá nhân đó và lấy ra dữ liệu. Mọi thông tin sẽ được gói gọn trong hồ sơ của người dùng. Và mỗi khi ai đó theo dõi chúng ta hoặc chúng ta theo dõi ai đó, thông tin đó sẽ được cập nhật vào hồ sơ cá nhân của chúng ta. Cách này cũng hợp lí phết nhờ . Nhưng mà tác động lên khi có tải ghi thì sẽ như thế nào? Khi có ai đó theo dõi chúng ta, chúng ta cần cập nhật hai cột đó. Chúng ta cần cập nhật số lượng người theo dõi. Thiết kế ban đầu ở đoạn đầu bài viết của chúng ta không gặp phải vấn đề này, nhưng nó cũng không thể mở rộng tốt như phương pháp này, theo ý kiến của mình. Bây giờ chúng ta có thể bắt đầu quan tâm đến tải ghi kiểu như tải ghi tăng theo thời gian hoặc peak tải thì thực tế case này mình cũng sẽ k để database phơi tải và chịu tải toàn bộ, chúng ta có thể sử dụng mesage queue để ghi bất đồng bộ và cập nhật sau. Đương nhiên sẽ có một chút trễ, nhưng không sao cả, hoàn toàn chấp nhận được, chỉ là số lượng người theo dõi thôi. Chúng ta sẽ đưa vào mesage queue và từ từ cập nhật.
3. Kết luận
Vì vậy, để tóm tắt lại toàn bộ lại mọi thứ đã trao đổi,
Thay vì làm việc với bảng chứa hàng tỷ bản ghi, chúng ta nên thử xử lý song song trước, hoặc có thể tránh việc có bảng chứa hàng tỷ bản ghi nếu có thể. Nếu không thể tránh, hãy xem xét việc lập chỉ mục các trường quan trọng. Sau đó, nếu có thể, hãy chia nhỏ bảng bằng cách partition nó. Nếu không thể partition hay lập chỉ mục, chúng ta có thể cần phải thực hiện việc phân mảnh (sharding) để chia bảng thành các phần nhỏ hơn trên nhiều máy chủ khác nhau.
4. Thông tin kết nối
Nếu anh em muốn trao đổi thêm về bài viết, hãy kết nối với mình qua LinkedIn và Facebook:
- LinkedIn: https://www.linkedin.com/in/nguyentrungnam/
- Facebook: https://www.facebook.com/trungnam.nguyen.395/
Rất mong được kết nối và cùng thảo luận!
All rights reserved