Автор: Hexi Lee (инженер-программист, стажер в PingCAP)

Транскреатор: Ран Хуан; Монтажер: Том Деван

TiKV — это механизм распределенного хранилища ключей и значений, отличающийся строгой согласованностью и устойчивостью к разделам. Он может действовать либо как механизм хранения для TiDB, либо как независимая транзакционная база данных "ключ-значение". Знаете, на что еще он способен?

На TiDB Hackathon 2020 наша команда создала распределенную файловую систему POSIX на основе TiKV, TiFS, которая наследует мощные функции TiKV, а также использует возможности TiKV помимо хранения данных.

В этом посте я подробно расскажу о TiFS: как мы пришли к идее, как реализовали файловую систему и результаты ее тестирования. Приступаем к взлому!

Зачем строить файловую систему на основе TiKV?

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

Мой друг однажды хотел попробовать TiDB, но у него был только один сервер. Я сказал ему, что он может запустить экземпляр TiKV на каждом диске, добившись устойчивости данных к сбоям, и ему больше не понадобится RAID!

Однако TiKV хранит только данные, а не файлы, поэтому вам все равно нужен RAID для восстановления ваших файлов после аварии. Но это натолкнуло меня на мысль: если бы TiKV мог хранить данные файловой системы, то можно было бы обеспечить аварийное восстановление файловой системы. Поэтому мы потратили несколько дней на создание TiFS. Это была файловая система POSIX, и в первой версии она была полна ошибок и была склонна к взаимоблокировкам — но идея сработала.

Зачем мы пошли на все эти усилия? Три причины:

  • В отличие от обычного хранилища для распределенных файловых систем, TiKV поддерживает распределенные транзакции, совместимые с ACID. Таким образом, мы можем гарантировать строгую согласованность файловой системы.
  • Файловая система POSIX не только соответствует требованиям локальной файловой системы, но также поддерживает совместную работу с файлами между компьютерами и хранит файлы для других распределенных приложений.
  • Что еще круче, если мы запускаем отдельное приложение, поддерживающее совместную работу с несколькими экземплярами на TiFS, оно становится распределенным приложением.

Поэтому мы начали этот проект для создания TiFS, файловой системы, столь же надежной, как Titanium.

Как мы внедряем TiFS

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

Ценности

TiFS хранит семь типов значений в TiKV: метаданные системы, метаданные файла, блок файла, обработчик файла, символическую ссылку, каталог и индекс файла. Блок файла — это прозрачные данные, записанные пользователем. Символическая ссылка сохраняет только целевой путь. Остальные пять значений представляют собой структурированные данные.

Системные метаданные

Вся файловая система имеет только одни системные метаданные (Meta), которые обновляются только во время операций mknod и mkdir. Системные метаданные содержат только одно целое число, используемое для генерации номера инода файла:

struct Meta {
    inode_next: u64,
}

Метаданные файла

У каждого файла есть соответствующие метаданные файла (Inode). В этой структуре данных:

  • file_attr хранит метаданные, необходимые для файловой системы POSIX, такие как номер инода файла, размер файла и количество блоков. Подробнее см. в разделе Атрибуты файлов Rust.
  • lock_state отслеживает текущее состояние блокировки и держатель блокировки, которые используются для реализации flock.
  • inline_data хранит небольшое количество файлового содержимого, чтобы повысить скорость чтения/записи небольших файлов.
  • next_fn — это целое число с автоинкрементом, используемое для создания обработчика файла.
  • opened_fn записывает количество открытых обработчиков файлов.
struct Inode {
    file_attr: FileAttr,
    lock_state: LockState,
    inline_data: Option<Vec<u8>>,
    next_fh: u64,
    opened_fh: u64,
}

Обработчик файлов

Каждый раз, когда пользователь вызывает open, файловая система генерирует соответствующий файловый обработчик (FileHandler) для хранения ограничений на чтение/запись обработчика:

struct FileHandler {
    flags: i32,
}

Каталог

В каждом каталоге хранится список подфайлов, чтобы можно было реализовать readdir. В списке каждый элемент DirItem хранит номер инода, имя и тип файла:

type Directory = Vec<DirItem>;
struct DirItem {
    ino: u64,
    name: String,
    typ: FileType,
}

Индекс файлов

Когда мы запрашиваем файл, файловая система может пройти через весь файловый каталог; но для более эффективного запроса файлов мы создаем индекс (Index) для каждого файла. Индекс файла содержит только номер инода целевого файла:

struct Index {
    ino: u64,
}

Ключ

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

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

Область системных метаданных содержит только одну пару "ключ-значение":

+ 1 byte +
|        |
|        |
|        |
|        |
|        |
|        |
|        v
+--------+
|        |
|    0   |
|        |
+--------+

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

Ключ метаданных файла выглядит следующим образом:

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

Массив ключей приведен ниже:

Ключ обработчика файла состоит из номера индексного дескриптора файла и номера обработчика файла в обратном порядке:

Ключ file index включает в себя номер инода родительского каталога в обратном порядке и имя файла в кодировке UTF-8:

Последовательность

TiKV поддерживает как оптимистичные, так и пессимистичные транзакции. Однако, поскольку клиент Rust экспериментально поддерживает только пессимистичные транзакции, а пессимистичные транзакции могут снижать производительность, когда транзакции не конфликтуют, мы реализуем TiFS только для оптимистичных транзакций.

Различные сценарии

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

Вот несколько вариантов использования TiFS, которые могут вас заинтересовать:

  • Удаленный репозиторий Git может напрямую использовать TiFS для хранения проекта и выполнения задач Git, таких как rebase или cherry-pick, без переноса файлов в локальную файловую систему.
  • Когда несколько узлов приложения читают или записывают один и тот же файл в TiFS, вы можете использовать flock для разрешения любых конфликтов.
  • Нет сложного SDK или API для управления пространством. Вы просто вызываете API файловой системы или запускаете сценарий оболочки.
  • TiFS позволяет вам взять отдельное приложение, поддерживающее совместную работу с несколькими экземплярами, и превратить его в распределенное приложение. Например, SQLite + TiFS = еще одна распределенная реляционная база данных. По общему признанию, чтобы использовать TiFS таким образом, приложение не может полагаться на кэш страниц или другие механизмы кэширования, чтобы избежать невидимости записи.

Тесты и бенчмарк

Во время хакатона мы использовали pjdfstest для проверки корректности TiFS. Но поскольку pjdfstest не охватывает корректность чтения/записи или параллелизма, нам нужно будет добавить другие тесты.

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

IOPS

Примечание. TiKV — это сложная система, в которой есть логическая продолжительность, продолжительность дискового ввода-вывода и продолжительность сети. В этой статье мы упрощаем TiKV до одной реплики в демонстрационных целях.

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

  • Tf: продолжительность ввода-вывода FUSE.
  • Tc: логическая продолжительность TiFS.
  • Tn: время передачи по сети.
  • Ts: логическая продолжительность TiKV.

Соответственно имеем такое уравнение:

Чтение операций ввода-вывода в секунду

Для операций чтения Tf положительно коррелирует с размером блоков загрузки. Размер данных, считываемых или записываемых TiFS для каждой операции ввода-вывода, должен быть целым числом, кратным размеру блоков файловой системы. Таким образом, Tn и Ts положительно коррелируют с большим значением между блоками загрузки и блоками файловой системы; однако при большем трафике сетевой и дисковый ввод-вывод может занять больше времени. Tc неизвестно.

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

Перед бенчмаркингом у нас были следующие прогнозы:

  • Когда размер блока файла и блока загрузки составляет 4 КБ, при увеличении блока загрузки увеличиваются Tf, Tn и Ts; следовательно, IOPS уменьшается.
  • Когда блок файла составляет 64 КБ или 1 МБ:
    Если блок загрузки меньше, чем блок файла, Tn и Ts практически не изменяются, но Tf увеличивается; следовательно, IOPS уменьшается.
    Если блок загрузки больше, чем блок файла, Tf, Tn и Ts все увеличиваются; следовательно, IOPS уменьшается.

Как видно на диаграмме, разброс практически идентичен нашим прогнозам.

Запись операций ввода-вывода в секунду

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

На следующей линейной диаграмме показано, как количество операций ввода-вывода в секунду при последовательной записи зависит от размера блоков загрузки. Если размер файловых блоков составляет 1 МБ (см. желтую линию), IOPS равно самый большой, в то время как блок файла и блок загрузки эквивалентны.

Более того, мы видим, что красная линия почти перекрывает синюю линию на первых двух точках данных. Это означает, что когда блок загрузки и блок файла составляют 4 КБ или 64 КБ, их значения IOPS почти одинаковы. При таких обстоятельствах минимальный трафик в секунду составляет 4 КБ 110 = 440 КБ, а максимальный — 64 КБ100 = 6,25 МБ, что оказывает небольшое давление на сеть и диски. Когда трафик достаточно мал, можно с уверенностью сказать, что IOPS достигает своего верхнего предела, поэтому основным фактором Tn становится сетевая задержка. (При локальном тестировании Tn считается равным 0 мс.)

На приведенной выше диаграмме, когда блоки файлов и блоки загрузки изменяются между 4 КБ и 64 КБ, IOPS практически не изменяется. В таких конфигурациях T определяется Tc и Ts, поэтому мы называем это фиксированной задержкой системной операции TiFS. Фиксированная задержка работы системы вызвана логической длительностью TiFS и TiKV. Если задержка достаточно высока, это приводит к ужасной производительности чтения/записи для небольших файлов. Мы все еще работаем над этой оптимизацией.

Скорость

Поскольку скорость чтения и записи является произведением IOPS на загрузочные блоки, и IOPS изменяется очень мало, когда загрузочный блок увеличивается с 4 КБ до 1 МБ, операции чтения и записи скорости достигают максимального значения, когда загрузочный блок составляет 1 МБ.

На следующих двух гистограммах сравниваются скорости чтения и записи в различных конфигурациях кластера, когда загрузочный блок равен 1 МБ. Синяя полоса представляет собой обычный TiKV, а красная полоса представляет собой TiKV с включенным Титаном.

Titan — это подключаемый модуль RocksDB для разделения ключей и значений, вдохновленный WiscKey, для уменьшения увеличения записи.

Из диаграмм видно, что на скорость записи в основном влияет размер блока файла и включен ли Титан, а скорость чтения колеблется лишь незначительно. Это связано с тем, что чем меньше блок файла, тем больше пар ключ-значение записывает TiKV, что занимает больше времени. Но RocksDB плохо работает с большими блоками файлов, поэтому включение Titan может уменьшить ненужное копирование значений и, таким образом, повысить производительность.

Наш следующий шаг

В TiFS хранение файловых блоков обходится очень дорого, поскольку TiKV реализует избыточность за счет использования нескольких реплик (по умолчанию три). Коэффициент избыточности (занимаемое пространство, деленное на объем записываемых данных) обычно равен трем или более.

Однако коэффициент избыточности составляет всего 1,2–1,5 в других распределенных файловых системах, которые поддерживают избыточность за счет кодирования стирания (EC), таких как HDFS, CephFS и JuiceFS. Избыточность EC требует кодирования и декодирования во время записи и восстановления данных, что требует дополнительных вычислительных ресурсов. Однако стратегия избыточности EC жертвует частью производительности чтения в обмен на более низкие накладные расходы на сеть и стоимость хранения.

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

Кроме того, мы сосредоточимся на проверке правильности и настройке производительности:

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

Если вы заинтересованы в TiFS, не стесняйтесь попробовать или присоединиться к нашему обсуждению!

Первоначально опубликовано на www.pingcap.com 24 мая 2021 г.