arka triymfalnaya

основы баз данных [перевод]

вольный перевод статьи Tony Solomonik "Database Fundamentals"

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

я заходил на сайты разных баз данных — и видел в основном маркетинг и набор слов, которые ничего не объясняют.

в итоге я взял и прочитал две отличные книги: database internals (alex petrov) и designing data-intensive applications (martin kleppmann).

этого хватило, чтобы захотеть написать свою маленькую базу данных — я назвал её dbeel.

этот пост — короткий конспект этих книг с фокусом на фундаментальные проблемы, о которых думает инженер баз данных (например, в душе).


bashdb

начнём с самой простой базы данных, которую только можно придумать — буквально две bash-функции:

#!/bin/bash
db_set() {
  echo "$1,$2" >> database
}
db_get() {
  grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
}

пример использования:

$ db_set 500 '{"movie": "Airplane!", "rating": 9}'
$ db_set 111 '{"movie": "Tokio Drift", "rating": 6}'
$ db_get 500
{"movie": "Airplane!", "rating": 9}

остановись на секунду и подумай: почему ты бы не стал использовать это в продакшене?

скорее всего, ты нашёл кучу проблем. сосредоточимся на основных:

  • durability — данные могут потеряться при падении системы
  • atomicity — запись может сохраниться частично
  • isolation — параллельные операции ломают данные
  • performance — поиск O(n)

что такое ACID

большинство баз данных стремятся гарантировать ACID:

  • atomicity — либо всё записалось, либо ничего
  • consistency — база не ломается от операций
  • isolation — нет гонок данных
  • durability — данные не теряются

durability

простое улучшение:

db_set() {
  echo "$1,$2" >> database && sync -d database
}

но что делает sync?

  • данные сначала пишутся в кеш (page cache)
  • потом ОС когда-нибудь сбрасывает их на диск

fsync / fdatasync заставляют сделать это сразу.

разница:

  • fsync — данные + метаданные
  • fdatasync — только данные

минус — это дорого по времени.


isolation

можно добавить блокировки через flock:

db_set() {
  (
    flock 9 && echo "$1,$2" >> database && sync -d database
  ) 9>database.lock
}
db_get() {
  (
    flock -s 9 && grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
  ) 9>database.lock
}

минус — блокируется вся база.


плохие новости

  • atomicity нормально не решить в bash
  • поиск всё ещё O(n)

но даже этот эксперимент даёт понимание, какие проблемы решают реальные базы данных.


storage engine

storage engine — это слой, который отвечает за:

  • запись и чтение данных
  • производительность
  • работу с диском

главная проблема — диск очень медленный.

пример:

операция	время
L1 cache	~0.5 ns
RAM	~100 ns
SSD	~150 μs
disk seek	~10 ms

вывод: нужно минимизировать обращения к диску.


mutable структуры: B-tree

B-tree — это структура, оптимизированная под диск:

  • поиск O(log n)
  • данные лежат рядом (spatial locality)
  • читаем блоками (pages)

пример:

        [7 | 16]
       /    |    \
 [1 2 5 6] [9 12] [18 21]

поиск:

def get(node, key):
  for i, child in enumerate(node.children):
    if child.key == key:
      return child.value
    if child.key > key:
      return get(node.nodes[i], key)
  return get(node.nodes[-1], key)

immutable структуры: LSM tree

идея:

  • писать только append
  • сначала в память
  • потом сбрасывать на диск в виде SSTable

дополнительно:

  • WAL (write-ahead log)
  • compaction (слияние файлов)

bloom filter

вероятностная структура:

  • быстро говорит, что элемента точно нет
  • иногда говорит, что он возможно есть

пример:

bloom = [0,0,0,0,0,0,0,0]
# вставка
bits = [hash1(key)%8, hash2(key)%8]
bloom[bits[0]] = 1
bloom[bits[1]] = 1

проверка:

if bloom[i] == 0:
  # точно нет

WAL

лог всех операций:

  • сначала пишем в WAL
  • потом в основное хранилище

при старте:

  • читаем WAL
  • восстанавливаем состояние

уровни изоляции

  • read uncommitted — можно читать грязные данные
  • read committed
  • repeatable read
  • serializable

пример dirty read:

BEGIN;
SELECT age FROM users;
-- параллельно
UPDATE users SET age = 21;
SELECT age FROM users; -- 21 (грязное чтение)

распределённые системы

нужны для:

  • отказоустойчивости
  • масштабирования

но добавляют сложность.


CAP теорема

нельзя одновременно гарантировать:

  • consistency
  • availability
  • partition tolerance

всегда выбираешь 2 из 3.


consistent hashing

распределение данных по узлам:

def get_node(nodes, key):
  return nodes[hash(key) % len(nodes)]

проблема — при добавлении узла всё ломается.

решение — hash ring.


репликация

каждый элемент хранится на нескольких узлах.

можно настраивать:

  • сколько узлов отвечает на чтение (R)
  • сколько подтверждают запись (W)

условие согласованности:

R + W > N / 2;

anti-entropy

механизмы синхронизации:

  • read repair
  • hinted handoff
  • merkle trees
  • gossip

вывод

базы данных — это не просто “куда-то сохранить данные”.

это:

  • работа с диском
  • структуры данных
  • конкуренция
  • отказоустойчивость
  • распределённые системы

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