Легковесные веб-серверы занимают особую нишу в инфраструктуре. Они потребляют минимум памяти, обрабатывают тысячи соединений на слабом оборудовании и справляются там, где тяжелые решения начинают задыхаться. Lighttpd построен вокруг этой философии, предлагая модульную архитектуру для проксирования, переписывания URL и детального логирования. Три механизма определяют его функциональность: mod_proxy для распределения нагрузки между бэкендами, mod_rewrite для манипуляций с запросами и mod_accesslog для анализа трафика.

Балансировка нагрузки через mod_proxy

Модуль mod_proxy превращает Lighttpd в reverse proxy, перенаправляя запросы на backend-серверы. Базовая конфигурация требует указания целевого хоста и порта:

server.modules += ( "mod_proxy" )

$HTTP["host"] == "api.example.com" {
    proxy.server = ( "" => (
        ( "host" => "192.168.1.10", "port" => 8080 )
    ))
}

Пустая строка в качестве расширения означает, что все запросы для данного хоста перенаправляются на бэкенд. Можно указать конкретные расширения или пути:

proxy.server = ( ".php" => (
    ( "host" => "127.0.0.1", "port" => 9000 )
))

Lighttpd поддерживает четыре алгоритма балансировки через параметр proxy.balance. Алгоритм "fair" распределяет запросы на основе текущей нагрузки каждого сервера:

$HTTP["host"] == "www.example.org" {
    proxy.balance = "fair"
    proxy.server = ( "" => (
        ( "host" => "10.0.0.10" ),
        ( "host" => "10.0.0.11" ),
        ( "host" => "10.0.0.12" ),
        ( "host" => "10.0.0.13" )
    ))
}

Каждый backend отслеживает количество активных соединений. Новый запрос направляется на сервер с наименьшей нагрузкой. Если один из узлов падает, его запросы автоматически перераспределяются на оставшиеся серверы.

Алгоритм "round-robin" циклически перебирает серверы:

proxy.balance = "round-robin"

Первый запрос идет на 10.0.0.10, второй на 10.0.0.11, третий на 10.0.0.12, четвертый на 10.0.0.13, пятый снова на 10.0.0.10. Простота алгоритма дает предсказуемость, но игнорирует фактическую нагрузку.

Алгоритм "hash" генерирует хеш от request URI и направляет одинаковые URI всегда на один сервер:

proxy.balance = "hash"

Запрос к /api/users/123 всегда попадает на один и тот же backend. Это критично для приложений с локальным кешем на каждом сервере. Повышение cache hit ratio может ускорить приложение в разы.

Алгоритм "sticky" привязывает клиента к серверу на основе IP-адреса. Все запросы от одного клиента обрабатываются одним backend. Useful для сессий без shared storage.

Множественные backend для одного extension создают pool:

proxy.server = ( ".php" => (
    "backend1" => ( "host" => "192.168.1.10", "port" => 9000 ),
    "backend2" => ( "host" => "192.168.1.11", "port" => 9000 ),
    "backend3" => ( "host" => "192.168.1.12", "port" => 9000 )
))

Метки "backend1", "backend2", "backend3" используются в mod_status для отображения статистики. Можно отследить, сколько запросов обработал каждый узел.

Unix domain sockets работают быстрее TCP при локальном взаимодействии:

proxy.server = ( ".php" => (
    ( "host" => "unix:/tmp/php-fastcgi.sock" )
))

Параметр proxy.debug включает детальное логирование для отладки:

proxy.debug = 1

Значения от 0 до 65535 контролируют уровень детализации. Единица показывает базовую информацию о соединениях и ошибках.

Заголовок Forwarded добавляет метаданные о клиенте:

proxy.forwarded = (
    "for" => 1,
    "proto" => 1,
    "host" => 1,
    "by" => 1
)

Backend получает заголовок Forwarded с информацией о реальном IP клиента, протоколе (HTTP/HTTPS), оригинальном Host и IP прокси-сервера.

Remapping host и URL через proxy.header изменяет запросы на лету:

proxy.header = (
    "map-host-request" => (
        "old-domain.com" => "new-domain.com"
    ),
    "map-urlpath" => (
        "/old-path" => "/new-path"
    )
)

Поддержка WebSocket через upgrade:

proxy.server = ( "" => (
    ( "host" => "127.0.0.1", "port" => 6080 ),
    "upgrade" => "enable"
))

Таймаут подключения и чтения настраивается глобально или для конкретного backend. Если backend не отвечает 10 секунд, запрос завершается с ошибкой 504.

Переписывание URL для гибкой маршрутизации

Модуль mod_rewrite манипулирует URL внутренне до обработки запроса. Базовый синтаксис:

server.modules += ( "mod_rewrite" )

url.rewrite-once = (
    "^/id/([0-9]+)$" => "/index.php?id=$1"
)

Regex сопоставляется с полным REQUEST_URI, включая query string. Запрос /id/12345 трансформируется в /index.php?id=12345. Группы в скобках захватываются и доступны через $1, $2, $3.

Разница между url.rewrite-once и url.rewrite-repeat критична. url.rewrite-once применяет первое совпадающее правило и останавливается:

url.rewrite-once = (
    "^/blog/([0-9]+)$" => "/blog.php?post=$1",
    "^/page/(.+)$" => "/page.php?name=$1"
)

url.rewrite-repeat продолжает применять правила, пока они совпадают:

url.rewrite-repeat = (
    "^/script/(.*)$" => "/script.php/$1"
)

Множественные правила в url.rewrite-once эквивалентны Apache RewriteRule с флагом [L].

Сложные паттерны с negative lookahead исключают конкретные пути:

url.rewrite-once = (
    "^/instadir/(?!index\.php|dmoz\.css|admin|img).*" => "$0",
    "^/instadir/([^?]*)(?:\?(.*))?" => "/instadir/index.php?area=browse&cat=$1&$2"
)

Первое правило пропускает запросы к index.php, dmoz.css, директориям admin и img. Второе правило перенаправляет остальные запросы на index.php с параметрами.

Начиная с версии 1.4.50, доступны предопределенные паттерны:

url.rewrite-once = (
    "^/api/(.*)$" => "/api.php?endpoint=$1${qsa}"
)

Паттерн ${qsa} (query string append) добавляет существующий query string к результату. Запрос /api/users?limit=10 становится /api.php?endpoint=users&limit=10.

Другие паттерны включают ${url.path}, ${url.authority}, ${url.query}:

url.rewrite-once = (
    "^/old-site/(.*)$" => "/new-site/${url.path}${qsa}"
)

Encoding модификаторы контролируют экранирование:

url.rewrite-once = (
    "^/search/(.+)$" => "/search.php?q=${esc:1}"
)

Модификатор ${esc:1} экранирует спецсимволы в захваченной группе. ${noesc:1} отключает экранирование. ${tolower:1} конвертирует в lowercase. ${toupper:1} в uppercase.

Условная перезапись на основе заголовков:

$HTTP["host"] =~ "^www\.(.*)$" {
    url.rewrite-once = ( "^/(.*)$" => "/%0/$1" )
}

Паттерн %0 содержит полное совпадение из regex condition. %1 содержит первую захваченную группу, %2 вторую.

Для framework с единой точкой входа:

$PHYSICAL["existing-path"] !~ "" {
    url.rewrite-once = ( "^/(.*)$" => "/index.php/$1" )
}

Условие $PHYSICAL["existing-path"] проверяет существование файла. Если файл не существует, запрос перенаправляется на index.php.

Вариант с url.rewrite-if-not-file:

url.rewrite-if-not-file = (
    "^/forums[^?]*(\?.*)?$" => "/forums.php$1"
)

Правило применяется только если запрошенный путь не является существующим файлом.

Удаление trailing slash через redirect и rewrite:

$HTTP["host"] == "www.example.com" {
    url.redirect-code = 301
    url.redirect = ( "^(.+)/$" => "$1" )
}

Combination с mod_redirect для внешних редиректов:

url.redirect = (
    "^/old-page$" => "https://example.com/new-page"
)

Lighttpd выполняет mod_rewrite до mod_redirect. Порядок модулей в server.modules определяет последовательность обработки.

Форматирование логов для анализа трафика

Модуль mod_accesslog записывает каждый запрос в файл или syslog. Формат по умолчанию близок к Combined Log Format:

server.modules += ( "mod_accesslog" )

accesslog.filename = "/var/log/lighttpd/access.log"
accesslog.format = "%h %V %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\""

Каждый placeholder представляет элемент запроса. %h это IP клиента, %V виртуальный хост, %u аутентифицированный пользователь, %t timestamp, %r request line, %>s статус код, %b размер ответа в байтах.

Полный список placeholders:

%% - процент
%h - IP адрес клиента
%l - ident name (не поддерживается)
%u - authenticated user
%t - timestamp окончания запроса
%r - request line (метод, URI, протокол)
%s - status code
%b - bytes sent (body)
%B - bytes sent (то же что %b)
%i - входящий HTTP заголовок
%o - исходящий HTTP заголовок
%a - remote address
%A - local address
%D - время обработки в микросекундах
%T - время обработки в секундах
%m - request method
%U - URL path
%q - query string
%H - request protocol
%v - server name
%V - virtual host
%p - server port
%f - физический путь к файлу
%k - количество keepalive запросов

Детальный формат с временем обработки:

accesslog.format = "%h %V %u %t \"%r\" %>s %b %D \"%{Referer}i\" \"%{User-Agent}i\""

%D показывает микросекунды. Запрос, обработанный за 150 миллисекунд, логируется как 150000.

Timestamp с кастомным форматом:

accesslog.format = "%h %V %u %{%Y-%m-%d %H:%M:%S}t \"%r\" %>s %b"

Синтаксис strftime внутри %{...}t позволяет любой формат времени.

Время начала запроса вместо окончания:

accesslog.format = "%h %V %u %{begin:%Y-%m-%d %H:%M:%S}t \"%r\" %>s %b"

Префикс begin: переключает timestamp на момент получения запроса.

Высокоточные timestamps с микросекундами:

accesslog.format = "%h %V %u %{%FT%T}t.%{usec_frac}tZ \"%r\" %>s %b"

Формат ISO 8601 с дробной частью микросекунд. Пример: 2026-02-05T14:23:17.543219Z.

Custom заголовки для логирования application-specific данных:

accesslog.format = "%h %V %u %t \"%r\" %>s %b \"%{X-Request-ID}i\""

Placeholder %{...}i читает входящий заголовок. %{...}o читает исходящий заголовок.

Логирование session ID через специальный заголовок:

accesslog.format = "%h %V %u %t \"%r\" %>s %b \"%{X-LIGHTTPD-SID}o\""

Префикс X-LIGHTTPD- в заголовке ответа особенный. Такие заголовки логируются, но не отправляются клиенту. PHP код:

<?php
session_start();
header("X-LIGHTTPD-SID: " . session_id());
?>

Session ID попадает в лог, но клиент его не видит.

JSON формат для структурированных логов:

accesslog.escaping = "json"
accesslog.format = "{ "
accesslog.format += "\"timestamp\": \"%{%FT%T}t.%{usec_frac}tZ\", "
accesslog.format += "\"remote_addr\": \"%h\", "
accesslog.format += "\"method\": \"%m\", "
accesslog.format += "\"url\": \"%U\", "
accesslog.format += "\"query\": \"%q\", "
accesslog.format += "\"status\": %s, "
accesslog.format += "\"bytes\": %b, "
accesslog.format += "\"duration_us\": %D, "
accesslog.format += "\"referrer\": \"%{Referer}i\", "
accesslog.format += "\"user_agent\": \"%{User-Agent}i\" "
accesslog.format += "}"

Параметр accesslog.escaping экранирует спецсимволы JSON. Каждая строка лога это валидный JSON объект.

Логирование compression ratio для mod_deflate:

accesslog.format = "%h %V %u %t \"%r\" %>s %b %{ratio}n"

%{ratio}n показывает коэффициент сжатия. Значение 0.3 означает, что сжатый файл составляет 30% от оригинала.

Отключение логирования для конкретных запросов:

$HTTP["url"] == "/health-check" {
    accesslog.filename = ""
}

Health check эндпоинты генерируют тысячи записей без ценности. Пустое значение filename отключает логирование.

Логирование в syslog вместо файла:

accesslog.use-syslog = "enable"
accesslog.syslog-level = 6

Уровень 6 соответствует "Informational" в syslog severity.

Pipe logger для ротации на лету:

accesslog.filename = "|/usr/sbin/cronolog /var/log/lighttpd/access-%Y-%m-%d.log"

Cronolog создает новый файл каждый день автоматически. Формат имени файла управляется через strftime паттерны.

Множественные логи для разных виртуальных хостов:

$HTTP["host"] == "api.example.com" {
    accesslog.filename = "/var/log/lighttpd/api-access.log"
}

$HTTP["host"] == "www.example.com" {
    accesslog.filename = "/var/log/lighttpd/www-access.log"
}

Каждый виртуальный хост пишет в свой файл.

Custom формат для совместимости с анализаторами:

accesslog.format = "%h %l %u %t \"%r\" %>s %b"

Common Log Format без Referer и User-Agent совместим с большинством legacy анализаторов типа Webalizer.

Проверка синтаксиса конфигурации:

lighttpd -tt -f /etc/lighttpd/lighttpd.conf

Флаг -tt тестирует конфигурацию без запуска сервера. Ошибки в accesslog.format выявляются до применения.

Интеграция компонентов в production

Сценарий 1: API gateway перед микросервисами. Lighttpd балансирует запросы между тремя instance:

server.modules = ( "mod_proxy", "mod_accesslog", "mod_rewrite" )

url.rewrite-once = (
    "^/api/v2/(.*)$" => "/v2/$1"
)

$HTTP["host"] == "api.company.com" {
    proxy.balance = "hash"
    proxy.server = ( "" => (
        ( "host" => "10.0.1.10", "port" => 8080 ),
        ( "host" => "10.0.1.11", "port" => 8080 ),
        ( "host" => "10.0.1.12", "port" => 8080 )
    ))
    
    accesslog.filename = "/var/log/lighttpd/api-access.log"
    accesslog.format = "%h %V %t \"%r\" %>s %b %D \"%{X-Request-ID}i\""
}

Hash балансировка направляет одинаковые endpoint на один сервер. Rewrite нормализует версионированные API. Лог включает Request-ID для трассировки.

Сценарий 2: Single page application с API backend. Статика на Lighttpd, API на Node.js:

server.document-root = "/var/www/app"

url.rewrite-if-not-file = (
    "^/api/.*$" => "$0",
    "^/.*$" => "/index.html"
)

$HTTP["url"] =~ "^/api/" {
    proxy.server = ( "" => (
        ( "host" => "127.0.0.1", "port" => 3000 )
    ))
}

Все запросы к /api проксируются на Node.js. Остальные запросы возвращают index.html для client-side роутинга.

Сценарий 3: Legacy приложение с постепенной миграцией. Старый backend на порту 8000, новый на 8001:

url.rewrite-once = (
    "^/new-feature/(.*)$" => "/feature/$1"
)

$HTTP["url"] =~ "^/new-feature/" {
    proxy.server = ( "" => (
        ( "host" => "127.0.0.1", "port" => 8001 )
    ))
}

$HTTP["url"] !~ "^/new-feature/" {
    proxy.server = ( "" => (
        ( "host" => "127.0.0.1", "port" => 8000 )
    ))
}

Запросы к новой функциональности направляются на новый backend. Остальной трафик идет на legacy систему. Постепенная миграция без простоя.

Понимание взаимодействия mod_proxy, mod_rewrite и mod_accesslog превращает Lighttpd из простого веб-сервера в мощный инструмент для построения распределенных систем с минимальными ресурсами.