|
1 | 1 | # Pixel-Battle backend
|
2 | 2 | [](https://github.com/emptybutton/Pixel-battle-backend/actions?query=workflow%3ACI)
|
3 | 3 | [](https://github.com/emptybutton/Pixel-battle-backend/actions/workflows/cd.yaml)
|
| 4 | +[](https://github.com/emptybutton/Pixel-battle-backend/releases) |
| 5 | +[](https://github.com/search?q=repo%3Aemptybutton%2FPixel-battle-backend+language%3APython+&type=code) |
4 | 6 | [](https://codecov.io/gh/emptybutton/Pixel-battle-backend)
|
5 | 7 |
|
6 |
| -Simple backend for pixel battle. |
| 8 | +Бэкенд-приложение, разрабатываемое с расчётом на нагрузки выше, чем у VK Pixel-Battle и Reddit r/place. |
7 | 9 |
|
| 10 | +## Предметная область |
| 11 | +- В игре есть холст размером 1000х1000 пикселей |
| 12 | +- Пользователи могут перекрашивать любой пиксель на холсте раз в минуту |
| 13 | +- Новые пользователи могут начать редактировать холст только через минуту после присоединения к игре (это сделано для предотвращения обхода ограничения на редактирование раз в минуту) |
| 14 | +- Пользователи не могут редактировать холст, когда пиксель-батл не активен |
| 15 | +- Конфигурированием времени проведения пиксель-батла занимаются админы |
| 16 | +- Админ может запланировать или изменить время проведения пиксель-батла, если у него есть админский ключ, соответствующий админскому ключу самого пиксель-батла |
| 17 | +- Холст разбит на 100 чанков — областей размером 100х100 пикселей |
| 18 | +- Каждый чанк характеризуется своим номером — ужатой минимальной позиции в своей области. Как пример, минимальная позиция чанка `1, 0` — это `100, 0`, чанка `5, 6` — это `500, 600` |
| 19 | + |
| 20 | +## Сценарии |
| 21 | +**Редактирования холста**: |
| 22 | +1. пользователь регестрируется в системе |
| 23 | +2. ожидает одну минуту |
| 24 | +3. перекрашивает пиксель |
| 25 | +4. `повторяет шаги 2 и 3 до окончания пребывания в игре` |
| 26 | + |
| 27 | +**Просмотр холста**: |
| 28 | +1. пользователь собирается просматривать области холста, расположенные в рамках определённых чанков |
| 29 | +2. клиент пользователя начинает отслеживать изменения этих чанков |
| 30 | +3. через некоторое время просматривает их устаревшие представления вместе с актуализирующими изменениями |
| 31 | +4. применяет актуализирущие изменения |
| 32 | +5. применяет накопленные отслеженные изменения |
| 33 | +6. отображает представления |
| 34 | +7. по мере поступления новых изменений применяет их |
| 35 | +8. `повторяется шаг 7` |
| 36 | +9. пользователь прекращает просмотр областей холста, расположенных в рамках определённых чанков |
| 37 | +10. чанки больше не отображаются и не отслеживаются |
| 38 | + |
| 39 | +**Планирование пиксель батла**: |
| 40 | +1. админ получает админский ключ вне системы. |
| 41 | +2. планирует проведение пиксель-батла в рамках определённого временного промежутка |
| 42 | + |
| 43 | +## Реализация |
| 44 | + |
| 45 | + |
| 46 | +> [!IMPORTANT] |
| 47 | +> Все сервисы — это один сервис, разворачиваемый как несколько сервисов для точечного масштабирования. |
| 48 | +
|
| 49 | +В системе два Redis-кластера: |
| 50 | +1. Кластер холста |
| 51 | +2. Кластер метаданных холста |
| 52 | + |
| 53 | +### Кластер холста |
| 54 | +Хранит данные состояния чанков. |
| 55 | + |
| 56 | +Каждый шард хранит данные только одного чанка (максимум 100 шардов). Если бы шард хранил данные разных чанков, сбой мог бы привести к неконсистентному состоянию чанков, данные которого хранит шард. |
| 57 | + |
| 58 | +Данные шарда: |
| 59 | +- два варианта изображения чанка |
| 60 | +- поток изменений чанка. Каждое событие — это данные отдельного пикселя, закодированные в 5 байт, где первые два — позиция, остальные три — RGB цвет (позиция хранится относительно минимальной позиции чанка, поэтому максимальное значения позиции это не `999, 999`, а `99, 99`) |
| 61 | +- смещение потока изменений чанка, определяющее какие события были применены к хранимому изображению, а какие нет. Используются именно ручное хранение смещений, вместо consumer groups, из-за того, что в системе необходимо читать события, которые не нужно после этого комитить. При этом чтение может быть конкуретным |
| 62 | +- разные распределённые локи, в рамках которых изменяются вышеперечисленные данные |
| 63 | + |
| 64 | +Изображение чанка представляется в таких вариантах: |
| 65 | +1. `png` картинка, не требующая каких-либо дополнительных преобразований для операций чтения |
| 66 | +2. сырые пиксельные данные, не закодированные в какой-либо формат, использующиеся библеотекой `Pillow` в качестве данных при редактировании изображений |
| 67 | + |
| 68 | +Вобщем, если не брать в расчёт время ввода-вывода, то с этим разделением операции чтения выполняются в \~1000 раз быстрее, а операции рефреша на 10%\~30% быстрее. |
| 69 | + |
| 70 | + |
| 71 | +### Кластер метаданных холста |
| 72 | +Хранит данные, не относящиеся к конкретным чанкам: |
| 73 | +- Состояние пиксель-батла (временной интервал) |
| 74 | +-Оркестрирующая очередь задач (номера чанков для рефреша) |
| 75 | +- Распределённые локи оркестратора рефреша |
| 76 | + |
| 77 | +Этот кластер имеет очень маленький обьем данных и низкую постоянную нагрузку, поэтому он не шардирован, но реплицирован, но не столько, что бы не потерять данные, столько что бы имелись замены в случае падения мастера. |
| 78 | + |
| 79 | +### Поток данных при изменении пикселя |
| 80 | +1. запись нового состояния пикселя происходит в `chunk_writing_service`, где он добавляется в очередь изменений чанка, к которому относится |
| 81 | +2. посредством вебсокетов, `chunk_streaming_service` посылает новое состояние пикселя всем слушающим клиетам того чанка, к которому относится пиксель |
| 82 | +3. копиться микробатч в очереди, перед его записью в представления |
| 83 | +4. до тех пор, пока пиксель в микробатче, операции `chunk_reading_service`-а читают его (и все остальные незафиксированные состояния пикселей) из очереди как актуализирующую дельту основного изображения |
| 84 | +5. `chunk_refresh_worker` приступает к рефрешу и применяет микробатч к изображениям и фиксируют его смещением |
| 85 | + |
| 86 | +### Оркестрация рефреша чанков |
| 87 | +- `chunk_refresh_worker` пулит очередь задач из кластера метаданных холста и рефрешит тот чанк, команду которого он вытащил. |
| 88 | +- `chunk_refresh_orchestrator` пушит очередь задач, таким образом, что команды хранятся зациклированно. |
| 89 | + |
| 90 | +Зациклированно значит, что если существуют комманды `А`, `Б`, `С`, то после пулинга `A`, будет спулет `Б`, потом спулет `C`, а после него опять спулет `A` и по кругу. |
| 91 | + |
| 92 | +> [!CAUTION] |
| 93 | +> В случае одновременной работы нескольких оркестраторов присутвует риск того, что команд в очереди будет больше 100 и что рефреш будет происходить немного чаще, тем самым уменьшая микробатчи некоторых чанков и увеличивая избыточное потребление ресурсов. |
| 94 | +> |
| 95 | +> Несмотря на это, можно убрать риск уменьшения времени хранения микробатчей, храня расписания оркестрации (пуша), но пока это не реализовано. |
| 96 | +
|
| 97 | +При сбое всех воркеров рефреш можно запустить вручную через `admin_cli`. |
| 98 | + |
| 99 | +### Данные пользователей |
| 100 | +Всего на одного пользователя необходимо сохранять только время, когда он обретёт право на перекрашивание пикселей, поэтому эти данные не хранятся на сервере, а их хранит сам клиент пользователя в качестве JWT, через http-only куку. |
| 101 | + |
| 102 | +### В действительности |
| 103 | +Это приложение уже развёрнуто, но из-за того, что оно не испытывает нагрузку, под которую проектировалось, оно работает не в виде 8+ сервисов и 2 кластеров на множество нод, а в виде одной ноды с 1 ядром и 1 GB RAM. Оно развёрнуто как единый сервис в рамках одного процесса с тремя процессами Redis-сервера, образующими один кластер (минимально необходимое количество для создания кластера), который заменяет оба запланированных кластера. |
0 commit comments