Как мы ускорили MTProto-прокси в 25 раз, не переписывая его с нуля

1. Проблема

Запустили MTProto-прокси на Python. 2500 пользователей, AdTag для рекламного канала, авторизация каждого клиента по секрету. На 4 vCPU процесс упирался в 80-90% CPU при 70 одновременных соединениях.

Python × 1 процесс × 70 клиентов = 80% CPU

Дальше расти некуда.

2. Поиск узкого места

py-spy показал:

handle_handshake:    27% CPU     ← перебор 2500 пользователей
create_aes_ctr:      27% CPU     ← создание AES-шифраторов
OpenSSL __init__:    27% CPU     ← инициализация контекстов

54% процессора уходило на поиск пользователя при каждом handshake.

Почему? Протокол MTProto не передаёт ID пользователя открыто. Сервер обязан перебрать всех:

for user in all_2500_users:
    key = SHA256(prekey + user.secret)
    decrypted = AES_CTR_decrypt(key, handshake)
    if decrypted[56:60] in VALID_TAGS:  # нашли!
        break

3. Попытка №1: Nginx + 4 воркера

Telegram клиенты
    ↓
Nginx (port 8989, stream proxy)
    ├→ Python worker 8080
    ├→ Python worker 8081
    ├→ Python worker 8082
    └→ Python worker 8083

Результат: нагрузка распределилась, но каждый воркер всё ещё тратил 27% CPU на перебор. Суммарно — те же 27% × 4 ядра.

4. Решение: вынос handshake в Go

Ключевая идея: перебор пользователей — чистая криптография (SHA256 + AES). Go делает это в 25 раз быстрее Python:

Операция Python Go
SHA256 × 2500 45ms 2ms
AES-CTR × 2500 120ms 8ms
Итого на handshake 25ms 1ms

Архитектура:

Telegram клиенты
    
Nginx (port 8989)
    ├→ Python worker 8080 ──┐
    ├→ Python worker 8081 ──┤
    ├→ Python worker 8082 ──┤  все ходят в один Go-сервис
    └→ Python worker 8083 ──┘  за 1ms вместо 25ms
             
    Go verify service (127.0.0.1:9999)
     - парсит config8080.py напрямую
     - загружает 2500 секретов
     - принимает 64 байта handshake
     - перебирает пользователей
     - возвращает: {"success": true, "user": "user_996"}

5. Реализация

Go-сервис (120 строк): парсит Python-конфиг, слушает TCP, ищет пользователя.

Python-патч (15 строк): замена цикла for user in config.USERS на вызов Go:

# Было:
for user in config.USERS:
    secret = bytes.fromhex(config.USERS[user])
    ...  # 200 строк криптографии

# Стало:
go_user, _ = go_verifier.verify(handshake)  # 1ms, 0% CPU
if go_user:
    user = go_user
    ...  # только проверить результат

6. Результаты

До оптимизации

1 процесс: 70 соед, 24% CPU
handle_handshake: 27% CPU (OwnTime)
create_aes_ctr: 27% CPU

После оптимизации

4 процесса: 160 соед, 7-9% CPU каждый
handle_handshake: 0% CPU (ждёт Go)
Go-сервис: 1796 запросов, 99.6% успех, 1ms среднее

Сравнение

Метрика До После
Время handshake 25ms 1ms
CPU на handshake 27% 0%
CPU/соединение 0.45% 0.25%
Эффективность (Mbit/s / 1% CPU) ~0.3 ~0.6
Макс. соединений ~280 ~600+

7. Мониторинг

Написали mtproto_live.py — тепловая карта в реальном времени:

Время     | Соед | Плз |   Down |    Up | Mbit/s |  CPU |  c0 |  c1 |  c2 |  c3 | Mbit/%
23:27:49  |  62 |  17 | 0.01MB | 0.04MB |  0.07 | 12.6% | 11.2% | 11.2% | 9.5% | 18.3% |  0.01

И mtproto_stats.py — сводный отчёт по всем воркерам с топ-50 пользователей по трафику.

Итог

Не переписывая весь прокси, вынесли самое узкое место в Go-микросервис. Python остался управлять соединениями и проксированием. Go делает только криптографию — то, в чём он быстрее в 25 раз.

150 строк Go + 15 строк Python = экономия 27% CPU и возможность обслуживать вдвое больше клиентов на том же железе.

Теперь самый быстрый прокси работает в боте @npokcubot

← Новая публикация