Files
hse-2026/homework/07-kv-sharding/readme.md
T
2026-11-07 22:19:18 +03:00

96 lines
17 KiB
Markdown

# Распределенное key-value хранилище с шардингом
В этом задании вам предстоит реализовать распределенную систему, в которой можно хранить записи вида _(ключ, значение)_. Ключами и значениями могут быть произвольные строки. В системе может храниться только одна запись с данным ключом. Для каких-то ключей записи могут отсутствовать. Распределение используемых ключей может быть любым, необязательно равномерным. Система должна поддерживать стандартные операции для работы с данными в подобных key-value хранилищах: записать значение для ключа, прочитать значение по ключу и удалить ключ.
Система состоит из нескольких идентичных узлов, на каждом из которых запущен процесс _StorageNode_. Для поддержки горизонтального масштабирования каждый узел отвечает за хранение только некоторой части данных. А именно, для любого возможного значения ключа должен быть определен (один и только один) узел, "отвечающий" за хранение соответствующей записи. Таким образом, каждый узел в системе отвечает за некоторое подмножество ключей и хранение связанных с этими ключами записей. Хранить данные на диске не требуется, достаточно держать все записи в памяти.
Нагрузка на узлы системы должна распределяться равномерно. А именно, каждый узел должен хранить примерно одинаковую долю записей - в идеале _R/N_ записей, где _R_ - суммарное число хранимых записей, а _N_ - число узлов. В тестах допустимым считается отклонение не более чем на 10% от идеального значения.
В ходе работы системы в неё могут добавляться новые узлы, а существующие - удаляться. Добавление и удаление узлов происходит по команде администратора. При этом каждому узлу в системе отправляется соответствующее уведомление. Как обычно, узлы также могут внезапно отказывать. Чтобы не создавать зависимость от предыдущего ДЗ, будем считать, что отказы узлов уже обнаруживаются и обрабатываются каким-то образом. Соответственно, от вас не требуется реализовывать функционал group membership. В случае отказа узла и невозможности быстро восстановить его работу, этот узел может быть удален из системы через описанный выше механизм. Хранимые на отказавшем узле записи становятся временно недоступны или теряются насовсем. Эту проблему мы будем решать позже в рамках следующего задания.
При изменении состава узлов должна происходить _перебалансировка_ - перераспределение ключей (зон ответственности) и записей (хранимых данных) между узлами для восстановления ранее описанных свойств. Во-первых, для любого возможного значения ключа должен существовать узел, отвечающий за него. Во-вторых, каждый узел по-прежнему должен хранить примерно одинаковую долю записей. Таким образом, при добавлении нового узла, он должен принять на себя некоторую часть ключей и хранимых записей. А при отключении узла, ключи, за которые он отвечал, и хранимые им записи должны распределяться между оставшимися узлами.
Перебалансировка сопряжена с расходами на перемещение хранимых данных между узлами. Требуется минимизировать эти расходы - в идеале должно перемещаться только _R/N_ записей. В тестах допустимым считается отклонение не более чем на 10% от идеального значения.
Клиенты системы не знают о распределении данных между узлами и могут отправлять свои запросы к любому узлу. Соответственно узел должен уметь обслуживать запросы на чтение и запись к _любым_ ключам. Если узел не отвечает за данный ключ, то он должен переслать запрос правильному узлу, дождаться ответа и вернуть его локальному клиенту. Таймауты и ошибки в ответах поддерживать необязательно.
Каждый узел должен локально определять владельца любого ключа, без необходимости делать дополнительные пересылки. Такая маршрутизация называется _zero-hop_. Для полного балла поиск владельца должен занимать _O(log N)_ или меньше, где _N_ - число узлов. Объем используемой для поиска памяти не должен зависеть от числа хранимых записей, поэтому просто хранить набор пар «ключ → узел» для всех ключей нельзя.
Если вам плохо понятны некоторые требования, изучите соответствующие тесты - это часть условия задания.
## Реализация
Для реализации и тестирования решения используется фреймворк AnySystem (см. материалы первого семинара). В папке `solution` размещена заготовка для решения [node.py](solution/node.py). Вам надо доработать реализацию класса `StorageNode` так, чтобы проходили все тесты.
При инициализации узлу передается его уникальный id, а также список id всех узлов в системе на данный момент. В начале теста узлы инициализируются одновременно, то есть каждый из них получает полный список узлов на момент старта системы.
Узел должен поддерживать обработку следующих локальных сообщений (форматы запросов и ответов описаны в заготовке):
- _GET(key)_ - вернуть значение записи с ключом `key` или `null`, если ключ отсутствует,
- _PUT(key, value)_ - сохранить запись с ключом `key` и значением `value`,
- _DELETE(key)_ - удалить запись с ключом `key` и вернуть прежнее значение или `null`, если ключ отсутствует,
- _NODE_ADDED_ - уведомление о добавлении нового узла в систему,
- _NODE_REMOVED_ - уведомление об удалении узла из системы,
- _COUNT_RECORDS_ - возвращает число записей, хранимых на данном узле (используется для тестирования),
- _DUMP_KEYS_ - возвращает ключи записей, хранимых на данном узле (используется для тестирования).
Для взаимодействия между узлами вы можете использовать любые собственные типы сообщений.
Ваши реализации процессов AnySystem должны соблюдать правило [изоляции процессов](../readme.md#изоляция-процессов-anysystem): изменяемое состояние каждого процесса должно храниться только в `self`, а общая изменяемая память между процессами запрещена. Все взаимодействия между процессами должны происходить через сообщения, как и в реальной распределённой системе.
### Рекомендации и замечания
1. Как вы могли догадаться, распределять по шардам необходимо только запросы _GET_, _PUT_ и _DELETE_. Кроме того, необходимо реализовать перебалансировку ключей при получении _NODE_ADDED_ или _NODE_REMOVED_.
2. Вашей реализации необязательно поддерживать узлы в консистентном состоянии в любой момент времени в процессе перебалансировки - достаточно, чтобы они пришли в такое состояние через какое-то ограниченное число сообщений.
3. Во время перебалансировки при добавлении узла система должна обрабатывать _GET_, _PUT_ и _DELETE_, поступающие на любой узел, включая новый. В тестах NODE ADDED DURING... проверяются такие запросы одного типа. Ответ можно отложить до завершения переноса. Перенос данных не должен затирать новые значения или восстанавливать удалённые записи.
4. Учтите, что в тестах окончание перебалансировки определяется по косвенным признакам, таким как отсутствие изменений числа хранимых записей на узлах (см. функцию `step_until_stabilized`), поскольку мы не стали требовать от вас возвращать явные ответы на _NODE_ADDED_ и _NODE_REMOVED_.
5. Не требуется реализовывать подтверждения и ретраи передаваемых между узлами запросов, в тестах сеть надежная.
## Тестирование
### Локальное тестирование
Тесты находятся в папке `tests`. Есть два варианта их запуска.
Рекомендуемый вариант запуска тестов - через готовый Docker-образ. В этом случае используемое окружение будет аналогично тестирующей системе. Для запуска тестов выполните команду:
```commandline
docker run --pull always --rm -t -v ./solution:/solution distsys.ru/course/kv-sharding:latest [ЗДЕСЬ МОЖНО УКАЗАТЬ ОПЦИИ]
```
Для запуска полного набора тестов с теми же параметрами, что и в тестирующей системе, выполните команду:
```commandline
docker run --pull always --rm -t -v ./solution:/solution distsys.ru/course/kv-sharding:latest
```
Вы также можете запустить тесты, скомпилировав их локально с помощью компилятора Rust. Такой вариант может быть удобен, если вы хотите лучше изучить или доработать тесты. Скомпилируйте тесты с помощью команды `cargo install --locked --path tests`. Для запуска тестов выполните команду:
```commandline
distsys-kv-sharding [ЗДЕСЬ МОЖНО УКАЗАТЬ ОПЦИИ]
```
Запустить только один из тестов можно с помощью опции `-t`. По умолчанию вывод тестов не содержит трассы (последовательности событий во время выполнения каждого из тестов), а только финальную сводку. Включить вывод трасс можно с помощью флага `-d`. Все доступные опции можно посмотреть с помощью флага `-h`.
В тестах MC NORMAL и MC NODE REMOVED вы можете столкнуться с ошибкой "state depth exceeds maximum allowed depth...". Эти тесты основаны на model checking (см. первый семинар) и, чтобы ограничить время тестирования, глубина проверяемых состояний системы в них ограничена. Соответственно, если за заданное число шагов обработка запроса не завершится, то будет выдана эта ошибка. Ограничения на глубину заданы так, чтобы в них легко укладывалась эффективное решение. В случае такой ошибки рекомендуется доработать решение, чтобы избавиться от избыточных действий.
Бонусы за пробелы в тестах начисляются по [общим правилам](../readme.md#бонусы-за-пробелы-в-тестах).
### Проверка в тестирующей системе
Отправьте ваше решение в тестирующую систему следуя [инструкции](../readme.md) и дождитесь результатов.
## Оценивание
Автоматические тесты приносят до 9 баллов. Если хотя бы один тест базового блока не пройден, итоговая оценка за задание - 0. После прохождения базового блока каждый из трёх остальных блоков приносит 2 балла, если пройдены все его тесты.
| Блок | Тесты | Баллы |
|---|---|---:|
| Базовые операции, маршрутизация и память | `SINGLE NODE`, `INSERTS`, `DELETES`, `ZERO HOP`, `MEMORY GROWTH`, `MC NORMAL` | 3 |
| Добавление и удаление узлов, перенос данных | `NODE ADDED`, `NODE REMOVED`, `NODE REMOVED AFTER CRASH`, `MC NODE REMOVED`, `MIGRATION`, `SCALE UP DOWN` | 2 |
| Запросы во время добавления узла | `NODE ADDED DURING INSERTS`, `NODE ADDED DURING DELETIONS`, `NODE ADDED DURING READS` | 2 |
| Равномерность распределения и число перемещённых записей | `DISTRIBUTION`, `DISTRIBUTION NODE ADDED`, `DISTRIBUTION NODE REMOVED` | 2 |
Ещё 1 балл можно получить на защите, если поиск владельца ключа занимает _O(log N)_ или меньше. Обоснуйте эту оценку в отчёте. Укажите также время обновления структуры маршрутизации и её расход памяти. Если используете виртуальные узлы, обоснуйте их число _V_ на физический узел и учтите _V_ в оценках сложности.
К решению приложите отчёт в файле `solution/readme.md` по [общим требованиям](../readme.md#отчёт). Без отчёта автоматические тесты запускаются, но защита не проводится и решение не засчитывается.