Tối ưu hiệu suất MongoDB bằng cách quản lý index
mongodb
13
White

Quăng viết ngày 19/06/2018

Ai cũng biết đánh index giúp tăng tốc các truy vấn tìm kiếm, tìm kiếm trong cây index (B-Tree) sẽ nhanh hơn scan trong toàn bộ bảng. Ai cũng biết nên không nói nhiều về index là gì nữa :trollface: vậy nhé.

Đây là MongoDB users dataset bạn có thể tải về chơi thử.

1. Sơ sơ về index

Index trong Mongo cũng tương tự các cơ sở dữ liệu quan hệ, ví dụ ta có một index được định nghĩa như sau:

db.users.createIndex({
  user_status: 1
}, {
  background: true,
  name: 'idx_users_by_user_status'
});

Câu trên sẽ nói với database build một index dựa trên giá trị của field user_status của collection users.

Ngoài ra nó có thêm một tùy chọn là backgroud: true để build index ở dạng backgroud, thông thường đánh index sẽ block tất cả các operation khác, và với các collection có kích thước lớn thì việc đánh index sẽ tốn tới vài giờ để hoàn thành và trong thời gian này database sẽ không thể response, đánh index background để tránh tính trạng block các operation khác [nhưng không hẳn đời lúc nào cũng như mơ].

Lưu ý là nên chạy query đánh index trong tmux để tránh rớt kết nối và để kiểm tra process của index ta có thể truy vấn:

db.currentOp(
  { "msg": /Index Build/ }
);

"msg" : "Index Build (background) Index Build (background): 3305028/574961784 0%"

Ok, tiếp tục với index phía trên, nó sẽ có tác dụng với các truy vấn tìm kiếm với điều kiện là field user_status, ví dụ như sau:

db.users.find({
  "user_status": "new_user"
});

Không có gì phải bàn cãi thêm, truy vấn trên sử dụng index idx_users_by_user_status và trả về kết quả với tốc độ của một vị thần gió. :triumph:

Giờ giả sử, ta có một truy vấn lấy ra tất cả các user confirmed NHƯNG trong một tháng của năm 2013 như sau:

var start = ISODate("2013-04-01T00:00:00Z");
var end = ISODate("2013-04-30T23:59:59Z");

db.users.find({
    "user_status": "confirmed",
    "created_at": { $gte: start, $lte: end}
});

Tương tự như trên, lấy ra danh sách các confirmed khá nhanh chóng do sử dụng index như câu truy vấn trên cùng, tuy nhiên trong danh sách trả ra confirmed ta cần lọc thêm một lần nữa để lấy ra các user đăng kí trong tháng đó, và do created_at không được đánh index nên có bao nhiêu confirmed thì ta cần quét toàn bộ từng đấy.

Để giải quyết truy vấn trên, ta cần đánh index trên 2 field, gọi là compound index như sau:

db.users.createIndex({
  user_status: 1,
  created_at: 1 
}, {
  background: true, 
  name: 'idx_users_by_user_status_and_created_at' 
});

Ok, trong có vẻ ngon rồi, truy vấn của chúng ta đã nhanh hơn rồi. Nhưng thử nghĩ xem, chúng ta có thể làm tốt hơn hay không nhỉ?

Giả sử bạn có 20 triệu register user (hoặc bạn có thể tưởng tượng bạn có 1 tỷ register user và bạn giàu hơn Mark Zuckerberg :slightly_smiling_face: cũng được) user_status có các giá trị sau:

  • new_user
  • confirmed
  • banned
  • deleted

Nếu không có index trên user_status, bạn cần quét hết 20 triệu docs này, đánh index trên user_status con số giảm từ 20 triệu -> 5 triệu (1/4).

Giờ giả sử, start-úp mặt của bạn ra đời năm 2015, đến bây giờ là 3 năm, giả sử user đăng kí đồng đều giữa các ngày (thực tế thì đéo phải như vậy đâu :sweat_smile:). Bạn index lại như sau:

db.users.createIndex({
  created_at: 1,
  user_status: 1
}, {
  background: true,
  name: 'idx_users_by_created_at_user_status'
});

Giờ với câu truy vấn trên, ta được một danh sách các user đăng ký trong vòng 1 tháng, là cỡ hơn 500 ngàn user (1 ngày có ~ 18 ngàn user đăng ký), sau đó lọc theo điều kiện new_user ta chỉ cần quét trong tập 500k user này so với 5 triệu như cách đánh index ban đầu.

Chốt lại, với compound index nên đánh index field có giá trị uniqe nhiều hơn trước. Ở trên thì đánh created_at trước vì các giá trị trong field này hầu hết đều khác nhau (trong khi user_status chỉ có 4 giá trị), nên sẽ giảm được vùng tìm kiếm của truy vấn nhiều hơn.

2. How to improve

Xong xuôi việc đánh index, bây giờ làm cách nào để ta có thể chắc chắn rằng database sẽ sử dụng index một cách hiệu quả? Index cũng như dữ liệu, được lưu trữ trên đĩa cứng, và để index có thể được sử dụng hiệu quả thì tốt nhất là nó nên được đặt trên RAM. Trong Mongo, RAM thì thường được sử dụng để giữ các dữ liệu và index thường xuyên truy vấn (WiredTiger internal cache) tránh phải đọc đĩa thường xuyên. Recommend là 50% physical memory, ví dụ server có 32GB thì WiredTiger internal cache là 15GB.

Nhưng data thì lúc nào cũng lớn hơn RAM, bạn không thể đặt toàn bộ dữ liệu lên Wiredtiger cache được, với index cũng vậy, đánh index thì các truy vấn đọc dữ liệu trên điều kiện sẽ nhanh, nhưng quá nhiều index dư thừa thì gây ra 2 vấn đề:

  • Không thể chứa tất cả các index trên memory.
  • Làm chậm các truy vấn ghi dữ liệu như Insert/Update/Delete -> nhưng thực ra nếu phần ghi chiếm tới 80-95% thì việc đánh đổi cũng đáng kể.

Bạn có thể kiểm tra kích thước tổng thể của index trên cơ sở dữ liệu như sau:

db.runCommand({ dbStats: 1, scale: 1 });
{
    "db" : "test",
    "collections" : 2,
    "objects" : 2000000,
    "avgObjSize" : 96.120665,
    "dataSize" : 192241330,
    "storageSize" : 374370304,
    "numExtents" : 0,
    "indexes" : 6,
    "indexSize" : 53964800,
    "ok" : 1
}

Cụ thể hơn (dữ liệu này ăn cắp trên production)

db.stats().indexSize
86508650496

=> kích thước của tất cả các index trên CSDL là 86GB, có nghĩa là index không thể fit hết trên memory, nên khi nào cần sử dụng tới index, nếu index đó không có sẵn trên memory thì cần đọc index đó từ đĩa lên.

Nói chung, xác định bao nhiêu memory là cần thiết không dễ, bạn có thể trả lời vài câu hỏi sau để tự xác định và điều chỉnh memory cho hợp lý:

  • Độ lớn data của bạn là bao nhiêu?
  • Độ lớn của index là bao nhiêu?
  • Độ phát triển của dữ liệu (trong 1 tháng, 1 năm?)
  • Độ lớn của tập dữ liệu thường xuyên truy cập nhất (gọi là working set)?

Vậy chiến lược để giữ index có kích thước nhỏ là như thế nào?

2.1 Xóa các index không sử dụng

db.users.aggregate([ { $indexStats: {} } ]);
[{
    "name": "idx_users_by_created_at_user_status",
    "key": {
        "created_at": 1,
        "user_status": 1
    },
    "host": "6b1b716ae456:27017",
    "accesses": {
        "ops": NumberLong(456550227),
        "since": ISODate("2018-05-31T15:31:09.020Z")
    }
} {
    "name": "idx_users_by_user_status",
    "key": {
        "user_status": 1
    },
    "host": "6b1b716ae456:27017",
    "accesses": {
        "ops": NumberLong(277641131),
        "since": ISODate("2018-05-31T15:05:39.148Z")
    }
} {
    "name": "_id_",
    "key": {
        "_id": 1
    },
    "host": "6b1b716ae456:27017",
    "accesses": {
        "ops": NumberLong(0),
        "since": ISODate("2018-05-31T15:03:12.197Z")
    }
}]

Ở ví dụ trên, ta có 3 index, ở giá trị accesses, ta thấy 2 index đầu tiên được sử dụng rất nhiều lần, trong khi đó index thứ 3 không hề được sử dụng. Con số bao nhiêu là hợp lí để loại bỏ một index tùy thuộc vào số lượng truy vấn và nghiệp vụ của chính bạn, bạn có thể tự cân nhắc, đo lường và loại bỏ nếu không cần thiết.

Cần lưu ý, ops có thể bị reset, con số hiện tại thể hiện số lần được sử dụng kể từ since -> thời gian mongod được restart.

2.2 Loại bỏ các index dư thừa

db.users.aggregate([ { $indexStats: {} } ])
[{
    "name": "idx_users_by_user_status_created_at",
    "key": {
        "user_status": 1,
        "created_at": 1
    },
    "host": "6b1b716ae456:27017",
    "accesses": {
        "ops": NumberLong(456550227),
        "since": ISODate("2018-05-31T15:31:09.020Z")
    }
} {
    "name": "idx_users_by_user_status",
    "key": {
        "user_status": 1
    },
    "host": "6b1b716ae456:27017",
    "accesses": {
        "ops": NumberLong(277641131),
        "since": ISODate("2018-05-31T15:05:39.148Z")
    }
}]

Ở ví dụ trên, cả 2 index đều được sử dụng. Tuy nhiên index đầu tiên có thể làm cho index thứ 2 trở lên dư thừa, vì các truy vấn chỉ trên điều kiện user_status có thể sử dụng index đầu tiên mà không có vấn đề gì.

2.3 Sử dụng sparse index

Đây là kiểu đánh index trên điều kiện, ví dụ ta có 20 triệu user, nhưng nếu ta chỉ thường truy vấn user_statusnew_user thì ta có thể đánh index riêng với tập new_user thôi -> giả sử số lượng new_user là 40% trên tổng số user -> index của chúng ta sẽ giảm xuống tới 60% kích thước.

2.4 Giảm bớt kích thước của collection

Cách duy nhất để giảm kích thước của collection đó là ... xóa dữ liệu, thực tế có những nghiệp vụ lưu trữ logs, events không cần giữ quá 1-2 năm để tra cứu thì ta có thể xóa bớt các dữ liệu không còn cần thiết (hoặc di chuyển nó sang một CSDL khác với tần suất tuy vấn thấp hơn). Khi kích thước của collection giảm, kích thước của index cũng sẽ giảm theo và data/index có thể fit vừa trên RAM.

2.4 Giữ index đơn giản

Compound index rất lợi hại trong trường hợp truy vấn dữ liệu trên nhiều điều kiện khác nhau, tuy nhiên nó cũng tốn chi phí bảo trì và dữ liệu càng lớn thì index size sẽ càng tăng nhanh. Tương tự như truy vấn, cố gắng giữ cho index một cách đơn giản nhất có thể.

3. Bonus

Đáng ra đéo có phần này, nhưng sợ anh em lại bảo thằng chuyên bài trừ Mongo mà nay lại viết về Mongo, thấy sai sai thế éo nào nên phải viết thêm tí.

Thật ra những lý thuyết trên về index và quản lý index đều có thể áp dụng cho các cơ sở dữ liệu quan hệ như MySQL hay PostgreSQL. Ví dụ với PostgreSQL cũng có index trên điều kiện đó là partial index tương tự như sparse index (MySQL thì không biết có không).

Bản chất, nếu để ý sẽ thấy các hệ CSDL có rất nhiều đặc điểm giống nhau (kể cả M$ SQL), lý thuyết này có thể áp dụng cho CSDL khác, và ngược lại. Cốt lõi của vấn đề là tư duy về việc đo lường, cách thức đo lường và đánh giá hiệu quả của mỗi hành động tác động lên hệ thống

Lần sau sẽ viết thêm về một số thứ liên quan đến metric trong CSDL quan hệ, hỗ trợ cho việc tối ưu hệ thống tương tự như trên MongoDB.

4. Ref


Bài viết gốc được đăng tải tại https://xluffy.github.io/post/improving-mongodb-perf-by-managing-indexes

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Quăng

24 bài viết.
220 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
132 13
Gần đây mình có present về chủ đề __Một số thao tác và lệnh đơn giản nhưng hữu ích khi sử dụng Linux__ cho một vài bạn bè. Mặc dù nó khá là đơn giả...
Quăng viết 2 năm trước
132 13
White
82 14
1. Giới thiệu Right tool for right job. Trước tiên phải hiểu là MySQL Replication không phải là giải pháp giải quyết mọi bài toán về quá tải hệ th...
Quăng viết gần 3 năm trước
82 14
White
53 7
Với những ai làm Linux System Admin hoặc DevOps thì việc quản lí vài chục, vài trăm server là chuyện bình thường. Việc nhớ các địa chỉ IP, port để ...
Quăng viết 3 năm trước
53 7
Bài viết liên quan
White
41 7
Giới thiệu MongoDB là một giải pháp nosql database. Data được lưu ở dạng các bson document. Hỗ trợ vertical scaling và horizontal scaling, dynamic...
manhdung viết 3 năm trước
41 7
White
5 0
Khoảng 5, 7 năm nay NOSQL là đề tài rất nóng bỏng. SQL phổ biến thì chỉ có khoảng vài ba cái như PostgreSQL, MySQL, MS SQL, Oracle nói chung na na ...
Ngoc Dao viết hơn 2 năm trước
5 0
White
5 1
Chatbotlà khái niệm không mới nhưng gần đây trở thành hot trend khi kết hợp cùng machine learning và AI. Mình sẽ có một bài viết cụ thể hơn về kỹ t...
Mầu Hà Quang viết 5 tháng trước
5 1
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
24 bài viết.
220 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!