Nginx + Lua + Redis. Эффективно обрабатываем сессию и отдаем данные

    image
    Предположим, у вас есть данные, которые вы хотите кэшировать и отдавать, не используя тяжелые языки, как php, при этом проверяя, что пользователь аутентифицирован и имеет право на доступ к данным. Сегодня я расскажу, как, используя связку nginx lua redis, выполнить эту задачу, снять нагрузку с сервера и увеличить скорость отдачи информации сервером в десятки раз.

    Для начала необходимо собрать nginx с модулем nginx_lua_module.

    Инструкция по установке
    Установим компилятор lua (версии 2.0 или 2.1)

    Скачаем luaJit и соберем его
    make && sudo make install
    


    Для сборки nginx с nginx devel kit необходим http_rewrite_module, а тот с свою очередь требует библиотеку pcre. Поэтому установим ее
    sudo apt-get update
    sudo apt-get install libpcre3 libpcre3-dev
    


    Скачаем зависимые модули и сам nginx
    nginx devel kit
    nginx lua module
    nginx

    Сконфигурируем и установим nginx
    export LUAJIT_LIB=/usr/local/lib // путь к библиотеке lua
    export LUAJIT_INC=/usr/local/include/luajit-2.1 //путь к luaJit
    
    ./configure 
    --prefix=/etc/nginx 
    --sbin-path=/usr/sbin/nginx
    --conf-path=/etc/nginx/nginx.conf 
    --error-log-path=/var/log/nginx/error.log
    --http-log-path=/var/log/nginx/access.log 
    --pid-path=/var/run/nginx.pid 
    --lock-path=/var/run/nginx.lock
    --http-client-body-temp-path=/var/cache/nginx/client_temp 
    --http-proxy-temp-path=/var/cache/nginx/proxy_temp 
    --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp 
    --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp
    --http-scgi-temp-path=/var/cache/nginx/scgi_temp 
    --user=nginx 
    --group=nginx  
    --with-ld-opt="-Wl,-rpath,/path/to/lua/lib" // путь к библиотеке Lua
    --add-module=/path/to/ngx_devel_kit //путь к nginx devel kit
    --add-module=/path/to/lua-nginx-module // путь к nginx lua module
    --without-http_gzip_module
    
    make -j2
    sudo make install
    


    Скачаем библиотеку lua для работы с redis lua redis lib и скопируем ее в папку библиотек lua командой
    sudo make install
    


    Подключим библиотеку lua redis в конфигурацию nginx

    http {
    ...
        lua_package_path lua_package_path "/path/to/lib/lua/resty/redis.lua;;"; // путь к библиотеке lua redis
    ...
    }
    


    Все. Теперь можно писать скрипы на lua, которые будут исполнятся nginx


    Чтобы быстро и эффективно отдавать кэшированные данные, мы положим самые часто используемые из них в redis сразу при прогреве кэша, а менее используемые будем класть по запросу. Отдавать данные будем с помощью lua на стороне nginx. В этой связке не будет участвовать php, что в разы ускорит выдачу данных и будет занимать намного меньше памяти у сервера.

    Для этого напишем Lua скрипт

    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 и перезагрузим nginx

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


    Теперь при запросе /search-by-string?string=smth lua подключится к redis и попробует найти данные по ключу search:smth. Если данных не окажется, то запрос обработает php. Но если данные уже закэшированы и лежат в redis, то они будут сразу же отданы пользователю.

    Но что, если нам нужно отдавать данные, только если пользователь аутентифицирован и при этом имеет определенную роль?

    В таком случае можно хранить сессию в redis и перед тем, как отдавать контент, проверять роль пользователя по данным сессии.

    Т.к. я работаю с фрэймворком Symfony2, то для него был написан небольшой бандл nginx-session-handler, с помощью которого можно хранить сессию в redis именно так, как нам удобно.

    В redis данные будут хранится в качестве хэш значения:
    phpsession — префикс ключа для сессии
    php-session — сама сессия php
    user-role — роль пользователя.

    Теперь нужно написать lua скрипт для обработки этих данных:

    session.lua
    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.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) -- если не удалось подключиться, 
    end                                          --  то возвращаем ответ со статусом 500
    
    
    local phpsession = ngx.var.cookie_PHPSESSID -- получаем id сессии из cookie пользователя
    local ROLE_ADMIN = "ROLE_ADMIN" -- роль, которой нужно предоставить доступ
    
    if phpsession == ngx.null then
      ngx.exit(ngx.HTTP_FORBIDDEN) -- если в cookie нет сессии(пользователь не аутентифицированн), 
    end                            -- то  возвращаем ответ со статусом 403
    
    local res, err = red:hget("phpsession:" .. phpsession, "user-role") -- получаем роль пользователя 
                                                                        -- из redis по id сессии
    
    if res == ngx.null or res ~= ROLE_ADMIN then 
        ngx.exit(ngx.HTTP_FORBIDDEN) -- если сессии нет(закончилось время жизни сессии) или 
    end                              --  у пользователя не та роль,  что нам нужна,
                                     -- то  возвращаем ответ со статусом 403
    



    Мы достаем id сессии пользователя из cookie, пытаемся получить роль пользователя по его id сессии из redis по запросу HGET phpsession:id user-role. Если у пользователя истекло время жизни сессии, он не аутенитифицированн или у него не роль ROLE_ADMIN, то сервер вернет код 403.

    Дописываем этот скрипт обработки сессии перед нашим скриптом получения данных и теперь данные могут получить только аутентифицированные пользователи с ролью ROLE_ADMIN.

    На деле скрипт обработки сессии будет необходим для нескольких location nginx. Чтобы не писать один и тот же код в разных местах, мы будем подключать этот файл там, где нам нужно.

    Для начала немного перепишем наш скрипт обработки сессии.

    session.lua
    local _M = {} --добавили переменную
    
    function _M.handle() -- записали в нее функцию и поместили в нее весь предыдущий код
    
        local redis = require "resty.redis"
        local red = redis:new()
    
        red:set_timeout(1000) -- 1 sec
    
        local ok = red:connect("127.0.0.1", 6379)
        if not ok then
            ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
        end
    
        local phpsession = ngx.var.cookie_PHPSESSID
        local ROLE_ADMIN = "ROLE_ADMIN"
    
        if phpsession == ngx.null then
            ngx.exit(ngx.HTTP_FORBIDDEN)
        end
    
        local res = red:hget("phpsession:" .. phpsession, "user-role")
    
        if res == ngx.null or res ~= ROLE_ADMIN then
            ngx.exit(ngx.HTTP_FORBIDDEN)
        end
    
    end
    
    return _M -- вернули переменную с функцией
    



    Теперь необходимо собрать session.o файл из session.lua с помощью компилятора luaJit и собрать nginx c этим файлом.

    Соберем session.o файл, выполнив команду компилятора lua
    /path/to/luajit/bin/luajit -bg session.lua session.o
    


    Добавим в конфигурацию для сборки nginx строку

    --with-ld-opt="/path/to/session.o"
    


    и соберем nginx(как собрать nginx описано выше)

    После этого можно подключать файл в любой lua скрипт и вызывать функцию handle() для обработки сессии пользователя

    local session = require "session"
    session.handle()
    


    В конце небольшой тест для сравнения.

    конфигурация компьютера
    Processor: Intel Xeon CPU X3440 @ 2.53GHz × 8
    Memory: 7.9 GiB

    Тесты, которые с помощью php или lua достают данные из redis

    ab -n 100 -c 100 php
    Server Software: nginx/1.9.4

    Concurrency Level: 100
    Time taken for tests: 3.869 seconds
    Complete requests: 100
    Failed requests: 0
    Requests per second: 25.85 [#/sec] (mean)
    Time per request: 3868.776 [ms] (mean)
    Time per request: 38.688 [ms] (mean, across all concurrent requests)
    Transfer rate: 6.66 [Kbytes/sec] received

    Connection Times (ms)
    min mean[±sd] median max
    Connect: 1 3 1.1 3 5
    Processing: 155 2116 1053.7 2191 3863
    Waiting: 155 2116 1053.7 2191 3863
    Total: 160 2119 1052.6 2194 3864

    Percentage of the requests served within a certain time (ms)
    50% 2194
    66% 2697
    75% 3015
    80% 3159
    90% 3504
    95% 3684
    98% 3861
    99% 3864
    100% 3864 (longest request)

    ab -n 100 -c 100 lua
    Server Software: nginx/1.9.4

    Concurrency Level: 100
    Time taken for tests: 0.022 seconds
    Complete requests: 100
    Failed requests: 0
    Requests per second: 4549.59 [#/sec] (mean)
    Time per request: 21.980 [ms] (mean)
    Time per request: 0.220 [ms] (mean, across all concurrent requests)
    Transfer rate: 688.66 [Kbytes/sec] received

    Connection Times (ms)
    min mean[±sd] median max
    Connect: 2 4 0.9 4 6
    Processing: 3 13 1.6 13 14
    Waiting: 3 13 1.6 13 14
    Total: 9 17 1.3 18 18

    Percentage of the requests served within a certain time (ms)
    50% 18
    66% 18
    75% 18
    80% 18
    90% 18
    95% 18
    98% 18
    99% 18
    100% 18 (longest request)

    Разница «количества запросов в секунду» в 175 раз.

    Такой же тест с другими парметрами
    ab -n 10000 -c 100 php
    Server Software: nginx/1.9.4

    Concurrency Level: 100
    Time taken for tests: 343.082 seconds
    Complete requests: 10000
    Failed requests: 0
    Requests per second: 29.15 [#/sec] (mean)
    Time per request: 3430.821 [ms] (mean)
    Time per request: 34.308 [ms] (mean, across all concurrent requests)
    Transfer rate: 7.51 [Kbytes/sec] received

    Connection Times (ms)
    min mean[±sd] median max
    Connect: 0 0 0.3 0 4
    Processing: 167 3414 197.5 3408 4054
    Waiting: 167 3413 197.5 3408 4054
    Total: 171 3414 197.3 3408 4055

    Percentage of the requests served within a certain time (ms)
    50% 3408
    66% 3438
    75% 3458
    80% 3474
    90% 3533
    95% 3633
    98% 3714
    99% 3866
    100% 4055 (longest request)

    ab -n 10000 -c 100 lua
    Server Software: nginx/1.9.4

    Concurrency Level: 100
    Time taken for tests: 0.899 seconds
    Complete requests: 10000
    Failed requests: 0
    Requests per second: 11118.29 [#/sec] (mean)
    Time per request: 8.994 [ms] (mean)
    Time per request: 0.090 [ms] (mean, across all concurrent requests)
    Transfer rate: 1682.94 [Kbytes/sec] received

    Connection Times (ms)
    min mean[±sd] median max
    Connect: 0 0 0.4 0 5
    Processing: 1 9 3.4 7 19
    Waiting: 1 9 3.5 7 18
    Total: 2 9 3.4 7 21

    Percentage of the requests served within a certain time (ms)
    50% 7
    66% 13
    75% 13
    80% 13
    90% 13
    95% 13
    98% 13
    99% 15
    100% 21 (longest request)

    Разница «количества запросов в секунду» в 381 раз.

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

    Подробнее
    Реклама
    Комментарии 12
    • +5
      Особо горячий кеш можно бы прямо в shared_dict самого nginx хранить, чтобы и в redis не ходить, не мучать его.
      • 0
        У нас значения в redis обновляются каждые 1-5 минут с помощью php. Поэтому данные нужно забирать именно от туда. Но про shared_dict хорошее замечание, посмотрю, можно ли его будет где применить.
        • +1
          Так можно же не вечно хранить в локальной памяти: поставьте ttl (в терминах модуля это параметр exptime) записям на секунды-минуты. Тут уж зависит от вашей бизнес логики)
          Один Redis не вытянет нагрузку, которую сможет принять и переварить nginx_lua с локальным shared_dict в качестве «сверх-горячего» кеша.
          • 0
            Не стоит забывать, что shared_dict хранится в памяти. В redis могут и гигабайты храниться, нехорошо будет всё это дублировать в памяти nginx. Хотя если данных немного а объём запросов огромный — почему бы и нет.
      • +5
        цифры ab — ниочём. т.е. 100 запросов не говорят абсолютно ни-че-го. общее время почти 4 сек похоже на первичный прогрев самого php (не знаю, что там именно в нём бывает на старте, но это магия какая-то) или коннект к редису из php… в любом случае выглядит, как нереальная чушь. ну сами посудите — это нормально, что php-скрипт, делающий выборку одного значения из redis, отрабатывает МИНИМУМ за 2.2 секунды? 75% были дольше 3х секунд.
        100 запросов на concurrency 100 это значит, что прилетает разом пачка в 100 запросов и они дружно «инитятся». сделайте 10к запросов хотя бы и тогда уже смотрите на числа.
        • 0
          Соглашусь, экономия на спичках. Такие вещи не всплывут в гите. Все же авторизация — часть приложения.
          • 0
            Они точно дружно инитятся или становятся в очередь php-fpm?
            • 0
              Я не знаю, что происходит со стороны nginx+php (с первым довольно посредственный опыт, второй вообще мимо меня), но ab делает 100 потоков и инитит 100 http-запросов. Как их там обрабатывает уже принимающая сторона — зависит от многого. Сразу все параллельно инитятся или в очередь встают (скорее второе, потому что уже начинает играть кол-во воркеров nginx)… Я больше про то, что -c 100 -n 100 — неимоверно бессмысленная конструкция.
              • 0
                Количество воркеров nginx не связано с количеством параллельно обрабабываемых соединений
                • 0
                  ага, ваще никак

                  Syntax: worker_connections number;
                  Sets the maximum number of simultaneous connections that can be opened by a worker process.
                  • 0
                    Ну так это общее количество запросов, включая и статику (картинки, js, CSS etc). Этому параметру можно поставить и 65000, но пхп при 65к запросов уже может загнуться.
          • 0
            Небольшой оффтоп, но близкий по теме.
            У меня есть задача — построить отказоустойчивый comet сервер(longpolling\websockets). Сейчас использую nginx-push-stream модуль, все хорошо работает, только если нода падает и клиент переключается на другую — сообщения теряются. Как один из вариантов решений, казалось бы безумным — а не построить ли это на HaProxy+Nginx+Lua+Redis. Еще опыта с nginx+lua нет, интересно насколько реалистично такое реализовать или есть другие решения?
            Особенность в том, что у каждого клиента свой канал и если клиент отключился на время, то сообщение все-равно должно сохраняться в очередь с некоторым TTL, вдруг клиент переподключится(разрыв связи) — и получит все сообщения.

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