Перейти к содержанию

KV-хранилища

В развитие предыдущего поста я хочу остановиться и немного подробнее рассказать о KV-хранилищах и их роли в архитектуре сети Sigil Gate. Всё-таки, это достаточно нишевая тема — даже для IT.

В прошлый раз я упомянул KV-кластер на базе etcd — и понял, что для большинства людей это звучит примерно как заклинание на латыни. Я постараюсь не углубляясь в технические дебри и рассказать об этой технологии в научно-популярном ключе: что это вообще такое, где применяется и почему мы выбрали именно etcd.

Пристегнитесь - мы взлетаем!

Общая концепция #

KV (key-value, «ключ-значение») — одна из старейших концепций в Computer Science. По сути, это структура данных, которая хранит пары: ключ и соответствующее ему значение. Примеры пар «ключ-значение» окружают нас повсюду:

  • Номер паспорта → имя владельца
  • Артикул товара → его описание
  • Доменное имя → IP-адрес
  • Номер рейса → время вылета

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

Концепция реализована на базе хеш-таблиц, впервые описанных Гансом Петером Луном в IBM в 1953 году — это раньше, чем появились такие языки программирования как Fortran (1957), COBOL (1959) и C (1972). Основной поинт здесь в том, что идея эта не только не нова, но и намного древнее чем большинство из современных языков программирования.

В языках программирования эта концепция реализована в виде коллекций, в просторечье называемых “мапами”. Их могут называть словарями (python), картами (go, js), ассоциативными массивами (php), а также хэш-картами (java) или хеш-таблицами (как бы намекая, что под капотом используется технология хеширования), в зависимости от контекста и сферы применения. Детали реализации различаются, но на уровне общей идеи — это одно и то же.

Но одно дело — KV-коллекция в памяти одной программы на одном компьютере. А что, если такие данные нужно хранить на нескольких серверах одновременно — и гарантировать, что все они видят одно и то же?

От словаря — к инфраструктуре #

В сетевой инфраструктуре масса задач, которые по своей природе сводятся к работе с парами «ключ-значение»:

  • Конфигурации сервисов — ключ: имя параметра, значение: настройка
  • Service discovery — «где сейчас живёт сервис X?» (ключ: имя сервиса, значение: адрес и порт)
  • DNS — по сути, классическое KV: домен → IP-адрес
  • Сессии пользователей — ключ: токен сессии, значение: данные пользователя
  • Распределённые блокировки и выбор лидера — ключ: ресурс, значение: кто его захватил
  • Кэширование — ключ: запрос, значение: результат

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

Redis — пожалуй, самый известный. Хранит данные в оперативной памяти, невероятно быстрый. Его используют Twitter, GitHub, StackOverflow — но именно как кэш и брокер сообщений, а не как источник истины. Консистентность — не его сильная сторона: если нода упала, данные могут потеряться. Для кэша это нормально. Для хранения состояния инфраструктуры — нет.

ZooKeeper — ветеран. Создан в Apache для экосистемы Hadoop, используется Kafka. Надёжный, проверенный временем. Но: написан на Java (тяжёлый), сложный API, непростая эксплуатация. У ZooKeeper есть полушутливая репутация в индустрии: он настолько сложен в обслуживании, что для его поддержки нужен отдельный зоопарк инженеров.

Consul (HashiCorp) — больше, чем KV-хранилище. Это целая платформа: service discovery, health checking, service mesh. KV в нём есть, но оно — часть большой экосистемы. Мощный инструмент, но для задачи «просто хранить ключи» — избыточен. Используется, когда нужна вся экосистема HashiCorp (Vault, Nomad, Terraform).

etcd — создан командой CoreOS специально для распределённого хранения конфигураций. Выбран как основа Kubernetes — и это, пожалуй, лучшая рекомендация. Компактный, написан на Go (один бинарник), простой HTTP/gRPC API, строгая консистентность через протокол Raft.

Все эти решения оптимизированы для скорости и минимальной задержки, активно используя оперативную память для быстрого доступа к данным (при этом etcd и ZooKeeper надёжно сохраняют данные на диск). Но различаются они не столько скоростью, сколько подходом к куда более фундаментальной проблеме.

Проблема, у которой нет идеального решения #

Помимо задачи хранения и обработки в реальном времени, есть ещё один класс проблем, на решение которых направлены усилия всех перечисленных систем. Это задачи синхронизации данных и обеспечения консистентности информации.

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

В таких системах всегда стоит задача обновления устаревающей информации, разрешения конфликтов и достижения трёх целей:

  • Согласованность (Consistency) — все узлы видят одни и те же данные в один момент времени
  • Доступность (Availability) — каждый запрос получает ответ, даже если часть узлов недоступна
  • Устойчивость к разделению (Partition tolerance) — система продолжает работать, даже если связь между узлами нарушена

Проблема в том, что эти три цели внутренне противоречивы и взаимоисключающи. Это не предположение, а математически доказанное ограничение — CAP-теорема, сформулированная Эриком Брюером в 2000 году и формально доказанная Сетом Гилбертом и Нэнси Линч из MIT в 2002-м. Своего рода закон физики для распределённых систем: из трёх — можно выбрать только два.

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

И вот тут становится понятно, почему перечисленные выше KV-хранилища такие разные — они по разному подходят к поиску компромисса в одной и той же неразрешимой задаче:

  • Redis выбирает скорость и доступность, жертвуя строгой консистентностью
  • etcd и ZooKeeper выбирают консистентность и устойчивость, жертвуя доступностью при потере кворума
  • Consul — где-то посередине, с настраиваемым уровнем консистентности

Raft: протокол согласия #

Для проекта Sigil Gate мы делаем выбор в пользу консистентности и устойчивости. Решать вопросы консистентности можно по-разному, и одним из решений является протокол Raft.

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

Работает он по принципу голосования:

  • Один узел выбирается лидером, остальные — последователи
  • Все записи идут через лидера — он рассылает их остальным
  • Запись считается успешной, когда большинство (кворум) узлов подтвердило получение
  • Если лидер выходит из строя — оставшиеся узлы выбирают нового за миллисекунды

Отсюда правило нечётного числа узлов: кластер из нечетного числа узлов (3, 5, …) выдерживает отказ до (N-1)/2 узлов, сохраняя работоспособность.

Это правило означает, что кластер из 3 узлов переживёт потерю 1, из 5 — потерю 2. Кластер из 2 узлов бесполезен — потеря одного означает потерю большинства. Как в парламенте: решение принято, когда большинство проголосовало «за», даже если кто-то покинул зал.

Почему etcd для Sigil Gate #

etcd — компактное KV-хранилище, построенное на протоколе Raft. Оно делает одну вещь — и делает её хорошо. Эта философия “Unix Way” и она идеально вписывается в задачи текущего этапа — создание прототипа требует несложных решений, обеспечивающих базовую функциональность.

Для нашей сети etcd закрывает ключевые потребности:

  • Строгая консистентность — в сети, где узлы принимают решения на основе данных из хранилища (маршруты, права доступа, статусы), нельзя допустить ситуацию, когда две ноды видят разное состояние
  • Компактность — один бинарник на Go, минимум зависимостей. Не нужно тащить JVM (ZooKeeper) или целую платформу (Consul)
  • Простой API — HTTP/gRPC, без магии. Легко интегрировать в CLI и автоматизацию
  • Естественное размещение — кластер etcd разворачивается прямо на Core-нодах, каждая нода — узел кластера. Никакой отдельной инфраструктуры на начальном этапе не требуется.

Это осознанный выбор — потому что etcd оптимален для текущего масштаба. Возможно, со временем мы перейдём на более комплексные решения — например, Consul, который помимо KV предоставляет встроенный health checking и интеграцию с системами мониторинга. Но на данном этапе etcd — ровно столько, сколько нужно. Не больше и не меньше.

Подробности о том, как KV-кластер и Git-репозиторий работают вместе — в разделе «Хранение данных» документации.