/home/mongodb-va-chuyen-xoa-du-lieu

MongoDB và chuyện xóa dữ liệu

Published on | Updated

MongoDB, ai cũng biết đây là một NoSQL database có tốc độ đọc ghi đáng nể, ngoài ra vì có tính linh động trong cấu trúc document nên thường được ưa chuộng trong các sản phẩm có sự thay đổi cấu trúc dữ liệu liên tục để phát triển (chém đấy, thích thì dùng thôi).

Đọc ghi thì đã rõ rồi, còn xóa thì sao, chẳng mấy ai đề cập đến việc xóa dữ liệu trong MongoDB, và trải nghiệm việc xóa dữ liệu đúng là ác mộng thực sự luôn.

Trong một dự án thử nghiệm lưu thêm 1 số metric, tui đã deploy 1 con mongodb, mục đích của nó chỉ là nơi lưu trữ trung gian để chờ 1 job khác đến đọc dữ liệu, xử lý nó và quăng sang nơi khác. Nên là dữ liệu ở đây thực tế chỉ quan trọng trong khoảng 3 ngày đến 1 tuần. Và thường chỉ nên lưu trữ thêm khoảng 1 tháng để tra soát/đối chiếu nếu lỡ có sai số nào đó muốn kiểm tra lại.

Trong một ngày đẹp trời, vì không quá để ý nên cái ổ cứng 80GB còi cọc đã không chứa nổi đống dữ liệu của mình. Thế là kế hoạch xóa dữ liệu được bắt đầu.
Các thử nghiệm trong bài viết này được test trên 1 server được nhân bản từ server chính, khuyến cáo không nên áp dụng trực tiếp trên production mà không có thử nghiệm trước.

Để đảm bảo trong lúc nghịch MongoDB vẫn được mô phỏng như môi trường thật thì tui có viết 2 script giả lập việc đọc/ghi liên tục vào DB này ( dĩ nhiên tần suất chậm hơn thực tế nhiều ) để xem DB có bị chậm/chết hay không?

1. Lần nghịch ngu đầu tiên

Nghĩ rằng Mongo ghi ngon thì xóa cũng bét nhè. Thế là đè em nó ra xóa theo cách ngu nhất, xóa tất cả record có ngày tạo trước 2019

use nhymxu;
// có thể dùng deleteMany tương tự
db.metric_logs.remove({
    "created_at": {
        $lt: ISODate("2019-01-01T00:00:00.000+0000") 
    }
});

Ẻn ẻn, lệnh gõ xong trong chớp mắt, bấm Enter cái rụp, mongo cũng bắt đầu xóa trong chớp mắt. Nhưng đời không như mơ, trong vài phút sau thì Disk I/O tăng vọt, CPU full tải. Mongo chậm dần rồi tèo hẳn, không thể read/insert nổi nữa. Chỉ còn cách restart lại mongod service.

=> Fail lòi dù giảm xuống giới hạn thời gian chỉ trong 1 tháng, 1 tuần

2. Cố giãn ra cho nó thở

Nghĩ rằng bên trên mình xóa lượng lớn quá, Mongo ghi trực tiếp lệnh xóa vào disk liên tục làm Disk I/O quá tải => xóa theo chunk, sau mỗi chunk sleep 1 ít thời gian cho dễ thở.
Nghĩ là làm, bắt tay nghiên cứu mày mò ngay. May thay tìm được 1 bài viết trên Percona có người viết hộ mình bài toán bulk delete rồi. Mang ra thử ngay thôi

function parseNS(ns){
    //Expects we are forcing people to not violate the rules and not doing "foodb.foocollection.month.day.year" if they do they need to use an array.
    if (ns instanceof Array){
        database =  ns[0];
        collection = ns[1];
    }
    else{
        tNS =  ns.split(".");
        if (tNS.length > 2){
            print('ERROR: NS had more than 1 period in it, please pass as an [ "dbname","coll.name.with.dots"] !');
            return false;
        }
        database = tNS[0];
        collection = tNS[1];
    }
    return {database: database,collection: collection};
}
DBCollection.prototype.deleteBulk = function( query, batchSize, pauseMS){
    //Parse and check namespaces
    ns = this.getFullName();
    srcNS={
        database:   ns.split(".")[0],
        collection: ns.split(".").slice(1,ns.length).join("."),
    };
    var db = this._db;
    var batchBucket = new Array();
    var totalToProcess = db.getSiblingDB(srcNS.database).getCollection(srcNS.collection).find(query,{_id:1}).count();
    if (totalToProcess < batchSize){ batchSize = totalToProcess; }
    currentCount = 0;
    print("Processed "+currentCount+"/"+totalToProcess+"...");
    db.getSiblingDB(srcNS.database).getCollection(srcNS.collection).find(query).addOption(DBQuery.Option.noTimeout).forEach(function(doc){
        batchBucket.push(doc._id);
        if ( batchBucket.length >= batchSize){
            printjson(db.getSiblingDB(srcNS.database).getCollection(srcNS.collection).remove({_id : { "$in" : batchBucket}}));
            currentCount += batchBucket.length;
            batchBucket = [];
            sleep (pauseMS);
            print("Processed "+currentCount+"/"+totalToProcess+"...");
        }
    })
    print("Completed");
}
function deleteFromCollection( sourceNS, query, batchSize, pauseMS){
    //Parse and check namespaces
    srcNS = parseNS(sourceNS);
    if (srcNS == false) { return false; }
    batchBucket = new Array();
    totalToProcess = db.getDB(srcNS.database).getCollection(srcNS.collection).find(query,{_id:1}).count();
    if (totalToProcess < batchSize){ batchSize = totalToProcess};
    currentCount = 0;
    print("Processed "+currentCount+"/"+totalToProcess+"...");
    db.getDB(srcNS.database).getCollection(srcNS.collection).find(query).addOption(DBQuery.Option.noTimeout).forEach(function(doc){
        batchBucket.push(doc._id);
        if ( batchBucket.length >= batchSize){
            db.getDB(srcNS.database).getCollection(srcNS.collection).remove({_id : { "$in" : batchBucket}});
            currentCount += batchBucket.length;
            batchBucket = [];
            sleep (pauseMS);
            print("Processed "+currentCount+"/"+totalToProcess+"...");
        }
    })
    print("Completed");
}
/** Example Usage:
    deleteFromCollection("foo.bar",{"type":"archive"},1000,20);
  or
    db.bar.deleteBulk({type:"archive"},1000,20);
**/

Nháp thử với dữ liệu 1 ngày và sleep khoảng 0.5s (500ms) thấy xóa trơn tru ngon lành. Test thêm lần nữa khoảng 1 tuần. vẫn thấy không bị sao.
Yên chí làm quả 2 tháng, đang xóa tự nhiên Mongo lăn đùng ra chết.
Kiểm tra lại thì Disk I/O lại lên cao chót vót, CPU full luôn.
Có vẻ cách này chơi dữ liệu ở mức trung bình thì ổn, chứ xóa một phát chục triệu document như mình thì không ổn lắm.

3. Dùng thử xóa tự động chính chủ

Sau một hồi tìm hiểu thì bản thân Mongo có hỗ trợ tự động xóa document theo 1 field datetime.

Cách này thì tui không ghi chi tiết ở đây, tui nhiên khi dùng thử thì cũng treo luôn. Vì mongo có 1 background task đi dọn dẹp các index hết hạn, và nếu dẹp nhiều quá 1 lúc thì nó cũng làm quá tải I/O luôn.


Note: cả 3 cách trên đều có 1 nhược điểm đó là sau khi xóa xong thì mongo không trả lại dung lượng cho OS ( check df -h thì nó vẫn nguyên si như vậy )
Dung lượng đó được tái sử dụng cho Mongo, nghĩa là khi bạn insert document vào collection đó thì disk sẽ không bị chiếm thêm cho đến khi nào cái đống dung lượng tái sử dụng đó được dùng hết.
Cụ thể tham khảo link [7] ở dưới


4. Endgame

Sau ti tỉ lượt tìm kiếm, thì tui tìm ra được cách giải quyết bài toán của tui. Dĩ nhiên, nó có thể phù hợp với bài toán này, nhưng không chắc sẽ phù hợp với bài toán khác.

Đáp ứng được các tiêu chí:

Cách đó là mỗi đầu tháng tui tự động rename collection. Khi đó, insert mới đầu tiên mongo sẽ tự tạo 1 collection mới. Thời gian rename cũng chỉ trong 1 tích tắc, không gây ảnh hưởng gì đến hệ thống. Sau đó mọi người có thể backup dữ liệu trên collection vừa mới rename rồi bấm xóa cái roẹt. Collection bay trong 1 nốt nhạc, server nguyên si, dung lượng ổ cứng trả về cho OS.

use nhymxu;
db.metric_logs.renameCollection("metric_logs_old");

Vậy đấy, có mỗi việc xóa dữ liệu thôi mà hành hạ nhau bao ngày liền. Dù lúc làm chỉ trong một nốt nhạc mà thôi.


Tài liệu tham khảo:

  1. https://docs.mongodb.com/v3.2/reference/method/db.collection.deleteMany/#db.collection.deleteMany
  2. https://docs.mongodb.com/v3.2/reference/method/db.collection.remove/#db.collection.remove
  3. https://docs.mongodb.com/manual/reference/method/Bulk.find.remove/#bulk-find-remove
  4. https://www.percona.com/blog/2018/04/05/managing-mongodb-bulk-deletes-inserts/
  5. https://docs.mongodb.com/manual/core/index-ttl/
  6. https://docs.mongodb.com/manual/tutorial/expire-data/
  7. https://docs.mongodb.com/manual/faq/storage/#how-do-i-reclaim-disk-space-in-wiredtiger
  8. https://docs.mongodb.com/manual/reference/method/db.collection.renameCollection/