Всем привет, меня зовут Стас, я техлид в Mish Product Lab.
Тема возникла не просто так: внутри команды у нас было немало споров и дискуссий о том, какой инструмент для проксирования и терминации SSL лучше использовать в различных ситуациях. Изначально все наши гипотезы были основаны больше на личных предпочтениях, чем на реальных данных. Мы долго спорили, надеясь, что истина будет где-то рядом с нашими любимыми решениями. Но в итоге пришли к выводу, что единственный способ получить действительно объективный ответ — это протестировать и сравнить различные варианты на практике.
Именно так родилась идея провести сравнительный анализ производительности HAProxy, Envoy, Nginx, Caddy и Traefik с поддержкой SSL/TLS. Мы хотели понять, какой из инструментов «из коробки» предоставляет наилучшую производительность и минимальные накладные расходы, особенно при обработке SSL-трафика, который, как известно, требует дополнительных ресурсов из-за шифрования и дешифрования.
Важно подчеркнуть, что наша задача не заключалась в поиске идеального или максимально оптимизированного решения. Для глубоких и тонких настроек всегда лучше обратиться к DevOps-инженерам, которые обладают необходимыми компетенциями и понимают нюансы каждого инструмента. Нашей целью было скорее провести «полевые испытания» и составить заметки, которые могли бы помочь нам самим и будущим поколениям разработчиков быстрее принимать решения на основе хотя бы приблизительных ориентиров.
Итак, мы готовы поделиться нашими наблюдениями и результатами тестов.
Работаем с тестовыми стендами
Для обеспечения объективности и прозрачности тестирования мы выбрали три отдельных стенда, каждый из которых отвечал за свою часть сценария нагрузки. Разделение ролей стендов было принципиально важным для минимизации возможных взаимных влияний и искажений результатов. Это позволило максимально точно зафиксировать поведение каждого инструмента и компонента инфраструктуры.
Все тестовые стенды были размещены в одной сети, что гарантировало стабильную и предсказуемую сеть без значительных задержек. Никаких настроек ядра, голые системы.
Тестовый стенд 1 (K6) предназначался для генерации и управления трафиком при помощи инструмента K6. Его ресурсы (32 vCPU, 128 ГБ RAM) обеспечивали возможность генерировать существенную нагрузку и давала нам уверенность в том, что сам инструмент генерации не станет узким местом.
Тестовый стенд 2 (Web) был основным объектом тестирования. Здесь были установлены различные инструменты для проксирования (HAProxy, Envoy, Nginx, Caddy, Traefik) и мониторинг штатными средствами хостера. Это позволило нам не только протестировать производительность каждого решения, но и подробно отслеживать потребление ресурсов. Ресурсы 32 vCPU, 128 ГБ RAM.
Тестовый стенд 3 (Backend) выполнял роль типичного приложения (backend), написанного на Golang, которое должно было принимать и обрабатывать запросы, проходящие через прокси. Ресурсы 8 vCPU, 32 ГБ RAM.
Такое разделение ролей и ресурсов дало возможность получить максимально чистые и объективные данные о производительности каждого инструмента.
Работаем с бэкэндом
Сервис делали на Golang, для обработки запросов использовали fasthttp — никаких излишеств.
package main
import (
"fmt"
"log"
"github.com/valyala/fasthttp"
)
func main() {
fmt.Println("Listening on :8080")
requestHandler := func(ctx *fasthttp.RequestCtx) {
switch string(ctx.Path()) {
case "/":
handleRoot(ctx)
default:
ctx.Error("Unsupported path", fasthttp.StatusNotFound)
}
}
if err := fasthttp.ListenAndServe(":8080", requestHandler); err != nil {
log.Fatalf("Error in ListenAndServe: %s", err)
}
}
func handleRoot(ctx *fasthttp.RequestCtx) {
ctx.SetStatusCode(fasthttp.StatusOK)
ctx.SetBodyString("hello world")
}
Запускаем стресс-тест на этот сервис: 50 000 rps, 1 минута
K6 (генерация трафика и проверка ответа, http code и body):
import http from 'k6/http';
import { check } from 'k6';
export let options = {
scenarios: {
constant_request_rate: {
executor: 'constant-arrival-rate',
rate: 50000,
timeUnit: '1s',
duration: '60s',
preAllocatedVUs: 1000,
maxVUs: 100000,
},
},
};
export default function () {
let res = http.get('http://10.0.0.8:8080');
check(res, {
'status is 200': (r) => r.status === 200,
'body contains expected content': (r) => typeof r.body === 'string' && r.body.includes('hello world'),
});
}
scenarios: (100.00%) 1 scenario, 100000 max VUs, 1m30s max duration (incl. graceful stop):
* constant_request_rate: 50000.00 iterations/s for 1m0s (maxVUs: 1000-100000, gracefulStop: 30s)
█ THRESHOLDS
checks
✓ 'rate>=0.95' rate=100.00%
http_req_duration
✓ 'p(95)<500' p(95)=387.29µs
http_req_failed
✓ 'rate<0.01' rate=0.00%
█ TOTAL RESULTS
checks_total.......................: 5993052 99880.644605/s
checks_succeeded...................: 100.00% 5993052 out of 5993052
checks_failed......................: 0.00% 0 out of 5993052
✓ status is 200
✓ body contains expected content
HTTP
http_req_duration.......................................................: avg=283.17µs min=110.92µs med=234.52µs max=235.41ms p(90)=324.36µs p(95)=387.29µs
{ expected_response:true }............................................: avg=283.17µs min=110.92µs med=234.52µs max=235.41ms p(90)=324.36µs p(95)=387.29µs
http_req_failed.........................................................: 0.00% 0 out of 2996526
http_reqs...............................................................: 2996526 49940.322303/s
EXECUTION
dropped_iterations......................................................: 3476 57.931271/s
iteration_duration......................................................: avg=349.05µs min=142.88µs med=277.87µs max=1.06s p(90)=374.7µs p(95)=453.85µs
iterations..............................................................: 2996526 49940.322303/s
vus.....................................................................: 15 min=10 max=183
vus_max.................................................................: 1044 min=1044 max=1044
NETWORK
data_received...........................................................: 438 MB 7.3 MB/s
data_sent...............................................................: 246 MB 4.1 MB/s
Результат — тест выполнен успешно.
Максимальная задержка — max=47.52ms
Пропускная способность — 49964.433616/s
Настройка и тестирование проксирования
K6 (генерация трафика и проверка ответа, http code и body):
import http from 'k6/http';
import { check } from 'k6';
export let options = {
scenarios: {
constant_request_rate: {
executor: 'constant-arrival-rate',
rate: 50000, // количество запросов в секунду
timeUnit: '1s', // единица времени для rate
duration: '60s', // длительность теста
preAllocatedVUs: 1000, // количество предварительно выделенных виртуальных пользователей
maxVUs: 100000, // максимальное количество VU, которое может быть задействовано
},
},
thresholds: {
checks: ['rate>=0.95'],
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<500'],
},
};
export default function () {
let res = http.get('https://test-backend.mish.design');
check(res, {
'status is 200': (r) => r.status === 200,
'body contains expected content': (r) => typeof r.body === 'string' && r.body.includes('hello world'),
});
}
Получаем сертификат:
sudo apt install certbot
sudo certbot certonly --standalone -d test-backend.mish.design
Сертификаты появятся тут:
/etc/letsencrypt/live/test-backend.mish.design/fullchain.pem
/etc/letsencrypt/live/test-backend.mish.design/privkey.pem
Теперь совместим их — это нужно для HAProxy:
cd /etc/letsencrypt/live/test-backend.mish.design/
cat fullchain.pem privkey.pem > haproxy.pem
HAProxy
Начать решили именно с HAProxy, так как он позволяет максимально быстро и эффективно протестировать работоспособность архитектуры без сложной предварительной настройки инфраструктуры.
docker-compose.yml
HAProxy запускается через Docker Compose, что позволяет легко воспроизводить окружение на разных серверах и машинах разработчиков.
Порты 80 и 443 прокидываются наружу, то есть сервис явно ориентирован на обработку HTTP и HTTPS-запросов.
SSL-сертификат Let’s Encrypt подключается напрямую через volume, таким образом сервис сразу готов к работе в защищённом режиме (HTTPS).
services:
haproxy:
image: haproxy:latest
container_name: haproxy
ports:
- "80:80"
- "443:443"
volumes:
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
- /etc/letsencrypt/live/test-backend.mish.design/haproxy.pem:/usr/local/etc/haproxy/certs/haproxy.pem:ro
haproxy.cfg
HAProxy настроен в режиме http, и весь трафик, поступающий на порт 443, перенаправляется в backend-сервис, указанный в конфигурации (10.0.0.8:8080).
defaults
mode http
frontend main
bind *:443 ssl crt /usr/local/etc/haproxy/certs/haproxy.pem
default_backend test_backend
backend test_backend
server test_backend_server 10.0.0.8:8080 check
Запускаем созданный выше сценарий
scenarios: (100.00%) 1 scenario, 100000 max VUs, 1m30s max duration (incl. graceful stop):
* constant_request_rate: 50000.00 iterations/s for 1m0s (maxVUs: 1000-100000, gracefulStop: 30s)
█ THRESHOLDS
checks
✓ 'rate>=0.95' rate=100.00%
http_req_duration
✓ 'p(95)<500' p(95)=1.16ms
http_req_failed
✓ 'rate<0.01' rate=0.00%
█ TOTAL RESULTS
checks_total.......................: 5967620 99455.517508/s
checks_succeeded...................: 100.00% 5967620 out of 5967620
checks_failed......................: 0.00% 0 out of 5967620
✓ status is 200
✓ body contains expected content
HTTP
http_req_duration.......................................................: avg=759.38µs min=330.64µs med=571.94µs max=1.07s p(90)=861.18µs p(95)=1.16ms
{ expected_response:true }............................................: avg=759.38µs min=330.64µs med=571.94µs max=1.07s p(90)=861.18µs p(95)=1.16ms
http_req_failed.........................................................: 0.00% 0 out of 2983810
http_reqs...............................................................: 2983810 49727.758754/s
EXECUTION
dropped_iterations......................................................: 16192 269.8536/s
iteration_duration......................................................: avg=984.55µs min=360.44µs med=643.34µs max=1.36s p(90)=968.37µs p(95)=1.35ms
iterations..............................................................: 2983810 49727.758754/s
vus.....................................................................: 34 min=28 max=357
vus_max.................................................................: 1293 min=1293 max=1293
NETWORK
data_received...........................................................: 374 MB 6.2 MB/s
data_sent...............................................................: 108 MB 1.8 MB/s
Нагрузка на систему


Основные выводы:
CPU
Загрузка до ~900% (9 ядер), стабильна весь тест → CPU справился.
K6 Test Summary
RPS (успешные): 49,728 / сек
Ошибки: 0% (http_req_failed = 0.00%)
Дропнутые итерации: 16,192 (≈0.5%)
Время ответа p(95): 1.16 ms
Макс. задержка: 1.07s
Объём данных:
Получено: 374 MB (6.2 MB/s)
Отправлено: 108 MB (1.8 MB/s)
VUs использовано: 28–357
Макс. допустимо: 1293 VUs
