Убийственная функция OpenResty: Динамичность
API7.ai
January 12, 2023
На данный момент мы почти завершили изучение материалов, связанных с производительностью OpenResty. Освоение и гибкое применение этих техник оптимизации может значительно улучшить производительность нашего кода. Сегодня, в последней части оптимизации производительности, давайте изучим часто недооцениваемую возможность OpenResty: "динамичность".
Начнем с того, что такое динамичность и как она связана с производительностью. В данном контексте динамичность означает, что программы могут изменять параметры, конфигурации и даже свой код во время выполнения, без необходимости перезагрузки. В частности, в NGINX и OpenResty вы можете изменять upstream, SSL-сертификаты и пороговые значения ограничения скорости без перезапуска сервиса, достигая динамичности. Что касается связи между динамичностью и производительностью, очевидно, что если такие операции нельзя выполнять динамически, то частые перезагрузки сервисов NGINX естественно приведут к потере производительности.
Однако мы знаем, что открытая версия NGINX не поддерживает динамические функции, поэтому для изменения upstream или SSL-сертификатов вам придется изменять конфигурационный файл и перезапускать сервис, чтобы изменения вступили в силу. NGINX Plus (коммерческая версия NGINX) предоставляет некоторые динамические возможности, и вы можете использовать REST API для обновления, но это лишь частичное улучшение.
В OpenResty этих ограничений нет, и динамичность является ключевой особенностью OpenResty. Вы можете задаться вопросом, почему OpenResty, основанный на NGINX, может поддерживать динамичность. Причина проста: логика NGINX реализована через модули на C, в то время как OpenResty использует Lua, скриптовый язык. Одним из преимуществ скриптовых языков является возможность динамического изменения во время выполнения.
Динамическая загрузка кода
Вот как можно динамически загружать Lua-код в OpenResty.
resty -e 'local s = [[ngx.say("hello world")]] local func, err = loadstring(s) func()'
Мы видим, что всего в нескольких строках кода можно превратить строку в Lua-функцию и запустить её. Давайте подробнее рассмотрим эти строки кода:
- Сначала мы объявляем строку, содержимое которой представляет собой Lua-код, выводящий
hello world; - Затем, используя функцию
loadstringв Lua, превращаем строку в объект функцииfunc. - Наконец, добавляем скобки к имени функции, чтобы выполнить
funcи вывестиhello world.
Конечно, мы можем расширить этот код для реализации более интересных и практичных функций. Давайте попробуем.
Функция 1: FaaS
Первая функция — это FaaS (Function-as-a-Service), которая в последнее время стала очень популярным направлением. Давайте посмотрим, как это можно реализовать в OpenResty. В упомянутом коде строка представляет собой Lua-код. Мы можем изменить её на Lua-функцию:
local s = [[ return function() ngx.say("hello world") end ]]
Как мы уже говорили, функции в Lua являются объектами первого класса, и этот код возвращает анонимную функцию. При выполнении этой анонимной функции мы используем pcall для обеспечения защиты. pcall запускает функцию в защищенном режиме и перехватывает исключения. Если выполнение прошло успешно, возвращается true и результат выполнения. Если произошла ошибка, возвращается false и информация об ошибке, как показано в следующем коде:
local func1, err = loadstring(s) local ret, func = pcall(func1)
Естественно, если объединить две части, получится полный и рабочий пример:
resty -e 'local s = [[ return function() ngx.say("hello world") end ]] local func1 = loadstring(s) local ret, func = pcall(func1) func()'
Чтобы пойти дальше, мы можем изменить строку s, содержащую функции, на форму, которую пользователь может указать, и добавить условия для её выполнения. Это прототип FaaS. Здесь я предоставляю полную реализацию. Если вы заинтересованы в FaaS и хотите продолжить исследования, перейдите по ссылке, чтобы узнать больше.
Функция 2: Edge Computing
Динамичность OpenResty может быть использована для FaaS, что позволяет уточнить динамичность скриптового языка до уровня функций и играть динамическую роль в edge computing.
Благодаря этим преимуществам мы можем расширить возможности OpenResty от областей API-шлюзов, WAF (Web Application Firewall), веб-серверов и других серверных частей до ближайших к пользователю edge-узлов, таких как IoT-устройства, CDN-узлы, маршрутизаторы и т.д.
Это не просто фантазия. OpenResty уже широко используется в вышеупомянутых областях. Например, Cloudflare, крупнейший пользователь OpenResty, уже давно реализовал динамическое управление CDN-узлами с помощью динамических возможностей OpenResty.
Подход Cloudflare похож на принцип динамической загрузки кода, описанный выше, и может быть разделен на следующие шаги:
- Сначала получить измененные файлы кода из кластера базы данных ключ-значение. Метод может быть фоновым таймером или режимом "публикация-подписка" для мониторинга;
- Затем заменить старый файл на локальном диске обновленным файлом кода и обновить кэш в памяти с помощью методов
loadstringиpcall;
Таким образом, следующий запрос клиента будет обработан через обновленную логику кода. Конечно, в практическом применении нужно учитывать больше деталей, таких как управление версиями и откат, обработка исключений, прерывание сети, перезапуск edge-узлов и т.д., но общий процесс остается неизменным.
Если мы перенесем подход Cloudflare с CDN-узлов на другие edge-сценарии, мы сможем динамически распределить много вычислительных ресурсов на edge-устройства. Это не только позволит полностью использовать вычислительные мощности edge-узлов, но и позволит пользователям получать более быстрые ответы на запросы, так как edge-узел обработает исходные данные и затем отправит их на удаленный сервер, что значительно сократит объем передаваемых данных.
Однако, чтобы успешно реализовать FaaS и edge computing, динамичность OpenResty — это лишь хорошая основа. Вам также нужно учитывать улучшение окружающей экосистемы и участие производителей, что выходит за рамки чисто технических аспектов.
Динамический Upstream
Теперь вернемся к OpenResty и посмотрим, как реализовать динамический upstream. lua-resty-core предоставляет библиотеку ngx.balancer для настройки upstream. Её нужно разместить на этапе balancer в OpenResty:
balancer_by_lua_block { local balancer = require "ngx.balancer" local host = "127.0.0.2" local port = 8080 local ok, err = balancer.set_current_peer(host, port) if not ok then ngx.log(ngx.ERR, "failed to set the current peer: ", err) return ngx.exit(500) end }
Функция set_current_peer устанавливает IP-адрес и порт upstream. Однако стоит отметить, что доменные имена здесь не поддерживаются. Для работы с доменными именами нужно использовать библиотеку lua-resty-dns для преобразования доменного имени в IP.
Однако ngx.balancer является относительно низкоуровневым. Хотя его можно использовать для настройки upstream, реализация динамического upstream далеко не проста. Поэтому перед ngx.balancer нужны две функции:
- Во-первых, определить, какой алгоритм выбора upstream использовать:
consistent hashилиroundrobin; - Во-вторых, механизм проверки здоровья upstream, который должен исключать нездоровые upstream и возвращать их, когда они становятся здоровыми.
Официальная библиотека OpenResty lua-resty-balancer содержит два типа алгоритмов: resty.chash и resty.roundrobin для выполнения первой функции, а также lua-resty-upstream-healthcheck для выполнения второй функции.
Однако есть две проблемы.
Первая — это отсутствие полной реализации последнего шага. Объединение ngx.balancer, lua-resty-balancer и lua-resty-upstream-healthcheck для реализации динамического upstream требует дополнительной работы, что останавливает большинство разработчиков.
Вторая — реализация lua-resty-upstream-healthcheck неполная. В ней есть только пассивная проверка здоровья, но нет активной проверки.
Пассивная проверка здоровья здесь запускается запросами клиентов, а затем анализирует возвращаемые значения upstream для определения состояния здоровья. Если запросов от клиентов нет, состояние здоровья upstream неизвестно. Активная проверка здоровья может исправить этот недостаток. Она использует ngx.timer для периодического опроса указанного интерфейса upstream для проверки состояния здоровья.
Поэтому на практике мы обычно рекомендуем использовать lua-resty-healthcheck для проверки здоровья upstream. Его преимущество в том, что он включает как активную, так и пассивную проверку здоровья и был проверен в нескольких проектах с высокой надежностью.
Кроме того, новый микросервисный API-шлюз Apache APISIX реализовал полную поддержку динамического upstream на основе lua-resty-upstream-healthcheck. Мы можем обратиться к его реализации. Всего 400 строк кода. Вы можете легко адаптировать его для использования в своем проекте.
Итог
Что касается динамичности OpenResty, в каких областях и сценариях вы можете использовать её? Вы также можете расширить содержание каждой части, представленной в этой главе, для более детального и глубокого анализа.
Поделитесь этой статьей и учитесь вместе с другими!