Сервер Haproxy имеет встроенные средства для выполнения скриптов Lua. Язык программирования Lua для расширения возможностей различных серверов используется очень широко. Например, на Lua можно программировать для серверов Redis, Nginx (nginx-extras, openresty), Envoy. Это вполне закономерно, так как язык программирования Lua как раз и был разработан для удобства встраивания в приложения в качестве скриптового языка. В этом сообщении я рассмотрю варианты использования Lua для расширения возможностей Haproxy. Согласно документации, скрипты Lua на сервере Haproxy могут выполняться в шести контекстах: Фактически есть еще один контекст выполнения, который не указан в документации: Начнем с самой простой конфигурации сервера Haproxy. Конфигурация состоит из двух секций frontend — то есть то, к чему обращается клиент с запросом, и backend — то, куда проксируется запрос клиента через сервер Haproxy: Теперь все запросы, приходящие на порт 80 Haproxy будут перенаправлены на порт 3000 сервера app. Services — это функции, определенные в скриптах Lua, которые формируют ответ без обращения к бэкенду. Эти функции регистрируются вызовом системной функции сore.register_service(function)). Определим простейший Service в файле guarde.lua: И зарегистрируем ее как Service в файле register.lua: Параметр "http" является триггером, который допускает использование Service только в контексте http запроса (mode http). Дополним конфигурацию сервера Haproxy: Теперь, обратившись к серверу Haproxy с запросом /hello_world, клиент получит не ответ с проксируемого сервера, а ответ сервиса lua.hello-world. В качестве параметра функции передается контекст запроса в параметре applet. Нет возможности передать дополнительные параметры файле конфигурации. Actions — действия, выполняемые после получения запроса от клиента или после получения ответа от проксируемого сервера. Actions могут выполнять асинхронные операции (например запросы к базе данных) и не имеют возвращаемого значения. С сервером Actions общаются путем установки переменных контекста запроса. Контекст запроса предается в качестве параметра при вызове Action. Традиционно имя этого параметра txn. Передать дополнительные параметры из файла конфигурации Haproxy в Action нельзя. Создадим Action, который будет проверять наличие авторизации Bearer в запросе: Зарегистрируем этот Action: Параметр { "http-req" } является триггером, который позволяет использовать этот Action только в контексте http и только на этапе запроса клиента (и запрещает использовать на этапе ответа проксируемого сервера). В конфигурации Haproxy, Action регистрируется в секции http-request: На основании значения переменных, установленных в Action, формируются ACL (Access Control Lists) — ключевой элемент в конфигурациях Haproxy: Полный листинг конфигурации сервера Haproxy для Action validate-token: Fetches — это значения которые вычисляются в процессе запроса. Они могут быть только синхронными, и принимают параметры, заданные в конфигурации Haproxy. Например, та же самая проверка авторизации может быть выполнена как Fetch: Установка ACL по значениям из Fetches задается так: Converters в качестве параметра принимают строку и возвращают значение. Converters, также как и Fetches, могут быть только синхронными и принимают параметры, задаваемые в конфигурации Haproxy. В конфигурации Haproxy Converters отделяются от значения, к которому они применяются, запятой. Создадим Converter, который будет заголовок Authorization преобразовывать в строку: В файле конфигурации использование конвертера задается следующим образом: К значениею заголовка Authorization, который извлекается системным Fetch hdr() применяется Converter lua.validate-token-converter. Stick Table — это хранилище пар ключ-значение, которое оптимизировано для учета количества запросов в единицу времени, и может служить для защиты серверов от атак DDoS или брутфорса (напрмер перебора паролей или выкачки запросами REST больших объемов данных). Хотя основное назначение этих таблиц связано с реализацией липких сессий. В сочетании с такими средствами как Fetches и Converters, эти таблицы могут подсчитывать количество запросов, например, с определенным сессионным cookie или jti, не давая тем самым использовать одну авторизацию для организации распределенной атаки с сотен тысяч устройств. К положительным сторонам Stick Table относится скорость работы и простота конфигурирования. К отрицательным — ограниченное количество регистров для учета значений (всего восемь регистров), потребление памяти, потеря данных после перегрузки сервера Haproxy. Рассмотрим как задаются правила в Stick Table: Строка 1. Создается таблица. В качестве ключа используется значение типа строка. Максимальный размер таблицы 100k. Срок хранения ключа 30 секунд. В качестве значения будут накапливаться количество запросов за последние 10 секунд с одинаковым значением ключа типа строка.
Строка 2. Задается, что значение ключа определяется из Fetch lua.validate-token(), и будет использоваться регистр 1, в котором будут накапливаться значения (track-sc1)
Строка 3. Если количество запросов с ключом, заданными в строке 2, накопленных в регистре 1 (sc_http_req_rate(1)) превышает 3 — сервер отдает ответ со статусом 429. Если есть необходимость использовать асинхронный код (например запросы в базу данных) — то Actions это единственный выбор. Любого разработчика будет волновать вопрос, насколько асинхронные запросы будут снижать призводительность работы сервера. Если сравнить в этом аспекте Haproxy c основными конкуретнтами Nginx/Openresty и Envoy, то расклад будет такой. Envoy разрешает выполнение асинхронного кода, но оно будет фактически блокировать работу сервера на время выполнения этого запроса, и поэтому не рекомендуется. Openresty, напротив, поощряет использование асинхронного кода, но только если используемые библиотеки были специально разработаны для Openresty. В этих библиотеках на время выполнение асинхроных операций происходит высвобождение ресурсов, примерно как это делает Nodejs, реализующий свою главную фичу — NIO (не блокирующий ввод-вывод). Именно из-за особенностей архитектуры этого решения, Openresty работет на версии Lua 5.1 и не переходит на более высокие версии Lua 5.2 или 5.3. Haproxy, в отличие от Openresty, позволяет использовать библиотеки общего назначения Lua без ограничений по версиям. Но, в отличие от Envoy, асинхронные вызовы не блокируют работу сервера. По моим выборочным замерам призводительности некоторых запросов они, близки к тому что выдает на асинхронных запросах Openresty — хотя я, не претендую в этой оценке на полную объективность. Сейчас мы рассмотрим обращение к серверу Redis. В предыдущем разделе мы рассматривали подсчет количества запросов с определенным ключом при помощи Stick Table. Они работают очень быстро, но не без недостатков. Судя по "поведению" реального севера, срок хранения ключа "продлевается" при каждом новом запросе. Это связано с тем что таблицы изначально создавались для хранения липких сессий и время хранения ключа считается не от времени его создания, а от времени последнего доступа. Это нужно для непрерывности работы липких сессий и одновременно для сброса сессии после заданного таймаута запросов. Это приводит к тому, что счетчик не "сбрасывается" по окончании заданного срока (сбрасывается по окончания таймаута запросов) и запросы начинают отвергаться все на 100%. Для того чтобы трафик возобновился, нужно чтобы на время действия ключа полностью прекратились запросы с этим ключом. Обычно это не то поведение, которое ожидается. Реализуем счетчики запросов на Redis, и будем отвергать запросы после превышения заданного лимита за заданный период времени, который отсчитывается от момента создания ключа: Код, использованный в данном сообщении доступен в репозитарии. В частности, там есть файл docker-compose.yml, который поможет поднять необходимую для работы среду. apapacy@gmail.com
5 декабря 2020 года Тестер