Статьи

Факты MongoDB: 80000+ вставок / сек на аппаратном оборудовании

При экспериментировании с некоторыми коллекциями временных рядов мне понадобился большой набор данных, чтобы убедиться, что наши запросы агрегации не становятся узким местом в случае увеличения загрузки данных. Мы согласились на 50 миллионов документов, так как помимо этого числа мы все равно рассмотрим возможность шардинга.

Каждый раз событие выглядит так:

1
2
3
4
5
{
        "_id" : ObjectId("5298a5a03b3f4220588fe57c"),
        "created_on" : ISODate("2012-04-22T01:09:53Z"),
        "value" : 0.1647851116706831
}

Поскольку мы хотели получить случайные значения, мы подумали о том, чтобы сгенерировать их с помощью JavaScript или Python (мы могли бы попробовать в Java, но мы хотели написать это как можно быстрее). Мы не знали, какой из них будет быстрее, поэтому мы решили проверить их.

Нашей первой попыткой был запуск файла JavaScript через оболочку MongoDB.

Вот как это выглядит:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var minDate = new Date(2012, 0, 1, 0, 0, 0, 0);
var maxDate = new Date(2013, 0, 1, 0, 0, 0, 0);
var delta = maxDate.getTime() - minDate.getTime();
 
var job_id = arg2;
 
var documentNumber = arg1;
var batchNumber = 5 * 1000;
 
var job_name = 'Job#' + job_id
var start = new Date();
 
var batchDocuments = new Array();
var index = 0;
 
while(index < documentNumber) {
    var date = new Date(minDate.getTime() + Math.random() * delta);
    var value = Math.random();
    var document = {       
        created_on : date,
        value : value
    };
    batchDocuments[index % batchNumber] = document;
    if((index + 1) % batchNumber == 0) {
        db.randomData.insert(batchDocuments);
    }
    index++;
    if(index % 100000 == 0) {  
        print(job_name + ' inserted ' + index + ' documents.');
    }
}
print(job_name + ' inserted ' + documentNumber + ' in ' + (new Date() - start)/1000.0 + 's');

Вот как мы его запустили и что получили:

1
2
3
4
5
6
7
mongo random --eval "var arg1=50000000;arg2=1" create_random.js
Job#1 inserted 100000 documents.
Job#1 inserted 200000 documents.
Job#1 inserted 300000 documents.
...
Job#1 inserted 49900000 documents.
Job#1 inserted 50000000 in 566.294s

Ну, это уже выше моих диких ожиданий (88293 вставок в секунду).

Теперь настала очередь Python. Вам нужно будет установить pymongo, чтобы правильно запустить его.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import sys
import os
import pymongo
import time
import random
 
from datetime import datetime
 
min_date = datetime(2012, 1, 1)
max_date = datetime(2013, 1, 1)
delta = (max_date - min_date).total_seconds()
 
job_id = '1'
 
if len(sys.argv) < 2:
    sys.exit("You must supply the item_number argument")
elif len(sys.argv) > 2:
    job_id = sys.argv[2]   
 
documents_number = int(sys.argv[1])
batch_number = 5 * 1000;
 
job_name = 'Job#' + job_id
start = datetime.now();
 
# obtain a mongo connection
connection = pymongo.Connection("mongodb://localhost", safe=True)
 
# obtain a handle to the random database
db = connection.random
collection = db.randomData
 
batch_documents = [i for i in range(batch_number)];
 
for index in range(documents_number):
    try:           
        date = datetime.fromtimestamp(time.mktime(min_date.timetuple()) + int(round(random.random() * delta)))
        value = random.random()
        document = {
            'created_on' : date,   
            'value' : value,   
        }
        batch_documents[index % batch_number] = document
        if (index + 1) % batch_number == 0:
            collection.insert(batch_documents)     
        index += 1;
        if index % 100000 == 0:
            print job_name, ' inserted ', index, ' documents.'     
    except:
        print 'Unexpected error:', sys.exc_info()[0], ', for index ', index
        raise
print job_name, ' inserted ', documents_number, ' in ', (datetime.now() - start).total_seconds(), 's'

Мы запускаем его, и вот что мы получили на этот раз:

1
2
3
4
5
6
7
python create_random.py 50000000
Job#1  inserted  100000  documents.
Job#1  inserted  200000  documents.
Job#1  inserted  300000  documents.
...
Job#1  inserted  49900000  documents.
Job#1  inserted  50000000  in  1713.501 s

Это медленнее по сравнению с версией JavaScript (29180 вставок в секунду), но не стоит расстраиваться. Python — это полнофункциональный язык программирования, так что как насчет использования всех наших ядер ЦП (например, 4 ядра) и запуска по одному сценарию на ядро, каждое из которых вставляет часть общего числа документов (например, 12500000).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import sys
import pymongo
import time
import subprocess
import multiprocessing
 
from datetime import datetime
 
cpu_count = multiprocessing.cpu_count()
 
# obtain a mongo connection
connection = pymongo.Connection('mongodb://localhost', safe=True)
 
# obtain a handle to the random database
db = connection.random
collection = db.randomData
 
total_documents_count = 50 * 1000 * 1000;
inserted_documents_count = 0
sleep_seconds = 1
sleep_count = 0
 
for i in range(cpu_count):
    documents_number = str(total_documents_count/cpu_count)
    print documents_number
    subprocess.Popen(['python', '../create_random.py', documents_number, str(i)])
 
start = datetime.now();
 
while (inserted_documents_count < total_documents_count) is True:
    inserted_documents_count = collection.count()
    if (sleep_count > 0 and sleep_count % 60 == 0): 
        print 'Inserted ', inserted_documents_count, ' documents.'     
    if (inserted_documents_count < total_documents_count):
        sleep_count += 1
        time.sleep(sleep_seconds)  
 
print 'Inserting ', total_documents_count, ' took ', (datetime.now() - start).total_seconds(), 's'

Запуск сценария Python для параллельного выполнения выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
python create_random_parallel.py
Job#3  inserted  100000  documents.
Job#2  inserted  100000  documents.
Job#0  inserted  100000  documents.
Job#1  inserted  100000  documents.
Job#3  inserted  200000  documents.
...
Job#2  inserted  12500000  in  571.819 s
Job#0  inserted  12400000  documents.
Job#3  inserted  10800000  documents.
Job#1  inserted  12400000  documents.
Job#0  inserted  12500000  documents.
Job#0  inserted  12500000  in  577.061 s
Job#3  inserted  10900000  documents.
Job#1  inserted  12500000  documents.
Job#1  inserted  12500000  in  578.427 s
Job#3  inserted  11000000  documents.
...
Job#3  inserted  12500000  in  623.999 s
Inserting  50000000  took  624.655 s

Это действительно очень хорошо (80044 вставок / секунд), даже если оно все еще медленнее, чем первый импорт JavaScript. Итак, давайте адаптируем этот последний скрипт Python для запуска JavaScript через несколько оболочек MongoDB.

Поскольку я не смог предоставить необходимые аргументы для команды mongo, для подпроцесса, запущенного основным скриптом python, я предложил следующую альтернативу:

1
2
3
4
5
6
7
for i in range(cpu_count):
    documents_number = str(total_documents_count/cpu_count)
    script_name = 'create_random_' + str(i + 1) + '.bat'
    script_file = open(script_name, 'w')
    script_file.write('mongo random --eval "var arg1=' + documents_number +';arg2=' + str(i + 1) +'" ../create_random.js');
    script_file.close()
    subprocess.Popen(script_name)

Мы генерируем сценарии оболочки динамически и позволяем python запускать их для нас.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Job#1 inserted 100000 documents.
Job#4 inserted 100000 documents.
Job#3 inserted 100000 documents.
Job#2 inserted 100000 documents.
Job#1 inserted 200000 documents.
...
Job#4 inserted 12500000 in 566.438s
Job#3 inserted 12300000 documents.
Job#2 inserted 10800000 documents.
Job#1 inserted 11600000 documents.
Job#3 inserted 12400000 documents.
Job#1 inserted 11700000 documents.
Job#2 inserted 10900000 documents.
Job#1 inserted 11800000 documents.
Job#3 inserted 12500000 documents.
Job#3 inserted 12500000 in 574.782s
Job#2 inserted 11000000 documents.
Job#1 inserted 11900000 documents.
Job#2 inserted 11100000 documents.
Job#1 inserted 12000000 documents.
Job#2 inserted 11200000 documents.
Job#1 inserted 12100000 documents.
Job#2 inserted 11300000 documents.
Job#1 inserted 12200000 documents.
Job#2 inserted 11400000 documents.
Job#1 inserted 12300000 documents.
Job#2 inserted 11500000 documents.
Job#1 inserted 12400000 documents.
Job#2 inserted 11600000 documents.
Job#1 inserted 12500000 documents.
Job#1 inserted 12500000 in 591.073s
Job#2 inserted 11700000 documents.
...
Job#2 inserted 12500000 in 599.005s
Inserting  50000000  took  599.253 s

Это тоже быстро (83437 вставок в секунду), но все еще не может побить нашу первую попытку.

Вывод

Конфигурация моего ПК не является чем-то необычным, и единственная оптимизация заключается в том, что у меня есть SSD-диск, на котором работает MongoDB.

vlad_pc

Первая попытка дала наилучшие результаты, и, отслеживая ресурсы ЦП, я понял, что MongoDB использует их все даже для одной консоли оболочки. Скрипт Python, работающий на всех ядрах, также был достаточно быстрым, и у него есть преимущество, позволяющее нам превратить этот скрипт в полностью работающее приложение, если мы хотим.

  • Код доступен на GitHub .