Разработка хранилища Key / Value с портированием на современный C ++

9

Я разрабатываю сервер базы данных, похожий на Cassandra.

Разработка была начата в C, но все стало очень сложно без классов.

В настоящее время я перенес все на C ++ 11, но я все еще изучаю "современный" C ++ и у меня есть сомнения по поводу многих вещей.

База данных будет работать с парами ключ / значение. У каждой пары есть еще какая-то информация - когда создается и когда истекает (0, если не истекает). Каждая пара неизменна.

Ключ - строка C, значение - void *, но, по крайней мере, на данный момент я работаю со значением как строка C.

Есть абстрактный IListкласс. Наследуется от трех классов

  • VectorList - C динамический массив - аналогичен std :: vector, но использует realloc
  • LinkList - сделано для проверки и сравнения производительности
  • SkipList - класс, который, наконец, будет использоваться.

В будущем я мог бы также сделать Red Blackдерево.

Каждый IListсодержит ноль или более указателей на пары, отсортированные по ключу.

Если IListстало слишком долго, его можно сохранить на диске в специальном файле. Этот специальный файл является своего рода read only list.

Если вам нужно найти ключ,

  • сначала в памяти IListищется ( SkipList, SkipListили LinkList).
  • Затем поиск отправляется по файлам, отсортированным по дате
    (сначала самый новый файл, последний файл - последний).
    Все эти файлы хранятся в памяти.
  • Если ничего не найдено, то ключ не найден.

У меня нет сомнений в реализации IListвещей.


То, что меня сейчас озадачивает, таково:

Пары имеют разный размер, они выделены, new()и они std::shared_ptrуказали на них.

class Pair{
public:
    // several methods...
private:
    struct Blob;

    std::shared_ptr<const Blob> _blob;
};

struct Pair::Blob{
    uint64_t    created;
    uint32_t    expires;
    uint32_t    vallen;
    uint16_t    keylen;
    uint8_t     checksum;
    char        buffer[2];
};

Переменная-член «буфер» - это переменная другого размера. Он хранит ключ + значение.
Например, если ключ - 10 символов, а значение - еще 10 байтов, весь объект будет sizeof(Pair::Blob) + 20(начальный размер буфера равен 2 из-за двух нулевых завершающих байтов)

Эта же схема используется и на диске, поэтому я могу сделать что-то вроде этого:

// get the blob
Pair::Blob *blob = (Pair::Blob *) & mmaped_array[pos];

// create the pair, true makes std::shared_ptr not to delete the memory,
// since it does not own it.
Pair p = Pair(blob, true);

// however if I want the Pair to own the memory,
// I can copy it, but this is slower operation.
Pair p2 = Pair(blob);

Однако этот другой размер является проблемой во многих местах с кодом C ++.

Например я не могу использовать std::make_shared(). Это важно для меня, потому что, если у меня есть 1M пар, у меня будет 2M.

С другой стороны, если я сделаю «буфер» для динамического массива (например, новый символ [123]), я потеряю «трюк» mmap, мне придется сделать две разыменования, если я хочу проверить ключ и добавлю один указатель - 8 байтов в класс.

Я также пытался «вытащить» все элементы Pair::Blobизнутри Pair, чтобы Pair::Blobбыть просто буфером, но когда я тестировал его, он был довольно медленным, вероятно, из-за копирования данных объекта.

Еще одно изменение, о котором я также думаю, - это удалить Pairкласс и заменить его на std::shared_ptrи «протолкнуть» все методы обратно Pair::Blob, но это не поможет мне с Pair::Blobклассом переменного размера .

Мне интересно, как я могу улучшить дизайн объекта, чтобы быть более дружественным к C ++.


Полный исходный код здесь:
https://github.com/nmmmnu/HM3

Ник
источник
2
Почему вы не используете std::mapили std::unordered_map? Почему значения (связанные с ключами) некоторые void*? Возможно, вам придется уничтожить их в какой-то момент; как и когда? Почему вы не используете шаблоны?
Василий Старынкевич
Я не использую std :: map, потому что я верю (или, по крайней мере, пытаюсь) сделать что-то лучше, чем std :: map для текущего случая. Но да, я думаю в какой-то момент обернуть std :: map и проверить его производительность как IList.
Ник
Выделение и вызов d-торов выполняется там, где элемент IList::removeили когда IList разрушен. Это займет много времени, но я собираюсь сделать это в отдельной теме. Это будет легко, потому что IList будет в std::unique_ptr<IList>любом случае. так что я смогу «поменять» его новым списком и сохранить старый объект где-нибудь, где я могу вызвать d-tor.
Ник
Я пробовал шаблоны. Они не являются лучшим решением здесь, потому что это не пользовательская библиотека, ключ всегда, C stringа данные всегда некоторый буфер void *или char *, таким образом, вы можете передать массив символов. Вы можете найти подобное в redisили memcached. В какой-то момент я мог бы решить использовать std::stringили исправить массив символов для ключа, но подчеркнуть, что это все еще будет строка C.
Ник
6
Вместо добавления 4 комментариев, вы должны отредактировать свой вопрос
Василий Старынкевич

Ответы:

3

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

Затем я бы порекомендовал вам предоставить как можно более чистую и максимально чистую реализацию без каких-либо проблем с производительностью. Мне кажется, это unordered_mapдолжен быть ваш первый выбор, или, может mapбыть, интерфейс должен демонстрировать какой-то порядок ключей.

Итак, сначала заставьте его работать чисто и минимально; затем примените его в реальном приложении; при этом вы найдете, какие проблемы вам нужно решить на интерфейсе; тогда, продолжайте и обратитесь к ним. Скорее всего, в результате изменения интерфейса вам потребуется переписать большие части реализации, поэтому каждый раз, когда вы уже инвестировали в первую итерацию реализации, вы тратите минимум времени, необходимого для того, чтобы он едва работа - это потеря времени.

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

Вы говорите, что надеетесь сделать лучше, чем map; Об этом можно сказать две вещи:

а) вы, вероятно, не будете;

б) избегать преждевременной оптимизации любой ценой.

Что касается реализации, вашей основной проблемой, по-видимому, является распределение памяти, так как вы, кажется, обеспокоены тем, как структурировать свой дизайн, чтобы обойти проблемы, которые вы предвидите, которые у вас возникнут в отношении распределения памяти. Лучший способ решить проблемы с выделением памяти в C ++ - это реализовать подходящее управление распределением памяти, а не путать и изгибать конструкцию вокруг них. Вам следует считать себя счастливым, если вы используете C ++, который позволяет вам самостоятельно управлять распределением памяти, в отличие от таких языков, как Java и C #, где вы в значительной степени застряли на том, что может предложить языковая среда выполнения.

Существуют различные способы управления памятью в C ++, и newможет оказаться полезной возможность перегрузки оператора. Упрощенный распределитель памяти для вашего проекта будет предварительно выделять огромный массив байтов и использовать его в качестве кучи. ( byte* heap.) У вас будет firstFreeByteиндекс, инициализированный нулем, который указывает первый свободный байт в куче. Когда приходит запрос на Nбайты, вы возвращаете адрес heap + firstFreeByteи добавляете Nв firstFreeByte. Таким образом, распределение памяти становится настолько быстрым и эффективным, что практически не становится проблемой.

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

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

Если вы хотите иметь возможность удалять объекты из кучи, чтобы иметь возможность восстановить их память, то все становится более сложным: вам нужно будет использовать не указатели, а указатели на указатели, чтобы вы всегда могли перемещать объекты вокруг в кучах, чтобы восстановить пространство удаленных объектов. Все становится немного медленнее из-за дополнительной косвенности, но все по-прежнему молниеносно по сравнению со стандартными процедурами выделения памяти во время выполнения библиотеки.

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

Майк Накис
источник