Короткий ответ
DMZ, или демилитаризованная зона, это сегмент сети между интернетом и внутренней сетью компании. В нём стоят системы, которые принимают запросы снаружи. LAN это внутренняя сеть, где работают учётные и прикладные системы компании, и доступ туда закрыт сильнее всего.
Если шлюз API стоит в DMZ, а соединения из DMZ в LAN запрещены, внутренний API можно опубликовать пятью способами. Выбор зависит от одного вопроса к службе безопасности: можно ли получить узкое правило DMZ → LAN на конкретный порт со взаимной проверкой сертификатов. Если можно, ставьте шлюзы в обеих зонах. Если нельзя, подойдёт обратный HTTP в HAProxy или брокер со штатным request-reply, например ActiveMQ Artemis. Request-reply поверх Kafka работает, но его приходится писать самим.
Все пять схем мы собирали на NEOMSA APIM, российской платформе управления API от Neoflex. Сводка по вариантам:
Вариант |
Нужно правило DMZ → LAN |
Свой код |
Потоковая передача |
Участков |
Состояние у нас |
Kafka |
нет |
~2500 строк |
нет |
5 |
рабочий стенд |
Artemis |
нет |
нет |
нет |
5 |
стенд и нагрузочные замеры |
Обратный HTTP |
нет |
нет |
да |
3 |
проработан, стенда не было |
Единый контур и шлюзы |
да |
нет |
да |
3 |
проработан и согласован |
Две инсталляции |
да |
нет |
да |
3 |
рабочий стенд с mTLS |
Ниже подробно про каждый вариант, методику замеров и цифры.
Есть задача, которая в больших компаниях всплывает с завидной регулярностью. Шлюз API стоит в DMZ, потому что именно он смотрит наружу. Сервисы, которые надо опубликовать, живут в LAN. А служба безопасности говорит: соединения из DMZ внутрь запрещены. Точка.
Сформулировано это обычно предельно коротко, и звучит как техническая мелочь. На деле это требование ломает самую очевидную схему — шлюз просто проксирует вызов внутрь — и заставляет придумывать что-то ещё.
Всё, о чём пойдёт речь, мы делали на NEOMSA APIM — отечественной платформе управления API, собранной на компонентах WSO2. В ней полный набор: шлюз, управляющий контур, портал разработчика, хранилище политик и Micro Integrator — интеграционный движок, на котором и построены три из пяти вариантов. Это важно для дальнейшего рассказа, потому что часть решений оказалась возможна именно благодаря тому, что интеграционный движок и шлюз входят в одну платформу.
Мы за последний год перебрали пять вариантов. Три из них довели до работающих стендов, один — до полноценных нагрузочных замеров, остальные проработали на уровне архитектуры и согласований. Ниже — что получилось, что оказалось дороже, чем выглядело на бумаге, и по какому признаку, на наш взгляд, между ними стоит выбирать.
Сразу оговорюсь про числа. Нагрузочные замеры в полном объёме мы провели только для варианта на Artemis. Стенды на Kafka и на двух инсталляциях платформы работали и проверялись функционально, но методичного нагрузочного тестирования там не было. Там, где приводятся цифры по остальным вариантам, это честные оценки, и я каждый раз это оговариваю. Выдавать прикидки за измерения было бы некрасиво, тем более что расхождение между ожиданием и реальностью — главное, чему нас научила эта работа.
Почему шлюз в DMZ не может просто проксировать вызов в LAN
Требований два, и они тянут в разные стороны.
Со стороны бизнеса: потребитель вызывает API и получает ответ в том же запросе. Это не прихоть. Переписать контракт на асинхронный — значит затронуть всех, кто уже интегрирован, а это обычно десятки систем и месяцы согласований. Стоимость такого изменения на порядок выше, чем стоимость любой инфраструктурной конструкции.
Со стороны безопасности: из DMZ нельзя устанавливать соединения в LAN. Разрешено только обратное направление.

Здесь важна одна деталь, которая многое определяет. Ограничение касается только того, кто устанавливает соединение. Куда по нему потом идут данные, оно не регулирует. Как только это проговорено вслух, появляется целый класс решений: пусть соединение открывает внутренняя сторона, а данные по нему ходят в обе стороны. Именно на этом построены первые два варианта.
А если ограничение допускает исключение — узкое правило на конкретный порт с проверкой сертификатов, — то открывается второй класс решений, куда более простых. На этом построены третий и четвёртый.
Собственно, весь выбор сводится к одному вопросу, который стоит задать безопасности первым: можно ли в принципе получить правило DMZ → LAN, пусть самое узкое? Ответ определяет половину архитектуры.
Вариант 1. Можно ли сделать синхронный request-reply через Kafka
Коротко. Работает, но request-reply поверх Kafka приходится писать самим. У нас это около 2500 строк Java плюс отдельное приложение во внутренней зоне.
Логика такая. Ставим Kafka в DMZ. Шлюз кладёт запрос в топик. Внутренний сервис сам подключается к брокеру из LAN — то есть соединение идёт наружу, правило соблюдено, — забирает запрос, вызывает нужную систему и кладёт ответ в другой топик. Шлюз всё это время держит открытым HTTP-соединение клиента и ждёт.

На бумаге выглядит стройно. На практике выяснилось, что мы взялись строить поверх Kafka то, чего в ней нет.
Kafka — это распределённый журнал. В ней нет понятия «ответ на сообщение». Нет поля «куда ответить». Нет способа сказать «отдай мне вот это конкретное сообщение и не трогай остальные». Всё это пришлось делать самим.
Самая неприятная часть — доставка ответа. Шлюзов несколько, HTTP-соединение клиента висит на одном конкретном узле, и ответ должен попасть именно туда. Мы решили это так: каждый узел статически закрепляет за собой партицию топика ответов, кладёт её номер в заголовок запроса, а внутренний сервис пишет ответ ровно в неё. То есть номер партиции работает как адрес узла. Плюс реестр ожидающих запросов в памяти, поток-получатель, сборщик протухших записей, ручное управление смещениями и перемотка в конец при старте, чтобы не переигрывать чужие ответы.
Получилось около двух с половиной тысяч строк Java, оформленных как класс-медиатор для Micro Integrator: собственно медиатор запрос-ответ, реестр ожидающих с фоновой очисткой по дедлайну, поток-получатель ответов, кодек полезной нагрузки, разбор конфигурации и сбор метрик через JMX. Плюс отдельное приложение на внутренней стороне, которое разбирало запросы и ходило к целевым системам.
Это был работающий стенд: JAR клался в каталог библиотек интеграционного движка, конфигурация распространялась через сборку CApp, сквозные вызовы проходили. Но каждая из этих строк — обязательство: её надо тестировать, сопровождать, передавать в эксплуатацию, чинить при смене версий брокера и объяснять тому, кто придёт после вас. Отдельная неприятность в том, что кастомный JAR в каталоге библиотек платформы — это ровно то место, из-за которого вендор вправе сказать «это не наша поддержка».
Отдельно стоит сказать про безопасность. Право писать в топик запросов означает возможность отправить любое сообщение, а значит вызвать любой метод у любой внутренней системы, до которой дотягивается внутренний сервис. Единой точки контроля нет, проверка — только на прикладном уровне, в вашем же коде.
Вывод по варианту простой: он работает, но вы пишете и потом годами сопровождаете функциональность, которая в брокерах другого класса есть из коробки.
Вариант 2. Request-reply через ActiveMQ Artemis и JMS
Коротко. Собственного кода ноль: мост собран штатными медиаторами Micro Integrator. Потолок на нашем стенде 50 запросов в секунду при 95-м перцентиле около 100 мс.
Топология та же самая — брокер в DMZ, внутренняя сторона подключается к нему из LAN. Меняется только брокер. И вместе с ним меняется объём работы.
В JMS запрос-ответ — штатный паттерн, ему больше двадцати лет. Есть поле «куда ответить». Есть идентификатор, который отвечающая сторона переносит из запроса в ответ. Есть механизм, позволяющий ожидающему подписаться именно на свой ответ, причём отбор сообщений берёт на себя брокер. Протухание сообщений, очередь недоставленных, удаление обработанного — всё это очередь умеет сама, руками писать ничего не нужно.
Собственного кода в этом варианте не осталось вообще. Ни строчки. Обе стороны — это Micro Integrator из состава платформы, и вся логика собрана из штатных медиаторов: property для заголовков и таймаутов, call для обращения к очереди и к внутренней системе, filter для разбора кодов ответа, payloadFactory для формирования тела ошибки, log для сквозной трассировки. На внешней стороне это API с тремя последовательностями — подготовка, разбор ответа, обработка отказа. На внутренней — proxy-сервис с транспортом JMS, который слушает очередь запросов.
Иными словами, весь мост описан теми же средствами, какими в платформе описывается любая интеграция. Разработки нет, есть конфигурация. Для сравнения: две с половиной тысячи строк против нуля.
Но было бы нечестно закончить на этом.
Первое, на что мы напоролись. По умолчанию ожидающая сторона ищет ответ по идентификатору, который сгенерировал брокер. Если отвечающая сторона подключена другим клиентом или по другому протоколу, этот идентификатор ей попросту не виден — вместо него приходит внутренний номер. Вернуть то, чего не получал, невозможно. Результат: ответ физически лежит в очереди, клиент получает таймаут, и ни в одном логе нет ошибки, потому что обе стороны отработали корректно. Диагностируется только ручным сопоставлением. Лечится тем, что идентификатор задаёт сам отправитель. Об этом надо знать заранее.
Второе. Синхронное ожидание поверх асинхронного транспорта имеет цену, и она не в миллисекундах. Ожидание ответа удерживает ресурсы на всё время обхода: поток, соединение, сессию. Мы несколько недель гонялись за потолком производительности, последовательно расширяя то число потребителей, то пул соединений, то потоки, и каждый раз потолок оставался на месте. Оказалось, что упирались мы поочерёдно в разные вещи: сначала в число потребителей, потом в сериализацию приёма ответов на общей сессии, потом в запись сообщений на диск брокера. Узкое место переезжало, и ловить его приходилось заново на каждом шаге.
Третье, и это, пожалуй, главный урок. Один HTTP-вызов через такой мост — это около полутора десятков обращений к брокеру: создать сессию, отправителя, отправить, создать получателя, принять, всё закрыть. И то же самое на второй стороне. Пока брокер стоит рядом, это дёшево. Но у нас он стоял за сетевой границей от обоих узлов, и схема добросовестно умножала сетевой джиттер на это число. Медиана держалась на уровне 25–30 миллисекунд, а хвост распределения регулярно улетал в секунды — при нагрузке, на которой очереди не было в принципе.
Итоговые числа: устойчивый режим до 50 запросов в секунду при отклике порядка сотни миллисекунд на девяносто пятом перцентиле. Для многих задач этого достаточно. Для целевых пятисот — нет, и без пересмотра размещения брокера туда не добраться.
Ещё два ограничения, о которых стоит помнить сразу. Потоковая передача и ответы неизвестного размера в такой схеме невозможны в принципе: брокер перемещает дискретные сообщения. И операции, меняющие состояние, требуют ключа идемпотентности — при таймауте вы не знаете, была ли операция выполнена, и повтор клиента может исполнить её дважды.
Вариант 3. Как опубликовать API без правила DMZ → LAN: обратный HTTP в HAProxy
Коротко. Правило DMZ → LAN не нужно, вызов остаётся синхронным HTTP, своего кода нет. Главный минус: механизм в HAProxy помечен как экспериментальный.
Здесь мы вернулись к исходной формулировке ограничения и перечитали её ещё раз. Запрещено устанавливать соединение из DMZ в LAN. Про то, кто по этому соединению шлёт запросы, не сказано ничего.
HAProxy умеет ровно это. Узел внутри LAN сам подключается к шлюзу в DMZ, после чего роли переворачиваются: тот, кто подключился, становится сервером, а шлюз — клиентом. Соединение попадает в пул шлюза и дальше используется как обычный upstream. Поскольку разговор идёт по HTTP/2, по одному TCP-соединению одновременно ходит множество независимых запросов.

Получается синхронный HTTP от начала до конца. Никакой очереди, никакого асинхронного шва, никакой корреляции — вопрос «как вернуть ответ туда, где висит клиент» просто не возникает, его решает транспорт. Собственного кода, как и в варианте с Artemis, ноль, только конфигурация.
Побочный эффект, который очень понравился безопасности: шлюз в DMZ не инициирует исходящих соединений вообще. Исходящие соединения из DMZ можно явно запретить правилом межсетевого экрана и тем самым отрезать сценарий, в котором скомпрометированный узел в DMZ используется как плацдарм для дальнейшего продвижения.
Плюс сокращается сама цепочка. В брокерных вариантах между шлюзом и внутренней системой пять сетевых участков, здесь — три.
Теперь про то, почему мы не понесли это сразу в продуктив.
Механизм помечен разработчиками HAProxy как экспериментальный. При этом он работает: появился в версии 2.9 и с тех пор развивается. Пометка означает, что конфигурация может измениться между релизами без обещаний совместимости. Для банковского контура это отдельный разговор с эксплуатацией, и разговор непростой.
Плюс это фактически единственная реализация такого механизма. Уйти на другой продукт, сохранив схему, не получится: вендорная привязка здесь возникает на уровне транспорта.
И конфигурация требует аккуратности в местах, которые в документации выглядят безобидно. Проверяя её по мануалу, мы нашли несколько настроек, где значение по умолчанию ломает схему: переиспользование соединений должно быть переведено в режим, при котором первый запрос сессии тоже идёт по существующему соединению — иначе шлюз будет пытаться открыть новое, чего он физически не умеет; периодическая проверка живости канала настраивается на разных сторонах разными параметрами, и перепутать их легко; а таймаут простоя по умолчанию рвёт канал при паузах в трафике.
Оценка накладных расходов, которую мы для себя приняли: примерно один сетевой обход при уже установленном соединении. То есть заметно меньше, чем у брокерных вариантов, и сопоставимо с прямым проксированием. Подчеркну: это оценка по устройству схемы. До нагрузочного стенда этот вариант у нас не дошёл.
Вариант 4. Один управляющий контур и шлюзы в DMZ и LAN
Коротко. Самая простая схема, если безопасность согласует узкое правило DMZ → LAN. Политики, ключи и лимиты задаются в одном месте для обоих шлюзов.
Здесь мы меняем предпосылку. Допустим, узкое правило DMZ → LAN получить всё-таки можно: конкретные порты, белый список адресов, взаимная проверка сертификатов.
Тогда схема становится радикально проще. Управляющий контур платформы выносится в отдельную административную сеть — в NEOMSA APIM это отдельно разворачиваемый компонент, и шлюзы подключаются к нему как к источнику конфигурации. В DMZ и в LAN разворачиваются только шлюзы, которыми этот контур управляет. Шлюз в DMZ принимает внешний вызов и проксирует его шлюзу в LAN, тот идёт к внутренней системе. Обычный HTTP от начала до конца, никакой асинхронности, никакого моста.
Что это даёт. Единая точка управления политиками, ключами, версиями API и лимитами — описали один раз, разъехалось на оба шлюза. Единая наблюдаемость: метрики и логи стекаются в одно место, и вопрос «где именно тормозит» перестаёт быть детективной историей. Собственного кода нет, и в отличие от брокерных вариантов нет и асинхронного шва с его таймаутами и идемпотентностью.
Что это стоит. Правило DMZ → LAN всё-таки нужно — узкое, с сертификатами, но нужно. Дальше это разговор с безопасностью, и он будет предметным: речь про конкретный порт к конкретному адресу со взаимной аутентификацией. Открывать весь сегмент никто не просит.
И появляется новый значимый компонент. Управляющий контур имеет доступ к шлюзам в обеих зонах, а значит сам становится объектом защиты первой категории. Его компрометация — это компрометация политик на обоих шлюзах сразу. Административная сеть, доступ к ней, процедуры изменений — всё это придётся прорабатывать всерьёз и с самого начала.
Вариант 5. Две независимые инсталляции API-платформы в DMZ и LAN
Коротко. Связь между инсталляциями настраивается штатными средствами платформы, без доработок. Цена: всё приходится вести дважды, и конфигурации со временем расходятся.
Самый прямолинейный из всех. Две полноценные инсталляции API-платформы: одна в DMZ, другая в LAN. Между ними то же узкое правило с сертификатами. Шлюз в DMZ публикует наружу проксирующий API, который вызывает API на внутренней инсталляции.
Этот вариант мы тоже собрали и проверили на стенде. И главное наблюдение по итогу: всё, что требовалось для связи между зонами, платформа даёт из коробки. Взаимная проверка сертификатов между шлюзами настраивается штатно, через хранилища ключей и доверенных сертификатов в конфигурации шлюза. Авторизация вызова с внешнего шлюза на внутренний — тоже штатный механизм: внешний шлюз выступает обычным потребителем внутреннего API и получает токен так же, как это делал бы любой другой клиент. Ничего дописывать не пришлось, и это заметно отличает вариант от брокерных, где мы либо писали код, либо неделями воевали с настройками транспорта.
Главное достоинство — простота. Разворачивается стандартно, без нестандартных связей между зонами. Нет общего компонента, чья недоступность останавливает обе стороны. Зоны отказа разделены: проблемы во внутренней инсталляции не трогают внешнюю и наоборот. Для команды эксплуатации это привычная конструкция, которую не надо объяснять.
Плата — дублирование. Два комплекта конфигурации, две базы, два портала разработчика, два набора ключей и политик. Описание API надо публиковать дважды. И самое неприятное: конфигурации со временем расходятся. Причина обычно простая: правку вносят там, где горит, и во вторую инсталляцию она доезжает не всегда. Через полгода выясняется, что лимиты в DMZ и в LAN разные, и никто не помнит почему.
Этот вариант хорош, когда число публикуемых API невелико и меняются они редко. Когда их сотни и релизы еженедельные, дублирование становится основным источником инцидентов.
Как мы проводили нагрузочное тестирование
Про методику стоит рассказать отдельно, потому что переделывать её посреди работы пришлось дважды, и оба раза из-за вещей, которые в начале казались несущественными.
Нагрузку надо задавать интенсивностью, то есть частотой запросов. Это первое и главное. Если зафиксировать число потоков, то при замедлении системы подаваемая нагрузка снижается сама собой: потоки дольше ждут ответа и реже отправляют новые запросы. Деградация маскируется, вы видите ровный график и делаете вывод, что всё в порядке. Мы начинали именно так и какое-то время радовались хорошим цифрам. Правильно — фиксировать частоту отправки и смотреть, справляется ли система, а число потоков подбирать так, чтобы в потолок упиралась система, а генератору мощности хватало с запасом.
Ступени по интенсивности. 25, 50, 100, 200, 400 запросов в секунду, по минуте на ступень. Такая лестница показывает не только потолок, но и характер выхода на него: плавное насыщение и резкое колено — разные болезни с разным лечением.
Прогрев тоже должен идти под нагрузкой. Мы сначала просто выжидали полминуты перед первой ступенью, и вся стоимость холодного старта — прогрев виртуальной машины Java, наполнение пулов, установление соединений — падала на первую ступень. Она показывала девяносто пятый перцентиль в семьсот миллисекунд там, где в прогретом состоянии было тридцать семь. Пришлось делать настоящий прогрев отдельной ступенью, результаты которой в расчёт не идут.
Один прогон ничего не доказывает. Разброс между прогонами на одной и той же конфигурации доходил у нас до полутора раз по потолку. Мы несколько раз связывали изменение результата с изменением настройки, а потом выяснялось, что дело было в неочищенных очередях от прошлого прогона или в том, что генератор запускался с ноутбука через корпоративный VPN вместо кластера. Без трёх повторов и медианы по ним выводы делать нельзя.
Ошибки надо разделять на два класса. Отказ приложения — это когда сервис ответил кодом ошибки: он работает, но что-то не так с запросом или с внутренней системой. Отказ уровня соединения — это когда ответа не было вовсе: обрыв, таймаут, сброс при установлении защищённого канала. Смешивать их в одну колонку «ошибки» бессмысленно, потому что расследуются они в противоположных направлениях. У нас был показательный эпизод: генератор насчитал несколько сотен ответов с кодом пятьсот, а в телеметрии шлюза их не было ни одного. Это само по себе оказалось диагнозом: отвечал слой перед шлюзом, и до шлюза эти запросы просто не доходили.
Общие перцентили не говорят, где уходит время. Пока мы смотрели на сводные цифры, гипотезы менялись каждую неделю. Всё изменилось, когда мы стали проставлять сквозной идентификатор запроса и собирать по нему временные метки со всех узлов: подготовка на внешней стороне, ожидание в очереди запросов, вызов внутренней системы, ожидание ответа. Первая же такая разбивка показала, что вызов внутренней системы занимает пять миллисекунд — и все подозрения в её адрес, которые мы вынашивали до этого, были беспочвенны с самого начала.
Одна тонкость, на которую мы напоролись. Участки, измеренные по часам разных узлов, зависят от синхронизации времени: расхождение часов перекладывает время из одного участка в другой, не меняя их суммы. Сумма достоверна всегда, деление — только при работающей синхронизации. Мы один раз сделали неверный вывод именно на этом, и подстраховались тем, что стали сверять разбивку с глубиной очередей, которая от часов не зависит вовсе.
Хвост распределения информативнее медианы. Нехватка мощности поднимает всю кривую разом. А вот нормальная медиана при тяжёлом хвосте означает редкие провалы, и тогда искать надо их источник. Мы потеряли изрядно времени, расширяя то, что и так не было узким местом, пока не научились читать этот признак.
Настройка по умолчанию — не всегда разумная настройка. Несколько параметров, определявших потолок, имели значения по умолчанию, рассчитанные на совсем другой профиль нагрузки. Найти их можно было только замером: в документации они выглядят безобидно.
Какую производительность дал мост на Artemis
Ещё раз оговорюсь: полноценно измерен только вариант с Artemis. Остальные числа — оценки, и я это отмечаю.
Вот что мы получили на стенде с Artemis. Внешний шлюз и оба интеграционных узла в Kubernetes, брокер на отдельной виртуальной машине, внутренняя система — управляемая заглушка, отвечающая за единицы миллисекунд.
Интенсивность |
Достигнуто |
Медиана |
95-й перцентиль |
Ошибки |
25 запросов/с |
25 |
24 мс |
37–54 мс |
0 % |
50 запросов/с |
50 |
21–23 мс |
60–98 мс |
0 % |
100 запросов/с |
40–77 |
больше секунды |
несколько секунд |
до 1 % |
Три вывода из этой таблицы.
Первый: до пятидесяти запросов в секунду схема ведёт себя хорошо. Медиана в двадцать с небольшим миллисекунд для конструкции, где запрос проходит через очередь и обратно, — вполне достойный результат.
Второй: колено резкое. Между пятьюдесятью и сотней медиана прыгает с двадцати миллисекунд до секунды с лишним. Плавного насыщения нет, и это важный признак: так проявляется упор в общий ресурс, который удерживается на время запроса. Нехватка мощности дала бы плавный рост.
Третий, и самый неприятный: выше колена пропускная способность падает. На двухстах и четырёхстах запросах в секунду фактически проходило около тридцати — меньше, чем на сотне. Система тратит ресурсы на работу, которая потом отваливается по таймауту. Гнать нагрузку выше колена вредно: пропускная способность от этого только падает.
Разбивка по участкам для медленных запросов выглядела так: подготовка на внешней стороне — около миллисекунды, вызов внутренней системы — пять миллисекунд, всё остальное время уходило на путь через брокер. Подбор ответа по идентификатору на стороне брокера, кстати, занимает две-четыре миллисекунды — то есть сам механизм корреляции быстрый, дело не в нём.
Потолок мы двигали несколько раз, и каждый раз узкое место переезжало: сначала упиралось в число обработчиков на внутренней стороне, потом в то, что приём ответов шёл строго по одному, потом в запись сообщений на диск брокера. Последнее, к слову, лечится отключением персистентности: для запроса, который теряет смысл через двенадцать секунд, переживать перезапуск брокера незачем, а платим мы за это четырьмя дисковыми операциями на каждый вызов.
Отдельно стоит показать, какие именно настройки определяли потолок. Все они выглядят в документации совершенно безобидно, и найти их можно было только замером.
Что настраивали |
По умолчанию |
Что происходит при значении по умолчанию |
Число обработчиков очереди |
1 |
внутренняя сторона разбирает запросы по одному |
Кеширование JMS на приёме |
не задано |
сессия и потребитель пересоздаются на каждое сообщение |
Кеширование JMS на отправке |
уровень сессии |
приём ответов идёт строго по одному, потолок упирается в это |
Пул соединений к брокеру |
10 |
при десятках запросов в полёте потоки ждут соединение |
Персистентность сообщений |
включена |
четыре дисковые операции на каждый вызов |
Пауза перед повтором после отказа |
−1 |
один таймаут выводит мост из строя до перезапуска |
Последняя строка — отдельная история. Значение минус единица здесь означает «подвесить навсегда», хотя интуитивно его читаешь как «не подвешивать». Мы потеряли на этом половину дня, прежде чем поняли, что после первой же ошибки мост перестаёт обслуживать вообще всё.
Оценка по Kafka. Замеров у нас нет, но прикинуть можно. Сам транспорт здесь вряд ли стал бы узким местом: Kafka пишет пачками и не делает отдельного сброса на диск для каждого сообщения, поэтому в дисковую стену, в которую упёрлись мы, она скорее всего не упёрлась бы. По задержке на низкой нагрузке картина была бы сопоставимой или чуть хуже — за счёт цикла опроса у потребителей, который по умолчанию добавляет десятки миллисекунд и требует отдельной настройки. А вот архитектурные ограничения абсолютно те же: поток, удерживаемый на время обхода; невозможность потоковой передачи; необходимость идемпотентности. Разница между вариантами сводится к двум с половиной тысячам строк, которые в одном случае надо написать и сопровождать, а в другом нет.
Оценка по обратному HTTP. По устройству схемы накладные расходы — примерно один сетевой обход при уже установленном соединении, то есть единицы миллисекунд. Потолок определяется числом открытых соединений, умноженным на число одновременных потоков в каждом, и при разумных настройках это тысячи параллельных запросов. Ограничение по потоковой передаче отсутствует в принципе: это обычный HTTP.
Оценка по вариантам со шлюзами. Это простое проксирование, один дополнительный сетевой участок. Накладные расходы — единицы миллисекунд, потолок определяется мощностью самих шлюзов и не имеет отношения к схеме доступа.
Если свести всё в одну картину: по производительности брокерные варианты заметно уступают остальным трём, и настройкой это не исправить: так устроена сама конструкция. Синхронный контракт поверх асинхронного транспорта стоит денег, и стоит он их всегда.
Сводная таблица по всем пяти вариантам приведена в начале статьи.
Какой вариант выбрать: порядок вопросов к безопасности
Пять вариантов удобно разложить на три группы: по тому, что каждая из них требует от сетевой службы и чем за это платит.
Требуют правила DMZ → LAN: два варианта со шлюзами. Правило узкое — конкретный порт, белый список, взаимная проверка сертификатов, — но оно нужно.
Не требуют ничего и дают обычный HTTP: обратный HTTP. Соединение открывает внутренняя сторона, дальше это полноценный синхронный вызов со всеми свойствами HTTP, включая потоковую передачу.
Не требуют ничего, но платят асинхронным швом: Kafka и Artemis. Здесь появляются таймауты, корреляция, идемпотентность, потолок производительности и невозможность потоковой передачи.
Отсюда порядок вопросов.
Можно ли получить правило DMZ → LAN? Задавать его надо первым, до всякого проектирования. Если да — берите шлюзы, это самая простая и быстрая конструкция, и городить мост через очередь при наличии такой возможности странно.
Если нельзя — приемлем ли экспериментальный статус механизма? Обратный HTTP архитектурно лучше брокерных вариантов почти во всём: меньше участков, нет асинхронного шва, работает потоковая передача, накладные расходы в единицы миллисекунд. Вся цена — в пометке «экспериментальный» и в том, что реализация фактически одна. Архитектор в одиночку этот вопрос не решит. Его решают вместе с эксплуатацией и безопасностью, и в разных компаниях по-разному.
Если и это не подходит — какой брокер? Здесь для нас вопрос закрыт. Kafka в этой роли заставляет писать запрос-ответ самому, Artemis даёт его готовым. Разница в две с половиной тысячи строк кода, которые иначе придётся сопровождать. Kafka остаётся отличным выбором для того, для чего она сделана, но это не запрос-ответ.
Если шлюзы — то один управляющий контур или две инсталляции? Зависит от того, сколько у вас API и как часто они меняются. Единый контур окупается там, где счёт идёт на десятки и сотни и где дублирование конфигурации станет постоянным источником расхождений. Две инсталляции проще запустить и понятнее эксплуатировать, пока объём невелик.
И ещё одно соображение, которое стоит держать в голове с самого начала. Брокерные варианты дают не только обход сетевого ограничения, но и развязку по времени: если внутренняя система лежит, запросы копятся в очереди и ждут. Правда, при синхронном контракте наружу эта развязка почти бесполезна — клиент всё равно ждёт и всё равно получит таймаут. Если же вы допускаете асинхронный контракт хотя бы для части операций, картина меняется, и брокер начинает окупаться уже по-настоящему.
Вместо вывода
Универсального ответа здесь нет, и подозреваю, что не будет. За ограничением DMZ → LAN стоит организационная задача, просто записанная языком сетевых правил. И решается она соответственно: сначала разговор с безопасностью о том, что вообще допустимо, и только потом выбор конструкции.
Если разрешено узкое правило — берите шлюзы и не усложняйте. Если нет и вы готовы жить с экспериментальным механизмом — обратный HTTP даст вам обычный синхронный вызов почти без накладных расходов. Если и это неприемлемо — берите брокер с нормальным запросом-ответом, закладывайте ограничения по производительности и потоковой передаче и сразу продумывайте идемпотентность.
А чего точно не стоит делать — так это строить request/reply поверх транспорта, который для него не предназначен. Мы попробовали, оно работает, и мы бы не стали повторять.
Частые вопросы
Как опубликовать внутренний API наружу, если соединения из DMZ в LAN запрещены?
Сделать так, чтобы соединение открывала внутренняя сторона. Запрет касается направления установления соединения, данные по нему могут идти в обе стороны. На этом построены обратный HTTP в HAProxy и мосты через брокер в DMZ на Kafka или Artemis.
Можно ли сделать синхронный request-reply через Kafka?
Можно, но штатного механизма в Kafka нет. Корреляцию запросов, доставку ответа на нужный узел шлюза и очистку зависших запросов придётся писать самим. У нас это вылилось примерно в 2500 строк Java.
Kafka или ActiveMQ Artemis для синхронного вызова через DMZ?
Для request-reply подходит Artemis. В JMS это стандартный паттерн с полями JMSReplyTo и JMSCorrelationID, мост собирается без своего кода. Kafka хороша для потоков событий, для запрос-ответ её придётся дописывать.
Что такое обратный HTTP в HAProxy?
Узел из LAN сам подключается к HAProxy в DMZ, после чего стороны меняются ролями: шлюз отправляет запросы по этому соединению как в обычный upstream. Механизм появился в HAProxy 2.9 и пока помечен как экспериментальный.
Какую нагрузку держит мост через брокер?
На нашем стенде с Artemis устойчиво держалось до 50 запросов в секунду при 95-м перцентиле около 100 мс. Выше пропускная способность резко падала. Чтобы выйти на 500 запросов в секунду, нужно пересматривать размещение брокера.
На какой платформе собраны эти схемы?
На NEOMSA APIM от Neoflex. Шлюзы платформы разворачиваются в DMZ и LAN под одним управляющим контуром или как две отдельные инсталляции. Micro Integrator из её состава мы использовали в брокерных вариантах. Описание платформы и состав модулей есть на сайте neomsa.ru.
skovpen
Так получается ответ на вопрос "Как опубликовать внутренний API из DMZ, если соединения в LAN запрещены: пять подходов, которые мы проверили" - при проектировании заменить kafka на брокер, у которого в sdk reply-replay под капотом?