Проксирование из коробки: сравнительный анализ HAProxy, Envoy, Nginx, Caddy и Traefik

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Всем привет, меня зовут Стас, я техлид в 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

Источник: https://habr.com/ru/articles/900438/


Интересные статьи

Интересные статьи

Анализ большого объема логов ‒ сложный и длительный процесс, и обычные алгоритмы редко выявляют больше, чем система активной защиты. Поэтому логичным и перспективным решением становится применение маш...
Netflix представила свою самую дешёвую подписку с рекламой в ноябре 2022 года для пользователей в США и 11 других стран. Сегодня компания отчиталась о 40 млн клиентов, которые перешли на этот тариф.
Применять паттерны программирования — хорошая практика. К сожалению, на проектах CMS Bitrix редко встречаются примеры использования. В статье я покажу на примере, как можно использовать паттерн D...
Общение с клиентом в переписке сильно отличается от общения вживую или по телефону. Одно из главных отличий — не всегда можно понять и отследить, «услышал» ли нас человек или сообщение ушло впустую.&n...
Здравствуйте. Я уже давно не пишу на php, но то и дело натыкаюсь на интернет-магазины на системе управления сайтами Битрикс. И я вспоминаю о своих исследованиях. Битрикс не любят примерно так,...