Контекст
29-30 января. Безопасность есть, тесты есть, баги починены. Теперь — скорость. fxTunnel работает, но каждое соединение через туннель добавляет задержку. Сколько именно и можно ли меньше? Продукт в продакшене, профиль нагрузки понятен — самое время оптимизировать.
sync.Pool — переиспользование буферов
При каждом соединении выделяется буфер для копирования данных (256KB). При сотнях одновременных соединений — это сотни мегабайт, которые создаются и сразу отправляются в сборщик мусора.
sync.Pool — встроенный пул объектов в Go. Создал буфер → использовал → вернул в пул. Следующее соединение берёт готовый вместо создания нового.
var bufPool = sync.Pool{
New: func() any {
buf := make([]byte, 256*1024)
return &buf
},
}
bufPtr := bufPool.Get().(*[]byte)
defer bufPool.Put(bufPtr)
io.CopyBuffer(dst, src, *bufPtr)
Когда это полезно: много одинаковых объектов с коротким временем жизни. Буферы, временные структуры, кодеки. Для долгоживущих объектов — не подходит, пул может их выбросить при GC. Применил sync.Pool для кодека протокола и UDP-фреймов тоже. Zero-allocation на горячем пути.
Пул yamux-стримов
Когда браузер загружает страницу, он открывает 12+ параллельных запросов (HTML, CSS, JS, картинки). Каждый запрос = открытие нового yamux-стрима = round-trip к клиенту. 12 round-trip'ов = заметная задержка. Решение: при подключении клиента заранее открываю 8 стримов. Когда приходит запрос — беру готовый из пула. Нет round-trip, нет задержки. Пул пуст — открываю новый как fallback. Burst-загрузка страницы стала ощутимо быстрее.
TCP-тюнинг
- TCP_NODELAY — отключает алгоритм Nagle, который копит мелкие пакеты. Для веба и SSH — критично.
- Буферы сокетов 512KB — увеличил с дефолтных для лучшей пропускной способности.
- yamux window 4MB — увеличил с 1MB. Больше данных «в полёте» = лучше утилизация канала.
Happy Eyeballs
Помните IPv4/IPv6 из третьей статьи? Тогда был sequential fallback: сначала IPv4, потом IPv6. Работает, но если IPv4 не отвечает — ждём таймаут (секунды!) прежде чем попробовать IPv6. Теперь — Happy Eyeballs: запускаем IPv4 сразу, IPv6 через 50ms. Кто первый ответит — того и берём. Максимум 50ms задержки вместо секунд. Плюс кэш адресов: при создании туннеля один раз проверяем, на каком адресе слушает сервис, и запоминаем. Дальше все соединения идут напрямую.
Мелочи, которые складываются
- Atomic counter вместо crypto/rand для ID соединений.
crypto/randдорогой — обращается к ОС за энтропией. Для внутренних ID хватит счётчика. - io.CopyBuffer вместо io.Copy — с pooled-буферами.
io.Copyвыделяет 32KB каждый раз.io.CopyBufferиспользует наш буфер из пула.
Prometheus: мониторинг
Как понять, что оптимизации работают? Метрики.
r.Handle("/metrics", promhttp.Handler())
Одна строка — и есть endpoint /metrics с десятками метрик: количество запросов, латентность (гистограмма), горутины, память. Добавил кастомные: активные туннели, активные соединения, латентность по роуту. Подключил Grafana — видно всё в реальном времени.
Install-скрипт
Бонус: bash-скрипт для установки CLI-клиента. Определяет ОС и архитектуру, скачивает правильный бинарник:
curl -fsSL https://myserver/install.sh | sh
Одна команда — клиент установлен.
Итог
29-30 января: - sync.Pool для буферов — zero-allocation - Пул yamux-стримов — быстрая burst-загрузка - TCP_NODELAY + большие буферы — минимальная латентность - Happy Eyeballs — 50ms fallback вместо секунд - Prometheus — мониторинг в реальном времени - Install-скрипт — установка одной командой
3e87fcb perf(proxy): optimize tunnel proxying for minimal overhead
d16564a perf(server): add yamux stream pool
74511e7 feat: add Prometheus metrics endpoint
f282bf7 perf(client): cache resolved local address
875df70 perf(client): race IPv4/IPv6 in parallel
a0f220a feat(web): add install script with platform picker
В следующей части: инспекция трафика — делаем свой мини-Charles. RingBuffer, SSE-стриминг и каждый HTTP-запрос в реальном времени.