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



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

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

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

    Установка



    Lua


    Поддержка чистого Lua поставляется в пакете nginx-extras
    apt-get install nginx
    apt-get install nginx-extras
    сс NiceDay

    OpenResty


    В этом случае сам Nginx устанавливать не нужно, OpenResty включает его в свою сборку. Если Nginx уже установлен, перед установкой его нужно отключить и остановить
    sudo systemctl disable nginx
    sudo systemctl stop nginx


    Затем
    sudo apt-get -y install --no-install-recommends wget gnupg ca-certificates
    wget -O - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
    echo "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main" \
        | sudo tee /etc/apt/sources.list.d/openresty.list
    sudo apt-get update
    sudo apt-get -y install openresty


    Наконец, запускаем 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 вообще.


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

    Возврат страницы через return
    server {
        ...
        location / {
            default_type text/plain;
            return 200 "Your IP: $remote_addr\n";
        }
    }



    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;


    Keep-Alive с 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-атак!

    VDSina.ru
    Серверы в Москве и Амстердаме

    Похожие публикации

    Комментарии 22

      +2
      Код выполняется асинхронно, не вмешиваясь в основной цикл событий Nginx

      Это значит, что новый поток создается на каждый запрос?
      Как понимаю, автотесты для такого кода не напишешь? Только отдельными утилитами, поднимая nginx в докере и кидая в него запросы.
        0
        Это значит, что новый поток создается на каждый запрос?

        Это значит, что используются асинхронные вызовы. Прекратите уже путать асинхронность и параллельность. Это не связанные между собой понятия, и возможны все четыре варианта их комбинации.

          –1
          Кажется это вы что-то напутали. Предлагаю наводящий вопрос: что произойдёт, если в lua-коде окажется вечный цикл?
            +2

            То поток заблокируется. Какой именно поток — зависит от реализации, скорей-всего из отдельного пула. Само по себе суждение "используются асинхронные вызовы" не даёт информации о том, в каком потоке это будет происходить. И уж тем более из этого не следует, что на каждый запрос будет создаваться поток. Асинхронно можно делать и в одном потоке (см. javascript).

              –1
              Мой наводящий вопрос был именно к тому, какой именно поток заблокируется.
                0
                Хотел бы уточнить куда более просто и прямо. В каком потоке будет исполняться lua? В потоке в котором идёт обработка соебинения, или в отдельном, дочернем потоке?
          +3
          Кэширование с Upstream

          Ничего не кеширует, просто будет держать соединения к апстриму за счет Keep-Alive.


          serve_image.lua

          мне кажется, проще будет это сделать на proxy_pass + proxy_cache*.


          Но за статью спасибо, было интересно.


          PS: На убунте, если не нужен весь openresty, а именно lua, достаточно ничего не компилируя поставить сам nginx (или нет, если у вас уже установлен) и доставить к нему модули. например, так:
          apt-get install nginx
          apt-get install nginx-extras

            0

            Там скорее всего просто lua, а не lua-jit, для высокопроизводительных решений это важно.

            0
            Спасибо, интересно.

            Никто не пытался с помощью того же Lua откусывать куки для статики?

            Бэкграунд: gtmetrix.com/serve-static-content-from-a-cookieless-domain.html
              +2

              Зачем так сложно-то? Размещаем статику просто на поддомене без кукисов и всё.

                0
                Спасибо, я в курсе.

                Извините, не помню источник, но суть вот в чем:
                Use Cookie-free Domains for Components

                When the browser makes a request for a static image and sends cookies together with the request, the server doesn't have any use for those cookies. So they only create network traffic for no good reason. You should make sure static components are requested with cookie-free requests. Create a subdomain and host all your static components there. If your domain is www.example.org, you can host your static components on static.example.org. However, if you've already set cookies on the top-level domain example.org as opposed to www.example.org, then all the requests to static.example.org will include those cookies. In this case, you can buy a whole new domain, host your static components there, and keep this domain cookie-free. Yahoo! uses yimg.com, YouTube uses ytimg.com, Amazon uses images-amazon.com and so on.

                Another benefit of hosting static components on a cookie-free domain is that some proxies might refuse to cache the components that are requested with cookies. On a related note, if you wonder if you should use example.org or www.example.org for your home page, consider the cookie impact. Omitting www leaves you no choice but to write cookies to *.example.org, so for performance reasons it's best to use the www subdomain and write the cookies to that subdomain.


                Сайт уже много лет работает, www не используется.
                Альтернативный вариант — только отдавать статику с другого домена.

                Собственно, вопрос по теме поста — программированию в nginx функционала по обрезанию кук на лету, а не «как избавиться от кук» в целом.
                  +2
                  Если задача сэкономить трафик и облегчить жизнь проксям, то резать куки в nginx уже поздновато, разве нет?
                    0
                    это зависит от двух вещей:
                    1. что проще в реализации
                    2. насколько сильный оверхед добавит подобный функционал.

                    Я понимаю, ваши уточняющие вопросы вполне закономерны, но они и близко не отвечают на поставленный вопрос.
                    Судя по всему, никто не пытался. Остальное — просто флейм на тему.
                    +1

                    Кусок из рабочего конфига:


                    header_filter_by_lua_block {
                        local NewCookies = ngx.header['Set-Cookie'] -- get cookies
                        <cut>
                        ngx.header['Set-Cookie'] = NewCookies
                    }

                    Для очистки, вероятно, будет достаточно присвоить пустой table или nil.

                0
                хочу чтобы в конце было всё для докера чтобы быстро запустить этот Hello World и не копипастить кучу команд
                  0
                  Бонус! Примеры без использования Lua вообще

                  Какие-то каличные примеры. Приведу своих. Они может и не идеальны, но работают исправно без лишней нагрузки на мозг.
                  1) Раздача файлов через nginx с проверкой на доступ к загрузке, авторизация или еще что (auth_request)
                  nginx
                  server {
                      root /var/www/example.com;
                      location ~ \.(zip|png)$ {
                          auth_request /auth;
                          add_header Cache-Control no-cache;
                      }
                  
                      location = /auth {
                          internal;
                          proxy_pass https://example.com/check_auth;
                          proxy_set_header X-Original-URI $request_uri;
                          proxy_pass_request_body off;
                          proxy_set_header Content-Length "";
                      }
                  }


                  php
                  function user_check_auth()
                  {
                      if (is_granted())
                      {
                          header('HTTP/1.1 200 OK');
                          header('Status: 200 OK');
                      } else {
                          header('HTTP/1.1 403 Forbidden');
                          header('Status: 403 Forbidden');
                      }
                      exit;
                  }
                  



                  2) Подсчет кол-ва загрузок (X-Accel-Redirect)
                  nginx
                  server{
                      root /var/www/example.com;
                      set $counter /var/www/files/src/counter.php;
                  
                      location ~ ^/files/(?<g1>.*\.zip)$ {
                          fastcgi_pass unix:/usr/local/var/run/php-fpm.sock;
                          fastcgi_param SCRIPT_FILENAME $counter;
                          include fastcgi_params;
                          fastcgi_param DOCUMENT_ROOT $realpath_root;
                          fastcgi_param REQUEST_URI /$g1;
                          fastcgi_param X-File-Type arch_zip;
                          set $_uri /$g1;
                      }
                  
                      location @counter {
                          root /var/www/upload.example.com;
                          try_files $_uri =404;
                      }
                  
                      location / {
                          try_files $uri $uri/ =404;
                      }
                  }
                  


                  php
                  // /var/www/files/src/counter.php
                  <?php
                  
                  header('X-Accel-Redirect: @counter');
                  header('Content-Type: application/octet-stream');
                  
                  use Symfony\Component\Console\Input\ArrayInput;
                  
                  if (!empty($_SERVER['REQUEST_URI']) && !empty($_SERVER['X-File-Type'])) {
                      $filename = parse_url(htmlspecialchars(basename($_SERVER['REQUEST_URI'])), PHP_URL_PATH);
                      $fileType = htmlspecialchars($_SERVER['X-File-Type']);
                      require_once __DIR__ . '/../vendor/autoload.php';
                  
                      set_time_limit(10);
                      $input = new ArrayInput([
                          'counter:tick',
                          'filename' => $filename,
                          'type' => $fileType,
                      ]);
                      $app = require __DIR__ . '/app.php';
                      require __DIR__ . '/../config/prod.php';
                  
                      /* @var \Symfony\Component\Console\Application $console */
                      $console = require __DIR__ . '/console.php';
                      try {
                          $console->run($input);
                      } catch (\Exception $exception) {
                          return;
                      }
                  }
                  
                  // /var/www/files/src/console.php
                  <?php
                  // ...
                  $console->register('counter:tick')
                      ->setDescription('Counter for download files')
                      ->setDefinition([
                          new InputArgument('filename', InputArgument::REQUIRED, 'File name'),
                          new InputArgument('type', InputArgument::REQUIRED, 'File Type')
                      ])
                      ->setCode(function (InputInterface $input) use ($app) {
                          $filename = $input->getArgument('filename');
                          $fileType = $input->getArgument('type');
                      
                          /* @var \Uploader\Entity\FileType $type */
                          $type = $app['repository.file_type']->findByType($fileType);
                          if (!$type) {
                              return;
                          }
                          
                          /* @var \Uploader\Entity\File $file */
                          $file = $app['repository.file']->findByTypeAndName($type->getType(), $filename);
                          if (!$file) {
                              return;
                          }
                          
                          $file->incDownloadCount();
                          $app['repository.file']->save($file);
                      });
                  

                    +1
                    а что мелочитесь с функциями «безопасности»?
                    $filename = parse_url(stripslashes(htmlentities(strip_tags(nl2br(quoted_printable_encode(htmlspecialchars(basename($_SERVER['REQUEST_URI']))))))), PHP_URL_PATH);
                      0
                      В 100 символов не помещаются
                    –1
                    Реинкарнация mod_perl?
                    Дурацкий вопрос — а зачем? Как только народ начнет накручивать логику на подобные сценарии, он наступит ровно на те же грабли, что возникали с mod_perl. Разве нет?
                      0
                      Лично я пользуюсь lua-resty-auto-ssl. Вполне удобный кейс.
                      0
                      Хорошая штука. У нас уже используется.
                      В связке с ngx_postgres — вообще бомба получается.
                        0
                        Отличная статья, спасибо. Обучаясь в одном иностранном колледже, писал простой учебный проект на OpenResty, REST сервис, работающий через Nginx+Lua с MySQL. Простейшие тесты показали огромную производительность на слабеньком учебном ПК.
                        На текущей работе используем связку OpenResty+PowerDNS в качестве распределённого по геозонам DNS кластера.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое