Перейти к основному содержимому

Прежде чем писать код: как MVP доказал жизнеспособность Netgap

· 8 мин. чтения
Андрей Ганюшкин
Enterprise Architect | Author & Lead Maintainer of Netgap

Прежде чем написать первую строчку боевого кода Netgap, я задал себе неудобный вопрос: а взлетит ли вообще эта идея? Можно ли реализовать паттерн logical airgap так, чтобы он не превратился в бутылочное горлышко, о которое разобьётся вся система? Чтобы ответить честно — не на словах, а цифрами — я собрал отдельный исследовательский прототип, NetGap MVP, прогнал его через нагрузочные тесты и вытащил наружу многие узкие места.

В этом посте я делюсь результатами: чего удалось добиться, где обнаружились «стеклянные потолки», как я их пробивал и почему в итоге принял решение двигаться дальше и разрабатывать полноценное приложение.

Идея: логический разрыв, который клиент не замечает

Напомню суть паттерна logical airgap. В классической сети прямая связность между внешним (недоверенным) и внутренним (доверенным) сегментом — это огромная поверхность для атаки. Netgap убирает этот прямой путь: между вашим сервисом и внешним миром больше нет сетевого маршрута, только строго контролируемый мост обмена данными.

Но у такого разрыва есть цена. Если внутри мы заменяем прямой вызов на асинхронную передачу через посредника, то главный вопрос звучит так:

Можно ли сохранить для внешнего клиента привычный синхронный HTTP-контракт, спрятав за ним асинхронную очередь — и не заплатить за это неприемлемой задержкой и потерей пропускной способности?

Именно эту гипотезу и проверял MVP.

Чем можно «разорвать» связность: варианты реализации

Прежде чем перейти к прототипу, стоит показать, что logical airgap — это не один конкретный продукт, а целое семейство подходов. Разрыв прямого маршрута можно построить по-разному, и у каждого варианта своя механика, свои трудности и своя выгода. Вот основные из них:

  • Очередь сообщений / брокер (Redis, RabbitMQ, Kafka). Между сегментами ставится брокер: одна сторона кладёт запрос, другая забирает и возвращает ответ через correlation ID. Как работает: прямого соединения клиент↔цель нет, есть только чтение/запись в очередь. Трудности: нужно вручную склеивать запрос и ответ, следить за TTL «подвисших» сообщений, а сам брокер становится узким местом (у Redis — однопоточность). Выгода: простая модель, естественный демпфер всплесков нагрузки и переживание кратковременной недоступности исполнителя. Именно этот вариант проверял MVP.

  • Контролируемый RPC-канал (gRPC/mTLS-шлюз). Между сегментами остаётся единственный строго регламентированный канал: шлюз на границе принимает запросы и передаёт их исполнителю по узкому protobuf-контракту. Как работает: вместо «любого TCP» разрешён ровно один тип вызова с валидацией схемы и взаимной аутентификацией. Трудности: нужно проектировать и версионировать контракт, поддерживать mTLS и управление сертификатами. Выгода: минимальная поверхность атаки, строгая типизация, низкие задержки, backpressure «из коробки». Именно этот подход будет реализован в Netgap первым.

  • Файловый обмен через контролируемую точку (data diode / shared spool). Данные передаются как файлы/пакеты через промежуточное хранилище, а иногда и через однонаправленный физический канал (диод). Как работает: сторона-источник пишет пакет, сторона-приёмник его валидирует и обрабатывает; при диоде трафик физически невозможен в обратную сторону. Трудности: высокая задержка, сложно организовать синхронный ответ, нужен свой протокол подтверждений. Выгода: максимальная изоляция, вплоть до гарантированной однонаправленности на уровне «железа».

  • Протокольный прокси/бастион с инспекцией. На границе стоит посредник, который терминирует соединение, инспектирует и пересобирает трафик, а внутрь идёт уже новый, «чистый» запрос. Как работает: сквозного соединения нет — есть два независимых, связанных логикой прокси. Трудности: прокси должен понимать протокол приложения, это точка отказа и потенциальное узкое место по производительности. Выгода: глубокая фильтрация на уровне содержимого и полный контроль над тем, что именно пересекает границу.

Все эти подходы объединяет одно: снаружи клиент видит привычный синхронный вызов, а внутри прямого сетевого маршрута нет. MVP осознанно начался с самого простого варианта — очереди на Redis, — чтобы честно измерить цену разрыва и понять, оправдывает ли модель переход к более строгому RPC-каналу.

Как был устроен прототип

Чтобы проверить идею максимально честно, я собрал уменьшенную, но полноценную модель контура из трёх сервисов на Rust:

  • producer — HTTP-точка входа. Внешний клиент обращается к нему как к обычному API. Producer кладёт запрос в очередь и ждёт ответ.
  • consumer — исполнитель в доверенном сегменте. Он забирает запрос из очереди, вызывает целевой сервис и возвращает ответ обратно.
  • mockapi — заглушка целевой системы, чтобы измерять именно накладные расходы транспорта, а не чужую бизнес-логику.

Роль моста-посредника между сегментами играл Redis с очередями на основе списков. Для внешнего клиента всё выглядело как обычный POST с ответом — но за этим фасадом клиент и цель уже не имели ни одного прямого сетевого соединения.

Для тех, кто захочет погрузиться глубже, — исходный код прототипа доступен здесь: netgap-mvp. Там вы найдёте сами сервисы на Rust, нагрузочные сценарии k6 и инфраструктурный код (Terraform + Ansible).

Ключевой приём — корреляция запроса и ответа: producer генерирует Correlation ID, дописывает его к запросу и ждёт ответ в персональной очереди с коротким TTL. Так каждый ответ гарантированно находит своего отправителя, а «подвисшие» ответы автоматически исчезают и не засоряют систему.

Про облачную часть скажу коротко, потому что она была лишь средством, а не целью: весь контур поднимался в Yandex Cloud через Terraform и Ansible, с эмуляцией сегментированной сети из нескольких подсетей, а к каждому сервису была прикручена наблюдаемость (метрики Prometheus + дашборды Grafana). Это нужно было ровно для одного — чтобы измерения были воспроизводимыми, а узкие места — видимыми, а не угадываемыми.

Методология: гонка за «стеклянными потолками»

Я сознательно строил исследование как серию экспериментов. На каждом шаге — упереться в потолок, понять его природу, пробить и зафиксировать результат числом. Вот этот путь:

Шаг 1. Синхронная реализация (blocking I/O + пул потоков). Первая наивная версия упёрлась в ~27–28k RPS — и упорно не хотела расти. Причины оказались классическими: накладные расходы на переключение контекста потоков, TCP-хендшейк на каждый запрос, переполнение listen backlog и, что особенно поучительно, банальный лимит файловых дескрипторов ОС. Это был потолок не идеи, а реализации.

Шаг 2. Переход на асинхронный стек Tokio. Снятие ограничения пула потоков дало кратный скачок: чистая заглушка вышла на ~142k RPS при p95 около 1.4 мс. Асинхронность оказалась не «красивым бонусом», а обязательным условием.

Шаг 3. Добавление очереди в цепочку. Вот здесь начинается самое интересное для темы logical airgap. Введение Redis как посредника «стоило» около 35% пропускной способности — цепочка держала ~100k RPS (p95 ≈ 1.5 мс). Для полноценной сетевой операции с посредником между сегментами это на удивление дёшево. Заодно проявилось новое узкое место — однопоточность Redis.

Шаг 4. Оптимизация всей цепочки в облаке. Тюнинг работы с потоками Tokio и переиспользованием соединений к Redis поднял сквозной результат с ~2200 до ~8700 RPS — рост почти на +295%.

Шаг 5. Стресс-тест до отказа. Наконец, я нагружал систему, пока она не начинала деградировать, чтобы точно знать её границы.

Что показали цифры

Итоговый профиль сквозной цепочки (клиент → producer → Redis → consumer → цель → обратно) получился таким:

Нагрузка (VUs)RPSp95 latencyСостояние
50~6756~10.4 мсоптимум
100~8734~16 мспик пропускной способности
200~8134~35.5 мсточка насыщения
300~6885~64.4 мсдеградация

О чём это говорит? Полный контур с разрывом связности и посредником уверенно держит ~8700 запросов в секунду с задержкой около 16 мс (а при более лёгкой нагрузке задержка опускается до ~10 мс) — и это при том, что каждый запрос проходит через очередь и двойной хоп между сегментами. Для внешнего клиента взаимодействие неотличимо от обычного HTTP-вызова. Гипотеза подтвердилась: логический разрыв можно спрятать за синхронным фасадом, не ломая ни латентность, ни пропускную способность.

Где узкое место и что с ним делать

Самый ценный результат исследования — не столько сами цифры, сколько точно локализованное главное узкое место: однопоточность Redis. Именно один-единственный CPU-поток брокера, а не сеть, не Rust и не ОС, определяет потолок сквозного RPS. Это отличная новость: узкое место понятное, изолированное и хорошо изученное индустрией.

Помимо этого исследование выявило несколько уровней, каждый со своими рычагами:

  • ОС и сеть — лимиты дескрипторов, размер backlog, стоимость установки соединений. Смягчается keep-alive, TCP_NODELAY и системным тюнингом.
  • Приложение — producer сейчас ждёт ответ в цикле опроса вместо нативного блокирующего чтения. Это осознанный компромисс прототипа и очевидный кандидат на оптимизацию: переход на выделенное блокирующее соединение снимет лишнюю нагрузку на CPU и Redis.

Перспективы и альтернативные пути

Главное, что дало исследование, — понимание, куда расти. И тут перспектив заметно больше, чем ограничений:

  • Горизонтальное масштабирование посредника. Однопоточный потолок Redis пробивается шардированием или несколькими инстансами — это снимает единственное серьёзное ограничение по пропускной способности.
  • Альтернативные транспорты для моста. Redis-очередь — лишь один из вариантов реализации разрыва. Тот же контракт можно построить на других брокерах или на контролируемом потоковом канале между сегментами (в боевой архитектуре Netgap роль моста между netgap-gateway и netgap-executor играет строго контролируемый gRPC-канал). MVP доказал, что сама модель «синхронный фасад над асинхронным мостом» устойчива и не зависит от конкретной технологии посредника.
  • Очередь как демпфер. Приятный побочный эффект: посредник естественным образом сглаживает всплески нагрузки и переживает кратковременную недоступность исполнителя — то, чего прямое соединение дать не может в принципе.
  • Дорога к продакшену. Понятны и оставшиеся задачи для боевого контура: durability и HA для канала, авторизация и шифрование, backpressure на уровне очереди. Это не сюрпризы, а спланированный roadmap.

Почему я решил двигаться дальше

MVP выполнил свою единственную задачу — снял главный риск. Он показал, что паттерн logical airgap в исполнении на Rust:

  1. функционально состоятелен — клиент получает привычный синхронный ответ, а прямой связности между сегментами нет;
  2. производителен — тысячи запросов в секунду при задержке в единицы миллисекунд, даже с посредником в цепочке;
  3. имеет понятный запас роста — единственное серьёзное узкое место локализовано и устранимо, а альтернативных путей масштабирования несколько.

Иными словами, решение начать полноценную разработку основано не на энтузиазме, а на измерениях. Прототип отработал, гипотеза подтверждена, риски посчитаны — можно строить продукт.


Это первый технический пост из серии о внутренностях Netgap. Дальше будет больше подробностей об архитектуре боевого контура, транспорте между сегментами и модели безопасности. Следите за обновлениями!