[Перевод] Я написал серверную SQLite
Меня зовут Бен Джонсон, и я написал встраиваемую базу данных, которая служит бэкендом систем вроде etcd, — это BoltDB. Сегодня я работаю над Open Source проектом Litestream в компании Fly.io. Благодаря репликации Litestream делает SQLite приемлемым для фулстек‐приложений. Если вы можете установить SQLite, то Litestream заставите работать за 10 минут.
Фулстек‐приложения обладают настолько привычной многослойной архитектурой, что легко забыть, что она вообще как‐то называется. Эта архитектура возникает, когда вы запускаете сервер приложений типа Rails, Django или Remix вместе с сервером БД, например Postgres.
По общепринятым представлениям SQLite в архитектуре занимает своё место, это место — юнит‐тесты. Представления пора дополнить. Думаю, во многих приложениях с большим числом пользователей и высокими требованиями к доступности у SQLite есть место лучше: в центре стека, в качестве ядра данных слоя хранения. Притязания большие, и они могут не подходить приложению, но вы должны подумать об SQLite. Я здесь, чтобы рассказать, зачем.
Краткая история баз данных
50 лет — недолгий срок, за который мы увидели много поразительных изменений в том, как программы управляют данными. В 70‐е, в начале истории баз данных, появились правила Кодда, давшие определение тому, что сейчас называется реляционными базами данных и базами данных вообще.
Эти правила вы знаете: все данные располагаются в таблицах, таблицы имеют колонки, а строки адресуются ключами. Есть CRUD, схемы данных и, конечно, SQL — буквальный язык передачи всех этих понятий и причина кембрийского взрыва баз данных в 80–90‐х: от DB2 и Oracle до Postgres и MySQL.
Не всё шло хорошо. 2000‐е принесли нам базы данных XML, хотя за это время индустрия искупила свою вину несколькими колоночными БД. В 2010‐х мы увидели десятки крупномасштабных распределённых баз данных Open Source — и теперь создать кластер и запрашивать терабайты данных может каждый.
Вместе с базами развивались стратегии их подключения к приложениям. Почти со времён Кодда мы разделили приложения на слои, и первым из них стали именно базы данных. С появлением memcached и Redis мы получили слои кеширования и фоновых задач, слои маршрутизации и распределения.
Учебные руководства делали вид, что слоёв всего три, но все мы знаем, почему приложения называются многослойными. Никто не может предугадать, сколько же будет слоёв. И уже чувствуется, куда мы идём.
Наши учёные сильно озабочены тем, смогут ли они сделать нечто. За те же полвека процессоры, диски и память стали в сотни раз быстрее и дешевле, а инновации 2010‐х на практике определил термин «большие данные».
Но развитие аппаратной части десять лет спустя сделало это понятие скользким: управлять базой на гигабайт в 1996 — большое дело. А в 2022? Просто запустите её на ноутбуке или t3.micro.
В размышлениях о новой архитектуре базы данных мысли об ограничениях масштабирования гипнотизируют нас: если архитектура не работает с терабайтом или около того, речи о ней не заходит. Но большинство приложений, даже успешных, никогда не столкнутся с терабайтом — и мы забиваем гвозди отбойным молотком.
Долгожданная БД
Но есть база данных, противостоящая многим из этих тенденций. Это одна из самых популярных баз. Настолько, что именно она — официальный формат архивов Библиотеки Конгресса. Она славится надёжностью и непостижимо огромным набором тестов, а её производительность хороша настолько, что цитирование цифр в сообщении на форум каждый раз порождает споры о том, не стоит ли исключить её из сравнений. Наверное, эта база не нуждается в представлении, но для человека с поднятой рукой уточню: я говорю об SQLite.
SQLite — встраиваемая БД. В слое архитектуры вы её не найдёте. Это просто библиотека, связанная с процессом сервера приложений, стандартная подпорка «приложений с одним процессом». И это сервер, который выполняется сам по себе, не полагаясь на ещё девять.
Подобные приложения заинтересовали меня, поэтому я разработал BoltDB — популярное в экосистеме Go хранилище данных типа ключ‐значение. BoltDB надёжна и бегает, как раскрашенный спорткар на азоте, а именно этого мы ждём от базы внутри процесса.
Но схема данных BoltDB определяется кодом на Go, так что на неё трудно мигрировать, и нужно написать всю обвязку, ведь нет даже REPL. Если вы будете внимательны, такая база может дать большую производительность. Но запускать базу для универсального применения, как спорткар без выхлопной системы, не хочется.
Я подумал о том, какую работу придётся проделать, чтобы сделать BoltDB жизнеспособной для большинства приложений, — и быстро понял, что для этого и существует SQLite.
SQLite, комментарий о чём вы уже, без сомнения, написали, не без ограничений. Самое большое из них — в том, что приложение с одним процессом имеет одну точку отказа: потерян сервер — и потеряна вся база. Но это не минус SQLite, а наследство её архитектуры.
Знакомьтесь, Litestream
По двум серьёзным причинам SQLite не используется по умолчанию. Первая — это неустойчивость к сбоям, а вторая — масштабирование конкурентности. И Litestream есть что сказать по этому поводу: он берёт на себя WAL‐журналирование SQLite.
В режиме WAL операции записи логируются в файл вне основного файла с данными SQLite. При чтении и удовлетворении запроса проверяются и основной файл с данными, и WAL. Обычно SQLite самостоятельно создаёт контрольные точки страниц WAL, возвращая их в базу.
Litestream вступает в середине: он открывает бесконечную транзакцию чтения, поэтому SQLite не создаёт контрольных точек. Мы перехватываем обновления WAL, реплицируем их и запускаем создание контрольной точки. Самое важное — понять, что Litestream — это просто SQLite.
Приложение использует SQLite со всеми её стандартными библиотеками. Мы не парсим запросы, не проксируем транзакции, и даже не добавляем зависимость, а просто берём преимущества журналирования и конкурентности в SQLite для инструмента, который выполняется вместе с вашим приложением.
Код по большей части может забыть о Litestream. Вы можете создать приложение Remix, которое поддерживается Litestream‐репликацией, а во время работы приложения взломать базу, изменив её стандартным REPL sqlite3. Прочитать об этом больше можно здесь.
Это звучит сложно, но на практике невероятно просто. Поиграв с кодом, вы увидите, что он просто работает. На сервере базы в режиме «репликации» вы запускаете бинарник Litestream:
litestream replicate fruits.db s3://my-bukkit:9000/fruits.db
И восстанавливаете репликацию в другом месте:
litestream restore -o fruits-replica.db s3://my-bukkit:9000/fruits.db
Зафиксируйте изменения. Вы увидите их после восстановления в новой копии.
Реплицировать можно почти везде: на S3 или Minio, Azure, или Backblaze B2, на Digital Ocean, Google Cloud, или какой‐нибудь SFTP‐сервер.
Сегодня люди обычно используют Litestream, чтобы реплицировать базу на S3: для большинства баз данных SQLite репликация в реальном времени на S3 обходится совсем недорого, что само по себе — большая операционная победа: база устойчива настолько, насколько вы попросите, а перемещать её, мигрировать и работать с ней легко.
Но с Litestream можно добиться большего. Предстоящий релиз позволит реплицировать SQLite прямо между базами данных, а значит, можно создать одну ведущую базу для записи и распределённые реплики — для чтения. Последние перехватят записи и направят их в ведущую базу: большинство приложений интенсивно читают данные, и такой подход даёт приложениям глобально масштабируемую базу.
Отнеситесь к этому варианту серьёзнее
Одна из первых моих работ в 2000‐x — администратор баз данных Oracle9i. Помню, как часами корпел над документацией и книгами, изучая Oracle снаружи и внутри. Руководство по администрированию почти в тысячу страниц — одно на более чем сотню руководств.
Изучать, какие ручки повернуть для оптимизации запросов или улучшения записи, — это могло быть важно 20 лет назад, когда за секунду диски считывали десятки мегабайт, а индекс получше превращал запрос на пять минут в тридцатисекундный.
Но оптимизация БД потеряла важность для типичных приложений. Если у вас база на гигабайт, диск NVMe потратит на всю загрузку меньше секунды. Столько же могут занять даже плохо настроенные запросы на обычных базах. И, как бы я ни любил настройку запросов, для большинства разработчиков это искусство умирает.
Современная Postgres — это чудо, и за годы чтения её кода я многому научился. У этой базы много функций: генетическая оптимизация запросов, политики безопасности для строк и полдюжины типов индексов. Если эти функции нужны вам, то они нужны. Но, наверное, они не нужны большинству, а ненужные функции мешают.
Даже если вы не используете несколько аккаунтов, придётся настроить и отладить аутентификацию по имени хоста, а ещё отключить брандмауэр от сервера Postgres. Документация Postgres содержит около 3000 страниц. Больше функций — больше документации, затрудняющей понимание инструментов, с которыми вы работаете.
SQLite обладает частью набора функций Postgres, эта часть покрывает 99,9% ваших нужд: прекрасная поддержка SQL, оконные функции, общие табличные выражения, полнотекстовый поиск, JSON. А если какой‐то функции нет, данные лежат совсем рядом с приложением, так что вытащить и обработать их в коде — это немного накладных расходов.
А сложные проблемы, которые действительно нужно решить, не решаются основными функциями базы. Оптимизировать хочется не эти функции, а только две вещи: задержку и опыт разработки.
Пользоваться SQLite намного проще, чем другими базами, и это одна из причин отнестись к ней серьёзно. Вы тратите своё время на написание кода приложения, а не на проектирование сложных слоёв базы данных. Но есть другая проблема.
Свет чертовски медленный
Вы столкнётесь с теоретическими пределами. В вакууме свет за тысячную долю секунды преодолевает 186 миль, — расстояние от Филиппин до Нью‐Йорка и обратно. Задержку увеличивают слои сетевых коммутаторов, брандмауэры и протоколы приложений. Каждый запрос Postgres в одном регионе AWS может задерживаться на время до 1 мс. Не потому, что Postgres медленная —, а потому, что мы упираемся в предел скорости передачи данных.
Теперь обработайте HTTP‐запрос в современном приложении. Десяток запросов к базе данных — и ещё до бизнес‐логики и рендеринга вы сожгли 10 мс, а мгновенными кажутся ответы не позже 100 мс.
Быстрые приложения — это довольные пользователи. 100 мс кажется многовато, но эта задержка важна настолько, что люди предварительно рендерят и отправляют их в сети доставки содержимого, просто чтобы сократить её. Лучше было бы просто переместить данные ближе к приложению.
Насколько близко? SQLite не просто делит с приложением одну машину, она на самом деле встроена в процесс приложения. Расположив данные рядом с приложением, вы увидите сокращение задержки каждого запроса на 10–20 мс — микросекунд, с приставкой μ. Это в 50–100 раз быстрее запроса Postgres в регионе AWS.
Но это ещё не всё. Эффективно устраняется задержка каждого запроса. Приложение становится быстрее и проще. Большие запросы можно разбить на много небольших, более управляемых, а время, которое мы тратили на поиск паттернов N+1 в крайних случаях, потратить на разработку новых функций.
Сводить задержку к наименьшей нужно не только на продакшене. Интеграционное тестирование с традиционной клиент‐серверной базой данных локально легко возрастает до минуты, и задержка не исчезнет, когда вы перейдёте в CI.
Сокращение цикла обратной связи от изменения кода до завершения теста не только экономит время, но и сохраняет концентрацию при разработке. Однострочное изменение может выполняться прямо в памяти, так что интеграционные тесты займут меньше секунды.
Маленькая, быстрая, надёжная, глобально распределяемая: выберите любые четыре
Litestream распределяется и реплицируется, и, самое важное, с ним легко разобраться. Серьёзно, попробуйте. Много знать не нужно. Я утверждаю, что, создавая надёжную и простую в применении репликацию для SQLite, мы делаем привлекательными все виды фулстек‐приложений, полностью работающих на ней.
Разумно было не замечать этот вариант 170 лет назад, когда было написано первое руководство по запуску блога на Rails, но сегодня SQLite может справиться с нагрузкой записи большинства приложений, а реплики — масштабировать чтение до выбранного количества экземпляров для балансировки нагрузки.
У Litestream есть ограничения. Я разработал его для приложений с одним узлом, поэтому он не будет хорошо работать на эфемерных, бессерверных платформах или при постепенном развёртывании. Litestream должен восстанавливать все изменения последовательно, а значит, восстановление может занять несколько минут. Мы выкатили репликацию реального времени, но модель отдельного процесса ограничивает управление её гарантиями. Можно сделать лучше.
В последний год я добивал ядро Litestream, делая упор на корректность, и доволен достигнутым. Litestream начинался как простой инструмент потокового резервного копирования, но постепенно превратился в надёжную распределённую базу данных.
Пришло время сделать его более быстрым и цельным — в этом заключается вся моя работа в Fly.io. В Litestream появятся улучшения, никак не связанные с Fly.io, — и мне волнительно поделиться ими. У Litestream на Fly.io появился новый дом, но он всегда будет проектом с открытым исходным кодом.
Мой план на следующие несколько лет — продолжать делать его более полезным, независимо от того, где работает приложение, и посмотреть, насколько далеко продвинется модель SQLite в том смысле, как могут работать базы данных.
А мы поможем прокачать ваши навыки или с самого начала освоить профессию, актуальную в любое время:
Выбрать другую востребованную профессию.
Краткий список курсов и профессий
Data Science и Machine Learning
Python, веб‐разработка
Мобильная разработка
Java и C#
От основ — в глубину
А также