Реализовать sharded cache на 10+ нод
ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Реализовать sharded cache на 10+ нод
1. Цель задачи
Разработать распределённый кэш-уровень с шардированием данных на основе consistent hashing по user_id, обеспечивающий горизонтальное масштабирование до 1 ТБ кэшируемых данных. В результате участник получит работающий прототип кэш-кластера минимум из 10 логических нод, способный корректно перераспределять данные при добавлении/удалении узлов с минимальным перемещением ключей (не более K/N ключей, где K – общее количество ключей, N – количество нод).
Ключевой результат Рабочий sharded cache на consistent hashing с виртуальными узлами (virtual nodes), клиентский API (get, set, delete), тесты масштабирования до 1 ТБ.
2. Исходные данные
| Что нужно | Откуда взять |
|---|---|
| Описание consistent hashing (алгоритм, виртуальные узлы) | Статья «Consistent Hashing» (Tom White), реализация в libketama, или собственная реализация |
| Библиотека для хэширования (MD5, SHA-256) | Стандартная hashlib в Python |
| Средство для создания множества процессов/контейнеров | multiprocessing, asyncio, или Docker Compose |
| Нагрузочный генератор | locust, k6 или скрипт на Python с замерами tps |
| Эталонная реализация для сравнения | Redis Cluster (опционально) |
Если нет реального кластера — симулируем:
- Развернуть 10+ нод как отдельные процессы на localhost (разные порты) с помощью multiprocessing.Process.
- Каждый процесс хранит данные в
dict(in-memory). - Взаимодействие через TCP (сокеты или aiohttp).
- Для теста масштабирования до 1 ТБ использовать синтетические данные: JSON-объекты среднего размера (1 КБ каждый) и симулировать 1 млрд записей (не храня все в памяти сразу, а проверять только распределение ключей и пропускную способность).
3. Технологический стек
| Компонент | Инструменты | Назначение |
|---|---|---|
| Язык | Python 3.11+ | Основная реализация |
| Хэширование | hashlib.md5 | Генерация хэшей ключей (user_id) |
| Виртуальные узлы | Собственная реализация (default VN=160 на ноду) | Равномерное распределение нагрузки |
| Сетевое взаимодействие | asyncio + aiohttp | Обмен данными между нодами и клиентом |
| Мониторинг нагрузки | psutil, timeit | Замеры latency и throughput |
| Тестирование | pytest, locust (опционально) | Юнит-тесты и нагрузочное тестирование |
| Хранение данных | In-memory dict (для прототипа) | Эмуляция кэша |
| Логирование | logging | Отладка и трассировка |
4. Этапы выполнения
Этап 1: Проектирование архитектуры (30 минут)
Действия
- Спроектировать consistent hashing ring:
- Определить диапазон хэшей:
[0, 2^160 - 1](SHA-1 или MD5 128 бит, но можно взять 64-битный хэш для простоты) - Для каждой ноды создать
VNвиртуальных узлов (рекомендуется VN=160 для равномерности). - Каждый виртуальный узел добавляется в кольцо как точка с координатой hash(node_id + ":" + vnode_index).
- Хранить отсортированный список всех точек.
- Определить диапазон хэшей:
- Определить протокол взаимодействия между клиентом и нодами:
- Определить механизм обработки добавления/удаления нод:
- При добавлении: новая нода со своими VN встаёт в кольцо; часть ключей, хэши которых попадают в интервал между её VN и предыдущими, переходят на неё.
- При удалении: ключи, принадлежавшие удалённой ноде, переходят к следующей ноде.
- Набросать схему классов:
ConsistentHashRing,CacheNode,ShardedCacheClient.
Ожидаемый результат этапа Документ с описанием архитектуры (можно в виде диаграммы Mermaid) и псевдокодом.
Этап 2: Реализация consistent hashing ring (2 часа)
Действия
- Реализовать класс
ConsistentHashRing:- Метод
__init__(self, vnodes_per_node=160) - Метод
add_node(node_id: str)– генерирует VN и вставляет их в кольцо (sorted list). - Метод
remove_node(node_id: str)– удаляет все VN этой ноды. - Метод get_node(key: str) -> str – вычисляет хэш ключа, находит в кольце ближайшую точку (бинарный поиск), возвращает
node_id.
- Метод
- Хэш-функция: hash = int(hashlib.md5(key.encode()).hexdigest(), 16) >> 2 (28-битный хэш для ускорения, или полный 128-бит – по желанию).
- Сохранять кольцо как отсортированный список кортежей
(hash_value, node_id). - Написать юнит-тесты:
- Проверить, что для одного ключа всегда возвращается одна и та же нода.
- При добавлении новой ноды проверяется, что доля перемещённых ключей ≤ (1/N + 5%).
- При удалении – аналогично.
# Пример реализации части кольца (неполный код)
class ConsistentHashRing:
def __init__(self, vnodes=160):
self.vnodes = vnodes
self.ring = [] # list of (hash, node_id)
self.node_map = {} # node_id -> list of hashes
def _hash(self, data: str) -> int:
return int(hashlib.md5(data.encode()).hexdigest(), 16)
def add_node(self, node_id: str):
for i in range(self.vnodes):
vnode_key = f"{node_id}:{i}"
h = self._hash(vnode_key)
self.ring.append((h, node_id))
self.ring.sort(key=lambda x: x[0])
self.node_map[node_id] = [self._hash(f"{node_id}:{i}") for i in range(self.vnodes)]
def get_node(self, key: str) -> str:
if not self.ring:
return None
h = self._hash(key)
# binary search (bisect)
import bisect
idx = bisect.bisect_left(self.ring, (h, None)) % len(self.ring)
return self.ring[idx][1]
Ожидаемый результат этапа Рабочий класс ConsistentHashRing, проходящий тесты (доля перемещённых ключей < 1/N + допуск).
Этап 3: Реализация кэш-ноды и клиента (3 часа)
Действия
- Реализовать
CacheNodeкак asyncio-сервер (aiohttp) на отдельном порту: - Реализовать
ShardedCacheClient: - Написать скрипт для запуска кластера:
# Пример запуска ноды (asyncio)
async def run_node(node_id, port):
app = web.Application()
cache = {}
async def get_handler(request):
key = request.match_info['key']
val = cache.get(key)
if val is None:
return web.Response(status=404)
return web.Response(text=val)
async def set_handler(request):
key = request.match_info['key']
val = await request.text()
cache[key] = val
return web.Response(status=200)
app.router.add_get('/cache/{key}', get_handler)
app.router.add_post('/cache/{key}', set_handler)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, 'localhost', port)
await site.start()
print(f"Node {node_id} started on port {port}")
# keep running
await asyncio.Event().wait()
Ожидаемый результат этапа Запущенный кластер из 10 нод, клиент может выполнять get/set/delete, данные хранятся только на правильной ноде (проверить прямым запросом к другой ноде – должен быть 404).
Этап 4: Тестирование масштабирования и отказоустойчивости (2 часа)
Действия
- Тест равномерности распределения:
- Сгенерировать 1 млн случайных user_id, записать их в кэш, затем подсчитать количество ключей на каждой ноде.
- Коэффициент дисбаланса = max/avg. Должен быть < 1.5.
- Тест добавления ноды:
- Запустить 11-ю ноду.
- Добавить её в
ConsistentHashRingи в клиент. - Проверить, что только ~1/11 часть ключей (допустимый разброс ±5%) оказалась на новой ноде, а остальные остались на старых.
- Проверить, что все ключи доступны через клиент после добавления.
- Тест удаления ноды:
- Удалить одну ноду из кольца.
- Проверить, что ключи, принадлежавшие удалённой ноде, теперь обслуживаются другой (следующей) нодой и доступны.
- Замерить downtime (время перестройки кольца).
- Нагрузочный тест до 1 ТБ симуляции:
- Поскольку 1 ТБ в памяти не поместится, оценить throughput и latency при большом количестве ключей.
- Создать 100 млн ключей (1 КБ значения) – всего ~100 ГБ (можно на нескольких машинах или с дисковым кэшем).
- Измерить
setиgetlatency (p50, p99). Ожидаемая p50 < 5ms при условии локальной сети. - Оценить, хватит ли 10 нод по 100 ГБ памяти каждая для хранения 1 ТБ (да, 10x100 = 1 ТБ).
- Проверить, что при равномерном распределении каждая нода получает ~10% ключей.
Ожидаемый результат этапа Отчёт с метриками: дисбаланс, процент перемещённых ключей при решардинге, latencies, подтверждение масштабирования до 1 ТБ.
Этап 5: Оптимизация и документация (1.5 часа)
Действия
- Оптимизировать бинарный поиск в кольце (можно использовать
bisect). - Реализовать поддержку виртуальных узлов с возможностью менять количество.
- Написать
README.mdс инструкцией по запуску, описанием API и примером использования. - Добавить тесты на отказоустойчивость (проверка, что при временном недоступе одной ноды клиент не падает, а возвращает ошибку или редиректит).
- (Опционально) Реализовать механизм репликации – primary/secondary для надёжности.
Ожидаемый результат этапа Полный код с модульной структурой, документация, тесты, отчёт.
5. Критерии приемки (Definition of Done)
- Consistent hashing ring реализован с виртуальными узлами, количество VN настраивается.
- При добавлении одной ноды на кластер из 10 нод доля перемещённых ключей не превышает 12% (1/10 + 2%).
- При удалении ноды все её ключи становятся доступными на соседней ноде (время перестройки < 1 сек).
- Распределение ключей по 10 нодам близко к равномерному (коэффициент дисбаланса < 1.5).
- Клиентское API поддерживает
get,set,deleteс корректной маршрутизацией поuser_id. - Нагрузочный тест показывает среднюю задержку
get< 5 мс (для операции в локальной сети) при 1 000 rps. - В документации описана процедура запуска кластера и клиента, а также результаты тестов.
- Код покрыт юнит-тестами на consistent hashing и интеграционными тестами на решардинг.
6. Ожидаемый результат
Основной результат
- Репозиторий с кодом, содержащий модули:
consistent_hash.py,cache_node.py,cache_client.py,cluster_manager.py. - Файл
demo.py, который запускает кластер из 10+ нод, выполняет серию операций и выводит статистику распределения. - Отчёт
test_report.mdс результатами тестов (равномерность, решардинг, latency).
Опционально (для углубления):
- Поддержка репликации (каждому ключу соответствует primary и 1-2 реплики).
- Экспорт метрик в Prometheus.
- Реализация graceful shutdown с перераспределением данных.
7. Возможные сложности и их решение
| Сложность | Решение |
|---|---|
| Неравномерное распределение при малом количестве виртуальных узлов | Увеличить VN (160–200). Использовать хэш-функцию с равномерным выходом (MD5). |
| При удалении ноды часть ключей временно недоступна | Реализовать протокол перераспределения (асинхронная миграция) или репликацию. |
| Высокая задержка из-за бинарного поиска в огромном списке | Использовать bisect (O(log N)). Для ускорения – хранить кольцо как массив и кэшировать последние запросы. |
| Тест на 1 ТБ не помещается в память одной машины | Использовать 10 отдельных процессов, каждый со своим лимитом. Симулировать 1 ТБ через создание большого числа ключей без полного хранения (проверять только хэши). |
| Сложность отладки сетевого взаимодействия | Логировать все операции. Использовать async-timeout для таймаутов. |
8. Бюджет времени (оценка)
| Этап | Время |
|---|---|
| Этап 1: Проектирование архитектуры | 30 мин |
| Этап 2: Реализация consistent hashing ring | 2 ч |
| Этап 3: Реализация кэш-ноды и клиента | 3 ч |
| Этап 4: Тестирование масштабирования | 2 ч |
| Этап 5: Оптимизация и документация | 1.5 ч |
| Итого | 9 ч |
Примечание: Для первого выполнения рекомендуется заложить 10–12 часов с учётом отладки.
9. Связанные вопросы из базы знаний
| Вопрос | Тема |
|---|---|
| 12 | Основы распределённых систем |
| 45 | Consistent hashing: алгоритм и виртуальные узлы |
| 78 | Хэш-функции для равномерного распределения |
| 112 | Метрики шардирования (skew, load factor) |
| 189 | Отказоустойчивость: механизмы replica и quorum |
| 201 | Rendezvous hashing vs consistent hashing |
| 304 | Протоколы решардинга (время миграции) |
| 415 | Latency в распределённых кэшах (p50, p99) |
| 567 | In-memory vs on-disk кэш |
| 678 | Тестирование распределённых систем (Chaos Engineering) |
10. Чек-лист самопроверки
- Я реализовал consistent hashing с виртуальными узлами и убедился, что при добавлении/удалении ноды переезжает не более 1/N + 10% ключей.
- Я написал тесты, доказывающие равномерное распределение (коэффициент дисбаланса < 1.5).
- Мой клиент корректно маршрутизирует запросы по
user_idи возвращает данные только с нужной ноды. - Я провёл нагрузочный тест и получил latency p50 < 5 мс для
getна кластере из 10 нод. - Я задокументировал процедуру запуска, описал API и приложил результаты тестирования.