Простое приложение rails на Puma генерирует segfault, не может обрабатывать параллелизм

У меня довольно простое приложение на Rails. Он слушает запросы в форме

example.com/items?access_key=my_secret_key

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

Однако нам нужно, чтобы эта поддержка поддерживала несколько запросов одновременно, и Puma кажется нам любимым / самым быстрым сервером для всех. Мы начали сталкиваться с проблемами, сравнивая его с ApacheBench. К вашему сведению, Puma настроена на 3 рабочих и min=1, max=16 потоков.

Если бы я должен был бежать

ab -n 100 -c 10 127.0.0.1:3000/items?access_key=my_key

затем эта ошибка выдается с большим количеством трассировки стека после нее:

/home/user/.gem/ruby/2.0.0/gems/mysql2-0.3.16/lib/mysql2/client.rb:70: [BUG] Segmentation fault
ruby 2.0.0p353 (2013-11-22 revision 43784) [x86_64-linux]

Редактировать: эта строка также появляется в огромном количестве информации, содержащейся в ошибке:

*** glibc detected *** puma: cluster worker 1: 17088: corrupted double-linked list: 0x00007fb671ddbd60

И мне кажется, что это срабатывает несколько раз. Я не смог точно определить, когда (по каким запросам) он отключается. Бенчмаркинг, кажется, все еще заканчивается, но он кажется довольно медленным (из ab):

Concurrency Level:      10
Time taken for tests:   21.085 seconds
Complete requests:      100
Total transferred:      3620724 bytes

21 секунда за 3 мегабайта? Даже если MySQL был медленным, это... плохо. Но я думаю, что это еще хуже - объем данных недостаточно высок. Когда я запускаю параллелизм 1, ошибок сегмента нет, а объем данных для -n 10 -c 1 составляет 17 мегабайт. Таким образом, puma отвечает с некоторой страницей ошибок, которую я не вижу - запуск "curl address" дает мне ожидаемые данные, и я не могу вручную выполнить параллелизм.

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

ab -n 1000 -c 10 127.0.0.1:3000/items?access_key=my_key

доходность

apr_socket_recv: Connection reset by peer (104)
Total of 199 requests completed

а также

ab -n 100 -c 50 127.0.0.1:3000/items?access_key=my_key

доходность

apr_socket_recv: Connection reset by peer (104)
Total of 6 requests completed

Бег top в другом окне замазки показано, что очень часто (в большинстве случаев я пытаюсь провести сравнительный анализ) только один из трех рабочих, созданных пумой, выполняет какую-либо работу. Редко все трое делают.

Поскольку кажется, что ошибка может быть где-то здесь, я покажу вам мой application_controller. Это коротко, но основная часть приложения (что, как я уже сказал, довольно просто).

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  def get_yaml_params
    YAML.load(File.read("#{APP_ROOT}/config/ecommerce_config.yml"))
  end

  def access_key_login
    access_key = params[:access_key]

    unless access_key
      show_error("missing access_key parameter")
      return false
    end

    access_info = get_yaml_params

    unless client_login = access_info[access_key]
      show_error("invalid access_key")
      return false
    end

    status = ActiveRecord::Base.establish_connection(
      :adapter  => "mysql2",
      :host     => client_login["host"],
      :username => client_login["username"],
      :password => client_login["password"],
      :database => client_login["database"]
    )
  end

  def generate_json (columns, footer)
    // config/application.rb includes the line 
    // require 'json'
    query = "select"
    columns.each do |column, name|
      query += " #{column}"
      query += " AS #{name}" unless column == name
      query += ","
    end
    query = query[0..-2] # trim ','

    query += " #{footer}"

    dbh = ActiveRecord::Base.connection

    results = dbh.select_all(query).to_hash

    data = results.map do |result|
      columns.map {|column, name| result[name]}
    end

    ({"fields" => columns.values, "values" => data}).to_json
  end 

  def show_error(msg)
    render(:text => "Error: #{msg}\n")
    nil
  end
end

И пример контроллера, который его использует

class CategoriesController < ApplicationController
  def index
    access_key_login or return

    columns = {
      "prd_type" => "prd_type",
      "prd_type_description"   => "description"
    }

    footer = "from cin_desc;"
    json = generate_json(columns, footer) 
    render(:json => json)
  end
end

Это почти все, что касается пользовательского кода. Я не могу найти ничего, что делает это не безопасным для потоков, поэтому я не знаю, в чем причина segfaults. Я не знаю, почему не все рабочие раскручиваются, когда поступают запросы. Я не знаю, какая ошибка возвращается в ApacheBench. Спасибо за помощь, я могу разместить больше информации, как вам нужно.

1 ответ

Похоже, что стабильная версия библиотеки mysql2, 0.3.17, НЕ является поточно-ориентированной. До тех пор, пока он не будет обновлен для обеспечения многопоточности, его использование с многопоточной пумой будет невозможно. Альтернативой было бы использовать Unicorn.

Другие вопросы по тегам