RubyMiddleExperience

Какие особенности runtime, type system или memory model Ruby реально влияют на архитектуру приложения?

GVL в MRI ограничивает CPU-параллелизм потоков. Ruby использует mark-and-sweep GC с incremental и compaction режимами. Всё является объектом — даже классы и методы — что влияет на memory layout и метапрограммирование.

GVL (Global VM Lock) и конкурентность

MRI Ruby (CRuby) имеет GVL — мьютекс, разрешающий в каждый момент времени выполнение Ruby-кода только одному потоку. Это означает:

  • Threads в Ruby отлично работают для I/O-bound задач: пока один поток ждёт ответа от БД/HTTP, GVL освобождается и другой поток выполняется.
  • Для CPU-bound задач (парсинг, шифрование, ML) Threads не дают параллелизма. Нужны либо Process.fork, либо C-расширения (GVL снимается в C-коде), либо JRuby/TruffleRuby.
  • Ractors (Ruby 3+) — экспериментальный примитив без GVL, но с жёсткими ограничениями на разделение объектов.
require 'benchmark'

# I/O-bound: потоки помогают
def parallel_http_calls(urls)
  urls.map { |url| Thread.new { Net::HTTP.get(URI(url)) } }.map(&:value)
end

# CPU-bound: потоки не помогают в MRI
# Используйте: Ractor (Ruby 3+) или ProcessPool
data_chunks.map { |chunk|
  Process.fork { expensive_compute(chunk) }
}

Garbage Collector

MRI использует tri-color incremental mark-and-sweep GC с поколениями (major/minor) и compaction (Ruby 2.7+).

  • Minor GC — собирает short-lived объекты в «молодом» поколении. Быстрый, частый.
  • Major GC — полный проход. Вызывает паузы. Триггер: превышение порогов.
  • Compaction (GC.compact) — перемещает объекты для уменьшения фрагментации; важно для fork-based серверов (Unicorn, Puma в cluster mode): compact перед fork = copy-on-write friendly.
# Настройка GC через env:
RUBY_GC_HEAP_GROWTH_FACTOR=1.1       # медленнее растёт heap
RUBY_GC_HEAP_FREE_SLOTS_MIN_RATIO=0.2

# Мониторинг:
puts GC.stat(:heap_allocated_pages)
puts GC.stat(:minor_gc_count)
puts GC.stat(:major_gc_count)

# Временно отключить GC в критичном блоке:
GC.disable
result = heavy_computation
GC.enable
GC.compact

Объектная модель

В Ruby всё является объектом: числа, строки, классы, методы. Каждый объект хранит ссылку на свой класс. Классы — тоже объекты класса Class.

  • Поиск методов идёт вверх по ancestor chain: объект → класс → включённые модули (в порядке include) → суперкласс → ...BasicObject.
  • Singleton class (eigenclass) — скрытый класс каждого объекта, где хранятся singleton-методы и методы класса.
class Animal; end
module Swimmable; end
class Duck < Animal
  include Swimmable
end

Duck.ancestors
# => [Duck, Swimmable, Animal, Object, Kernel, BasicObject]

Type system: duck typing + RBS

Ruby динамически типизирован — типы проверяются в runtime. С Ruby 3 появился RBS (Ruby Signature) — отдельный файл типов для статического анализа через steep или sorbet.

# .rbs файл:
class Order
  attr_reader total: Integer
  def process: (Gateway gateway) -> Result
end

# Запуск steep:
# steep check

Архитектурные последствия

  • Нет CPU-параллелизма в threads → для фоновых задач используйте Sidekiq (processes) или Async (Fibers для I/O).
  • GC-паузы → в latency-sensitive API используйте jemalloc, настройте GC-параметры, делайте GC.compact после прогрева.
  • Object allocation — главный враг производительности. Frozen string literals, Symbol вместо String для ключей хэша, избегайте создания объектов в hot loops.

Подводные камни

  • Thread.new без join — необработанные исключения в потоках по умолчанию тихо проглатываются. Установите Thread.abort_on_exception = true или явно вызывайте thread.join.
  • ObjectSpace утечки — хранение объектов в глобальных структурах (@@var, $global) мешает GC. Используйте WeakRef для кэшей.
  • fork + threads — Process.fork копирует только текущий поток. Потоки-мьютексы из родительского процесса могут быть заблокированы в fork. Форкайте до создания потоков.
  • JRuby — другие правила — JRuby нет GVL, есть настоящий параллелизм потоков. Код, написанный для MRI без Mutex, может иметь data races на JRuby.

What hurts your answer

  • Знать термины Ruby, но не понимать связи между абстракциями
  • Объяснять поведение через отдельные примеры вместо причинной модели
  • Не связывать mental model с диагностикой ошибок

What they're listening for

  • Понимает ключевые абстракции Ruby
  • Может предсказывать поведение системы через mental model
  • Связывает модель с debugging и production decisions

Related topics