Интеграция 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 минут.

ссылка на оригинал статьи http://habrahabr.ru/post/225313/


Комментарии

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

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