Назад к блогу

Go vs Python для сетевых утилит: опыт из fxTunnel

Реальный опыт разработки сетевых инструментов на Go и Python — goroutines vs asyncio, потребление памяти, кросс-компиляция и дистрибуция бинарников.

Go vs Python для сетевых утилит: опыт из fxTunnel

Когда я начинал разработку fxTunnel — утилиты для проброса TCP-туннелей через NAT — у меня был прототип на Python. Asyncio, aiohttp, всё как полагается. Прототип работал, но когда дело дошло до продакшена и дистрибуции — я переписал всё на Go. Не потому что «Go быстрее», а потому что для конкретно этой задачи Go оказался правильным выбором. Вот сравнение из моего опыта.

Конкурентность: goroutines vs asyncio

Первое, что бросается в глаза при написании сетевого кода — модель конкурентности.

Python с asyncio:

import asyncio

async def handle_connection(reader, writer):
    while True:
        data = await reader.read(4096)
        if not data:
            break
        writer.write(process(data))
        await writer.drain()
    writer.close()

async def main():
    server = await asyncio.start_server(handle_connection, '0.0.0.0', 8080)
    async with server:
        await server.serve_forever()

Go:

func handleConnection(conn net.Conn) {
    defer conn.Close()
    buf := make([]byte, 4096)
    for {
        n, err := conn.Read(buf)
        if err != nil {
            return
        }
        processed := process(buf[:n])
        conn.Write(processed)
    }
}

func main() {
    ln, _ := net.Listen("tcp", ":8080")
    for {
        conn, _ := ln.Accept()
        go handleConnection(conn)
    }
}

Синтаксически оба варианта лаконичны. Но разница проявляется под нагрузкой. В Python asyncio — это однопоточный event loop. Если одна корутина начнёт делать что-то CPU-bound (даже случайно — логирование, сериализация, валидация), она заблокирует весь цикл. В Go каждая goroutine — лёгкий поток, управляемый рантаймом. Scheduler Go распределяет goroutines по системным потокам, и блокирующий вызов в одной не останавливает остальные.

На практике: в Python-версии fxTunnel при 500+ одновременных соединениях я начал замечать периодические задержки. Не из-за самого парсинга заголовков — а из-за того, что GC Python срабатывал в event loop и подвешивал все соединения на 10-30ms. В Go-версии с тем же количеством соединений латентность оставалась стабильной.

Замеры: цифры из реального проекта

Я провёл сравнительное тестирование обеих версий на одном и том же сервере (4 vCPU, 8GB RAM, Ubuntu 22.04). Тест: проксирование TCP-трафика через туннель, эхо-сервер на другом конце. Замеры не были суперстрогими — скорее порядок величин, чтобы понять разницу.

МетрикаPython (asyncio)Go 1.22
Соединений/сек (новых)~2,800~12,000
Память при 1000 соединениях~180 MB~45 MB
Время старта~280 ms~8 ms
Латентность p99 (1000 conn)~12 ms~2 ms
Размер бинарника~30 MB (PyInstaller)~8.5 MB

Разница в несколько раз по соединениям/сек — не потому что Python медленный, а потому что asyncio тратит больше ресурсов на управление корутинами, и GC работает агрессивнее при большом количестве объектов.

Память — ключевой фактор. Каждая goroutine стартует с 2KB стека и растёт по мере необходимости. Каждая корутина Python тянет за собой фреймы, словари для локальных переменных и объекты Future — это дороже.

Оговорка: я не профилировал Python-версию глубоко. Возможно, с uvloop и тюнингом GC разрыв был бы меньше. Но для меня решающим фактором была не производительность, а дистрибуция.

Дистрибуция: где Go побеждает безоговорочно

Вот реальная причина, почему fxTunnel написан на Go:

# Собрать для всех платформ
GOOS=linux GOARCH=amd64 go build -o fxtunnel-linux-amd64
GOOS=darwin GOARCH=arm64 go build -o fxtunnel-darwin-arm64
GOOS=windows GOARCH=amd64 go build -o fxtunnel-windows-amd64.exe
GOOS=linux GOARCH=arm64 go build -o fxtunnel-linux-arm64

Четыре команды — четыре бинарника. Пользователь скачивает один файл и запускает. Без Python, без pip, без виртуальных окружений, без requirements.txt, без конфликтов версий.

С Python для дистрибуции CLI-утилиты варианты такие:

  • PyInstaller — 30+ MB бинарник, медленный старт, проблемы с антивирусами на Windows
  • pip install — требует Python на машине пользователя, конфликты зависимостей
  • Docker — для сетевой утилиты, которую пользователь запускает локально, это неудобно

Для fxTunnel, который ставят на VPS, локальные машины, Raspberry Pi — Go с его кросс-компиляцией был единственным разумным выбором.

Когда Python всё-таки лучше

Я не фанатик Go. Вот задачи, где я по-прежнему выбираю Python:

API-клиенты и интеграции. Когда нужно работать с REST API, парсить JSON, трансформировать данные — Python с его экосистемой (httpx, pydantic, rich) делает это быстрее в разработке. Мой python-ozon-api — тому пример.

Прототипирование. Проверить идею за вечер — Python. REPL, Jupyter, динамическая типизация — всё это ускоряет эксперименты.

Data processing. Если сетевая утилита собирает метрики и потом их анализирует — pandas и numpy не имеют аналогов в Go.

Скрипты автоматизации. Для одноразовых задач, cron-скриптов, ботов — Python быстрее в разработке, и накладные расходы рантайма не имеют значения.

Гибридный подход: Go + Python

В Tirebase я пришёл к тому, что разные части системы пишутся на разных языках:

fxtunnel (Go) — ядро, туннелирование, CLI
    gRPC
fxtunnel-manager (Python) — управление, мониторинг, API
    REST
Dashboard (Vue.js) — веб-интерфейс

Go делает то, что должен делать Go: обрабатывает тысячи соединений, проксирует трафик, работает как демон. Python делает то, что должен делать Python: предоставляет удобное API для управления, собирает и агрегирует метрики, отправляет уведомления.

Связь между ними — gRPC. Protobuf-контракт гарантирует совместимость, а кодогенерация работает для обоих языков. Правда, поддержка protobuf в Python иногда раздражает — генерация кода не такая гладкая, как в Go, и типизация неполная. Но работает.

Чек-лист: что выбрать

Прежде чем начинать проект, я задаю себе эти вопросы:

  1. Бинарник без зависимостей критичен? Go.
  2. Тысячи одновременных соединений? Go.
  3. Кросс-компиляция для ARM/Windows/Mac? Go.
  4. Интеграция с внешними API, парсинг данных? Python.
  5. Быстрый прототип за вечер? Python.
  6. Нужно и то, и другое? Разделите систему на компоненты и используйте каждый язык для своей задачи.

Итог

Нет лучшего языка для сетевых утилит — есть задача и инструмент. fxTunnel как ядро — на Go, потому что дистрибуция, память и латентность критичны. Управление fxTunnel — на Python, потому что скорость разработки и экосистема важнее наносекунд.

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