Speeding Things Up
Наш мартовский Ruby-дайджест предлагает поразмышлять над консервативным подходом к внедрению новых функций в язык. Также подумаем, как предупредить возникновение memory bloat в Rails и узнаем, как Джереми Эванс решил ускорить запуск процессов.
Консерватизм в Ruby
Разработчики часто сталкиваются с дилеммой: применять консервативный подход и годами отточенные решения или же отслеживать и применять самые новые функции. Первое не требует каких-либо усилий и уменьшает вероятность того, что приложение станет работать нестабильно. Второе даёт разработчику значительно больше способов решения тех или иных задач.
Изучение новых функций языка программирования очень похоже на процесс изучения иностранного языка. Чем больше слов, конструкций и устойчивых выражений вы узнаёте, тем лучше и точнее вы можете выразить свою мысль. Если глобально взглянуть на программирование, то его можно сравнить с моделированием реальности при помощи ограниченного набора слов и конструкций. Чем больше у вас слов в запасе, тем проще будет описывать сложные алгоритмы и структуры данных.
Ruby в 2007 году с точки зрения разработчика на С/С++, Java или PHP явно казался странным и обладал очень непривычным синтаксисом. Здесь уместно вспомнить о гипотезе лингвистической относительности, утверждающей, что структура языка влияет на мировосприятие и воззрения его носителей, а также на их когнитивные процессы. Роман «Вавилон-17», вдохновивший Юкихиро Мацумото на создание Ruby, как раз был основан на этой гипотезе. Кстати, мы брали у него интервью — советуем взглянуть, если ещё не читали.
Время шло и язык неуловимо менялся. Приоритет был сделан на стабильность и надёжность, а скорость внедрения новых функций существенно снизилась. Для некоторых разработчиков, таких как Lucian Ghinda, это стало индикатором замедления развития языка. В своей статье «We should adopt and use new Ruby features» он высказал мнение, что желание оградить язык от нововведений может негативно сказаться на его развитии.
В качестве доказательства он приводит вышеупомянутую гипотезу лингвистической относительности и гипотезу лингвистического детерминизма. Все элементы языка программирования могут влиять на то, как разработчики определяют проблему и решение, которое они выбирают для этой проблемы. Так что основным пожеланием к дальнейшему развитию языка будет экспериментирование с новыми функциями. Это расширит границы языка и даст разработчикам больше инструментов для решения повседневных задач.
Борьба с memory bloat в Rails
В одном из прошлых дайджестов мы уже упоминали раздувание памяти в Ruby. Вполне рядовая ситуация с извлечением данных из БД или Redis, чтением файлов с диска или выполнением сетевого запроса может привести к резкому выделению большого объема памяти. Если это происходит на локальной машине разработчика, то не приводит к каким-либо проблемам. Но вот в больших Rails-инсталляциях раздувание памяти может обойтись очень дорого.
Наглядно можно увидеть разницу между Transaction.sum(:amount) (где SELECT SUM(amount) FROM transactions) и Transaction.all.sum(&:amount) (где SELECT * FROM transactions с последующей отправкой по сети) на следующем примере. Для инстансов с Active Record это может выглядеть так:
ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(Module.new do
def build_result(*args, **kwargs, &block)
io_bytesize = kwargs[:rows].sum(0) do |row|
row.sum(0) do |val|
((String === val) ? val : val.to_s).bytesize
end
end
Aggregator.instance.increment(io_bytesize)
super
end
end)
Подписываемся на уведомления от контроллера:
ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*args|
io_bytesize = Aggregator.instance.io_bytesize
body_bytesize = args.last[:response].body.bytesize
ratio = body_bytesize.to_f / io_bytesize
Rails.logger.info "Loaded from I/O #{io_bytesize}, response bytesize #{body_bytesize}, I/O to response ratio #{ratio}"
end
Завершающим штрихом создадим контроллер с нашей парой действий:
class App < Rails::Application
routes.append do
get '/slow', to: 'app#slow'
get '/fast', to: 'app#fast'
end
end
class AppController < ActionController::Base
def slow
render json: {sum: Transaction.all.sum(&:amount)}
end
def fast
render json: {sum: Transaction.sum(:amount)}
end
end
Запускаем и разница будет очевидна:
Started GET "/slow" for 127.0.0.1 at 2023-02-04 23:01:27 +0300
Processing by AppController#slow as */*
Transaction Load (4.5ms) SELECT "transactions".* FROM "transactions"
Completed 200 OK in 43ms (Views: 0.1ms | ActiveRecord: 11.7ms | Allocations: 95836)
Loaded from I/O 69899, response bytesize 15, I/O to response ratio 4659.933333333333
Started GET "/fast" for 127.0.0.1 at 2023-02-04 23:01:35 +0300
Processing by AppController#fast as */*
Transaction Sum (3.1ms) SELECT SUM("transactions"."amount") FROM "transactions"
Completed 200 OK in 4ms (Views: 0.1ms | ActiveRecord: 3.1ms | Allocations: 336)
Loaded from I/O 7, response bytesize 15, I/O to response ratio 0.4666666666666667
Понятно, что когда проблема возникла, поправить ситуацию не слишком сложно. Можно загружать данные партиями или перенести вычисления куда-либо в другое место. Но было бы здорово не ликвидировать последствия катастрофы, а заранее предсказать её появление. Идеальный вариант — прикинуть сколько данных будет загружено из I/O и сравнить с размером потенциального ответа. Соотношение станет тем показателем, которое может выступить тревожным звоночком и которое стоит отслеживать.
Чтобы проверить такой подход на реальном приложении, не нужно писать код с нуля. Тот же io_monitor отлично упрощает жизнь. После установки его нужно сконфигурировать:
IoMonitor.configure do |config|
config.publish = [:logs, :notifications, :prometheus] # defaults to :logs
config.warn_threshold = 0.8 # defaults to 0
config.adapters = [:active_record, :net_http, :redis] # defaults to [:active_record]
end
И включить те контроллеры, которые нужно будет проверить:
class MyController < ApplicationController
include IoMonitor::Controller
end
Если проблем не найдено, то в логах будет чисто. О наличии проблем будет сигнализировать строчка вида:
ActiveRecord I/O to response payload ratio is 0.1, while threshold is 0.8
Ruby-by-by
При разработке больших приложений скорость запуска процессов часто становится бутылочным горлышком и требуется много усилий, чтобы приложение соответствовало требованиям заказчика. Анализируя профиль быстродействия можно заметить, что причиной часто становится загрузка библиотек. С этим можно успешно бороться.
Каждый раз, когда процесс запускается, Ruby ищет нужные библиотеки и анализирует файлы в каждой из них. Чем больше библиотек, тем дольше загрузка. Сократить время можно, сделав их предварительную загрузку. Если у нас в памяти уже будут заранее подгружены все необходимые библиотеки, то Ruby не станет тратить на это драгоценное время. Это стало ключевой идеей создания предварительного загрузчика библиотек, разработанного Джереми Эвансом. Интервью с ним также есть у нас на сайте.
Новый загрузчик называется by. Подразумевается, что это Ruby, в котором первые две буквы уже предварительно загружены. Использована клиент-серверная архитектура, где сервер занимается предзагрузкой библиотек и слушает Unix-сокет. Клиент использует этот сокет для подключения и запуска процесса, после чего отсылает необходимые аргументы и ждёт exit code. В ответ сервер создаёт процесс, использующий текущий каталог, stdin/stdout/stderr и среду клиентского процесса. Присланные клиентом аргументы обрабатываются и финально сервер возвращает exit code 0, если всё хорошо или exit code 1 в случае ошибки.
Установить by можно одной командой:
gem install by
Использовать тоже легко. Вначале стартуем сервер с указанием библиотек, требующих предзагрузки, например:
$ by-server sequel roda capybara
Теперь запускаем Ruby через by, указывая предзагруженные библиотеки:
$ by -e 'p [Sequel, Roda, Capybara]'
[Sequel, Roda, Capybara]
Теперь Ruby не станет искать их и заниматься бесполезным парсингом. Выигрыш в скорости получается значительный:
$ /usr/bin/time ruby -e 'require "sequel"; require "roda"; require "capybara"'
1.67 real 0.93 user 0.66 sys
$ /usr/bin/time by -e 'require "sequel"; require "roda"; require "capybara"'
0.37 real 0.20 user 0.15 sys
Те, кому этого мало, могут заняться экстримом и «вырезать» загрузку Rubygems. Детальная инструкция есть в репозитории проекта.
Митапы
Ruby meetup №20
19 апреля 2023
Весной мы соберёмся на замечательный Ruby Meetup. Программа мероприятия формируется, но регистрация уже открыта. Кстати, вы уже можете подать доклад прямо в режиме онлайн. Заявки на участие принимаются до 1 апреля!
Теперь следить за митапами Evrone стало удобнее. В Telegram-канале Evrone meetups мы выкладываем анонсы с подробными описаниями докладов, а также студийные записи после мероприятий. А ещё, у нас можно выступить, мы поможем оформить вашу экспертизу в яркое выступление. Подписывайтесь и пишите @andrew_aquariuss, чтобы узнать подробности.
Вакансии
Удаленка / Офис
Evrone
Мы открыты для новых Ruby-разработчиков. В Evrone можно работать удалённо с первого дня, мы поддерживаем и оплачиваем участие в Open-source проектах и выступления на конференциях, а расти в грейдах можно с помощью честной системы проверки навыков и менторства.