Speeding Things Up

03 марта 2023
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 проектах и выступления на конференциях, а расти в грейдах можно с помощью честной системы проверки навыков и менторства.

Подробнее

Подписаться
на Digest →
Важные новости и мероприятия без спама
Технологии которыми вы владеете и которые вам интересны
Ваш адрес электронной почты в безопасности - вот наша политика конфиденциальности.