Как устроена кнопка «Авто»
Когда в подписке есть локация «Авто / Оптимальный», раскидывающая пользователя по нескольким exit-нодам, — это не магия, а конкретный механизм: отдельный шаблон подписки с injectHosts плюс балансировщик xray. Навигатор «Авто» — обычный host, но с привязкой к шаблону-балансиру. В шаблоне ключ injectHosts[0].selector — жёсткий список UUID хостов (type: uuids), tagPrefix: proxy. Эти хосты инжектятся в клиентскую конфигурацию как outbounds proxy, proxy-2 и так далее; балансировщик выбирает между ними по селектору ["proxy"].
Важно: ноги балансира — ровно то, что перечислено в injectHosts, а не «все скрытые хосты автоматически».
Правила состава ног
- Ноги должны быть скрытыми хостами (
isHidden: true). Видимый UUID коллизит сам с собой — он уже присутствует в подписке как прямая локация — и панель молча выкидывает его из рендера плеч. Классический итог: настроено 4 плеча, у пользователей доезжают 2, явной ошибки нигде нет. - Один вход на ноду на порт. Второй вход той же ноды — только на другом порту (другой endpoint), иначе дубль.
- Отдельный инбаунд для плеча не обязателен: плечо может жить на общем инбаунде с видимым хостом. Важно лишь, чтобы хост-плечо был скрытым и попадал в
injectHosts.
Управление пулом — аддитивно и обратимо: снапшот шаблона → правка списка UUID (добавить/убрать элемент, не пересобирать) → PATCH → проверить в выдаче подписки, что число proxy-outbounds совпало с ожидаемым.
Пара «стратегия ↔ источник RTT»
Частая ошибка — стратегия без парного observatory-блока:
leastLoad↔burstObservatory(pingConfig);leastPing↔observatory(probeUrl/subjectSelector).
Перепутать — получить балансировщик без источника RTT, который ведёт себя непредсказуемо. Меняя стратегию, всегда меняйте и парный блок.
Медленное плечо: почему «моросит»
Одиночные ноды могут быть здоровы — синтетика показывает UP по каждой, — а пользователи на «Авто» жалуются: задержка скачет, страницы то летают, то замирают. Причина в том, что observatory меряет RTT пробой от плеча до контрольной точки (например gen_204), а не сквозную задержку до реального выхода. Далёкое медленное плечо (условно ~500 мс против ~180 мс у остальных) балансировщик считает равноценным и льёт на него заметную долю коннектов — в разобранном кейсе до 38%. Пользователь видит то 180, то 500 мс. Это и есть «моросит».
Лечение — убрать медленное плечо из пула явно, не полагаясь на leastLoad. При желании оставить его отдельной ручной локацией. И держать инвариант в мониторинге: плечо с реальной задержкой выше порога — алерт, чтобы оно не вернулось в пул незамеченным. Однородность пула по латентности важнее его размера.
Входной инбаунд балансировщика — не фантом
При чистке конфигурации легко найти инбаунд, который ни одна нода не слушает напрямую, решить «мёртвый, сношу» — и у пользователей пропадёт вся автоматическая локация. Этот инбаунд — входная точка балансировщика: к нему привязан хост-навигатор, трафик идёт через шаблон и его плечи, а не через прямой листенер. На уровне «кто слушает порт» он выглядит пустым, но несёт клиенту целую локацию.
Правило: перед сносом любого инбаунда сверяйте список хостов. Если на инбаунд ссылается хоть один хост (inbound.configProfileInboundUuid) — он отдаётся пользователю. Пугает имя — переименуйте в balancer-in, но не удаляйте.
Подводные камни
nodes[]у хоста — no-op без injectHosts. Клиент идёт поaddress, а не поnodes[].- Анти-рецепт: observatory + leastPing под нагрузкой. При 300–400 Мбит и выше observatory начинает прыгать между соседними по пингу нодами каждые ~10 секунд — внутри сессии рвётся контекст (cookies, гео-IP). К тому же ICMP-подобный пробинг не равен реальному TCP-handshake: сервер отвечает на ping, а xray на нём мог отвалиться. Для failover между exit-нодами надёжнее внешний nginx-stream: переключение ~3 секунды, реальный TCP-check, в логах видно, что переключилось.
- Коллизия «видимый хост + его нога на одном reality-инбаунде». При
fingerprint: chromeClientHello детерминированный — две идентичные сессии на один ключ, гонка. Разводите видимый хост и ногу по разным инбаундам и ключам (например :443 vision и :8443 xhttp). - Список локальных direct-доменов может жить только в основном шаблоне. В балансир-шаблон его нужно перенести отдельно, иначе локальные сервисы поедут через VPN.
Вывод. Балансировщик exit-нод в Xray — это injectHosts (жёсткий список UUID скрытых хостов) плюс правильная пара стратегия/observatory. Главные грабли: положить в пул видимый хост (выпадет), медленную далёкую ногу (часть пользователей «моросит») или перепутать leastLoad с burstObservatory. Управляйте пулом аддитивно, со снапшотом, и проверяйте выдачу подписки после каждой правки.