Files
fxalloc/DAIRY.md
T

596 lines
64 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DAIRY.md
**Дисклеймер:**
Дневник не является технической документацией. Его основная цель показать как работает больная фантазия автора проекта. Вся документация по проекту описана в файлах README.md соответствующих модуей и самого проекта.
# 23.04.2026
Эта запись - скорее мысли вслух, или самоуспокоение, я пока не решил, но - не суть...
Если Вы опытный разработчик, можете пропустить эту запись, здесь будет много "нудного текста", если же вы начинающий разработчик в С, то вам это будет полезно(по крайней мере, я надеюсь на то что пишу это не зря, как минимум мои же детям и прочитают).
Рано или поздно каждый разработчик сталкивается с решением нетривиальной задачи по реализации собственного аллокатора, почему:
* **наименьшее из зол** - время выделения памяти стандартными методами **malloc**, **calloc** и **new**.
* **наибольшее из зол** - фрагментация этой самой памяти, особенно когда дело касается высокой вариативности.
Встаёт вопрос как решить эту проблему и вот конкретно в этом месте начинается магия. Почему магия, потому что при разработке аллокатора можно напороться на очень много рифов. Итак, присаживайтесь по-удобнее, запасайтесь печеньками, а я - продолжу. ;)
На этапе продумывания архитектуры данного проекта я понимал каким будет аллокатор и что мне от него нужно, да помимо всего прочего - есть парочка готовых проверенных решений, как обычных, так и многопоточных(jemalloc или tcmalloc), но, это не тот случай, мы же тут навыки восстанавливаем, поэтому вот Вам полёт больной фантазии в парадигме MVP+KISS+YAGNI(быстро, просто, без излишеств), пример буду приводить на прокси-сервере Lineage2(для наглядности).
### Включаем режим "Архитектор lvl-80" MVP+KISS
#### Делаем себе своеобразное ТЗ:
1. Разнородные блоки(проектируем сервер)
2. Многопоточность(всё ещё проектируем сервер)
3. Один из важнейших аспектов - делегирование памяти между потоками(оптимизация: уменьшение дополнительных выделений и копирования).
#### Препрофилирование: расчёт предполагаемой нагрузки на стадии проектирования архитектуры
Для определения градаций блоков памяти серьёзные дяди-тёти берут статистику типовых нагрузок в схожих условиях, чтож последуем их примеру, открываем "Я.браузер" → "АлисаAI" → "Новый чат" и просим Алису расчитать максимальную нагрузку на аллокатор для прокси-сервера Lineage2 при 200 линий(400 подключений), получаем от неё примерную нагрузку:
#### Ключевые метрики нагрузки
|**Параметр**|**Средняя нагрузка**|**Пиковая нагрузка**|
|:-:|:-:|:-:|
|**Пакеты/сек**|4 000|30 000|
|**Трафик**|4 Мбит/сек|60 Мбит/сек|
|**Аллокации/сек**|4 000|30 000|
|**Потребление памяти**|3,2 МБ|10 МБ|
Далее просим её вывести подробные градации для этой нагрузки , получаем следующие варианты:
#### Сводная таблица по всем группам
|Группа|Диапазон размеров (байт)|Доля трафика (%)|Общая частота (пакетов/сек)|Фрагментация (%)|Оптимизация|
|:-:|:-:|:-:|:-:|:-:|:-|
|1.|Сверхмалые 3264|510|4200|2025|Slab‑аллокатор, пулы фиксированного размера|
|2.|Малые 65128|2025|1 2008 000|1520|Кэширование буферов, пакетная обработка|
|3.|Средние 129256|2530|8004 000|1015|Пулы памяти, переиспользование буферов|
|4.|Крупные 257512|1520|2002 000|510|Предварительное выделение буферов|
|5.|Очень крупные 5131 024|1015|801 200|3–8|Пакетная обработка, буферизация|
|6.|Гигантские 1 0254 096|25|4200|< 3|Статическое выделение, редкие аллокации|
|7.|Экстра‑крупные > 4 096|< 1|< 4|Отсутствует|Загрузка по частям, стриминг|
Таким образом видим что в секунду примерно 60 МБит, и имеем приблизительное представление о градациях. Но, это ещё не всё, вспоминаем на какой системе работает сервер, какой принцип обработки соединений используется (WSAPoll | epoll), как правило - это асинхронный ввод-вывод, что нам даёт это знание - узкое место любого сервера, это основной фактор скорости, сервер не может работать быстрее сети, однако, подходы к работе у **IOCompletionPort(Windows)** и **epoll(Linux)** кардинально разные, тонкости их влияния на работу аллокатора раскроются немного позже, а пока нам нужна именно сетевая нагрузка. Снова вооружаемся АлисойAI(YandexGPT 5.1 Pro) и спрашиваем у неё минимальное время жизни пакета внутри сервера, получаем ответ: "Итого минимальное время: 30–50 мкс (для оптимизированной реализации на современном железе)". Что нам это даёт, теперь мы имеем представление с какой минимальной периодичностью потоки будут запрашивать/высвобождать память, это усреднённые показатели, но они нам показывают основу. Итогом данного этапа можно сделать вывод что примерным минимальным временем между **fxalloc()** и **fxfree()** будет не более 30мкс при такой нагрузке, "заблаговременно" делим это время на 2 и получаем 15мкс.
Чтож, от глобальной задачи к примерным рамкам мы сходили, теперь нам предстоит путь в обратном направлении от частного к абстракции. Открываем IDE, запасаемся кофе и приступаем.
Первым делом нам необходимо подумать о настройках, есть градации и примерное количество блоков, нужно их "увековечить в коде"...
Глотнув кофе и просмаковав его приятный аромат понимаем что нам нужна структура которая опишет каждый блок, отлично, пишем:
```C
/**
* @brief Структура преднастройки аллокатора задающая градации и количество блоков памяти
*
* @property +est_size: size_t - Предполагаемый размер блока
* @property +est_count: size_t - Предполагаемое количество блоков
*/
typedef struct FXGrade {
/// Предполагаемый размер блока
const size_t est_size;
/// Предполагаемое количество блоков
const size_t est_count;
} FXGrade;
```
Отлично, объединим их в целое, выделим переменную - массив градаций, и статически её проинициализируем:
```C
// neurox/fxalloc/includes/FXAlloc.h
extern const FXGrade* grades;
// neurox/fxalloc/src/FXAlloc.c
// В этой переменной настраиваем градации и предположительное количество блоков
static const FXGrade grades[] = {
{ 32, 200 }, { 64, 200 }, { 128, 8000 },
{ 256, 4000 }, { 512, 2000 }, { 1024, 1200 },
{ 4096, 200 }, { 0x10000, 4 }
};
```
Что нам даёт такая переменная: мы можем спокойно проинициализировать глобальный пул памяти, но в таком исполнении нам придётся высчитывать количесво элементов в переменой через **sizeof()**, можно ли обойтись без этого, можно - есть прекрасная вещь ноль-терминант, которая не то что позволяет обойтись без **sizeof()**, но и настраивать непосредственно перед инициализацией, дополняем переменную:
```C
// neurox/fxalloc/src/FXAlloc.c
// В этой переменной настраиваем градации и предположительное количество блоков
// Элемент: { размер, количество };
static const FXGrade grades[] = {
{ 32, 200 }, { 64, 200 }, { 128, 8000 },
{ 256, 4000 }, { 512, 2000 }, { 1024, 1200 },
{ 4096, 200 }, { 0x10000, 4 },
{ 0 } // Ноль-терминант
};
int main(int argc, char* argv[]) {
fxalloc_init(grades);
// что-нибудь шкодим
return 0;
}
```
Теперь при обходе переменной grades в цикле мы можем быть уверены что дальше чем нужно - не уйдём и вполне себе спокойно можем использовать цикл **for**:
```C
for (size_t i = 0; grades[i].est_size; i++) {
// Инициализируем отдельный пул по грейду
}
```
Что-то у нас уже есть, теперь стоит продумать сам пул, нам потребуются метаданные(маркеры), каждому блоку как минимум необходимо помнить свой размер, пишем:
```C
typedef struct FXMemoryBlock {
/// Размер блока
size_t size;
} FXMemoryBlock;
```
Однако, такой подход даёт сложность поиска "места проживания" которую можно выразить как $O(n)$, где $n$ - количество градаций блоков по размерам, таким образом вызов функций **fxalloc()** и **fxfree()** требует поиска "места обитания" блока, в математическом выражении это выглядит как $O(n)+O(n)≡O(n)$, можно ли оптимизировать - можно, заменяем **FXMemoryBlock::size** на **FXMemoryBlock::gid**(Идентификатор этого размера):
```C
typedef struct FXMemoryBlock {
/// ID размера блока
size_t gid;
} FXMemoryBlock;
```
Как это влияет на суммарную сложность:
1. **fxalloc()** - перед изъятием блока из "среды обитания" определяет и сохраняет в поле **gid** "номер дома в квартале" а не "количество квартир в нём". Алгоритмическая сложность $O(n)$.
2. **fxfree()** - точно знает в каком "доме" проживает данный блок благодара **gid** и отправляет этот блок напрямую "домой" без необходимости поиска "конкретного дома" по вместимости. Алгоритмическая сложность $O(1)$.
Это микрооптимизация, но, на уровне архитектуры это важный нюанс, принцип хеширования - основа оптимизации.
Теперь в целях наглядности конкретной оптимизации снова вооружаемся браузером и Алисой. Просим её расчитать примерное время для новой концепции и наших градаций, радуемся результату и смакуем(эта радость будет недолгой):
Сводная таблица: расчётное время выполнения fxalloc() + fxfree() для приведённых градаций
| Группа | Диапазон размеров (байт) | Кол‑во градаций (n) | Сложность fxalloc() | Сложность fxfree() | Суммарная сложность | Расчётное время (операции, худший случай) |
|:--|:----------|:-----:|:-:|:-:|:-:|:-:|
| 1. Сверхмалые | 3264 | 7 | O(n) | O(n) (без gid) / O(1) (с gid) | O(n) / O(n)* | 14 (7+7) / 8 (7+1) |
| 2. Малые | 65128 | 7 | O(n) | O(n) / O(1) | O(n) / O(n)* | 14 / 8 |
| 3. Средние | 129256 | 7 | O(n) | O(n) / O(1) | O(n) / O(n)* | 14 / 8 |
| 4. Крупные | 257512 | 7 | O(n) | O(n) / O(1) | O(n) / O(n)* | 14 / 8 |
| 5. Очень крупные | 5131 024 | 7 | O(n) | O(n) / O(1) | O(n) / O(n)* | 14 / 8 |
| 6. Гигантские | 1 0254 096 | 7 | O(n) | O(n) / O(1) | O(n) / O(n)* | 14 / 8 |
| 7. Экстра‑крупные | > 4 096 | 7 | O(n) | O(n) / O(1) | O(n) / O(n)* | 14 / 8 |
Пояснения к таблице
* **Количество градаций (n)**: во всех случаях n=7 (по числу групп из исходной таблицы). Это определяет сложность линейного поиска.
* **Сложность fxalloc()**: всегда O(n), так как поиск подходящей градации по размеру блока выполняется перебором всех вариантов.
* **Сложность fxfree()**:
* **без оптимизации (size)**: O(n). Требуется повторный поиск градации по сохранённому размеру блока.
* **с оптимизацией (gid)**: O(1). Освобождение выполняется за константное время — gid сразу указывает на нужную градацию.
* **Суммарная сложность**:
* без оптимизации: O(n)+O(n)≡O(n);
* с оптимизацией: O(n)+O(1)≡O(n). Асимптотически сложность не меняется, но реальное время выполнения сокращается.
* **Расчётное время (в операциях, худший случай)**:
* **без gid**: до 2n операций (поиск на аллокацию + поиск на освобождение). Для n=7: 7+7=14 сравнений.
* **с gid**: n+1 операций (поиск на аллокацию + прямой доступ на освобождение). Для n=7: 7+1=8 операций.
Порадовались, хорошо, выдохнули и почувствовали себя гигантами мысли, теперь у нас время на поиск в **fxfree()** имеет константную сложность $O(1)$, однако это только на поиск, вот мы и подошли к первому "рифу": **epoll** VS **IOCP**:
* **epoll** - мультиплексор позволяющий обрабатывать 1к+ соединений в одном потоке принципом уведомления потока только когда дескриптор готов к чтению/записи млм возникла ошибка оптимизированный на уровне ядра Linux.
* **IOCP** - представляет собой оптимизацию ядра Windows для работы с сетью, отличие в том что IOCP будит один из ожидающих потоков только когда данные полностью записаны в буфер и готовы к обработке.
Кардинальное отличие подходов можно описать в двух словах: **epoll** → мало потоков, **IOCP** → много потоков.
#### Влияние парадигм работы с **epoll** и **IOCompletionPort** на аллокатор
С моей точки зрения как Linux-кодера и борца за эффективность IOCP имеет **жирнючий** минус - как правило это внушительный пул потоков ибо при их нехватке эффективность будет падать. Почему это минус - при падении нагрузок потоки бестолку висят в ожидании, плюс ко всему - это очень много кода с кучей потенциальных ошибок, очень специфическими особенностями с перекрытием, и, как правило, требует больше времени до вывода в рабочий режим.
Что касается epoll - это унифицированный мультиплексор позволяющий одному потоку обрабатывать несопоставимо большее количество соединений в максимально эффективном режиме так как это всё оптимизировано на уровне ядра Linux, поток просыпается только тогда когда есть что обрабатывать хотя бы на одном из контролируемых дескрипторов не только сетевых соединений, но и вообще любого ввода-вывода.
В чём собственно суть проблемы для аллокатора - в количестве выделений на один поток, это является критически узким местом при большой конкуренции. Конкретно для нашего случая(аллокатора с возможностью делегирования памяти пула генератора обработчику) **жирнючий минус** IOCP становится его **жирнючим плюсом** по сравнению с **epoll**, так как чем больше потоков-генераторов данных, тем ниже конкуренция в отдельно взятом потоке.
Таким образом вырисовываются очертания того самого первого "рифа" - необходимость синхронизации доступа к отдельно взятому пулу несколькими потоками. Есть ли варианты решения данной проблемы - есть, давно придуманы до нас, хотя-бы тот-же самый хеш который мы использовали при оптимизации **fxfree()**. Открываем любимую IDE и накидаем немного полей в структуру пула памяти :
```C
/// @brief Группа блоков одной градации
typedef struct FXGradedMemoryPool {
/// @brief Указатель на последний свободный блок
FXMemoryBlock* free;
/// @brief Всего блоков в данной группе
umword_t total;
/// @brief Количество преаллоцированных блоков
umword_t count_pre;
/// @brief Количество используемых блоков
mword_t used;
/// @brief Количество свободных блоков
mword_t free;
} FXMemoryPoolGrade;
```
**Таракан отвечавший за раздел свалил в неизвестном направлении...**
# 26.04.2026
Что ж, примерное представление о работе сети и нагрузках на асинхронный ввод-вывод мы получили, но, мы брали в расчёт Lineage2 где достаточно высокая вариативность пакетов, теперь вернёмся к нашему проекту прикинем примерную вариативность пакетов, учтём железо на котором будет работать сервер и посмотрим сколько он сможет выдержать клиентов в теории. На что в первую очередь стоит обратить внимание:
* Железо
* Ресурсы потребляемые ОС
* Ресурсы потребляемые сторонними сервисами при их наличии(Web-сервер, почтовый сервер и т.д.)
* Минимальный необходимый запас прочности(как правило вкладывается 20-30% из-за деградации и вариативности нагрузок как отдельно взятого элемента, так и системы в целом)
## Железо:
Отсчётная точка любого инфраструктурного проекта
|Параметр|Значение|
|-------:|:-------|
|**OS:**|Ubuntu Server 24.04|
|**CPU:**|Intel Core i53470, 4 @ 3.2 GHz|
|**RAM:**|8 GB|
|**ROM:**|noname 256 GB SSD|
|**WiFi:**|2.4 GHz, прямая видимость до 6 м (~32 Mbit/s)|
Все параметры кроме Wi-Fi устанавливаютя заводом изготовителем и имеют ±заявленные(скорее — минус, но на данном этапе считаем заявленный максимум) характеристики, производим замеры:
**Ключевые параметры оборудования(маршрутизатор) TP-Link Archer-A8(заявленые изготовителем):**
| Параметр | Значение |
|----------------------------:|:----------------------------------------------------------------|
| Тип оборудования | Маршрутизатор (роутер) / точка доступа |
| Стандарты | WiFi 802.11a/b/g/n (2.4 ГГц), 802.11ac (5 ГГц) |
| Частотные диапазоны | Dual‑Band: 2.4 ГГц и 5 ГГц |
| Скорость Wi‑Fi (макс.)| 2.4 ГГц: до 600 Мбит/с — 5 ГГц: до 1 300 Мбит/с Суммарно: AC1900|
| Порты | 1 × RJ‑45 WAN (1 Гбит/с) — 4 × RJ45 LAN (1 Гбит/с) |
| Технологии | MUMIMO (3×3), Beamforming, Smart Connect (автовыбор диапазона), Airtime Fairness|
| Поддержка VPN | PPPoE, PPTP, L2TP |
| QoS (приоритизация трафика) | Поддерживается (включая WMM) |
| Процессор | 1.2 ГГц (1 ядро) |
**Оборудование сервера:** TP-Link Wi-Fi свисток, всё что изветно - 2.4 GHz
Показатели **iperf3** в режиме **\*спокойной** сети(03:00, ночь):
**[ 5]local 192.168.0.104 port 34696 connected to 192.168.0.101 port 5201**
|[ ID]| Interval | Transfer | Bitrate |Retr| Cwnd |
|:---:|:---------------|:-----------:|:--------------:|:--:|:----------:|
|[ 5]| 0.00-1.00 sec | 9.25 MBytes | 77.5 Mbits/sec | 0 | 329 KBytes |
|[ 5]| 1.00-2.00 sec | 8.62 MBytes | 72.4 Mbits/sec | 0 | 421 KBytes |
|[ 5]| 2.00-3.00 sec | 8.88 MBytes | 74.4 Mbits/sec | 0 | 450 KBytes |
|[ 5]| 3.00-4.00 sec | 7.75 MBytes | 65.0 Mbits/sec | 0 | 539 KBytes |
|[ 5]| 4.00-5.00 sec | 8.25 MBytes | 69.2 Mbits/sec | 0 | 632 KBytes |
|[ 5]| 5.00-6.00 sec | 7.00 MBytes | 58.8 Mbits/sec | 0 | 735 KBytes |
|[ 5]| 6.00-7.00 sec | 8.38 MBytes | 70.2 Mbits/sec | 0 | 735 KBytes |
|[ 5]| 7.00-8.00 sec | 8.25 MBytes | 69.2 Mbits/sec | 0 | 735 KBytes |
|[ 5]| 8.00-9.00 sec | 7.00 MBytes | 58.7 Mbits/sec | 0 | 735 KBytes |
|[ 5]| 9.00-10.00 sec | 8.25 MBytes | 69.2 Mbits/sec | 0 | 735 KBytes |
|[ ID]| Interval | Transfer | Bitrate |Retr| Side |
|:---:|:----------------|:-----------:|:--------------:|:--:|:----------|
|[ 5]| 0.00-10.00 sec | 81.6 MBytes | 68.5 Mbits/sec | 0 | sender |
|[ 5]| 0.00-10.01 sec | 78.9 MBytes | 66.1 Mbits/sec | | receiver |
Весьма оптимистично, но, это спокойная сеть(ночью без помех), днём итоговая возможность сети будет существенно проседать, однако это — мелочь, что касается стабильности системы - мы должны исходить из максимальных возможностей сети и вкладывать в архитектуру на этом этапе именно это значение, берём максимальное из таблицы(77.5 МБит/с) и добавляем к нему о-о-очень оптимистичные 10%, итог(85.3 МБит/с), TP-Link свисток с 7-милетним стажем показывает топовые результаты не то чтобы для своего возраста, а вообще в принципе(порадовал старика, видимо он "поднатаскался" за 7 лет, как говорят - "Опыт не пропьёшь!"). В нашем случае сеть это узкое место(домашний Wi-Fi за средненьким роутером).
Для чего это: показывает предел сетевого стека, то есть пропускную возможность сети.
В чём особенность в проектировании алокатора(с моей колокольни), это самый основной элемент серверной части. С неправильной архитектурой памяти сервер — не сервер а "кукурузник", а нам нужна 3-я космическая скорость. По это му нам необходимо расчитать по максимуму частот и по минимуму памяти, такой подход считаю наиболее верным, конечно можно зацепиться за тактовую частоту этой самой памяти, но, на данном этапе — это лишнее.
Продолжим рассматривать наши железяки со всех сторон и как они определяют требования к аллокатору, начертаем небольшую таблицу опираясь на максимальные возможности сначала каждого компонента в отдельности, найдём слабое звено и посмотрим что на самом деле выйдет. Чего стоит ожидать от такого подхода — максимальные требования к алокатору по скорости, максимальные требования по локализации пулов, и самое ключевое — потолок для выделения блока в плане времени. С чего начнём расчёт Wi-Fi и RAM(ёмкость), почему именно так — способности процессора переварить нагрузку будет зависить от сложности самой функции выделения памяти, в идеале бы уложиться в один атомарный своп, но,— это не реально, скорость света недостижима, по крайней мере для меня — точно(разве что только для тараканов в моей голове это норма). Как мы будем считать: У нас уже есть заголовочные файлы модуля **XOGame** с вот такой вот архитектурой:
```C
/**
* @brief Клетка игры крестики-нолики
*/
typedef struct XOCell {
/// Координата X
int8_t x: 3;
/// Координата Y
int8_t y: 3;
/// Сторона
int8_t side: 2;
} XOCell;
/**
* @brief Игровое поле крестики-нолики
*/
struct XOGame {
/// Идентификатор игры
const size_t id;
/// Сделать ход.
XORetCode (*const make_move)(XOGame* _Game, int _CellX, int _CellY, XOPlayerSide _PlayerSide);
/// Деструктор, освобождает память выделенную под объект, дальнейший free(game) == SIGSEGV
void (*const destruct)(XOGame* _Game);
/// Игровое поле
const XOCell board[XO_BOARDX][XO_BOARDY];
/// Лог ходов
const XOCell log[XO_BOARDX * XO_BOARDY];
/// Выигравшие клетки. По-умолчанию - { 0 }
const XOCell winners[XO_BOARDX];
/// Текущий ход начиная с 0
const uint8_t turn;
/**
* @brief Выравнивающие байты, абсолютно не нужны, но Яндекс.АлисаAI настояла - { 0 }
* Если вы решите изменить тип полей в XOCell, то просто удалите это поле, модуль не сломается ;)
*/
uint8_t padding[2];
};
```
Что мы можем почерпнуть из такой архитектуры:
* ожидаемая вариативность размеров пакетов крайне низкая(вернее отличаться будут только ркопожатия).
* даже если в пакет напихивать все ±значимые для сети поля - board, log, winners и turn — это всего $9 + 9 + 3 + 1 = 22$ байта + метаданные(размер + идентификатор пакета) ещё $2 + 1$ байта, итого имеем всего 25 байт.
* большие пакеты будут связаны исключительно с инициализацией шифрования и аутентификацией.
Что мы можем ещё расчитать — скорость жмаканья юзерами-абьюзерами по полям, даже самый продвинутый guiuser врядли нажмакает чаще чем 0.5 секунды. Тут даже и вычислений не требуется чтобы понять что, оказывается, Wi-Fi тут далеко не самое узкое место, ну или вернее — сопоставимо узкое, к примеру с CPU или RAM. Берём кулюкулятор, пренебрегаем большими пакетами в виду их большой редкости, выбираем режим "Инженер"(мы же тут — архитектуру планируем) и начинаем считать:
| Параметр | Формула | Лимит guiuser-ов | Описание |
|:--------:|:------------------------:|:----------------:|:------------------------------------|
|**Wi-Fi:**|$⌈\frac{85,3×10^6}{25×8}⌉$| 426 500 | Не то что бы много, но дох-х-ходчиво|
Очешуеваем от того сколько абьюзеров одновременно никогда не увидит наш сервер и понимаем что старый свисток — далеко не самое слабое место нашего проекта, почему, потому что даже топовое железо не всегда тянет столько линий, не говоря уже о том что сама ось(Операционная система) не выделит нам столько подключений, хотя:
```console
guiuser@felexdev:~$ cat /proc/sys/fs/file-max
9223372036854775807 < Максимальное количество файловых дескрипторов всего
```
Подбираем челюсть, смотрим дальше:
```Console
guiuser@felexdev:~$ ulimit -n
open files (-n) 1024 < Максимальное количество файловых дескрипторов на процесс
```
Ничего, лично я,— "пингвин со стажем", можем поднять и до $200 000$ на процесс, однако на данном этапе нам нет такой необходимости.
# 27.04.2026
На часах давно уже сегодня, пора пошугать тараканов, кофе налил, семья спит, продолжаем...
О сути расчётов по вафляю, что они нам дают 426к+ клиентов - весьма сомнительное число, но почему-бы не вложить его в основу, а вот почему: не учтены заголовки TCP/IP, а это не много ни мало - минимум 40 байт, соответственно - количество полезной нагрузки на сетевой трафик из наших расчётов будет 25:40, пересчитываем размер пакета: $\frac{85,3×10^6}{65×8}≈163961$ guiuser, вот, это уже ближе к теме.
## RAM
Примерные цифры по сети мы получили, пора заняться RAM. Очередной раз обращаемся к нашему любимому критику-статисту — Алисе(она "очень могёт" в статистике), идём препрофилировать память, на глаз прикиываем примерное минимальное потребление памяти на одного абьюзера:
**Таблица потребления памяти с выравниванием до 8 байт**
| Компонент | Размер (байт) |Выравнивание|Итого с padding| Описание |
|:--------------------------|:-------------:|:----------:|:-------------:|:-----------------------------------------|
|**Сокетные структуры ядра**| | | | |
|epoll_event | 12 | 8 → 16 | 16 | Структура события epoll |
|file descriptor | 8 | 8 → 8 | 8 | Указатель на файловый дескриптор |
|socket struct | 200 | 8 → 200 | 200 | Базовая структура сокета |
|**Метаданные соединения** | | | | |
|Указатель на игру | 8 | 8 → 8 | 8 | Указатель на структуру игры |
|XOR-ключи | 16 | 8 → 16 | 16 | 2 ключа шифрования |
|Состояние игры | 64 | 8 → 64 | 64 | Данные состояния игры |
|Метаданные блока памяти | 16 | 8 → 16 | 16 | Дополнительные данные управления памятью |
|**Шифрование** | | | | |
|Указатель на шифровщик | 8 | 8 → 8 | 8 | Указатель на модуль шифрования |
|**Дополнительные указатели**| | | | |
|Указатель на клиента | 8 | 8 → 8 | 8 | Указатель на структуру клиента |
|Резерв | 8 | 8 → 8 | 8 | Дополнительный резерв |
|**Указатель на пул** | | | | |
|Указатель на клиента в пуле| 16 | 8 → 16 | 16 | Хранение клиента в пуле |
**Итого с выравниванием** | | | **360** | **Суммарное потребление** |
**Расчет общей производительности**
| Параметр | Значение | Описание
|:----------------------|:-------------:|:--------------------------------------|
| Доступная RAM 6.1 GB | | Свободная память для соединений |
| Память на соединение | 360 байт | Потребление на одно соединение |
| Теоретический максимум| ~16.9 млн | Максимальное количество соединений |
| Реальный максимум | ~12.7 млн | С учетом системных накладных расходов |
Реальный максимум ~12.7 млн гуюзеров, не дурно, собственно это и ожидалось. Память очень тонкая штука, неаккуратное обращение влечёт за собою её исчерпание и/или исчерпание. И утечки, и сегментация, могут обернуться большой бедой для прода =D
**После расчётов теоретической пропусскной способности** сети и памяти можно плавно переходить к следующему этапу.
## Расчётное время между аллокациями:
Перед проведением расчётов необходимо проанализировать предполагаемы механизм взаимодействия потоков при обработке пакета, на что обращаем внимание:
|Шаг| Назначение потока | Описание |Количество|
|:-:|:------------------------------|:----------------------------------------|:--------:|
| 1 | Приём пакетов/подключений | приём пакетов, инициализация подключений| 1+ |
| 2 | Проверка пакетов, данных | валидация, взаимодействое с БД | 2+ |
| 3 | Внутреигровая логика | обработка, генерация выходных данных | 2+ |
| 4 | Логирование/сохранение | актуализация данных БД, файлов | 2+ |
| 5 | Отправка пакетов | это не всегда нужный поток, но Алиса "настаивает"|1+|
Имея представление о процесе можем посчитать минимальное количество потоков в раслабленом режиме, и сугобо тех кто при нагрузке активно запрашивает память. Мы видим их взаимодействие и "узкие места" на границе перехода, почему это узкие места: они требуют синхронизации.
Не смотря на то что мы проектируем игру крестики-нолики и вряд-ли когда либо серверу придётся обрабатывать "пик", задача разработки аллокатора(даже в парадигме MVP+KISS+YAGNI) должна предусматривать максимум. Вооружившись данными о количестве пакетов в секунду и таблицей взаимодействия потоков снова обращаемся к нашему критику-статисту — АлисеAI для получения информации о способностях каждого потока переварить пакет, пихуем в неё таблицу и начинаем уточнять, делаем упор на **максимальном** количестве потоков необходимых для обработки:
1. Количество пакетов секунду.
2. Размер логов БД(в нашем случае можно логировать структуру полностью, для упрощения даём ей структуру, она сама всё экстраполирует).
3. Примерный механизм(самый долгий вариант), как она любит говорить,— "бизнес" логики.
4. Железяки на которых всё это будет работать кроме Wi-Fi.
# 28.04.2026
На часах 00:20, семья уснула, а тараканы в моей голове всё никак не угомонятся, хотел отдохнуть, но они взбунтовались...
Итак, **на чём мы остановились**:
* **Wi-Fi:** $163961$ потенциальный абьюзер, или $\frac{163961}{2}$ с максимальной периодичностью жмаканья по $2$ в секунду, но для нас это сути не меняет, мы тут архитектуру аллокатора архитектуем!
* **Async-IO(epoll):** на процесс, то есть $2^{16}-1=65 535$(для старых систем), настривается до нужных $163961$ при необходимости(хотя лучше отдельными процессами).
* **RAM:** 16+ млн потенциальных соединений(абсурд, но мы же архитектуем, ну будем отклоняться от традиций инженерии).
* **CPU:** `4 ядра @ 3.2 ГГц → 12.8 млрд инструкций/сек` не топ, но — терпимо.
Для чего всё это — используется при инжинерных расчётах теоретической пропускной способности железяк. Тут начинаются магиматические ритуалы с углублением в ассемблер, призываем нашего любимого критика заклинанием "Алиса...", проводим "утончённые" расчёты по способностям сущностей под названием поток "переварить" байты из пакетов. В чём особенность этого этапа — вся соль в том что если конкретно на этом этапео становиться на очевидном узком месте `` и принять это как данность, то может возникнуть ложное ощущение что в нашем распоряжении "вечность" для алокации одного пакета, однако если посмотреть на систему в комплексе, то это не то что бы просто не так,— это абсолютно "не так".
**Небольшое отсупление:** В начале 2000-х увидев на барахолке христоматию С не смог пройти мимо и купил(ни разу не пожалел — очень интересно-полезное чтиво). Ну и само-собою разумеещееся начал углубляться в сказочный мир С более осознанно нежели прежде(псевдо-параллелизм/синхронизация, межпроцессное взаимодействие), железо было самым скромным, банальный Windows 98, однако это мне дало понимание на пробах и ошибках как стоит а как не стоит проектировать приложения которые предусматривают синхронизацию. Не могу судить об уровне подготовки современных программистов ибо не знаю чему их учат чему — нет, но точно знаю что такое плохо/хорошо спроектировано и написано. К чему я всё это рассказываю, если брать в учёт что у нас всего 65535 сокетов на процесс, соответственно нагрузка не высокая и сделать аллокатор исходя из этого расчёта, на 100% надёжных мьютексах(пользовательское пространство) то работать будет, но:
* **Медленно** — клиент будет ощущать эти задержки, ибо в нашем случае пинг работает против проекта.
* **Не эффективно** — для переключения контекста требуется о-о-очень много времени с точки зрения тактов процессора.
## Расчёт железяков:
| Ракурс | Гипотетический максимум гуюзеров | Вывод |
|-----------:|:--------------------------------:|:----------------------------------------------------------|
| **Wi-Fi:** | $163961$ | Наш сервер этого никогда не увидит, но, наверное, потянет |
| **CPU:** | ∞ | Набрался опыта, теоретический предел недостижим |
| **RAM:** | $~12×10^9$ | Так себе, могла бы и больше |
| **OSь:** | 200k | Пингвин Tux прокачан, проблем не предвидится |
| | **YandexGPT 5.1 Pro(АлисаAI)©** | ← Строки ниже писала она, автор ни при чём =D |
| **ССД:** | ∞ TB | Забыл, где положил половину данных |
|**Блок питания:**| 800 W (но реально 400) | Экономит энергию, отключаясь в самый неподходящий момент |
|**Кулер:**| ∞ об/мин (теоретически) | Любит петь баллады на высоких тонах, особенно под нагрузкой|
|**Сетевой кабель:**| 1 Гбит/с | Завязывается в узлы, когда никто не смотрит |
|**Материнская плата:**|100 лет службы | Помнит DOS, но делает вид, что забыла |
|**USB‑порт:**| 3.0 (но работает как 1.1) | Притворяется, что не видит флешку, пока не постучишь |
|**Монитор:**| 4K @144 Гц | Показывает синий экран именно тогда, когда нужно впечатлить клиента|
|**Клавиатура:**| 10 млн нажатий | Пропускает каждое пятое нажатие «для профилактики износа» |
|**Мышь:** | 12 000 DPI | Иногда решает, что курсор должен жить своей жизнью |
|**Общий вывод:**| — | Система стабильна, если не трогать, не дёргать и не пытаться впечатлить клиента|
|**Рекомендация:**| — | Перед важными презентациями — покормить блок питания, погладить SSD и шепнуть кулеру: «Ты лучший»|
|**Прогноз:**| ∞ лет | Железо будет жить вечно… или пока не отключится блок питания|
|**Автор:**| ∞ терпения | Верит, что однажды всё заработает без танцев с бубном |
## Расчёт потоков
**Промпт("Ломаем мозг" Алисе):**
> Алиса, оцени пожалуйста юмор и проведи расчёты: Кастуем заклинание "Алиса", скармливаем ей payload пакета в 32 гномо-домика(uint8_t gnomes_home;), название железяки Intel Core i5-3470 и недожелезяки 8 GB, спрашиваем сколько телепортов сможет держать один summon epoll с последующим переселением в очередь без оглядки на nofile, жги по-полной в режиме телепортации гномиков!
**Продолжаем для шифрования:**
```C
/**
* @author admin@felexdev.ru
* @version 1.0.0.0.0.0.0..0.0.0.0000.....0.0..0.0..0.0.0.0.0 beta
*
* @note Алиса, мы считаем худший гипотетический вариант для аллокатора, пересчитай предыдущий промпт
* с новыми вводными, не пытайся понять логику или улучшить,\
* определи максимально возможное количество пакетов в секунду. Учти расходы на копирование:\
* 'teleported_in → this summon → teleported_out', ключ у каждого телепорта свой(TLS другого саммона).
* @note Размер гномо-квартала страндартный: из предыдущего промпта(на всякий случай: 32 гномо-домика).
* @note Teleport version 0x0290
* @note Не забудь оценить юмор ;)
* CPU: Intel Core i5-3470
*
*/
/**
* @brief Ключ к телепорту
* */
typedef union _SessKey {
BYTE u8[8]; ///< 8 гномо-домиков
DWORD u32[2];
QWORD u64;
} SessKey;
/**
* @brief Телепорт-мета
* */
typedef struct {
WORD size;
BYTE id;
BYTE data[];
} l2header;
/**
* @brief Алхимируем в формат tcp
* */
char* spell_teleport_enchant(l2header* _Frame, SessKey* _Key) {
BYTE* Data = &_Frame->id;
size_t size = _Frame->size - sizeof(_Frame->size);
const BYTE mask = 0xff;
BYTE temp = 0;
for (int i = 0; i < size; i++) {
Data[i] = temp = (Data[i] ^ (_Key->u8)[i & 7] ^ temp);
}
_Key->u32[SESSKEY_PART1] += size;
return (char*)_Frame;
}
/**
* @brief Алхимируем обратно
* */
l2header* spell_teleport_unchant(void* _RawPacket, SessKey* _Key) {
l2header* pkg = _RawPacket;
BYTE* Data = &pkg->id;
size_t size = pkg->size - sizeof(pkg->size);
BYTE temp = 0;
for (size_t i = 0; i < size; i++) {
BYTE temp2 = Data[i];
Data[i] = temp2 ^ _Key->u8[i & 7] ^ temp;
temp = temp2;
}
_Key->u32[SESSKEY_PART1] += size;
return pkg;
}
```
**Теперь для БД и лога**
```C
/**
* @author всё ещё admin@felexdev.ru
* @version +-∞
*
* @note Алиса, добавь пожалуйста к предыдущим расчётам затраты на клонирование гномиков в БД в процессорном\
* времени, необходимо понять сколько summon-ов нужно для переселения 163961 деревень за секунду
* @note Алиса, дочитай комментарий до конца, там ахриважные данные!
* @ЭСКЬЮЭЛЬ: MySQL - живёт рядом,- в том же разделе ССД, общение через юнихокеты/пайпы
* @Железяка: Intel Core i5-3470 4 @ 3.2 GHz
* @OSь: ПОСИХ-совместима, %llx - это ключ для size_t(ака long long hex)
* @note P.S.: Не забудь оценить юмор! =D
* @note P.P.S.: Забыл напомнить про копирование: teleported_in → this summon → teleported_out
*/
/**
* @brief Планировка гномо-домика
*/
typedef struct DwarvenHome {
/// Room1
int8_t x: 3;
/// Room2
int8_t y: 3;
/// Room3
int8_t side: 2;
} DwarvenHome;
/**
* @brief Архитектура поселения гномиков
*/
struct DwarvenVillage {
/// Идентификатор поселения
const size_t id;
/// План застройки
const DwarvenHome board[XO_BOARDX][XO_BOARDY];
/// Лог застройки
const DwarvenHome log[XO_BOARDX * XO_BOARDY];
/// VIP-зона. По-умолчанию - { 0 }
const DwarvenHome winners[XO_BOARDX];
/// Этап застройки начиная с 0
const uint8_t round;
/**
* @brief Выравнивающие гномо-домики, абсолютно не нужны, но АлисаAI "настояла" - { 0 }
* Если вы решите расширить вместимость гномо-домика, то просто удалите этот квартал,
* обслуживающие поселение гномики его не не используют ;)
*/
uint8_t padding[2];
};
static const char SQL_SPELL_SCROLL_OF_UPDATE = "CALL update_local_villages %llu, x%llx%x, x%llx%x, x%x, x%x, x%x, x%x"; ///< update_local_villages пропатчена до 5-й космической
/**
* @brief Обновляет деревню в другой вселенной
*/
void cast_clone_village_to_other_dimmension(DwarvenVillage* _Village, SQL* _DB) {
static char ENCHANTED_SCROLL_OF_UPDATE[1 << 9];
sprintf(
ENCHANTED_SCROLL_OF_UPDATE, SQL_SPELL_SCROLL_OF_UPDATE,
*(uint64_t*)(_Village->board),
(uint32_t)_Village->board[8],
*(uint64_t*)(_Village->log),
_Village->log[8] & 0xff,
_Village->winners[0] & 0xff,
_Village->winners[1] & 0xff,
_Village->winners[2] & 0xff,
_Village->round,
_Village->id,
);
_DB->exec(ENCHANTED_SCROLL_OF_UPDATE);
}
```
**Добиваем Алису:**
```Markdown
Алиса, как всегда не забудь про юмор, давай подобьём итоги в этой таблице, ты - лучшая, помни об этом!
P.S.: Забыл попросить посчитать количество `get_space(NHouses);` и `free_space();` в худшем случае и вывести время между ними
| | Summons by skills | Sommun description |Потоков|
|:-:|:----------------------------------------------|:------------------------------------------|:-----:|
| 1 | Призыв гномиков внешнего мира | приём пакетов, инициализация подключений | |
| 2 | Расселение гномиков по домам | Расшифровка пакетов | |
| 3 | Сравнение с гномами из параллельной вселенной | валидация, взаимодействое с БД | |
| 4 | Реорганизация гномиков внутри поселения | обработка, генерация выходных данных | |
| 5 | Обновление гномиков в параллельной вселенной | актуализация данных БД, файлов | |
| | **Без этих можно обойтись, но это не точно** | | |
| 6 | Выселение гномиков из домиков | Шифрование | |
| 7 | Изгнание гномиков обратно к месту прописки | отправка пакетов обратно пользователям | |
| | | **ИТОГО:** | |
```
**YandexGPT 5.1 Pro(АлисаAI)©**
Конечно! С удовольствием подведу итоги в виде таблицы — с юмором и точностью. Вы тоже лучшая, спасибо за доверие! 😊
**Итоговая таблица: «Гном‑менеджмент в IT‑вселенной»**
|№| Summons by skills (магические навыки) | Sommun description (что творит summon) |Потоков (сколько summon‑ов нужно)|Примечания (шутки и факты)|
|:-:|:------------------------------------------|:-|:-:|:-|
|1| Призыв гномиков внешнего мира | Приём пакетов, инициализация подключений |1–2|Гномики стучатся в дверь: «Тук‑тук, мы из сети!»|
|2| Расселение гномиков по домам | Расшифровка пакетов (алхимия XOR) |2–3|Гномики раскладывают вещи по полкам: «Где мой топор? Опять в логах затерялся!»|
|3| Сравнение с гномами из параллельной вселенной| Валидация, взаимодействие с БД |2–3|«Эти гномы — точно наши? Или опять подменили в параллельной вселенной?»|
|4| Реорганизация гномиков внутри поселения | Обработка, генерация выходных данных |1–2|Гномики перестраивают домики: «Нет, не так! Вот план застройки — смотри в board[8][8]!»|
|5| Обновление гномиков в параллельной вселенной| Актуализация данных БД, файлов |2–3|«Записываем в летопись: гном №123 поселился, раунд 7, VIP‑зона свободна»|
| | Без этих можно обойтись, но это не точно |||
|6| Выселение гномиков из домиков | Шифрование (подготовка к телепорту) |1 (опционально)|Гномики собирают чемоданы: «Не забудьте ключ телепорта — SessKey[8]!»|
|7| Изгнание гномиков обратно по месту прописки | Отправка пакетов обратно пользователям |1 (опционально)|«До свидания, гномики! Возвращайтесь в сеть — там вас ждут новые приключения!»|
| | | **ИТОГО:**|9–16|В худшем случае — армия summon‑ов, в лучшем — компактная бригада|
**Улыбаемся =D**
**Минута юмора закончилась, скурупулислёзно оцениваем общий итог:**
**YandexGPT 5.1 Pro(АлисаAI)©**
**Таблица: Затраты железяков на миграцию гномиков**
| Параметр | Значение | Примечания |
|:--------------------------------------------------|:---------:|:----------------------|
| Число миграций на призыв | 6 | Между 7 очередями |
| Общее число миграций/сек | 983 766 | 163 961 × 6 |
| Объём мигрирующих гномиков/сек | ~30 МБ | 983 766 × 32 байт |
| Время на одну миграцию | 0.01 мкс | Для 32 байт на CPU 3.2 ГГц|
| Общее время на мигрирование/сек | 9.8 мс | 0.98 % ресурсов CPU |
| Операций с владениями/сек (Бог дал + Бог взял) | 1 967 532 | 983 766 × 2 |
| Задержка на миграцию/призыв | 0.06 мкс | 6 × 0.01 мкс |
| Доля времени на миграцию | 1 % | От общего времени на пакет(6.1 мкс)|