Khởi động ứng dụng Ruby/Rails lớn nhanh hơn với bootsnap
Bài đăng này đã không được cập nhật trong 3 năm
Bootsnap
là một thư viện có thể cắm vào Ruby, với sự hỗ trợ tùy chọn cho ActiveSupport và YAML, nhằm tối ưu hóa và tính toán các tính toán đắt tiền
Hiệu năng
- Thời gian khởi động giảm khoảng 50%, từ khoảng 3s đến 6s trên một máy
- Ví dụ đối với nền tảng Shopify- Khởi động nhanh hơn khoảng 75% giảm từ khoảng 25s xuống còn 6,5s
Cài đặt Bootsnap
Gem này hoạt động trên MacOS
và Linux
- Thêm
bootsnap
vào fileGemfile
gem "bootsnap", require: false
- Nếu bạn sử dụng rails, thêm vào file
config/boot.rb
dòng sau:require "bootsnap/setup
- Nếu không sử dụng rails hoặc bạn muốn kiểm soát tốt hơn về mọi thứ. Hãy thêm vào thiết lập ứng dụng của bạn như sau:
require 'bootsnap' env = ENV['RAILS_ENV'] || "development" Bootsnap.setup( cache_dir: 'tmp/cache', # Path to your cache development_mode: env == 'development', # Current working environment, e.g. RACK_ENV, RAILS_ENV, etc load_path_cache: true, # Optimize the LOAD_PATH with a cache autoload_paths_cache: true, # Optimize ActiveSupport autoloads with cache disable_trace: true, # (Alpha) Set `RubyVM::InstructionSequence.compile_option = { trace_instruction: false }` compile_cache_iseq: true, # Compile Ruby code into ISeq cache, breaks coverage reporting. compile_cache_yaml: true # Compile YAML into a cache )
Cách thức hoạt động của Bootsnap
Bootsnap
tối ưu hóa các phương pháp để lưu trữ kết quả của tính toán đắt tiền, và có thể được nhóm lại thành hai loại sau:
- Path Pre-Scanning :
*
Kernel#require
vàKernel#load
được chỉnh sửa để loại bỏ quét$LOAD_PATH
*ActiveSupport::Dependencies.{autoloadable_module?,load_missing_constant,depend_on}
được ghi đè để loại bỏ việc quétActiveSupport::Dependencies.autoload_paths
- Compilation Caching:
*
RubyVM::InstructionSequence.load_iseq
được thực hiện để cache kết quả của việc biên dịchruby bytecode
*YAML.load_file
được chỉnh sửa để cache kết quả tải một đối tượngYAML
trong định dạngMessagePack
(hoặcMarshal
nếu thông báo dử dụng kiểu không được hỗ trợ bởiMessagePack
)
Path Pre-Scanning
(Đây là một sự phát triển nhở của bootscale)
Khi bootsnap khởi động hoặc sửa đổi đường dẫn, Bootsnap::LoadPathCache
sẽ tìm nạp danh sách các mục yêu cầu từ bộ nhớ cache hoặc thực hiện quét toàn bộ và kết quả bộ nhớ cache nếu thấy cần
Sau đó, khi chạy (eg: require 'foo'
, Ruby sẽ lặp lại mọi mục trên $LOAD_PATH ['x', 'y', ...]
, tìm kiếm x/foo.rb
, y/foo.rb
... Bootsnap nhìn vào tất các cached
của mỗi LOAD_PATH và thay thế đường dẫn mở rộng đầy đủ của ruby vào.
Sơ đồ sau sẽ trình bày cách ghi đè cho các tính năng *_path_cache
Bootsnap
phân loại đường dẫn thành 2 loại :stable
và volatile
.
* volatile
được quét mỗi lần khởi động ứng dụng và bộ đệm của chúng chỉ có giá trị trong vòng 30s
* stable
: không hết hạn, Một khi nội dung của chúng được quét, nó sẽ không bao giờ thay đổi
Các thư mục duy nhất được coi là stable
là những thứ trong thư mục ruby cài đặt tiền tố RbConfig::CONFIG['prefix']
, eg: /usr/local/ruby
or ~/.rubies/x.y.z
, và những thứ dưới Gem.path
(eg: ~/.gem/ruby/x.y.z
) hoặc Bundler.bundle_path
. Những thứ còn lại được coi là volatitle
Dưới đây là mã ngồn để làm rõ cách thức hoạt động:
require_relative '../explicit_require'
module Bootsnap
module LoadPathCache
class Cache
AGE_THRESHOLD = 30 # seconds
def initialize(store, path_obj, development_mode: false)
@development_mode = development_mode
@store = store
@mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
@path_obj = path_obj
@has_relative_paths = nil
reinitialize
end
# Does this directory exist as a child of one of the path items?
# e.g. given "/a/b/c/d" exists, and the path is ["/a/b"], has_dir?("c/d")
# is true.
def has_dir?(dir)
reinitialize if stale?
@mutex.synchronize { @dirs[dir] }
end
# { 'enumerator' => nil, 'enumerator.so' => nil, ... }
BUILTIN_FEATURES = $LOADED_FEATURES.reduce({}) do |acc, feat|
# Builtin features are of the form 'enumerator.so'.
# All others include paths.
next acc unless feat.size < 20 && !feat.include?('/')
base = File.basename(feat, '.*') # enumerator.so -> enumerator
ext = File.extname(feat) # .so
acc[feat] = nil # enumerator.so
acc[base] = nil # enumerator
if [DOT_SO, *DL_EXTENSIONS].include?(ext)
DL_EXTENSIONS.each do |dl_ext|
acc["#{base}#{dl_ext}"] = nil # enumerator.bundle
end
end
acc
end.freeze
# Try to resolve this feature to an absolute path without traversing the
# loadpath.
def find(feature)
reinitialize if (@has_relative_paths && dir_changed?) || stale?
feature = feature.to_s
return feature if absolute_path?(feature)
return File.expand_path(feature) if feature.start_with?('./')
@mutex.synchronize do
x = search_index(feature)
return x if x
# Ruby has some built-in features that require lies about.
# For example, 'enumerator' is built in. If you require it, ruby
# returns false as if it were already loaded; however, there is no
# file to find on disk. We've pre-built a list of these, and we
# return false if any of them is loaded.
raise LoadPathCache::ReturnFalse if BUILTIN_FEATURES.key?(feature)
# The feature wasn't found on our preliminary search through the index.
# We resolve this differently depending on what the extension was.
case File.extname(feature)
# If the extension was one of the ones we explicitly cache (.rb and the
# native dynamic extension, e.g. .bundle or .so), we know it was a
# failure and there's nothing more we can do to find the file.
when '', *CACHED_EXTENSIONS # no extension, .rb, (.bundle or .so)
nil
# Ruby allows specifying native extensions as '.so' even when DLEXT
# is '.bundle'. This is where we handle that case.
when DOT_SO
x = search_index(feature[0..-4] + DLEXT)
return x if x
if DLEXT2
search_index(feature[0..-4] + DLEXT2)
end
else
# other, unknown extension. For example, `.rake`. Since we haven't
# cached these, we legitimately need to run the load path search.
raise LoadPathCache::FallbackScan
end
end
end
if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
def absolute_path?(path)
path[1] == ':'
end
else
def absolute_path?(path)
path.start_with?(SLASH)
end
end
def unshift_paths(sender, *paths)
return unless sender == @path_obj
@mutex.synchronize { unshift_paths_locked(*paths) }
end
def push_paths(sender, *paths)
return unless sender == @path_obj
@mutex.synchronize { push_paths_locked(*paths) }
end
def each_requirable
@mutex.synchronize do
@index.each do |rel, entry|
yield "#{entry}/#{rel}"
end
end
end
def reinitialize(path_obj = @path_obj)
@mutex.synchronize do
@path_obj = path_obj
ChangeObserver.register(self, @path_obj)
@index = {}
@dirs = Hash.new(false)
@generated_at = now
push_paths_locked(*@path_obj)
end
end
private
def dir_changed?
@prev_dir ||= Dir.pwd
if @prev_dir == Dir.pwd
false
else
@prev_dir = Dir.pwd
true
end
end
def push_paths_locked(*paths)
@store.transaction do
paths.map(&:to_s).each do |path|
p = Path.new(path)
@has_relative_paths = true if p.relative?
next if p.non_directory?
entries, dirs = p.entries_and_dirs(@store)
# push -> low precedence -> set only if unset
dirs.each { |dir| @dirs[dir] ||= true }
entries.each { |rel| @index[rel] ||= p.expanded_path }
end
end
end
def unshift_paths_locked(*paths)
@store.transaction do
paths.map(&:to_s).reverse_each do |path|
p = Path.new(path)
next if p.non_directory?
entries, dirs = p.entries_and_dirs(@store)
# unshift -> high precedence -> unconditional set
dirs.each { |dir| @dirs[dir] = true }
entries.each { |rel| @index[rel] = p.expanded_path }
end
end
end
def stale?
@development_mode && @generated_at + AGE_THRESHOLD < now
end
def now
Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i
end
if DLEXT2
def search_index(f)
try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f + DLEXT2) || try_index(f)
end
else
def search_index(f)
try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f)
end
end
def try_index(f)
if p = @index[f]
p + '/' + f
end
end
end
end
end
Hoặc bạn có thể xem sơ đồ dứoi để hiểu về cách hoạt động:
Cần lưu ý là LoadErrors
có thể rất tốn kém tài nguyên. Nếu require 'something'
, nhưng file đó lại không nằm trên $LOAD_PATH
nó sẽ mất 2*$LOAD_PATH.lenght hệ thống tập tin truy cập để xác định nó. Bootsnap sẽ cache kết quả này, đưa ra LoadError
mà không cần chạm vào hệ thống tập tin ở tất cả.
Complilation Caching
(Tài liệu dễ hiểu hơn của khái niệm này có thể được tìm thấy ở yomikomu)
Lưu ý: Chúng ta sẽ tống rất nhiều thời gian để tải các tài liệu YAML trongquá trình khởi động ứng dụng và những mesage như MessagePack
và Marshal
nhanh hơn nhiều so với YAML. Chúng tôi sử dụng cùng một cách biên dịch bộ nhớ cached cho tài liệu YAML, Tương đương với định dạng bytecode
của Ruby là một tài liệu của MessagePack
(hoặc Marshal
với các tài liệu không hỗ trợ bởi MessagePack
).
Các kết quả biên dịch này được lưu trữ trong một thư mục bộ nhớ cache, với tên tập tin được tạo ra bằng cách lấy một băm của đường dẫn mở rộng đầy đủ của tập tin đầu vào (FNV1a-64).
Trong khi trước đây, trình tự các syscalls tạo ra để yêu cầu một tập tin sẽ như sau:
open /c/foo.rb -> m
fstat64 m
close m
open /c/foo.rb -> o
fstat64 o
fstat64 o
read o
read o
...
close o
Thì với Bootsnap
sẽ nhận được:
open /c/foo.rb -> n
fstat64 n
close n
open /c/foo.rb -> n
fstat64 n
open (cache) -> m
read m
read m
close m
close n
Bootsnap
viết một tệp tin cache có chứa một tiêu đề 64 byte theo sau là nội dung của bộ nhớ cache. Tiêu đề là một khóa cache bao gồm một số trường:
version
: Phiên bản schemaos_version
: Một hash của phiên bản kernel hiện tại(MacOS, BSD) hoặc phiên bản Glibc (Linux)Compile_option
: Những thay đổi vớiRubyVM::InstructionSequence.compile_option
Ruby_version
: Phiên bản của ruby.Size
: Kích thước sourcemtime
: Timestamp lần sửa đổi cuối cùngdata_size
: số byte theo tiêu đề, mà chúng ta cần phải đọc nó vào bộ đệm.
Nếu khóa là hợp lệ, kết quả được nạp từ giá trị. Ngược lại, nó được tái tạo và hủy bộ nhớ cache hiện tại
Thử nghiệm
Hãy tưởng tượng ta có cấu trúc sau:
/
├── a
├── b
└── c
└── foo.rb
Và có $LOAD_PATH
:
["/a", "/b", "/c"]
Khi chúng ta gọi require 'foo'
mà không dùng Bootsnap
. Ruby sẽ sinh ra chuỗi syscalls này:
open /a/foo.rb -> -1
open /b/foo.rb -> -1
open /c/foo.rb -> n
close n
open /c/foo.rb -> m
fstat64 m
close m
open /c/foo.rb -> o
fstat64 o
fstat64 o
read o
read o
...
close o
Nhưng với bootsnap thì sẽ như sau:
open /c/foo.rb -> n
fstat64 n
close n
open /c/foo.rb -> n
fstat64 n
open (cache) -> m
read m
read m
close m
close n
Nếu chúng ta gọi require 'nope'
mà không dùng bootsnap:
open /a/nope.rb -> -1
open /b/nope.rb -> -1
open /c/nope.rb -> -1
open /a/nope.bundle -> -1
open /b/nope.bundle -> -1
open /c/nope.bundle -> -1
Còn nếu chạy với Bootsnap thì sẽ không sinh ra bất cứ gì.
Tài Liệu tham khảo: bootsnap
All rights reserved