Разница между HTTP/2 мультиплексированием и SOCKS5 при парсинге через прокси: где теряется скорость
Содержание
- Что вообще происходит под капотом
- Мультиплексирование: экономия на handshake
- Где SOCKS5 реально быстрее
- Таблица: что где теряется
- Пример: парсер на Python с обоими режимами
- Когда прокси ломает мультиплексирование
- Кейс с парсингом маркетплейса
- Что делать на практике
- Настройки, которые реально влияют
- Итог без итога
Что вообще происходит под капотом
Когда ты гонишь парсинг через прокси, есть два принципиально разных уровня, на которых живёт «много запросов одновременно». HTTP/2 мультиплексирование — это уровень прикладного протокола. SOCKS5 — это уровень сессии, транспорт. Их постоянно путают, потому что оба «умеют много соединений», но делают это совершенно по-разному и с разной ценой.
HTTP/2 берёт одно TCP-соединение и нарезает его на потоки (streams). Каждый stream имеет свой ID, чётные/нечётные, и живёт независимо. SOCKS5 просто устанавливает TCP-туннель до целевого хоста — и всё, дальше клиент сам решает, что туда пихать. Один SOCKS5-коннект = один TCP-сокет до цели. Хочешь 50 параллельных запросов — открываешь 50 SOCKS5-соединений.
Отсюда растёт вся разница в скорости. И в граблях.
Мультиплексирование: экономия на handshake
TCP handshake — это 1 RTT. TLS handshake — ещё 1-2 RTT (TLS 1.2 — два, TLS 1.3 — один). Если ты открываешь 100 соединений к одному хосту, ты платишь этот RTT-налог 100 раз. При задержке 80 мс до сервера это 8000 мс чистого ожидания только на установку, ещё до первого байта полезной нагрузки.
HTTP/2 платит один раз. Дальше все 100 запросов идут в уже установленном соединении. На бумаге — огромная экономия. На практике всё упирается в то, как прокси-сервер обрабатывает эти потоки.
Вот типичный замер через curl с разными режимами:
```bash
curl -w "connect: %{time_connect} tls: %{time_appconnect} total: %{time_total}\n" \
-o /dev/null -s https://target.example/api/item/1
for i in \$(seq 1 50); do
curl -w "%{time_total}\n" -o /dev/null -s \
--proxy socks5h://127.0.0.1:1080 \
https://target.example/api/item/\$i
done | awk '{s+=\$1; n++} END {print "avg:", s/n, "n:", n}'
```
Первый вариант покажет полный handshake. Второй — усреднённое время по 50 запросам, каждый со своим SOCKS5-туннелем. Разница между этими числами и есть та цена, которую ты платишь за архитектуру.
Где SOCKS5 реально быстрее
Парадокс: в некоторых сценариях SOCKS5 обгоняет HTTP/2 мультиплексирование. Причины две.
Первая — head-of-line blocking на уровне TCP. HTTP/2 мультиплексирует потоки поверх одного TCP-соединения. Если один TCP-сегмент потерялся, ядро блокирует доставку всех последующих сегментов, включая данные других streams. Все потоки встают, пока не придёт ретрансмит. При потерях 1-2% на длинном плече (Москва — Франкфурт, например) это превращается в регулярные паузы по 200-400 мс. SOCKS5 с 50 отдельными соединениями теряет только одно из них — остальные 49 едут дальше.
Вторая — реализация прокси. Многие прокси-серверы терминируют HTTP/2 на входе, но внутри открывают обычные HTTP/1.1-соединения к цели. То есть мультиплексирование есть только на участке клиент-прокси, а дальше — снова зоопарк из одиночных коннектов. Выигрыш съедается.
Таблица: что где теряется
| Параметр | HTTP/2 мультиплекс | SOCKS5 (N коннектов) |
|---|---|---|
| Handshake на запрос | 0 после первого | 1 RTT + TLS каждый |
| TCP HOL blocking | Есть, блокирует все streams | Нет, изоляция по сокетам |
| Память на соединение | ~64 КБ буфер + HPACK | ~200 КБ на сокет (ядро) |
| Лимит параллелизма | SETTINGS_MAX_CONCURRENT_STREAMS (обычно 100-128) | Ограничен ulimit -n и портами |
| Стоимость на 1000 запросов | 1 сокет | 1000 сокетов в TIME_WAIT |
| Работа через прокси | Требует CONNECT или h2-туннель | Нативно, любой TCP |
Цифры по памяти — усреднённые для Linux 5.x с дефолтным `net.ipv4.tcp_rmem`. На 1000 параллельных SOCKS5-соединений ты легко упираешься в `ulimit -n 1024` и получаешь `Too many open files` посреди парсинга. Это классические грабли.
Пример: парсер на Python с обоими режимами
Покажу разницу на живом коде. Сначала SOCKS5 через `httpx` с `socksio`:
```python
import asyncio
import httpx
PROXY = "socks5://127.0.0.1:1080"
async def fetch_socks(client, url):
r = await client.get(url)
return r.status_code
async def main_socks(urls):
limits = httpx.Limits(max_connections=200, max_keepalive_connections=0)
async with httpx.AsyncClient(proxy=PROXY, limits=limits, timeout=15) as c:
tasks = [fetch_socks(c, u) for u in urls]
return await asyncio.gather(*tasks, return_exceptions=True)
```
`max_keepalive_connections=0` тут намеренно — каждый запрос открывает новый SOCKS5-туннель. Если поставить keepalive, ты фактически получишь пул TCP-соединений до цели, что уже ближе к тому, что делает HTTP/2, только без мультиплексирования.
Теперь HTTP/2 через тот же прокси, но с явным h2:
```python
import httpx
async def main_h2(urls):
async with httpx.AsyncClient(
proxy="http://127.0.0.1:8080",
http2=True,
limits=httpx.Limits(max_connections=1, max_keepalive_connections=1),
timeout=15,
) as c:
tasks = [c.get(u) for u in urls]
return await asyncio.gather(*tasks, return_exceptions=True)
```
Обрати внимание: `max_connections=1`. Весь параллелизм идёт через streams одного соединения. Если прокси на том конце поддерживает h2 end-to-end — это самый быстрый вариант. Если прокси даунгрейдит до HTTP/1.1 — ты получишь очередь, потому что HTTP/1.1 не умеет параллельные запросы в одном соединении без pipelining (который почти никто не включает).
Когда прокси ломает мультиплексирование
Тут начинается самое интересное. Прокси-сервер может:
1. Поддерживать h2 на входе и на выходе — тогда всё ок.
2. Поддерживать h2 на входе, HTTP/1.1 на выходе — теряешь весь смысл.
3. Работать только через CONNECT-туннель — тогда HTTP/2 идёт end-to-end, прокси видит только зашифрованный поток.
Третий вариант часто самый быстрый, но требует, чтобы прокси умел CONNECT и не резал по SNI. Проверить, что реально происходит, можно так:
```bash
curl -v --proxy http://proxy:8080 --http2 https://target.example/ 2>&1 | grep -E "ALPN|HTTP/2|CONNECT"
```
Если в выводе `ALPN, server accepted to use h2` — хорошо. Если `HTTP/1.1` — прокси даунгрейдит, и мультиплексирование работает только до прокси. Это критично для парсинга: ты думаешь, что гонишь 100 параллельных streams, а по факту стоишь в очереди на одном HTTP/1.1-коннекте к цели.
Кейс с парсингом маркетплейса
Пример: парсер карточек товара, 5000 URL, цель — один домен за прокси во Франкфурте. Задержка клиент-прокси 45 мс, прокси-цель 12 мс.
Первый запуск — SOCKS5, 200 параллельных туннелей, без keepalive. Результат: 5000 запросов за 340 секунд, среднее 68 мс на запрос. Ошибок 4% — часть по таймауту, часть по 429 от целевого сервера, потому что 200 одновременных TCP-соединений с одного IP выглядят как флуд.
Второй запуск — HTTP/2 через тот же прокси, `max_connections=1`. Прокси поддерживал h2 end-to-end. Результат: 5000 запросов за 95 секунд, среднее 19 мс. Потому что handshake один раз, HPACK-сжатие заголовков, и целевой сервер видит один stream-мультиплекс, а не 200 сокетов. 429 не пришло ни разу.
Третий запуск — HTTP/2, но прокси с даунгрейдом до HTTP/1.1. Результат: 5000 запросов за 610 секунд. Хуже SOCKS5, потому что один HTTP/1.1-коннект обрабатывает запросы строго последовательно. Вот она, разница между «поддерживает h2» и «делает вид».
Что делать на практике
Не выбирай между ними вслепую. Сначала выясни, что умеет твой прокси. Потом считай.
Если цель — один домен и прокси держит h2 — иди в HTTP/2. Экономия на handshake окупает всё. Через lexic.ml, например, h2-туннелирование работает end-to-end, и это снимает главную проблему — даунгрейд на выходе.
Если целей много (сотни доменов) — HTTP/2 мультиплексирование теряет смысл, потому что одно соединение к одному хосту. Тут SOCKS5 с пулом коннектов и ротацией выигрывает. Или гибрид: SOCKS5 для установки соединений, HTTP/2 внутри каждого туннеля.
Если потери пакетов выше 1% — SOCKS5 с изоляцией по сокетам надёжнее. HOL blocking в HTTP/2 на плохом канале убивает всю параллельность.
Настройки, которые реально влияют
На стороне клиента для SOCKS5-пула критичны:
```bash
ulimit -n 65535
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
sysctl -w net.ipv4.tcp_tw_reuse=1
```
Без этого 5000 запросов через SOCKS5 упрутся в исчерпание портов или файловых дескрипторов. TIME_WAIT на 60 секунд при 200 rps — это 12000 висящих сокетов, которые жрут порты.
Для HTTP/2 критично:
```nginx
http2_max_concurrent_streams 256;
http2_idle_timeout 60s;
http2_recv_buffer_size 256k;
```
`http2_max_concurrent_streams` по умолчанию 128. Если твой парсер шлёт больше — лишние запросы ждут в очереди, и ты не понимаешь, почему скорость упала. Поднимай до 256, но следи за памятью на прокси.
Итог без итога
HTTP/2 мультиплексирование выигрывает, когда: один домен, хороший канал, прокси держит h2 end-to-end. Проигрывает при потерях пакетов, множестве доменов и даунгрейд-прокси.
SOCKS5 выигрывает на разнородных целях, плохих каналах и когда нужна изоляция запросов. Проигрывает на handshake-налоге и лимитах ОС.
Мерить надо. Один прогон на 1000 URL в обоих режимах даст больше, чем любая теория. И смотри не на среднее время, а на p95 и p99 — хвосты показывают, где именно теряется скорость.