Программируем прямо в Nginx

от автора

Nginx — великолепный веб-сервер. Все мы привыкли использовать его в связке с бекендомами на разных языках программирования. Но оказывается можно писать простые программы прямо внутри конфигурационного файла Nginx. Это можно использовать для балансировки, написания простых API и даже отдавать динамические страницы прямо из конфига.

В статье мы разберем примеры написания простых программ в конфиге nginx.

Выглядит это как написание кода в конфиге, что выглядит диковато, но удобно. Код выполняется асинхронно, не вмешиваясь в основной цикл событий Nginx, без коллбэков. Работает быстро и, что немаловажно, в совместимости с другими модулями и всем базовым функционалом.
Основным решением для Lua + Nginx считается OpenResty. Там много готовых модулей, как собственных на Lua, так и интегрированных из Nginx. Он отлично масштабируется и при этом сохраняет высокую производительность и пропускную способность Nginx.

Установка

Сам Nginx устанавливать не нужно, OpenResty включает его в свою сборку.

wget https://openresty.org/download/openresty-1.15.8.3.tar.gz tar -xvf openresty-1.15.8.3.tar.gz cd openresty-1.15.8.3/ sudo apt-get install build-essential 

Также доустановим ещё несколько пакетов для поддержки CLI, регулярных выражений и SSL:

sudo apt-get install libreadline-dev libncurses5-dev libpcre3-dev libssl-dev perl

И начнём сборку

./configure -j2 --with-pcre-jit --with-ipv6 make -j2 sudo make install 

Наконец, запускаем OpenResty

sudo /usr/local/openresty/bin/openresty

Вывода не последует, сервер просто запустится и будет доступен:

Остановка:

sudo /usr/local/openresty/bin/openresty -s quit

Hello world

Сначала создадим директорию и конфиг для нашего сайта

sudo mkdir /usr/local/openresty/nginx/sites sudo nano /usr/local/openresty/nginx/sites/default.conf
server {     listen 80 default_server;     listen [::]:80 default_server;      root /usr/local/openresty/nginx/html/default;      index index.html index.htm;      location / {         default_type 'text/plain';          content_by_lua_file /usr/local/openresty/nginx/html/default/index.lua;     } }

Исполнять скрипты можно прямо в конфиге, но удобнее сразу подключить внешний файл

sudo nano /usr/local/openresty/nginx/html/default/index.lua
local name = ngx.var.arg_name or "Anonymous" ngx.say("Hello, ", name, "!")
sudo mkdir /usr/local/openresty/nginx/html/default sudo mv /usr/local/openresty/nginx/html/index.html /usr/local/openresty/nginx/html/default

Примеры

Ниже собраны более практичные примеры из разных источников:

ruhighload.com

Вывод HTML

server {   location /hello {     default_type 'text/html';      content_by_lua '         ngx.say("Hello <b>world</b>!")     ';   } }

Несколько обработчиков

server {   location / {     default_type 'text/plain';     content_by_lua_file /var/www/lua/index.lua;   }    location /admin {     default_type 'text/plain';     content_by_lua_file /var/www/lua/admin.lua;   } }

Глобальные переменные

http {     # объявляем глобальный контейнер     lua_shared_dict stats 1m;      server {         location / {             content_by_lua ' 		# увеличим переменную hits на 1 при каждом запросе                 ngx.shared.stats:incr("hits", 1) 		 		# выведем текущее значение                 ngx.say(ngx.shared.stats:get("hits"))             ';         }     } }

Скрипт для подсчета количества запросов в Redis

apt-get install lua-nginx-redis

server {         location / {             content_by_lua ' 		local redis = require "nginx.redis" 		local red = redis:new() 		local ok, err = red:connect("127.0.0.1", 6379) 		ok, err = red:incr("test") 		local res, err = red:get("test") 		ngx.say("hits: ", res)             ';         } }

openresty.org

Routing MySQL Queries Based On URI Args
Dynamic Request Routing Based on Redis
Web App for OpenResty User Survey
Code and data for the openresty.org site — любой сайт, посвящённый определенной веб-технологии, использует её, и openresty.org не исключение

habr.com/ru/post/270463

Поиск с кэшированием запросов

-- search.lua local string = ngx.var.arg_string  -- получим параметр из GET запроса if string == nil then     ngx.exec("/") -- если параметра нет, то сделаем редирект end  local path = "/?string=" .. string  local redis = require "resty.redis" -- подключим библиотеку по работе с redis local red = redis:new()  red:set_timeout(1000) -- 1 sec  local ok, err = red:connect("127.0.0.1", 6379) if not ok then     ngx.exec(path) -- если нельзя подключиться к redis, то сделаем редирект end  res, err = red:get("search:" .. string); -- получим данные из redis  if res == ngx.null then     ngx.exec(path) -- если данных нет, то сделаем редирект else     ngx.header.content_type = 'application/json'     ngx.say(res) -- если данные есть, то отдадим их end

# nginx.conf location /search-by-string {    content_by_lua_file lua/search.lua; }

habr.com/ru/post/326486

Load balancer

В блоке http {} инициализируем lua.

Код с комментариями:

# путь до локально установленных *.lua библиотек с добавлением системных путей lua_package_path "/usr/local/lib/lua/?.lua;;"; init_by_lua_block {     -- подключение основного модуля     -- в принципе, этот блок можно опустить     require "resty.core"     collectgarbage("collect")  -- just to collect any garbage }

в блоках *_lua_block уже идёт lua-код со своим синтаксисом и функциями.

Основной сервер, который принимает на себя внешние запросы.

Код с комментариями:

server {     listen 80;     server_name test.domain.local;    location / {     # проверяем наличие cookie "upid" и если нет — выставляем по желаемому алгоритму     if ($cookie_upid = "") {             # инициализируем пустую переменную nginx-а, в которую запишем выбранный ID бэкенда             set $upstream_id '';             rewrite_by_lua_block {                 -- инициализируем математический генератор для более рандомного рандома используя время nginx-а                 math.randomseed(ngx.time())                 -- также пропускаем первое значение, которое совсем не рандомное (см документацию)                 math.random(100)                 local num = math.random(100)                 -- получив число, бесхитростно и в лоб реализуем веса 20% / 80%                 if num > 20 then                     ngx.var.upstream_id = 1                     ngx.ctx.upid = ngx.var.upstream_id                 else                     ngx.var.upstream_id = 2                     ngx.ctx.upid = ngx.var.upstream_id                 end                 -- ID запоминаем в переменной nginx-а "upstream_id" и в "upid" таблицы ngx.ctx модуля lua, которая используется для хранения значений в рамках одного запроса              }     # отдаём клиенту куку "upid" со значением выбранного ID     # время жизни явно не задаём, потому она будет действительна только на одну сессию (до закрытия браузера), что нас устраивает     add_header Set-Cookie "upid=$upstream_id; Domain=$host; Path=/";     }      # если же кука у клиента уже есть, то запоминаем ID в ngx.ctx.upid текущего запроса     if ($cookie_upid != "") {         rewrite_by_lua_block {             ngx.ctx.upid = ngx.var.cookie_upid         }     }      # передаём обработку запроса на блок upstream-ов     proxy_pass http://ab_test;   } }

Блок upstream, который используя lua заменяет встроенную логику nginx.

Код с комментариями:

upstream ab_test {   # заглушка, чтобы nginx не ругался. В алгоритме не участвует   server 127.0.0.1:8001;      balancer_by_lua_block {         local balancer = require "ngx.balancer"          -- инициализируем локальные переменные         -- port выбираем динамически, в зависимости от запомненного ID бэкенда         local host = "127.0.0.1"         local port = 8000 + ngx.ctx.upid          -- задаём выбранный upstream и обрабатываем код возврата         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         -- в общем случае надо, конечно же, искать доступный бэкенд, но нам не к чему     } }

Ну и простой демонстрационный бэкенд, на который в итоге придут клиенты.

код без комментариев:

server {   listen        127.0.0.1:8001;   server_name   test.domain.local;    location / {     root                /var/www/html;     index               index.html;   } }  server {   listen        127.0.0.1:8002;   server_name   test.domain.local;    location / {     root                /var/www/html;     index               index2.html;   } }

При запуске nginx-a с этой конфигурацией в логи свалится предупреждение:

use of lua-resty-core with LuaJIT 2.0 is not recommended; use LuaJIT 2.1+ instead while connecting to upstream

2Гис (пост)

Эту часть придумал и сделал наш коллега AotD. Есть хранилище картинок. Их надо показывать пользователям, причем желательно производить при этом некоторые операции, например, resize. Картинки мы храним в ceph, это аналог Amazon S3. Для обработки картинок используется ImageMagick. На ресайзере есть каталог с кэшем, туда складываются обработанные картинки.
Парсим запрос пользователя, определяем картинку, нужное ему разрешение и идем в ceph, затем на лету обрабатываем и показываем.

serve_image.lua

require "config" local function return_not_found(msg)     ngx.status = ngx.HTTP_NOT_FOUND     if msg then         ngx.header["X-Message"] = msg     end     ngx.exit(0) end  local name, size, ext = ngx.var.name, ngx.var.size, ngx.var.ext if not size or size == '' then     return_not_found() end if not image_scales[size] then     return_not_found('Unexpected image scale') end  local cache_dir =  static_storage_path .. '/' .. ngx.var.first .. '/' .. ngx.var.second .. '/' local original_fname = cache_dir .. name .. ext local dest_fname = cache_dir .. name .. size .. ext  -- make sure the file exists local file = io.open(original_fname) if not file then     -- download file contents from ceph     ngx.req.read_body()     local data = ngx.location.capture("/ceph_loader", {vars = { name = name .. ext }})     if data.status == ngx.HTTP_OK and data.body:len()>0 then         os.execute( "mkdir -p " .. cache_dir )         local original = io.open(original_fname, "w")         original:write(data.body)         original:close()     else         return_not_found('Original returned ' .. data.status)     end end                                                                                                                                                                                                                                   local magick = require("imagick")                                                                                                                                                                                                  magick.thumb(original_fname, image_scales[size], dest_fname)                                                                                                                                                                      ngx.exec("@after_resize")

Подключаем биндинг imagic.lua. Должен быть доступен LuaJIT.

nginx_partial_resizer.conf.template

# Old images location ~ ^/(small|small_wide|medium|big|mobile|scaled|original|iphone_(preview|retina_preview|big|retina_big|small|retina_small))_ {     rewrite /([^/]+)$ /__CEPH_BUCKET__/$1 break;     proxy_pass __UPSTREAM__; } # Try get image from ceph, then from local cache, then from scaled by lua original # If image test.png is original, when user wants test_30x30.png: # 1) Try get it from ceph, if not exists # 2) Try get it from /cache/t/es/test_30x30.ong, if not exists # 3) Resize original test.png and put it in /cache/t/es/test_30x30.ong location ~ ^/(?<name>(?<first>.)(?<second>..)[^_]+)((?<size>_[^.]+)|)(?<ext>\.[a-zA-Z]*)$ {     proxy_intercept_errors on;     rewrite /([^/]+)$ /__CEPH_BUCKET__/$1 break;     proxy_pass __UPSTREAM__;     error_page 404 403 = @local; } # Helper failover location for upper command cause you can't write # try_files __UPSTREAM__ /cache/$uri @resizer =404; location @local {     try_files /cache/$first/$second/$name$size$ext @resize; }  # If scaled file not found in local cache resize it with lua magic! location @resize { #    lua_code_cache off;     content_by_lua_file "__APP_DIR__/lua/serve_image.lua"; }  # serve scaled file, invoked in @resizer serve_image.lua location @after_resize {     try_files /cache/$first/$second/$name$size$ext =404; }  # used in @resizer serve_image.lua to download original image # $name contains original image file name location =/ceph_loader {     internal;     rewrite ^(.+)$ /__CEPH_BUCKET__/$name break;     proxy_set_header Cache-Control no-cache;     proxy_set_header If-Modified-Since "";     proxy_set_header If-None-Match "";     proxy_pass __UPSTREAM__; }  location =/favicon.ico {     return 404; }  location =/robots.txt {}

Firewall для API. Валидация запроса, идентификация клиента, контроль rps и шлагбаум для тех, кто нам не нужен.

firewall.lua

module(..., package.seeall); local function ban(type, element)     CStorage.banPermanent:set(type .. '__' .. element, 1);     ngx.location.capture('/postgres_ban', { ['vars'] = { ['type'] = type, ['value'] = element} }); end local function checkBanned(apiKey)     -- init search criteria     local searchCriteria = {};     searchCriteria['key'] = apiKey;     if ngx.var.remote_addr then         searchCriteria['ip'] = ngx.var.remote_addr;     end;     -- search in ban lists     for type, item in pairs(searchCriteria) do         local storageKey = type .. '__' .. item;         if CStorage.banPermanent:get(storageKey) then             ngx.exit(444);         elseif CStorage.banTmp:get(storageKey) then             -- calculate rps and check is our client still bad boy 8-)             local rps = CStorage.RPS:incr(storageKey, 1);             if not(rps) then                 CStorage.RPS:set(storageKey, 1, 1);                 rps=1;             end;             if rps then                 if rps > config.app_params['ban_params']['rps_for_ip_to_permanent_ban'] then                     CStorage.RPS:delete(storageKey);                     ban(type, item);                     ngx.exit(444);                 elseif config.app_params['ban_params']['rps_for_ip_to_tmp_ban'] > 0 and rps == config.app_params['ban_params']['rps_for_ip_to_tmp_ban'] then                     local attemptsCount = CStorage.banTmp:incr(storageKey, 1) - 1;                     if attemptsCount > config.app_params['ban_params']['tmp_ban']['max_attempt_to_exceed_rps'] then                         -- permanent ban                         CStorage.banTmp:delete(storageKey);                         ban(type, item);                     end;                 end;             end;             ngx.exit(444);         end;     end; end;  local function checkTemporaryBlocked(apiKey)     local blockedData = CStorage.tmpBlockedDemoKeys:get(apiKey);     if blockedData then         --storage.tmpBlockedDemoKeys:incr(apiKey, 1); -- think about it.         return CApiException.throw('tmpDemoBlocked');     end; end;  local function checkRPS(apiKey)     local rps = nil;     -- check rps for IP and ban it if it's needed     if ngx.var.remote_addr then         local ip = 'ip__' .. tostring(ngx.var.remote_addr);         rps = CStorage.RPS:incr(ip, 1);         if not(rps) then             CStorage.RPS:set(ip, 1, 1);             rps = 1;         end;         if rps > config.app_params['ban_params']['rps_for_ip_to_permanent_ban'] then             ban('ip', tostring(ngx.var.remote_addr));             ngx.exit(444);         elseif config.app_params['ban_params']['rps_for_ip_to_tmp_ban'] > 0 and rps > config.app_params['ban_params']['rps_for_ip_to_tmp_ban'] then             CStorage.banTmp:set(ip, 1, config.app_params['ban_params']['tmp_ban']['time']);             ngx.exit(444);         end;     end;      local apiKey_key_storage = 'key_' .. apiKey['key'];     -- check rps for key     rps = CStorage.RPS:incr(apiKey_key_storage, 1);     if not(rps) then         CStorage.RPS:set(apiKey_key_storage, 1, 1);         rps = 1;     end;     if apiKey['max_rps'] and rps > tonumber(apiKey['max_rps']) then         if apiKey['mode'] == 'demo' then             CApiKey.blockTemporary(apiKey['key']);             return CApiException.throw('tmpDemoBlocked');         else             CApiKey.block(apiKey['key']);             return CApiException.throw('blocked');         end;     end;      -- similar check requests per period (RPP) for key     if apiKey['max_request_count_per_period'] and apiKey['period_length'] then         local rpp = CStorage.RPP:incr(apiKey_key_storage, 1);         if not(rpp) then             CStorage.RPP:set(apiKey_key_storage, 1, tonumber(apiKey['period_length']));             rpp = 1;         end;          if rpp > tonumber(apiKey['max_request_count_per_period']) then             if apiKey['mode'] == 'demo' then                 CApiKey.blockTemporary(apiKey['key']);                 return CApiException.throw('tmpDemoBlocked');             else                 CApiKey.block(apiKey['key']);                 return CApiException.throw('blocked');             end;         end;     end; end;  function run()     local apiKey = ngx.ctx.REQUEST['key'];     if not(apiKey) then         return CApiException.throw('unauthorized');     end;     apiKey = tostring(apiKey)     -- check permanent and temporary banned     checkBanned(apiKey);     -- check api key     apiKey = CApiKey.getData(apiKey);      if not(apiKey) then         return CApiException.throw('forbidden');     end;     apiKey = JSON:decode(apiKey);     if not(apiKey['is_active']) then         return CApiException.throw('blocked');     end;      apiKey['key'] = tostring(apiKey['key']);     -- check is key in tmp blocked list     if apiKey['mode'] == 'demo' then         checkTemporaryBlocked(apiKey['key']);     end;      -- check requests count per second and per period     checkRPS(apiKey);     -- set apiKey's json to global parameter; in index.lua we send it through nginx to php application     ngx.ctx.GLOBAL['api_key'] = JSON:encode(apiKey); end;

validator.lua

module(..., package.seeall);  local function checkApiVersion()     local apiVersion = '';     if not (ngx.ctx.REQUEST['version']) then         local nginx_request = tostring(ngx.var.uri);         local version = nginx_request:sub(2,4);         if tonumber(version:sub(1,1)) and tonumber(version:sub(3,3)) then             apiVersion = version;         else             return CApiException.throw('versionIsRequired');         end;     else         apiVersion = ngx.ctx.REQUEST['version'];     end;      local isSupported = false;     for i, version in pairs(config.app_params['supported_api_version']) do         if apiVersion == version then             isSupported = true;         end;     end;      if not (isSupported) then         CApiException.throw('unsupportedVersion');     end;      ngx.ctx.GLOBAL['api_version'] = apiVersion; end;  local function checkKey()     if not (ngx.ctx.REQUEST['key']) then         CApiException.throw('unauthorized');     end; end;  function run()     checkApiVersion();     checkKey(); end;

apikey.lua

module ( ..., package.seeall )  function init()     if not(ngx.ctx.GLOBAL['CApiKey']) then         ngx.ctx.GLOBAL['CApiKey'] = {};     end end;  function flush()     CStorage.apiKey:flush_all();     CStorage.apiKey:flush_expired(); end;  function load()     local dbError = nil;     local dbData = ngx.location.capture('/postgres_get_keys');     dbData = dbData.body;     dbData, dbError = rdsParser.parse(dbData);     if dbData ~= nil then         local rows = dbData.resultset         if rows then             for i, row in ipairs(rows) do                 local cacheKeyData = {};                 for col, val in pairs(row) do                     if val ~= rdsParser.null then                         cacheKeyData[col] = val;                     else                         cacheKeyData[col] = nil;                     end                 end                 CStorage.apiKey:set(tostring(cacheKeyData['key']),JSON:encode(cacheKeyData));             end;         end;     end; end;  function checkNotEmpty()     if not(ngx.ctx.GLOBAL['CApiKey']['loaded']) then         local cnt = CHelper.tablelength(CStorage.apiKey:get_keys(1));         if cnt == 0 then             load();         end;         ngx.ctx.GLOBAL['CApiKey']['loaded'] = 1;     end; end;  function getData(key)     checkNotEmpty();     return CStorage.apiKey:get(key); end;  function getStatus(key)         key = getData(key);         local result = '';         if key ~= nil then             key = JSON:decode(key);             if key['is_active'] ~= nil and  key['is_active'] == true then                 result = 'allowed';             else                 result = 'blocked';             end;         else             result = 'forbidden';         end;         return result; end;  function blockTemporary(apiKey)     apiKey = tostring(apiKey);     local isset = getData(apiKey);     if isset then         CStorage.tmpBlockedDemoKeys:set(apiKey, 1, config.app_params['ban_params']['time_demo_apikey_block_tmp']);     end; end;  function block(apiKey)     apiKey = tostring(apiKey);     local keyData = getData(apiKey);     if keyData then         ngx.location.capture('/redis_get', { ['vars'] = { ['key'] = apiKey } });         keyData['is_active'] = false;         CStorage.apiKey:set(apiKey,JSON:encode(cacheKeyData));     end; end;

storages.lua

module ( ..., package.seeall )  apiKey = ngx.shared.apiKey; RPS = ngx.shared.RPS; RPP = ngx.shared.RPP; banPermanent = ngx.shared.banPermanent; banTmp = ngx.shared.banTmp; tmpBlockedDemoKeys = ngx.shared.tmpBlockedDemoKeys;

Бонус! Примеры без использования Lua вообще.

Только конфиги, только хардкор

force no-www

server {     listen 80;     server_name example.org; }  server {     listen 80;     server_name www.example.org;     return 301 $scheme://example.org$request_uri; }

force https

server {     listen 80;     return 301 https://$host$request_uri; }  server {     listen 443 ssl;      # let the browsers know that we only accept HTTPS     add_header Strict-Transport-Security max-age=2592000;      ... }

Редирект на определенный путь в URI

location /old-site {     rewrite ^/old-site/(.*) http://example.org/new-site/$1 permanent; }

Кэш файлов

open_file_cache max=1000 inactive=20s; open_file_cache_valid 30s; open_file_cache_min_uses 2; open_file_cache_errors on;

Кэширование с Upstream

upstream backend {     server 127.0.0.1:8080;     keepalive 32; }  server {     ...     location /api/ {         proxy_pass http://backend;         proxy_http_version 1.1;         proxy_set_header Connection "";     } }

Напоследок, большой список конфигурационных шаблонов с Lua и без, с разной степенью сложности

Заключение

Lua в Nginx в общем и OpenResty в частности гораздо быстрее и легковеснее php. Они помогают расширить базовый функционал Nginx, сделать его гибче, сохранив скорость обработки запроса и возможность тонкой настройки. OpenResty использует в проде огромное количество компаний, обеспечивая ему богатую экосистему и сильную поддержку комьюнити. Поле для экспериментов с Lua почти не ограничено, поэтому он может пригодиться в самых неожиданных местах. Если вы еще не пробовали Lua-in-Nginx, самое время изучить эту тему подробнее.


На правах рекламы

Необходим сервер для размещения сайта? Наша компания предлагает надёжные серверы с посуточной или единоразовой оплатой, каждый сервер подключён к интернет-каналу в 500 Мегабит и бесплатно защищён от DDoS-атак!

ссылка на оригинал статьи https://habr.com/ru/company/vdsina/blog/504308/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *