Статьи

Сценарий вырожденной производительности для LMDB

Я мирно работал над некоторыми вещами с Вороном, когда столкнулся с действительно плохим исполнением в довольно важном сценарии. Я решил, что это, вероятно, то, что я делал неправильно, и решил воспроизвести тот же сценарий в LMDB, чтобы выяснить, что я делаю неправильно.

Вот код:

int main(int argc,char * argv[])
{
    int i = 0, j = 0, rc;
    UUID id;
    MDB_env *env;

    MDB_val key, data;

    MDB_stat mst;
    MDB_cursor *cursor;
    char sval[32];
    clock_t start = clock(), end;

    srandom(time(NULL));

    rc = mdb_env_create(&env);
    rc = mdb_env_set_mapsize(env, 1024*1024*768);
    rc = mdb_env_set_flags(env, MDB_WRITEMAP, 1); 
    rc = mdb_env_open(env, "E:\\data\\lmdb2", 0, 0664);

    key.mv_size = sizeof(UUID);
    data.mv_size = sizeof(sval);
    data.mv_data = sval;
    
    for (i=0;i<100;i++) {    
        MDB_txn *txn;
        MDB_dbi dbi;

        rc = mdb_txn_begin(env, NULL, 0, &txn);
        rc = mdb_open(txn, NULL, 0, &dbi);

        for (j= 0; j < 100; j++)
        {
            UuidCreate(&id);
            
            key.mv_data = &id;
            rc = mdb_put(txn, dbi, &key, &data, 0);
        }

        rc = mdb_txn_commit(txn);
        mdb_close(env, dbi);
    }

    end = clock();

    printf("%i", (end - start));
    fgetc(stdin);
    mdb_env_close(env);

    return 0;
}

Как видите, мы вставляем 10000 единиц (100 транзакций по 100 единиц в каждой). Каждый элемент имеет ключ 16 байтов и значение 100 байтов. Теперь вы можете заметить, что это, вероятно, наихудший сценарий для дерева B +, UUID неупорядочены, и это приводит к большой фрагментации в дереве. Плохой сценарий, да, но также относительно распространенный, и тот, который нужно правильно обрабатывать для наших нужд. Мне потребовалось много времени, чтобы понять, что же на самом деле происходит. Сначала я был убежден, что проблема была в реализации файлов отображения памяти в Windows, но в конце концов я понял, что выбор не нарушен .

Вот трассировка Process Monitor первых нескольких транзакций. Выделенные вызовы относятся к FlushBuffersFile, как выглядит FlushFileBuffers, что указывает на фиксацию.

образ

Как я уже сказал, это первые несколько транзакций. Вы можете видеть, что ОС довольно хорошо справляется со слиянием записей и избегает поисков. Однако, как проходит время …

образ

И по мере того, как проходит больше времени, мы добираемся до (есть еще ряды наверху, которые мне пришлось удалить, чтобы я мог сделать снимок)…

образ

Фактически я поместил данные в Excel и получил:

образ

И это действительно плохие цифры, я думаю, вы согласитесь. После очень короткого периода времени стоимость совершения транзакции превышает 50 запросов и записи 350 КБ.

Давайте сравним это с тем, что происходит, когда мы на самом деле пишем последовательные данные (используя UuidCreateSequential вместо UuidCreate). Тест завершен в два раза, и фактическая статистика также очень интересна.

образ

Мы пишем намного меньше, но гораздо более важно, мы делаем много меньше стремится. Для справки, вот последняя транзакция, которую мы имеем со сценарием последовательной записи:

образ

Я провел эти тесты на жестком диске, поэтому время поиска очень важно, но я получил аналогичные результаты при работе на SSD.

Теперь интересный вопрос: что именно происходит ? И я думаю, что большая часть этого — способ, которым LMDB распределяет страницы. Он использует метод копирования при записи, что означает, что после завершения транзакции ранее использованные страницы становятся свободными. Это означает, что они могут быть восстановлены, и следующая транзакция сделает именно это. Проблема с этим подходом состоит в том, что он имеет тенденцию распространять записи по всему файлу. Это экономит дисковое пространство, но требует гораздо больше поисков, когда вам нужно совершить транзакцию.

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