Rò rỉ bộ nhớ là tình trạng sử dụng bộ nhớ tăng lên một cách không chủ ý, không kiểm soát được và không ngừng. Dù nhỏ đến đâu thì cuối cùng, một sự rò rỉ sẽ khiến quá trình của bạn hết bộ nhớ và gặp sự cố. Ngay cả khi bạn khởi động lại ứng dụng của mình theo định kỳ để tránh sự cố này (không cần phán xét, tôi đã làm điều đó!), bạn vẫn phải chịu những tác động về hiệu suất do rò rỉ bộ nhớ.
Trong bài đăng này, phần đầu tiên của loạt bài gồm hai phần về rò rỉ bộ nhớ, chúng ta sẽ bắt đầu bằng cách xem cách Ruby quản lý bộ nhớ, cách thức hoạt động của Garbage Collection (GC) và cách tìm ra rò rỉ.
Trong phần thứ hai, chúng ta sẽ đi sâu hơn vào việc theo dõi các rò rỉ.
Hãy bắt đầu!
Quản lý bộ nhớ Ruby
Các đối tượng Ruby được lưu trữ trên heap và mỗi đối tượng sẽ lấp đầy một vị trí trên heap.
Trước Ruby 3.1, tất cả các vị trí trên heap đều có cùng kích thước - chính xác là 40 byte. Các đối tượng quá lớn để vừa với một khe sẽ được lưu trữ bên ngoài vùng nhớ heap. Mỗi vị trí bao gồm một tham chiếu đến nơi các đối tượng được di chuyển.
Trong Ruby 3.1, phân bổ độ rộng thay đổi cho String các đối tượng đã được hợp nhất. Chẳng bao lâu nữa, việc phân bổ chiều rộng thay đổi sẽ là tiêu chuẩn cho tất cả các loại đối tượng.
Phân bổ độ rộng thay đổi nhằm mục đích cải thiện hiệu suất bằng cách cải thiện vị trí bộ nhớ đệm — tất cả thông tin của một đối tượng sẽ được lưu trữ ở một vị trí thay vì trên hai vị trí bộ nhớ.
Nó cũng sẽ đơn giản hóa (một số phần) việc quản lý bộ nhớ. Hiện tại, có hai 'đống':
- Đống Ruby (hoặc vùng GC) lưu trữ các đối tượng Ruby nhỏ hơn.
- Đống C (hoặc malloc/đống tạm thời) lưu trữ các đối tượng lớn hơn.
Khi việc phân bổ chiều rộng thay đổi đã trở thành tiêu chuẩn thì không cần thiết phải có vùng nhớ heap thứ hai.
Heap bắt đầu ở một kích thước nhất định (10.000 vị trí theo mặc định) và các đối tượng được gán vào các vị trí trống khi chúng được tạo. Khi Ruby cố gắng tạo một đối tượng và không còn chỗ trống nào, Garbage Collection (GC) sẽ tạo ra một số chỗ trống.
Nếu có quá ít vị trí trống sau GC, vùng heap sẽ được mở rộng (sẽ nói thêm về điều này sau).
Dưới đây là các yếu tố bạn có thể kiểm soát cùng với các biến môi trường của chúng:
- Kích thước ban đầu của vùng heap -
RUBY_GC_HEAP_INIT_SLOTS - Số lượng vị trí trống sẽ có sẵn sau khi GC xảy ra -
RUBY_GC_HEAP_FREE_SLOTS - Số lượng heap được mở rộng thêm -
RUBY_GC_HEAP_GROWTH_FACTOR
Thu gom rác trong Ruby
Bộ sưu tập rác trong Ruby 'ngăn chặn thế giới' - không có quy trình nào khác xảy ra khi GC xảy ra. Bộ sưu tập rác trong Ruby (kể từ 2.1) cũng có tính chất thế hệ , nghĩa là bộ thu gom rác có hai chế độ:
- GC nhỏ - kiểm tra các đối tượng 'trẻ' (các đối tượng được tạo gần đây)
- GC chính - kiểm tra các đối tượng 'cũ' cũng như các đối tượng 'trẻ' (tất cả các vật thể)
Lưu ý :Một đối tượng 'cũ' đã tồn tại 3 GC chạy, chính hoặc phụ.
Khi vùng heap đầy, GC nhỏ được gọi trước tiên. Nếu nó không thể giải phóng đủ các khe dưới giới hạn, thì GC chính sẽ được gọi. Chỉ khi đó, nếu vẫn không còn đủ chỗ trống thì heap mới được mở rộng.
GC chính đắt hơn GC nhỏ vì nó xem xét nhiều đối tượng hơn.
Lý thuyết đằng sau lý do tại sao GC thế hệ hoạt động hiệu quả hơn là các đối tượng thường chia thành hai loại:
- Các đối tượng được phân bổ và sau đó nhanh chóng vượt ra khỏi phạm vi. Trong ứng dụng Rails, các mô hình được tìm nạp từ DB để hiển thị một trang sẽ nằm ngoài phạm vi khi yêu cầu kết thúc.
- Các đối tượng được phân bổ và lưu giữ trong thời gian dài. Các lớp và bộ nhớ đệm có thể vẫn được sử dụng trong suốt vòng đời của ứng dụng.
GC chính cũng sẽ chạy sau GC nhỏ nếu số lượng đối tượng cũ vượt quá ngưỡng nhất định, ngay cả khi có đủ chỗ trống. Giới hạn này tăng lên khi kích thước của vùng heap tăng lên và có thể được kiểm soát bởiRUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR biến môi trường.
Khi bạn bị rò rỉ, bạn tạo ra những đồ vật không thể dọn sạch được — ngày càng nhiều old đồ vật. Điều này có nghĩa là GC chính (đắt tiền) sẽ chạy thường xuyên hơn mức cần thiết. Vì không có gì khác chạy khi GC đang chạy nên bạn đang lãng phí thời gian.
Tôi đã để lại một số liên kết ở cuối bài viết này để đọc thêm về bộ nhớlayout và trình thu gom rác trong Ruby.
Rò rỉ bộ nhớ trong Ruby trông như thế nào?
Bạn có thể thấy rò rỉ bộ nhớ bằng các công cụ đơn giản có sẵn trên hệ thống AnyUnix. Lấy đoạn mã sau làm ví dụ.
Nói mã này 'rò rỉ' thì hơi không công bằng - tất cả những gì nó làm chỉ là rò rỉ! —nhưng nó phục vụ mục đích của chúng tôi.
Chúng ta có thể quan sát rò rỉ khá đơn giản từ dòng lệnh bằng cách chạy chương trình này trong một thiết bị đầu cuối và watch - tăng bộ nhớ theo thời gian vớips .
pgrep -f "ruby ./leaky.rb" tìm ID tiến trình cho chúng tôi để chúng tôi có thể hạn chếps chỉ xuất ra quy trình mà chúng tôi quan tâm. Như bạn có thể đoán, nó giống như grep cho các quy trình.
watch công cụ cho phép chúng tôi thăm dò đầu ra của một lệnh nhất định và cập nhật lệnh đó tại chỗ, cung cấp cho chúng tôi trang tổng quan trực tiếp trong thiết bị đầu cuối của chúng tôi.
Bạn sẽ nhận được kết quả như thế này, cập nhật cứ sau vài giây.
Bạn sẽ thấy %MEM và RSS ngày càng tăng. Họ là:
%MEM- Dung lượng bộ nhớ mà tiến trình sử dụng tính theo phần trăm bộ nhớ trên máy chủ.RSS(kích thước cài đặt thường trú) - Dung lượng RAM mà quá trình sử dụng tính bằng byte.
Thông tin cơ bản chỉ dành cho hệ điều hành này đủ để phát hiện xem bạn có bị rò rỉ hay không — nếu bộ nhớ tiếp tục tăng, điều đó có nghĩa là bạn đã bị rò rỉ!
Tìm rò rỉ Ruby bằng Mô-đun thu gom rác
Chúng tôi cũng có thể phát hiện rò rỉ trong chính mã Ruby bằng GC mô-đun.
GC.stat phương thức sẽ trả về một hàm băm với nhiều thông tin hữu ích. Ở đây, chúng tôi quan tâm đến :heap_live_slots , là số lượng vị trí trên heap đang được sử dụng. Điều đó trái ngược với :heap_free_slots .Ở cuối vòng lặp, chúng tôi buộc một GC chính và in ra số lượng ô đã sử dụng, tức là số lượng đối tượng còn lại sau GC.
Khi chúng tôi chạy chương trình nhỏ của mình, chúng tôi thấy mức tăng này tăng lên vô cùng. Chúng tôi có một rò rỉ! Chúng ta cũng có thể sử dụng GC.stat(:old_objects) có tác dụng tương tự.
Trong khi GC mô-đun có thể được sử dụng để xem if chúng tôi có một rò rỉ và (nếu bạn thông minh với puts của mình câu lệnh) nơi có thể xảy ra rò rỉ, chúng ta có thể xem loại đối tượng có thể bị rò rỉ bằng ObjectSpace mô-đun.
ObjectSpace.count_objects phương thức trả về một hàm băm với số lượng đối tượng đang hoạt động. T_STRING , ví dụ:là số chuỗi tồn tại trong bộ nhớ. Đối với chương trình khá rò rỉ của chúng tôi, giá trị này tăng theo mỗi vòng lặp, ngay cả sau GC. Chúng ta có thể thấy rằng chúng ta đang rò rỉ các đối tượng chuỗi.
Giám sát hiệu suất ứng dụng trong sản xuất bằng AppSignal
Trong khi chơi với ps và GC có thể là một lộ trình hợp lý cho các dự án đồ chơi—chúng cũng rất thú vị và hữu ích khi sử dụng! — Tôi sẽ không hãy đề xuất chúng làm giải pháp phát hiện rò rỉ bộ nhớ trong các ứng dụng sản xuất.
Đây là nơi bạn sẽ sử dụng công cụ Giám sát Hiệu suất Ứng dụng (APM). Nếu bạn là một công ty rất lớn, bạn có thể tự xây dựng những thứ này. Tuy nhiên, đối với những bộ trang phục nhỏ hơn, việc chọn APM có sẵn là cách tốt nhất. Bạn cần phải trả tiền đăng ký hàng tháng nhưng thông tin họ cung cấp còn nhiều hơn thế.
Để phát hiện rò rỉ bộ nhớ, bạn muốn tìm biểu đồ sử dụng bộ nhớ của máy chủ hoặc xử lý (đôi khi được gọi là RSS) theo thời gian. Dưới đây là ảnh chụp màn hình mẫu từ trang tổng quan 'sử dụng bộ nhớ quy trình' của AppSignal về một ứng dụng hoạt động tốt ngay sau khi được triển khai:

Và đây là một ứng dụng không lành mạnh sau khi triển khai:

AppSignal thậm chí sẽ hiển thị các số liệu thống kê của Ruby VM như GC và các khe heap, điều này có thể cung cấp cho bạn tín hiệu rõ ràng hơn về rò rỉ bộ nhớ. Nếu số lượng máy đánh bạc trực tiếp tiếp tục tăng lên thì bạn đã bị rò rỉ!

Đọc thêm về AppSignal dành cho Ruby.
Tóm lại và đọc thêm
Trong bài đăng này, chúng tôi đã giới thiệu nhanh về trình quản lý bộ nhớ và trình thu gom rác của Ruby. Sau đó, chúng tôi đã chẩn đoán cách phát hiện rò rỉ bộ nhớ bằng các công cụ Unix và mô-đun GC của Ruby.
Lần tới chúng ta sẽ xem cách sử dụng memory_profiler và derailed_benchmarks để tìm và sửa các chỗ rò rỉ.
Trong thời gian chờ đợi, bạn có thể đọc thêm về các công cụ chúng tôi đã sử dụng:
watchpspgrep
Đọc thêm:
GCtài liệu mô-đunObjectSpacetài liệu mô-đun- Thu gom rác chuyên sâu
- Phân bổ chiều rộng thay đổi
Chúc bạn viết mã vui vẻ và hẹn gặp lại lần sau!
Tái bút. Nếu bạn muốn đọc các bài đăng của Ruby Magic ngay khi chúng được đăng tải, hãy đăng ký nhận bản tin Ruby Magic của chúng tôi và không bao giờ bỏ lỡ một bài đăng nào!