Một số mẹo nhỏ trong thiết kế database lớn

Từ khi bắt đầu xây dựng dự án ta thường xuyên phải ra các quyết định khiến cho việc phát triển quy mô sau này trở nên dễ dàng hoặc khó khăn hơn. Đôi khi hướng đến những mục tiêu ngắn hạn không phải là một ý kiến tồi, nhất là khi deadline đã đến gần và ta phải hoàn thành sớm nhất có thể. Nhưng cũng có những lúc ta buộc phải lựa chọn những giải pháp kỹ thuật khó khăn hơn để đạt được mục tiêu lâu dài.

Dưới đây là một vài mẹo nhỏ để giúp cho database giữ được tính ổn định trong khi lượng thông tin lớn dần lên:

Sử dụng UUID

Khi nhắc đến "primary key" - hay "khóa chính", đa số chúng ta thường liên tưởng đến một trường ID tự động tăng. Phường pháp này rất tiện lợi với các cơ sở dữ liệu nhỏ, nhưng nó sẽ trở thành một vấn đề lớn khi cơ sở dữ liệu đủ lớn.

Với hệ quản trị cơ bản MySQL thì trường ID có định dạng là UNSIGNED INT có giá trị tối đa là 4,294,967,295 - không đủ để đánh ID cho toàn bộ người trên trái đất. Có một cách để tạm thời giải quyết vấn đề này là mở rộng bộ nhớ lên định dạng UNSIGNED BIGINT, giá trị tối đa lên đến 18,446,744,073,709,551,615.

Thêm vào đó, hệ quản trị CSDL truyền thống cần đảm bảo tính thống nhất. Có nghĩa là các primary key phải được đánh dấu lưu trữ trên một server duy nhất, vậy nên sẽ là một rắc rối lớn nếu có hàng nghìn yêu cầu ghi cùng một lúc.

Giải pháp UUID được đưa ra với hy vọng có thể giải quyết cả hai vấn đề trên. Nếu bạn chưa từng gặp UUID, thì nó là một key đặc trưng có dạng như sau: 123e4567-e89b-12d3-a456-426655440000.

Đây là định nghĩa đơn giản của wikipedia:

A universally unique identifier (UUID) is a 128-bit number used to identify information in computer systems. The term globally unique identifier (GUID) is also used. When generated according to the standard methods, UUIDs are for practical purposes unique, without requiring a central registration authority or coordination between the parties generating them. The probability that a UUID will be duplicated is not zero, but is so close to zero as to be negligible. Thus, anyone can create a UUID and use it to identify something with near certainty that the identifier does not duplicate one that has already been created to identify something else, and will not be duplicated in the future. Information labeled with UUIDs by independent parties can therefore be later combined into a single database, or transmitted on the same channel, without needing to resolve conflicts between identifiers.

Tạm dịch:

Một định danh phổ cập duy nhất (universally unique identifier - UUID) là một số 128 bit sử dụng để định danh dữ liệu trong hệ thống máy tính. Khái niệm này còn được gọi là định danh toàn cầu duy nhất (globally unique identifier - GUID). Có thể được tạo ra bởi các phương pháp thông thường, UUID phục vị mục đích thực tiễn là tạo ra các key duy nhất, mà không cần đến một trung tâm đăng ký chung hoặc liên kết giữa các cơ quan có thể sinh ra nó. Vì vậy. tỉ lệ một UUID bị trùng không hoàn toàn bằng 0, nhưng có gần 0 đến mức có thể được bỏ qua. Thêm vào đó, bất cứ ai có thể tạo ra một UUID và sử dụng nó để định danh gần như có thể chắc chắn rằng định danh nó không bị trùng lặp và đã được sử dụng để định danh thứ khác, và sẽ không bị sử dụng lại trong tương lai. Các thông tin được đánh dấu bởi UUID của các cơ sở độc lập có thể được trộn vào làm một, hoặc truyền trên cùng một kênh, mà không lo phải xử lý việc nhầm lẫn hay xung đột giữa các định danh.

Khi sử dụng UUID làm primary key, các yêu cầu ghi không nhất thiết phải đi qua một database. Thay vào đó database có thể được chia nhỏ trên nhiều server.

Thêm vào đó, nó còn cho phép thực hiện những yêu cầu như tạo ra ID cho một bản ghi trước khi nó được lưu vào database. Việc này có thể hữu dụng nếu muốn gửi bản ghi đến một server cache hoặc search, nhưng không muốn chờ đến khi transaction của database hoàn tất.

Để sử dụng UUID trên Rails app rất dễ dàng. Chỉ cần chỉnh sửa trên vào config file:

# config/application.rb
config.active_record.primary_key = :uuid

Và thêm vào một bảng muốn sử dụng UUID với Rails migrations:

create_table :users, id: :uuid do |t|
  t.string :name
end

Với những tùy chọn đơn giản, nếu được bật lên từ lúc bắt đầu phát triển, sẽ giúp tiếp kiệm được rất nhiều effort xử lý khi database trở nên quá lớn. Một tác dụng phụ khác là nó sẽ khỏ hơn với bot để có thể đoán được các private URL.

Count và counter

Khi thực sự quan sát, các trường hợp đếm số có mặt ở khắp mọi nơi. Email client đếm số lương mail chưa đọc, các trang blog đếm số bài viết, số comment và số trang để hiển thị list các bài viết đó.

Có hai vấn đề với dữ liệu lớn ở đây:

  1. Các query có dạng như SELECT COUNT(*) FROM users thường rất chậm. Nó thường là một vòng lặp chạy qua tất cả các bản ghi để đưa ra kết quả. Trong trường hợp có vài triệu bản ghi, việc đếm này sẽ mất một khoảng thời gian.
  2. Tăng tốc bằng cách sử dụng "counter cache" tỏ ra khá hiệu quả, nhưng cách này lại bị hạn chế trong việc chia database ra nhiều server.

Cách giải quyết dễ dàng nhất ở đây là tránh sử dụng counter bất cứ khi nào có thể, điều này cần được thực hiện ở bước đầu thiết kế.

Ví dụ, ta có thể chọn cho hiện list thông tin theo khoảng thời gian thay vì phân trang theo tổng số. Ta có thể chọn hiển thị những chỉ số tính toán phức tạp sau, hoặc lưu trữ giá trị vào database để làm giảm gánh nặng cho query. Nhưng giải pháp đại loại như vậy.

Hạn sử dụng và cất trữ dữ liệu

Trong thực tế, tồn tại một giới hạn trên của số dữ liệu có thể lưu trong một bảng SQL bất kì tùy thuộc vào số lượng RAM, CPU và ổ đĩa ngoài sở hữu. Điều này có nghĩa là sẽ đến một thời điểm khi mà ta cần phải chuyển những data không quan trọng ra khỏi các bảng chính.

Cùng xem xét một trường hợp đơn giản. Bạn muốn xóa bỏ những bản ghi cũ hơn 1 năm trong cơ sở dữ liệu, tổng cộng khoảng vài GB.

Để thực hiện điều này cần có một dòng code kiểu như:

MyRecords.where("created_at < ?", 1.year.ago).destroy

Vấn đề là câu query này có thể tốn hàng giờ, thậm chí lên đến hàng ngày để hoàn thành nếu database quá lớn.

Đây thực sự là một vấn đề đau đầu, bởi vì ta thường không nhận ra điều này cho đến khi quá muộn. Thường thí khó có người nào tính trước đến cả các chiến lược dọn dẹp dữ liệu thừa từ khi sản phẩm còn nhỏ, hay với các môi trường thử nghiệm chỉ với vài nghìn bản ghi.

Nhưng nếu đã có kế hoạch từ trước, thì ta có thể chuẩn bị một giải pháp đơn giản. Đó là phân mảnh các bảng. Ví dụ như thay vì viết tất cả dữ liệu vào bảng my_records, ta viết dữ liệu năm nay vào bảng my_records_1, dữ liệu năm sau vào bảng my_records_2... Khi đến thời gian loại bỏ dữ liệu cũ, ta chỉ cần chạy lệnh DROP TABLE my_records_1. Không giống lệnh delete, việc này chỉ tốn một khoảng thời gian rất ngắn.

Việc phân mảnh các trường khác cũng có thể được thực hiện tương tự.

Có cả một extension dành cho postgreSQL tên là pg_partman giúp xử lý phân vùng database mà không cần phải chỉnh sửa code. Hoặc nếu muốn điều khiển phân vùng bằng Ruby, có một gem hỗ trợ tên là partitionable.

Lời kết

Lần sau khi bạn bắt đầu xây dựng một project mới, hy vọng những mẹo nhỏ trên đây có thể có tác dụng trong việc lên trước kễ hoạch với những database lớn. Xin cảm ơn.