html текст
All interests
  • All interests
  • Design
  • Food
  • Gadgets
  • Humor
  • News
  • Photo
  • Travel
  • Video
Click to see the next recommended page
Like it
Don't like
Add to Favorites

Nginx на стероидах — расширяем функционал с помощью LUA

Для обеспечения работы всех наших внешних продуктов мы используем популярный nginx. Это быстро и это надежно. Проблем с ним почти нет. Наши продукты также постоянно развиваются, появляются новые сервисы, добавляется новый функционал, расширяется старый. Аудитория и нагрузка только растет. Сейчас мы хотим рассказать о том, как мы ускорили разработку, неплохо увеличили производительность и упростили добавление в наши сервисы этого нового функционала, при этом сохранив доступность и отказоустойчивость затронутых приложений. Речь пойдет о концепции “nginx as web application”.
А именно, о сторонних модулях (в основном LUA), позволяющих делать совершенно магические вещи быстро и надежно.
image


Проблемы и решение

Основная задумка довольно простая. Возьмем следующие факторы:
— сложность логики приложения,
— количество компонентов приложения,
— размер аудитории.
С определенного момента становится довольно сложно поддерживать приложение отзывчивым и быстрым, иногда даже и работоспособным. Продукт становится многокомпонентным, географически распределенным. И им пользуются все больше людей. При этом существуют требования бизнеса по отзывчивости и отказоустойчивости, которые надо в первую очередь соблюдать.
Путей решения этой проблемы несколько. Можно все вообще сломать и переделать на другие технологии. Безусловно, этот вариант работает, но нам он не очень понравился и мы решили переделывать постепенно. За основу была взята сборка openresty (nginx+LUA). Почему LUA. Без помощи cgi, fastcgi и других cgi прямо в конфигурационном файле nginx можно заскриптовать мощный, красивый и быстрый функционал. Все работает асинхронно. Причем не только с клиентами, но и с бекендами. При этом не вмешиваясь в event loop вебсервера, без callbacks, полностью используя имеющийся функционал nginx.

На данный момент доступны следующие бекенды:
— Redis
— Memcache
— MySQL
— PostgreSQL
В дополнении можно подключить еще модули для использования, например RabbitMQ и 0MZ.
Это работает довольно быстро. Во всяком случае быстрее, чем php-fpm ))

Логичный вопрос, а почему бы все вообще не переписать на C? Писать на LUA сильно проще и быстрее. И мы сразу избавлены от проблем, связанных с асинхронностью и nginx event loop.

Примеры. Идеи

Мы, как обычно, не будем приводить полный код, только основные части. Эти все штуки раньше были сделаны на php.

1. Эту часть придумал и сделал наш коллега 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 {}


2. 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;


3. Дополнительные сервисы, например межкомпонентное взаимодействие по протоколу AMQP. Пример здесь.

4. Как я уже писал. Модуль самодиагностики приложения с возможностью “умного” управления маршрутами прохождения запроса через бекенды. Еще в разработке.

5. Адаптеры для интерфейсов API. В некоторых случаях необходимо подправить, дополнить или расширить имеющиеся методы. Чтобы все не переписывать, LUA поможет. Например, json<->xml conversion на лету.

6…. идей еще много.

Бенчмарков как таковых не будет. Продукты слишком сложны и рпс после бенча сильно зависит от многих факторов. Однако, для наших продуктов, мы добились 20-кратного увеличения производительности для затронутого функционала, а в некоторых случаях все стало быстрее до ~200 раз.

Плюсы и минусы

Ощутимые плюсы. Все, что раньше было 5 мегабайтами кода на php, превращается в 100кб файл на lua.
— скорость разработки,
— скорость работы приложения,
— надежность,
— асинхронная работа с клиентами и бекендами, не ломающая при этом event loop nginx,
— LUA sugar feel good! Корутины, shared dictionary для всех форков nginx, сабреквесты, куча биндингов.

Неощутимые минусы.
— делать надо все аккуратно и помнить про асинхронность и event loop nginx.
— фронтенд работает настолько быстро, что это может не понравиться бекенду. Между ними прямая связь, без прослоек. Я, например, уверен, что 10000 запросов в секунду LUA на фронтенде прожует легко. Но, если при этом оно захочет пойти в базу, тут могут возникнуть проблемы.
— довольно непросто отладить, если что-то пойдет не так.

Кстати, пока пишется эта статья, прямо в этот момент наш программист рассказывает про все это в подробностях на highload.

С удовольствием ответим на вопросы в комментариях.

Напоследок, здесь можно найти небольшую подборку информации по теме.
Читать дальше
Twitter
Одноклассники
Мой Мир

материал с habrahabr.ru

10

      Add

      You can create thematic collections and keep, for instance, all recipes in one place so you will never lose them.

      No images found
      Previous Next 0 / 0
      500
      • Advertisement
      • Animals
      • Architecture
      • Art
      • Auto
      • Aviation
      • Books
      • Cartoons
      • Celebrities
      • Children
      • Culture
      • Design
      • Economics
      • Education
      • Entertainment
      • Fashion
      • Fitness
      • Food
      • Gadgets
      • Games
      • Health
      • History
      • Hobby
      • Humor
      • Interior
      • Moto
      • Movies
      • Music
      • Nature
      • News
      • Photo
      • Pictures
      • Politics
      • Psychology
      • Science
      • Society
      • Sport
      • Technology
      • Travel
      • Video
      • Weapons
      • Web
      • Work
        Submit
        Valid formats are JPG, PNG, GIF.
        Not more than 5 Мb, please.
        30
        surfingbird.ru/site/
        RSS format guidelines
        500
        • Advertisement
        • Animals
        • Architecture
        • Art
        • Auto
        • Aviation
        • Books
        • Cartoons
        • Celebrities
        • Children
        • Culture
        • Design
        • Economics
        • Education
        • Entertainment
        • Fashion
        • Fitness
        • Food
        • Gadgets
        • Games
        • Health
        • History
        • Hobby
        • Humor
        • Interior
        • Moto
        • Movies
        • Music
        • Nature
        • News
        • Photo
        • Pictures
        • Politics
        • Psychology
        • Science
        • Society
        • Sport
        • Technology
        • Travel
        • Video
        • Weapons
        • Web
        • Work

          Submit

          Thank you! Wait for moderation.

          Тебе это не нравится?

          You can block the domain, tag, user or channel, and we'll stop recommend it to you. You can always unblock them in your settings.

          • habrahabr.ru
          • домен habrahabr.ru

          Get a link

          Спасибо, твоя жалоба принята.

          Log on to Surfingbird

          Recover
          Sign up

          or

          Welcome to Surfingbird.com!

          You'll find thousands of interesting pages, photos, and videos inside.
          Join!

          • Personal
            recommendations

          • Stash
            interesting and useful stuff

          • Anywhere,
            anytime

          Do we already know you? Login or restore the password.

          Close

          Add to collection

             

            Facebook

            Ваш профиль на рассмотрении, обновите страницу через несколько секунд

            Facebook

            К сожалению, вы не попадаете под условия акции