Интеграция Ruby в Nginx



    Уже достаточно давно существует всем известная связка Nginx + Lua, в том числе здесь был ряд статей. Но время не стоит на месте. Примерно год назад появилась первая версия модуля, интегрирующего Ruby в Nginx.

    MRuby


    Для интеграции был выбран не полноценный Ruby, а его подмножество, которое предназначено для встраивания в другие приложения, устройства и тд. Имеет некоторые ограничения, но в остальном полноценный Ruby. Проект называется MRuby. На текущий момент имеет уже версию 1.0.0, т.е. считается стабильным.
    MRuby не позволяет подключать другие файлы во время выполнения, поэтому вся программа должна быть в одном файле. При этом есть возможность преобразовать программу в байткод и выполнять уже его, что положительно сказывается на производительности.
    Т.к. нет возможности подгружать другие файлы, то и существующие gem-ы не подходят для него. Для расширения функционала используется свой формат, который представляет из себя как C код, так и Ruby местами. Данные модули собираются вместе с самой библиотекой во время компиляции и являются ее неотъемлемой частью. Имеются биндинги к различным базам данных, для работы с файлами, сетью и так далее. Полный список доступен на сайте.
    Также там имеется модуль, позволяющий интегрировать данный движок в Nginx, который особенно заинтересовал.

    ngx_mruby


    Итак, знакомьтесь: ngx_mruby. Модуль для подключения ruby скриптов к nginx. Имеет схожий функционал с Lua версией. Позволяет выполнять операции на различных этапах обработки запроса.

    Модуль собирается довольно просто, на сайте есть подробная инструкция. Кто не хочет заморачиваться со сборкой, могут скачать готовый пакет:
    http://mruby.ajieks.ru/st/nginx_1.4.4-1~mruby~precise_amd64.deb
    MRuby в данной сборке содержит следующие дополнительные модули:

    Как видите, есть почти все необходимое для работы. Единственное, что не обнаружил в API данного модуля, это возможности делать запрос наружу. Скорее всего, его нужно будет реализовать как расширение и сделать обвязку вокруг nginx API.

    Автор показывает красивый график с тестами, но конфигурации окружения так и не нашел. Поэтому просто приложу его для красоты:
    image

    Попробуем использовать


    Итак, сервер у нас уже установлен. Все функционирует, статика отдается. Добавим немного к этому динамики.
    В качестве примера я выбрал задачу по парсингу Markdown разметки и отдачи ее в HTML без дополнительного серверного приложения. А также нумерации строк в исходниках на Ruby.
    Для этого сделан клон репозитория sinatra и настроен nginx для решения поставленной задачи.

    Markdown

    Для обработки разметки воспользуемся подключенным в сборку модулем mruby-discount. Он предоставляет простой класс для работы с разметкой. В основе лежит одноименная библиотека на C, потому вопрос производительности, думаю, особо стоять не будет.
    Для начала напишем программу, которая будет считывать запрошенный файл с диска, обрабатывать его и отдавать пользователю.
    r = Nginx::Request.new
    
    m = Discount.new("/st/style.css", "README")
    
    filename = r.filename
    filename = File.join(filename, 'README.md') if filename.end_with?('/')
    
    markdown = File.exists?(filename) ? File.read(filename) : ''
    Nginx.rputs m.header
    Nginx.rputs m.md2html(markdown)
    Nginx.rputs m.footer
    

    Первой строкой получаем экземпляр объекта запроса, содержащий всю необходимую информацию, включая запрошенный файл, заголовки, URL, URI и т.д.
    Следующей строкой создаем экземпляр класса Discount, указывая файл стиля и заголовк страницы.
    Данный код не делает обработку 404 ошибки, поэтому даже если файла нету, всегда будет 200 код возврата.
    Подключаем теперь все это
        location ~ \.md$ {
            add_header Content-Type text/html;
            mruby_content_handler "/opt/app/parse_md.rb" cache;
        }
    

    Результат:
    mruby.ajieks.ru/sinatra
    mruby.ajieks.ru/sinatra/README.ru.md

    Файлы Ruby

    Первоначально планировал сделать не просто нумерацию, а так же раскраску кода, используя когда-то написанный код https://github.com/fuCtor/chalks. Однако после всех произведенных адаптаций в его работе возникли проблемы. Код, вроде, работал, но на определенном этапе падал с Segmentation fault. Первоначальное подозрение было на нехватку памяти выделяемой, но даже после уменьшения ее потребление проблема не пропала. После удаления кода, связанного с раскраской, все заработало, но не так красиво, как хотелось.
    Результат изменений
    module CGI
    TABLE_FOR_ESCAPE_HTML__ = {"&"=>"&", '"'=>""", "<"=>"<", ">"=>">"}
    def self.escapeHTML(string)
      string.gsub(/[&\"<>]/) do |ch|
      TABLE_FOR_ESCAPE_HTML__[ch]
      end
    end
    end
    
    class String
      def ord
        self.bytes[0]
      end
    end
    
    class Chalk
    
      COMMENT_START_CHARS = {
          ruby: /#./,
          cpp: /\/\*|\/\//,
          c: /\/\//
      }
      COMMENT_END_CHARS = {
          cpp: /\*\/|.\n/,
          ruby: /.\n/,
          c: /.\n/,
      }
    
      STRING_SEP = %w(' ")
      SEPARATORS = " @(){}[],.:;\"\'`<>=+-*/\t\n\\?|&#"
      SEPARATORS_RX = /[@\(\)\{\}\[\],\.\:;"'`\<\>=\+\-\*\/\t\n\\\?\|\&#]/
    
      def initialize(file)
        @filename = file
        @file = File.new(file)
        @rnd = Random.new(file.hash)
        @tokens = {}
        reset
      end
    
      def parse &block
        reset()
    
        @file.read.each_char do |char|
          @last_couple = ((@last_couple.size < 2) ? @last_couple : @last_couple[1]) + char
    
          case(@state)
            when :source
              if start_comment?(@last_couple)
                @state = :comment
              elsif STRING_SEP.include?(char)
                  @string_started_with = char
                  @state = :string
              else
                process_entity(&block) if (@entity.length == 1 && SEPARATORS.index(@entity))  || SEPARATORS.index(char)
              end
    
            when :comment
              process_entity(:source, &block) if end_comment?(@last_couple)
    
            when :string
              if (STRING_SEP.include?(char) && @string_started_with == char)
                @entity += char
                process_entity(:source, &block)
                char = ''
              elsif char == '\\'
                @state = :escaped_char
              else
              end
            when :escaped_char
              @state = :string
          end
          @entity += char
        end
      end
    
      def to_html(&block)
        html = ''
        if block
          block.call( '<table><tr><td><pre>' )
        else
          html = '<table><tr><td><pre>'
        end
        line_n = 1
        @file.readlines.each do
          if block
            block.call( "<a href='#'><b>#{line_n}</b></a>\n" )
          else
            html += "<a href='#'><b>#{line_n}</b></a>\n"
          end
          line_n += 1
        end
    
        @file = File.open(@filename)
        if block
          block.call( '</pre></td><td><pre>' )
        else
          html += '</pre></td><td><pre>'
        end
        parse do |entity, type|
          entity = entity.gsub("\t", '  ')
          if block
            block.call( entity )
            #block.call(highlight( entity , type))
          else
            html += entity
            #html += highlight( entity , type)
          end
        end
    
        if block
          block.call( '</pre><td></tr></table>' )
        else
          html + '</pre><td></tr></table>'
        end
      end
    
      def language
        @language ||= case(@file.path.to_s.split('.').last.to_sym)
          when :rb
            :ruby
          when :cpp, :hpp
            :cpp
          when  :c, :h
            :c
          when :py
            :python
          else
            @file.path.to_s.split('.').last.to_s
        end
      end
    
      private
    
    
      def process_entity(new_state = nil, &block)
        block.call @entity, @state if block
        @entity = ''
        @state = new_state if new_state
      end
    
      def reset
        @file = File.open(@filename) if @file
        @state = :source
        @string_started_with = ''
        @entity = ''
        @last_couple = ''
      end
    
      def color(entity)
        entity = entity.strip
    
        entity.gsub! SEPARATORS_RX, ''
    
        token = ''
        return token if entity.empty?
        #return token if token = @tokens[entity]
    
        return '' if entity[0].ord >= 128
    
        rgb = [ @rnd.rand(150) + 100, @rnd.rand(150) + 100, @rnd.rand(150) + 100 ]
    
        token = String.sprintf("#%02X%02X%02X", rgb[0], rgb[1], rgb[2])
        #token = "#%02X%02X%02X" % rgb
        #@tokens[entity] = token
        return token
      end
    
      def highlight(entity, type)
        esc_entity = CGI.escapeHTML( entity )
        case type
          when :string, :comment
            "<span class='#{type}'>#{esc_entity}</span>"
          else
    
            rgb = color(entity)
            if rgb.empty?
              esc_entity
            else
              "<span rel='t#{rgb.hash}' style='color: #{rgb}' >#{esc_entity}</span>"
            end
    
        end
      end
    
      def start_comment?(char)
        rx = COMMENT_START_CHARS[language]
        char.match rx if rx
      end
    
      def end_comment?(char)
        rx = COMMENT_END_CHARS[language]
        char.match rx if rx
      end
    end
    


    И собственно код, который выполняет чтение файла и нумерацию:
    r = Nginx::Request.new
    
    Nginx.rputs '<html><link rel="stylesheet" href="/st/code.css" type="text/css" /><body>'
    begin
        ch = Chalk.new(r.filename)
        data = ch.to_html
        Nginx.rputs data
    
    rescue => e
      Nginx.rputs e.message
    end
    
    Nginx.rputs '</body></html>'
    

    Подключаем все. Т.к. класс Chalk используется постоянно, подгрузим его заранее:
    mruby_init '/opt/app/init.rb';
    Данная строка добавляется перед server секцией в настройках. Далее уже указываем наш обработчик:
      location ~ \.rb$ {
            add_header Content-Type text/html;
            mruby_content_handler "/opt/app/parse_code.rb" cache;
       }
    

    Все, теперь можно посмотреть на результат: mruby.ajieks.ru/sinatra/lib/sinatra/main.rb

    Заключение


    Таким образом, возможно реализовать расширенную обработку запросов, фильтрацию, кэширование, используя еще один из языков. Готов ли данный модуль для использования в боевых условиях, не знаю. Пока тестировал, бывали зависания всего сервера, но есть вероятность кривизны рук, либо все же не все до конца доработано. Буду следить за развитием проекта.

    Желающие могут погонять на производительность указанные в статье скрипты по ссылкам выше.
    Сервер развернут на DigitalOcean на самой простой машине, Ubuntu 12.04 x64. Количество процессов 2, подключений 1024. Никаких дополнительных настроек не делалось. На случай зависания сервера поставил перезагрузку nginx каждые 10 минут.

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 28

      +3
      А где сравнение с Lua? ;)
        0
        Есть deb пакет nginx + lua для 12.04, либо если кто напишет эквивалентные решения (хотябы для Markdown) то можно выложить и проверить. Сам в Lua не силен.
          0
          +2
          Расскажите подробнее для чего это используется? То есть можно работать с запросами на более низком уровне и при этом на Ruby? Или можно прям Rails (Middleman, Sinatra) -проект запустить минуя Passenger (Puma, Unicorn etc)?
            +2
            Как я понял, это полный аналог nginx-lua модуля, который позволяет много чего интересного.
              +2
              Да именно для работы с запросами на низком уровне.
              К примеру, увам необходимо ограничить доступ к файлам на сервере. Варианта два, использовать модуль для Nginx: nginx.org/ru/docs/http/ngx_http_secure_link_module.html Но он не сильно гибкий, а можно реализовать это используя либо Ruby либо Lua + Redis|Memcache или другую БД. Тогда логика может быть сколь угодно сложной + поведение при отказе опять же контролируется.

              Вот даже пример есть: github.com/matsumoto-r/ngx_mruby/wiki/Use-Case#file-server-like-dropbox-link

              Либо нужно лимитировать доступ на основе некоторых квот прозрачно для нижележащего приложения.

              Область применения подобных решений это сложная логика обработки запросов, либо постобработка. Заменой полноценной версии они не являются.
                0
                Очень круто! Спасибо за разъяснение :-)
                  +1
                  А не синхронно ли пойдут запросы на базу?
                  А не встанет ли nginx колом если база хоть на секунду задумается?

                  Я боялся делать логику в Nginx с внешними данными. Когда всё в памяти — хорошо, можно делать. А вот ходить куда-то…
                    0
                    Про lua модуль могу сказать лишь в контексте работы с Redis (активно использую на чтение/запись) и MySQL (только на чтение). Там «из коробки» дается пул соединений, а все сетевое взаимодействие происходит асинхронно для nginx, но блокирующе для lua. Сам пул при этом можно настраивать (размеры, таймауты).
                      0
                      Да, похоже в Lua озаботились сильной интеграцией с nginx. Я больше на mod_perl, там асинхронность никто не обещает, поэтому избегал.

                      А вообще обходной путь через fastcgi backend и X-Accel-Redirect не намного сложнее, а софт стандартный.
                        0
                        У нас lua используется в задачах, где дергать backend выходит накладно. Для более «редких» запросов, да, можно и что-то отдельно завести: в нашем случае php и python.
                +4
                Как насчет блокирующих операций в ruby? Реализация mruby для nginx умеет встраиваться в event loop самого nginx?
                  0
                  От Ruby как такового там только синтаксис и некоторый набор Core классов. Все остальное реализуется в C модулях. Посмотрел как там реализовано подключение тому-же Redis, то судя по всему используется блокирующее API => в идеале нужно переписать отдельно на модуль с поддержкой nginx.
                  В Lua работа с Redis реализована на самом Lua, а так как там реализована обвязка над сокетами через nginx event loop, то там иная ситуация.

                  Все это узко специализированное решение. Когда нет необходимости в неблокирующих вызовах, имеет место быть. Хотя все нужно мерить, сравнивать, проверять. Может что не углядел в коде и оно совсем не так как показалось.
                    +1
                    Чем мне нравится lua модуль, так тем, что там идет упор на неблокирующие операции (диск, сеть...). Возможность писать линейный код с блокирующими вызовами, которые реально не блокируют, очень приятно. Если же в этом нет необходимости и можно блокироваться — тогда лучше уж, имхо, выносить логику на backend, а не выполнять ее в самом nginx.

                    Погонял чуть модули в сравнении (портировал на lua в меру своих познаний ruby) через siege в 10-300 потоков на 30 секунд.

                    Сопоставимые HelloWorld'ы
                    ngx.print('Hello ngx_lua/'..ngx.config.ngx_lua_version..' world!')
                    

                    Nginx::rputs "Hello #{Nginx::module_name}/#{Nginx::module_version} world!"
                    

                    Выполняются одинаково на уровне погрешности.

                    Пример с факториалом из mruby (и да простит меня сервер, на котором я это тестил)
                    local function fib(n)
                        return n < 2 and n or fib(n-2) + fib(n-1)
                    end
                    
                    ngx.print(fib(39))
                    

                    def fib n
                      return n if n < 2
                      fib(n-2) + fib(n-1)
                    end
                    
                    puts fib(39)
                    


                    -c 20 -t 30S
                    mruby — 7 hits — 0.24 trans/sec
                    lua — 51 hits — 1.72 trans/sec

                    -c 10 -t 60S
                    mruby — 10 hits — 0.17 trans/sec
                    lua — 163 hits — 2.76 trans/sec


                    Как с файлами работать в mruby я не в курсе (если вообще можно). Если кто даст пример — погоняю.
                      +1
                      P.S. В один поток (wget) mruby и lua считают 39fib за 19 и 2.7 сек соотвественно. Почему так медленно у mruby?
                        0
                        Хм, странно, посмотрю сейчас в сравнении с полноценной версией и без привязки к nginx.
                          0
                          $ time ./mruby fi.rb
                          63245986
                          
                          real    0m16.064s
                          user    0m15.769s
                          sys     0m0.028s
                          

                          Lua 5.1.4  Copyright (C) 1994-2008 Lua.org, PUC-Rio
                          $ time lua fi.lua
                          63245986
                          
                          real    0m10.548s
                          user    0m10.325s
                          sys     0m0.020s
                          

                          ruby 1.9.3p0 (2011-10-30 revision 33570) [x86_64-linux]
                          $ time ruby fi.rb
                          63245986
                          
                          real    0m12.904s
                          user    0m12.525s
                          sys     0m0.036s
                          
                          

                          $ time ./mruby -b fi.mrb
                          63245986
                          
                          real    0m15.483s
                          user    0m15.185s
                          sys     0m0.016s
                          


                          Последняя версяя ruby обгоняет уже lua, но все же, mruby не на столько шустр, как его позиционирует автор. Хотя это все синтетика и не понятно как на других вести себя будет тестах.
                            +4
                            Я тестировал на luajit. Обычный lua не рекомендуется авторами lua_nginx, а также, даже если собрать nginx с ним, не будут работать некоторые модули.

                            А вот на моей локальной машине:
                            $ time ruby /tmp/fib.rb
                            63245986

                            real 0m9.331s
                            user 0m9.322s
                            sys 0m0.011s

                            $ time /tmp/nl/mruby/bin/mruby /tmp/fib.rb
                            63245986

                            real 0m11.551s
                            user 0m11.507s
                            sys 0m0.008s

                            $ time luajit /tmp/fib.lua
                            63245986

                            real 0m0.884s
                            user 0m0.884s
                            sys 0m0.001s
                              0
                              Какую версию руби используете тоже укажите пожалуйста, есть большая разница.
                                0
                                На локальной машине:
                                ruby 1.9.3p484 (2013-11-22 revision 43786) [x86_64-linux]
                                LuaJIT 2.0.2
                                nginx version: nginx/1.6.0

                                На сервере:
                                ruby 1.8.7 (2011-06-30 patchlevel 352) [x86_64-linux]
                                LuaJIT 2.0.0-beta9
                                nginx version: nginx/1.6.0
                                  0
                                  mruby 1.0.0 (2014-01-10)
                                  ruby fib.rb 15.03s user 0.00s system 99% cpu 15.040 total

                                  ruby 1.9.3p484 (2013-11-22 revision 43786) [x86_64-darwin13.1.0]
                                  ruby fib.rb 12.08s user 0.01s system 99% cpu 12.087 total

                                  ruby 2.0.0p353 (2013-11-22 revision 43784) [x86_64-darwin13.0.0]
                                  ruby fib.rb 11.11s user 0.01s system 99% cpu 11.123 total

                                  ruby 2.1.1p76 (2014-02-24 revision 45161) [x86_64-darwin12.0]
                                  ruby fib.rb 10.76s user 0.01s system 99% cpu 10.773 total

                                  Какой-то mruby совсем грустный, на 50% медленнее MRI
                          0
                          Так понимаю, больше hits => больше rps?

                          Сам по себе mruby не умеет с файлами работать, нужно подключать mruby_io, но в ngix_mruby он уже подключен.
                          Чтение файла целиком:
                          File.read(filename)
                          Открытие файла на чтение
                          f = File.open(filename, 'r')
                          f.read

                          А на счет неблокируемости Lua реализации согласен. Но тут есть конечно вариант, реализовать сокеты для mruby поверх nginx API, тогда можно будет реализовать простейшие протоколы для того же redis, memcached и тп на чистом Ruby. Думаю есть куда развиваться модулю, если будет интерес со стороны разработчиков, то и реализации подтянутся.
                            +1
                            hits тут — это сколько всего запросов было обработано за все время тестирования (30 и 60 секунд соотвественно).

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

                            Я весьма активно пользуюсь lua_nginx в своих задачах. Отличнейшая штука. Если ruby версия пойдет таким же путем, то может, наверное, сыскать успех у любителей синтаксиса данного ЯП.
                              0
                              Потестил по простому на файлах.
                              К слову, File в mruby не идет сразу в сборке. Все gems нужно руками указывать в build_config.rb (в формате url на github), потом пересобирать mruby и nginx. У вас в посте написано, что они уже идут в сборке. Как вы собирали?

                              И результаты:

                              local f = io.open('/dev/zero', 'rb')
                              for i = 1, 1000 do
                                  ngx.print(f:read(4096))
                              end
                              f:close()
                              

                              f = File.open('/dev/zero', 'rb')
                              for i in 1..1000
                                  Nginx::rputs f.read(4096)
                              end
                              f.close
                              


                              Второй тест тоже самое, но читаемый за запрос кусок уменьшен до 1024. Итоги:
                              chunk 4096
                              lua   - 3065 hits - 105.54 trans/sec
                              mruby - 785 hits  - 26.52 trans/sec
                              
                              chunk 1024
                              lua   - 7645 hits - 262.99 trans/sec
                              mruby - 3305 hits - 112.61 trans/sec
                              

                              Вот как-то так выходит.
                                0
                                Резюмируя все выше указанное, возникает резонный вопрос: Как автор получил те цифры что на приложенном графике?
                              +1
                              lua- изначально быстрее руби…
                              сперва сравните бенчмарки для консольного приложения.

                              если сделать на любой движок обвязку, хоть nginx, libevent_http или любую иную, то он от этого ни быстрее, ни медленнее не станет… только если специально кривыми руками не замедлить.
                                0
                                В консоли ситуация такая же. Вот планирую потестить на файлах, хоть какое-то сравнение встраивания в nginx.
                          +7
                          О качестве кода красноречиво повествуют эти строки:

                              ctx = ngx_http_get_module_ctx(r, ngx_http_mruby_module);
                              if (ctx == NULL) {
                                ngx_log_error(NGX_LOG_ERR
                                  , r->connection->log
                                  , 0
                                  , "get mruby context failed."
                                );
                              }
                          
                              if (ctx->rputs_chain == NULL) {
                                chain = ngx_pcalloc(r->pool, sizeof(ngx_mrb_rputs_chain_list_t));
                                chain->out = ngx_alloc_chain_link(r->pool);
                                chain->last = &chain->out;
                              }
                          


                          Проверить *ctx на NULL, а далее попытаться по нему обратиться — это весело, это гарантированный сигфолт.

                          И там такое повсюду, только открыл — сразу глазом зацепился. Практически вся аллокация памяти без проверок на то, что память нам действительно выделили. И это пожалуй ещё самые малые проблемы модуля.

                          Only users with full accounts can post comments. Log in, please.