Как стать автором
Обновить

Зачем вы юзаете ActiveRecord без Rails?

Время на прочтение5 мин
Количество просмотров2.5K

Текст для тех, кто хочет писать мелкие сервисы на руби без рельс

Уже много раз видел примеры, где всякие микросервисы, консьюмеры и продюсеры (для кафки например), используют ActiveRecord. Да, окей, вам надо ходить в базу. Да, окей, вы кодите прилу на Rails и привыкли к ActiveRecord, но ведь это часть фреймворка. По сути этот гем - часть большого механизма, вы же не покупаете колесо от машины и не едете на нем? Да, колесо катится хорошо, но вот тормозить оно само по себе не может, тормоза остались в машине. Собственно, “отсутствие тормозов” у ActiveRecord и сподвигло на статью.

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

Давайте разберем плюсы и минусы использования ActiveRecord в качестве ORM для некоторого условного микросервиса: из плюсов вижу - знакомый интерфейс: то как мы объявляем модели, DB config; а вот минусов вижу дофига:

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

  • Во-вторых, он же такой тяжелый и сущности тяжелые, там столько ненужной шелухи, всяких callback’и, до сохранения, после, во время, сами сущности тоже тяжелые и тд.

  • В-третьих, если погуглить, то можно найти какой-нибудь Sequel (да да, там тоже есть хуки), который обладает похожим интерфейсом и поставляется как standalone решение. В общем, не понимаю зачем так делать. 

Живой пример

А сейчас я вам расскажу что может произойти, если использовать ActiveRecord в более-менее нагруженном сервисе. 

Например: нам нужно написать сервис, который будет уметь доставать данные из базы, немного их обрабатывать и отдавать клиенту. 

Решение: Берем, пишем этот сервис, в качестве ходилки в базу выбираем ActiveRecord, мы же его знаем, как облупленного, тестим - работает, выливаем на прод - работает. 

Первые проблемы: Работает год, два. А потом начинают сыпаться ошибки, да стремные такие, системные. Говорит, мол, не смог сокет открыть, заняты все, или что коннекшены в базу все закончились. Стектрейсы до сишных файлов гема pg. Пока что можно забить на это, потому что есть ретрай и он помогает, мало ли что там, кубер косячит или еще чего… Уберем в дальний бэклог, пока что проблем не вызывает.

Проблемы усиливаются: Но потом, где-то лет через 5, это приобретает лавинообразный вид, ретраи не помогают с первого раза, встают в очередь, не срабатывают и со второго раза, бывает доходит и до ста попыток, некоторые бедолаги после исчерпания ретрай лимита умирают и освобождают очередь. Тут то уже от этой проблемы не отвязаться, это явно не кубер. 

Ищем решение

Идем проверять весь код, от и до, ничего сверхестественного нету. Падает на обычном селекте, который должен занять не больше пары десятков миллисекунд. Пару дней медитируем в сорцах pg (гема), ведь ошибка то в нем, судя по трейсу. Не помогает. Коллеги ничем помочь не могут, все разводят руками. 

Первые зацепки: Далее строим схему, как должен ActiveRecord в базу ходить и понимаем, что этой проблемы быть не должно, ведь у нас есть ConnectionPool, он же не должен дать выжрать все коннекшены, сокеты и тд, там их всего 20 штук выделяется на весь сервис.

Проверка гипотезы

Быстренько пишем скрипт, который ходит в базу через standalone ActiveRecord, ходит он парраллельно из нескольких тредов, проверяешь на каждой итерации самочувствие ConnectionPool… И да, собака тут зарыта, он не используется.

10.times do
  threads << Thread.new do
    begin
      users_count += User.count
      puts ActiveRecord::Base.connection_pool.stat.to_json
    rescue => e
      puts ActiveRecord::Base.connection_pool.stat.to_json
      errors << e
    end
  end
end
threads.each(&:join)
puts "Finished with #{errors.count} errors #{errors.uniq}"
stdout
{"size":5,"connections":2,"busy":2,"dead":0,"idle":0,"waiting":5,"checkout_timeout":5.0}
{"size":5,"connections":5,"busy":4,"dead":1,"idle":0,"waiting":5,"checkout_timeout":5.0}
{"size":5,"connections":5,"busy":3,"dead":2,"idle":0,"waiting":5,"checkout_timeout":5.0}
{"size":5,"connections":5,"busy":2,"dead":3,"idle":0,"waiting":5,"checkout_timeout":5.0}
{"size":5,"connections":5,"busy":1,"dead":4,"idle":0,"waiting":5,"checkout_timeout":5.0}
{"size":5,"connections":5,"busy":0,"dead":5,"idle":0,"waiting":4,"checkout_timeout":5.0}
{"size":5,"connections":5,"busy":0,"dead":5,"idle":0,"waiting":3,"checkout_timeout":5.0}
{"size":5,"connections":5,"busy":0,"dead":5,"idle":0,"waiting":2,"checkout_timeout":5.0}
{"size":5,"connections":5,"busy":0,"dead":5,"idle":0,"waiting":1,"checkout_timeout":5.0}
{"size":5,"connections":5,"busy":0,"dead":5,"idle":0,"waiting":0,"checkout_timeout":5.0}

// Finished with 5 errors: #<ActiveRecord::ConnectionTimeoutError: could not obtain a connection
// from the pool within 5.000 seconds (waited 5.001 seconds); all pooled connections were in use>

При недолгом ковырянии исходников рельсы выясняется, что где-то там в мидлваре, она оборачивает текущий исполняемый код в ConnectionPool.with_connection {...}. Проверяем работу с with_connection и вуаля, это оно.

10.times do
  threads << Thread.new do
    begin
      ActiveRecord::Base.connection_pool.with_connection do
        users_count += User.count
      end
      puts ActiveRecord::Base.connection_pool.stat.to_json
    rescue => e
      puts ActiveRecord::Base.connection_pool.stat.to_json
      errors << e
    end
  end
end
threads.each(&:join)
puts "Finished with #{errors.count} errors"
stdout
{"size":5,"connections":5,"busy":3,"dead":0,"idle":2,"waiting":6,"checkout_timeout":5.0}
{"size":5,"connections":5,"busy":2,"dead":0,"idle":3,"waiting":6,"checkout_timeout":5.0}
{"size":5,"connections":5,"busy":3,"dead":0,"idle":2,"waiting":4,"checkout_timeout":5.0}
{"size":5,"connections":5,"busy":2,"dead":0,"idle":3,"waiting":4,"checkout_timeout":5.0}
{"size":5,"connections":5,"busy":4,"dead":0,"idle":1,"waiting":1,"checkout_timeout":5.0}
{"size":5,"connections":5,"busy":4,"dead":0,"idle":1,"waiting":0,"checkout_timeout":5.0}
{"size":5,"connections":5,"busy":3,"dead":0,"idle":2,"waiting":0,"checkout_timeout":5.0}
{"size":5,"connections":5,"busy":2,"dead":0,"idle":3,"waiting":0,"checkout_timeout":5.0}
{"size":5,"connections":5,"busy":1,"dead":0,"idle":4,"waiting":0,"checkout_timeout":5.0}
{"size":5,"connections":5,"busy":0,"dead":0,"idle":5,"waiting":0,"checkout_timeout":5.0}
// Finished with 0 errors

Поиск best practce

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

Мораль такова - не ленись погуглить standalone решения. Не привязывайся к рельсе и ее спутникам, да и к любому другому фреймворку. 

А зачем вы юзаете ActiveRecord(или ActiveЧтоТоТам) без Rails?

Бонус

Решил проверить как там дела у сиквела и вот что получилось:

10.times do |i|
  threads << Thread.new do
    begin
      users_count += User.where{price > i}.count
      puts({:available_connections => DB.pool.available_connections.count}.to_json)
    rescue => e
      errors << e
    end
  end
end
threads.each(&:join)
puts "Counted #{users_count}"
puts "Finished with #{errors.count} errors #{errors.map(&:message).uniq}"
stdout
{"available_connections":1}
{"available_connections":1}
{"available_connections":1}
{"available_connections":1}
{"available_connections":1}
{"available_connections":1}
{"available_connections":2}
{"available_connections":3}
{"available_connections":4}
{"available_connections":5}
Counted 232717
Finished with 0 errors []

Видно что тут коннекшен пул работает в многопоточной среде без явного его использования.

Теги:
Хабы:
Всего голосов 8: ↑8 и ↓0+8
Комментарии8

Публикации

Истории

Работа

Программист Ruby
4 вакансии
Ruby on Rails
4 вакансии

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань