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-репозиторий работают вместе — в разделе «Хранение данных» документации.