Начало работы с LevelDB — Получите полное представление о функциях LevelDB

Redis LevelDB
Начало работы с LevelDB — Получите полное представление о функциях LevelDB

В этом разделе мы подробно рассмотрим различные функции LevelDB. Языком разработки LevelDB является C++.Учитывая, что не так много студентов могут использовать язык C++, в этом разделе мы будем использовать язык Java для описания возможностей LevelDB. Изучающим другие языковые стеки не о чем беспокоиться, потому что API-интерфейсы интерфейса для управления LevelDB на разных языках одинаковы, и использование аналогично.

открывать и закрывать

Данные LevelDB хранятся в определенном каталоге с множеством файлов данных, файлов журналов и т. д. Используйте API LevelDB, чтобы открыть этот каталог и получить ссылку на db. Позже мы будем использовать эту ссылку базы данных для выполнения операций чтения и записи. Приведенный ниже код представляет собой псевдокод, описанный на языке Java.

class LevelDB {
  public static LevelDB open(String dbDir, Options options);
  void close(); // 关闭数据库
}

Существует множество вариантов открытия базы данных, таких как установка размера блочного кэша, сжатие и т.д.

Базовый API

LevelDB похож на HashMap, но немного слабее HashMap, потому что метод put не может вернуть старое значение, а операция удаления не знает, действительно ли существует соответствующий ключ.

class LevelDB {
  byte[] get(byte[] key)
  void put(byte[] key, byte[] value)
  void delete(byte[] key)
  ...
}

Атомарная пакетная обработка

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

class WriteBatch {
  void put(byte[] key, byte[] value);
  void delete(byte[] key);
}

class LevelDB {
  ...
  void write(WriteBatch wb);
}

журнальный файл

Когда мы вызываем метод put LevelDB для записи данных в библиотеку, он сначала записывает данные в память, а затем сохраняет их на диск с помощью специальной стратегии. У этого есть проблема.Если произойдет внезапный простой, данные, которые слишком поздно будут записаны на диск, будут потеряны. Таким образом, LevelDB также использует стратегию, аналогичную журналу Redis AOF: сначала журнал операции модификации записывается в файл на диске, а затем обрабатывается фактический процесс операции записи.

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

Безопасная запись (синхронная запись)

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

Левельдб также похоже. Если используется предыдущая небезопасная запись, хотя вызов API успешен, соответствующий журнал операций может быть потерян при столкновении проблемы простоя. Таким образом, он обеспечивает безопасные операции записи за счет плохой производительности.

class LevelDB {
  ...
  void putSync(byte[] key, byte[] value);
  void deleteSync(byte[] key);
  void writeSync(WriteBatch wb);
}

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

параллелизм

Дисковые файлы LevelDB будут помещены в файловую директорию, содержащую множество связанных файлов данных и журналов. Он не поддерживает одновременное открытие этого каталога несколькими процессами для чтения и записи с использованием API LevelDB. Но для того же процесса LevelDB API поддерживает безопасное многопоточное чтение и запись. LevelDB внутри использует специальные блокировки для управления одновременными операциями.

траверс

Все ключи в LevelDB упорядочены и расположены в лексикографическом порядке от меньшего к большему. LevelDB предоставляет API обхода для последовательного доступа ко всем парам ключ-значение один за другим, и может указывать обход с середины.

class LevelDB {
  ...
  Iterator<KV> scan(byte[] startKey, byte[] endKey, int limit);
}

Изоляция снимка

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

class Snapshot {
  byte[] get(byte[] key)
  void put(byte[] key, byte[] value)
  void delete(byte[] key)
  void write(WriteBatch wb);
  ...
  void close();  // 关闭快照
}

class LevelDB {
  ...
  Snapshot getSnapshot();
}

Хотя снимок потрясающий, его принцип на самом деле очень прост, и мы подробно объясним его позже.

Пользовательский компаратор ключей

Ключи LevelDB по умолчанию используют лексикографический порядок, но он также обеспечивает индивидуальную сортировку. Вы можете зарегистрировать пользовательскую функцию сортировки, например сортировку по номерам. Необходимо следить за тем, чтобы параметры сортировки оставались неизменными на протяжении всего жизненного цикла базы данных, поскольку параметры сортировки влияют на порядок хранения пар ключ-значение на диске, а порядок хранения на диске нельзя изменить динамически.

Options options = new Options();
options.comparator = new CustomComparator();
db = LevelDB.open("/tmp/ldb", options);

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

блок данных

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

Options options = new Options();
options.blockSize = 8092;
db = LevelDB.open("/tmp/ldb", options);

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

компрессия

Дисковое хранилище LevelDB по умолчанию сжато, что является широко используемым алгоритмом Snappy в отрасли.Эффективность сжатия очень высока, поэтому не нужно беспокоиться о потере производительности. Вы также можете отключить его динамически, если не хотите использовать сжатие. Отключение переключателя сжатия обычно не дает заметного прироста производительности, поэтому мы стараемся не трогать его.

Options options = new Options();
options.compression = CompressionType.kSnappyCompression;
// options.compression = CompressionType.kNoCompression; // 关闭压缩
db = LevelDB.open("/tmp/ldb", options);

блочный кеш

В памяти LevelDB хранятся недавно прочитанные и записанные горячие данные.Если запрошенные данные не могут быть найдены в горячих данных, их необходимо искать в файле на диске, и эффективность будет сильно снижена. Чтобы сократить количество операций поиска файлов на диске, LevelDB добавляет блочный кеш, в котором кэшируется распакованное содержимое недавно часто используемых блоков данных.

Options options = new Options();
options.blockCache = LevelDB.NewLRUCache(100 * 1024 * 1024); // 100M
db = LevelDB.open("/tmp/ldb", options);

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

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

Фильтр Блума

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

Файлы на диске LevelDB хранятся в слоях. Сначала они будут найдены на уровне 0. Если они не смогут найти их, они перейдут на уровень 1, чтобы найти их, рекурсивно к нижнему слою. Поэтому, если вы будете искать несуществующий ключ, потребуется много операций чтения файлов на диске, которые займут очень много времени. Фильтр Блума может помочь вам сэкономить более 95% времени поиска файлов на диске.

Фильтр Блума похож на структуру набора памяти, в которой хранится информация об отпечатках всех ключей в определенном диапазоне указанного файла на диске. Когда он обнаруживает, что отпечаток ключа не может быть найден в коллекции Set, он может сделать вывод, что ключ не должен существовать.

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

Если фильтр Блума может точно знать, существует ли ключ, не будет неправильной оценки и не будет ненужных операций чтения с диска. Такой крайней формой фильтра Блума является HashSet — все Keys хранятся в памяти, конечно, объем памяти естественно неприемлем.

Options options = new Options();
// 每个 key 的指纹大小是 10bit
options.filterPolicy = LevelDB.NewBloomFilterPolicy(10);
db = LevelDB.open("/tmp/ldb", options);

При использовании фильтров Блума нам необходимо найти компромисс между потреблением памяти и производительностью. Если вы хотите глубже понять принцип работы фильтра Блума, вы можете перейти к «Redis Deep Adventure», в которой есть отдельная глава, посвященная внутреннему принципу работы фильтра Блума.

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

Проверка достоверности данных

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

class LevelDB {
  ...
  public void static repairDB(String dbDir, Options options);
}

При открытии базы данных опция обязательной проверки не включена по умолчанию, если она включена, то при обнаружении ошибки проверки будет сообщено об ошибке. Если действительно есть проблема с данными, LevelDB также предоставляет метод восстановления данных, repairDB() может помочь нам восстановить как можно больше данных.

резюме

После этого раздела обучения учащиеся должны быть в состоянии сформировать в уме следующую концептуальную карту. «Горячие данные» на рисунке относятся к парам ключ-значение, которые были недавно изменены, и пары ключ-значение здесь читаются быстрее всего. Если горячие данные не могут быть прочитаны, они перейдут в блочный кеш для чтения. Если вы не можете его прочитать, возможны два случая: во-первых, он не существует, а во-вторых, он существует на диске. Если он существует на диске, он считывается после ограниченного уровня чтения, обычно чем холоднее данные внизу. Если он не существует, он будет проходить через фильтр Блума, чтобы значительно сократить операции ввода-вывода при поиске на диске.Данные фильтра Блума и данные пары ключ-значение совместно размещаются в иерархическом файле данных.

图片

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