Trong phần đầu tiên của loạt bài gồm hai phần về rò rỉ bộ nhớ, chúng ta đã xem xét cách Ruby quản lý bộ nhớ và cách thức hoạt động của Garbage Collection (GC).
Bạn có thể mua được những chiếc máy mạnh mẽ với nhiều bộ nhớ hơn và ứng dụng của bạn có thể khởi động lại thường xuyên đến mức người dùng không nhận thấy, nhưng việc sử dụng bộ nhớ vẫn là vấn đề quan trọng.
Phân bổ và thu gom rác không miễn phí. Nếu bạn bị rò rỉ, bạn sẽ ngày càng dành nhiều thời gian hơn cho Bộ sưu tập rác thay vì làm những gì bạn xây dựng ứng dụng của mình để làm.
Trong bài đăng này, chúng ta sẽ tìm hiểu sâu hơn về các công cụ bạn có thể sử dụng để phát hiện và chẩn đoán rò rỉ bộ nhớ.
Hãy tiếp tục!
Tìm rò rỉ trong Ruby
Phát hiện rò rỉ là đủ đơn giản. Bạn có thể sử dụng GC , ObjectSpace và biểu đồ RSS trong công cụ APM để xem mức sử dụng bộ nhớ của bạn tăng lên. Nhưng chỉ biết bạn bị rò rỉ là không đủ để sửa nó. Bạn cần biết nó đến từ đâu. Những con số thô không thể cho bạn biết điều đó.
May mắn thay, hệ sinh thái Ruby có một số công cụ tuyệt vời để gắn bối cảnh với những con số đó. Hai là memory-profiler và derailed_benchmarks .
memory_profiler trong Ruby
memory_profiler gem cung cấp một API rất đơn giản và một báo cáo chi tiết về bộ nhớ được phân bổ và giữ lại (mặc dù hơi nhiều) — bao gồm các lớp đối tượng được phân bổ, kích thước của chúng và nơi chúng được phân bổ. Thật đơn giản để thêm vào chương trình bị rò rỉ của chúng tôi.
Xuất ra một báo cáo tương tự như thế này.
Có rất nhiều thông tin ở đây, nhưng nhìn chung,allocated objects by location và retained objects by location các phần có thể hữu ích nhất khi tìm kiếm rò rỉ. Đây là các vị trí tệp phân bổ các đối tượng, được sắp xếp theo số lượng đối tượng được phân bổ.
allocatedđối tượng là tất cả các đối tượng được phân bổ (được tạo) trongreportchặn.retainedđối tượng là đối tượng chưa được thu gom rác vào cuốireportkhối. Chúng tôi đã buộc chạy GC trước khi kết thúc khối để có thể nhìn thấy các đối tượng bị rò rỉ rõ ràng hơn.
Hãy cẩn thận khi tin tưởng vào retained số lượng đối tượng. Chúng phụ thuộc rất nhiều vào phần mã bị rò rỉ nằm trong report chặn.
Ví dụ:nếu chúng ta di chuyển khai báo an_array vào report chặn, chúng ta có thể bị lừa rằng mã không bị rò rỉ.
Phần đầu của báo cáo kết quả sẽ không báo cáo nhiều đối tượng được giữ lại (chỉ báo cáo).
derailed_benchmarks trong Ruby
derailed_benchmarks gem là một bộ công cụ rất hữu ích cho mọi loại công việc hiệu suất, chủ yếu nhắm vào các ứng dụng Rails. Để tìm ra rò rỉ, chúng tôi muốn xem xét perf:mem_over_time , perf:objects , vàperf:heap_diff .
Các tác vụ này hoạt động bằng cách gửi curl yêu cầu tới một ứng dụng đang chạy, vì vậy chúng tôi không thể thêm chúng vào chương trình bị rò rỉ nhỏ của mình. Thay vào đó, chúng ta sẽ cần thiết lập một ứng dụng SmallRails với điểm cuối làm rò rỉ bộ nhớ, sau đó cài đặtderailed_benchmarks trên ứng dụng đó.
Bây giờ bạn có thể khởi động ứng dụng với bin/rails s . Bạn sẽ có thể curl một điểm cuối bị rò rỉ theo từng yêu cầu.
Bây giờ chúng ta có thể sử dụng derailed_benchmarks để xem hoạt động rò rỉ của chúng tôi.
perf:mem_over_time
Điều này sẽ cho chúng ta thấy việc sử dụng bộ nhớ theo thời gian (tương tự như cách chúng ta theo dõi sự phát triển bộ nhớ của tập lệnh bị rò rỉ với watch và ps ).
Derailed sẽ khởi động ứng dụng ở chế độ sản xuất, liên tục nhấn vào điểm cuối(/ theo mặc định) và báo cáo mức sử dụng bộ nhớ. Nếu nó không ngừng phát triển thì chúng ta sẽ bị rò rỉ!
Lưu ý :Derailed sẽ khởi động ứng dụng Rails ở chế độ sản xuất để thực hiện kiểm tra. Theo mặc định, nó cũng sẽ require rails/all đầu tiên. Vì không có cơ sở dữ liệu trong ứng dụng này nên chúng tôi cần ghi đè hành vi này bằng DERAILED_SKIP_ACTIVE_RECORD=true .
Chúng tôi có thể chạy điểm chuẩn này dựa trên các điểm cuối khác nhau để xem điểm nào (nếu có) bị rò rỉ.
perf:objects
perf:objects tác vụ sử dụng memory_profiler dưới mui xe nên báo cáo được tạo ra sẽ trông quen thuộc.
Báo cáo này có thể giúp thu hẹp nơi bộ nhớ bị rò rỉ của bạn đang được phân bổ. Trong ví dụ của chúng tôi, phần cuối cùng của báo cáo — theRetained String Report — cho chúng tôi biết chính xác vấn đề của chúng tôi là gì.
Chúng tôi đã rò rỉ 10.000 chuỗi chứa "ABC" từ LeaksController trực tuyến 3. Trong một ứng dụng không tầm thường, báo cáo này sẽ lớn hơn đáng kể và chứa các chuỗi được giữ lại mà bạn muốn giữ lại — bộ nhớ đệm truy vấn, v.v. —nhưng phần này và các phần 'theo vị trí' khác sẽ giúp bạn thu hẹp rò rỉ của mình.
perf:heap_diff
perf:heap_diff điểm chuẩn có thể hữu ích nếu báo cáo từ perf:objects quá phức tạp để biết rò rỉ của bạn đến từ đâu.
Đúng như tên gọi, perf:heap_diff tạo ra ba đống dữ liệu và tính toán sự khác biệt giữa chúng. Nó tạo ra một báo cáo bao gồm các loại đối tượng được giữ lại giữa các bãi chứa và vị trí đã phân bổ chúng.
Bạn cũng có thể đọc Theo dõi rò rỉ bộ nhớ Ruby vào năm 2021 để hiểu rõ hơn chuyện gì đang xảy ra.
Báo cáo chỉ cho chúng tôi chính xác nơi chúng tôi cần đến đối với ứng dụng dành cho trẻ em bị rò rỉ. Ở đầu phần khác biệt, chúng ta thấy 999991 đối tượng chuỗi được giữ lại được phân bổ từ LeaksController ở dòng 3.
Rò rỉ trong ứng dụng Ruby và Rails thực
Hy vọng rằng những ví dụ chúng tôi sử dụng cho đến nay chưa bao giờ được đưa vào ứng dụng thực tế — tôi hy vọng không ai có ý định rò rỉ bộ nhớ!
Trong các ứng dụng không tầm thường, rò rỉ bộ nhớ có thể khó theo dõi hơn nhiều. Các đối tượng được giữ lại không phải lúc nào cũng xấu —bộ nhớ đệm chứa các mục được thu thập rác sẽ không có tác dụng nhiều.
Tuy nhiên, có một điểm chung giữa tất cả các rò rỉ. Ở đâu đó, một đối tượng cấp gốc (một lớp/toàn cục, v.v.) giữ một tham chiếu đến một đối tượng.
Một ví dụ phổ biến là bộ đệm không có giới hạn hoặc chính sách trục xuất. Theo định nghĩa, điều này sẽ làm rò rỉ bộ nhớ vì mọi đối tượng được đưa vào bộ đệm sẽ tồn tại mãi mãi. Theo thời gian, bộ nhớ đệm này sẽ chiếm ngày càng nhiều bộ nhớ của ứng dụng và tỷ lệ bộ nhớ được sử dụng thực tế ngày càng nhỏ hơn.
Hãy xem xét đoạn mã sau để lấy điểm cao cho một trò chơi. Nó giống với thứ tôi từng thấy trong quá khứ. Đây là một yêu cầu tốn kém và chúng tôi có thể dễ dàng xóa bộ nhớ đệm khi nó thay đổi, vì vậy chúng tôi muốn lưu nó vào bộ nhớ đệm.
@scores hàm băm hoàn toàn không được chọn. Nó sẽ phát triển để giữ mọi điểm cao cho mọi người dùng — không lý tưởng nếu bạn có nhiều điểm cao.
Trong ứng dụng Rails, chúng tôi có thể muốn sử dụng Rails.cache thay vào đó có thời hạn sử dụng hợp lý (rò rỉ bộ nhớ trong Redis vẫn là rò rỉ bộ nhớ!)
Trong một ứng dụng không phải Rails, chúng tôi muốn giới hạn kích thước băm, loại bỏ các mục cũ nhất hoặc ít được sử dụng gần đây nhất. LruRedux là một sự thực hiện tốt đẹp.
Một phiên bản tinh vi hơn của vụ rò rỉ này là bộ nhớ đệm có giới hạn nhưng các khóa của nó có kích thước tùy ý. Nếu các khóa tự phát triển thì bộ đệm cũng sẽ phát triển. Thông thường, bạn sẽ không đạt được điều này. Tuy nhiên, nếu bạn đang tuần tự hóa các đối tượng dưới dạng JSON và sử dụng nó làm khóa, hãy kiểm tra kỹ xem bạn có đang không tuần tự hóa những thứ tăng theo mức sử dụng hay không — chẳng hạn như danh sách các tin nhắn đã đọc của người dùng.
Tài liệu tham khảo thông tư
Tài liệu tham khảo vòng có thể được thu gom rác. Bộ sưu tập rác trong Ruby sử dụng thuật toán "Đánh dấu và quét". Trong bài trình bày giới thiệu về phân bổ băng thông thay đổi, Peter Zhu và Matt Valentine-House đã đưa ra lời giải thích tuyệt vời về cách hoạt động của thuật toán này.
Về cơ bản, có hai giai đoạn:đánh dấu và quét.
-
Trong phần đánh dấu giai đoạn này, trình thu gom rác bắt đầu tại các đối tượng gốc (các lớp, toàn cục, v.v.), đánh dấu chúng và sau đó xem xét các đối tượng được tham chiếu của chúng.
Sau đó nó đánh dấu tất cả các đối tượng được tham chiếu. Các đối tượng được tham chiếu đã được đánh dấu sẽ không được xem lại. Điều này tiếp tục cho đến khi không còn đối tượng nào để xem — tức là tất cả các đối tượng được tham chiếu đã được đánh dấu.
-
Sau đó, người thu gom rác sẽ chuyển sang quét giai đoạn. Bất kỳ đối tượng nào không được đánh dấu sẽ được dọn sạch.
Do đó, các đối tượng có tham chiếu trực tiếp vẫn có thể được dọn sạch. Miễn là đối tượng gốc cuối cùng không tham chiếu đến đối tượng thì nó sẽ được thu thập. Bằng cách này, các cụm đối tượng có tham chiếu vòng tròn vẫn có thể được thu gom rác.
Giám sát hiệu suất ứng dụng:Dòng thời gian sự kiện và biểu đồ đối tượng được phân bổ
Như đã đề cập trong phần đầu tiên của loạt bài này, mọi ứng dụng ở cấp độ sản xuất đều phải sử dụng một số dạng Giám sát hiệu suất ứng dụng (APM).
Nhiều tùy chọn có sẵn, bao gồm cả việc tự lăn (chỉ được đề xuất cho các đội lớn hơn). Một tính năng chính bạn sẽ nhận được từ APM là khả năng xem số lượng phân bổ mà một hành động (hoặc công việc nền) thực hiện. Các công cụ APM tốt sẽ phân tích vấn đề này, cung cấp thông tin chi tiết về nguồn phân bổ — bộ điều khiển, chế độ xem, v.v.
Điều này thường được gọi là 'dòng thời gian sự kiện'. Điểm thưởng nếu APM của bạn cho phép bạn viết mã tùy chỉnh để chia nhỏ dòng thời gian hơn.
Hãy xem xét đoạn mã sau cho bộ điều khiển Rails.
Khi được APM báo cáo, 'dòng thời gian sự kiện' có thể trông giống như ảnh chụp màn hình sau đây từ AppSignal.

Điều này có thể được thiết kế để chúng tôi có thể xem phần nào của mã thực hiện phân bổ trong dòng thời gian. Trong các ứng dụng thực, mã có thể sẽ ít rõ ràng hơn 😅
Đây là ví dụ về dòng thời gian sự kiện được đo lường, một lần nữa từ AppSignal:

Biết nơi đặt nhạc cụ thường có thể khó nắm bắt. Không có cách nào thay thế được việc thực sự hiểu mã ứng dụng của bạn, nhưng có một số tín hiệu có thể đóng vai trò là 'mùi'.
Nếu APM của bạn hiển thị các lượt chạy hoặc phân bổ GC theo thời gian, bạn có thể tìm kiếm các mức tăng đột biến để xem liệu chúng có khớp với các điểm cuối nhất định bị tấn công hay các công việc đang chạy nền nhất định hay không. Đây là một ví dụ khác từ bảng điều khiển ma thuật Ruby VM của AppSignal:

Bằng cách xem xét phân bổ theo cách này, chúng ta có thể thu hẹp tìm kiếm khi xem xét các vấn đề về bộ nhớ. Điều này giúp việc sử dụng các công cụ nhưmemory_profiler dễ dàng hơn nhiều và derailed_benchmarks một cách hiệu quả.
Đọc về các tính năng bổ sung mới nhất cho viên ngọc Ruby của AppSignal, như theo dõi số liệu phân bổ và GC.
Kết thúc
Trong bài đăng này, chúng tôi đã đi sâu vào một số công cụ có thể giúp tìm và khắc phục rò rỉ bộ nhớ, bao gồm memory_profiler , derailed_benchmarks , perf:mem_over_time , perf:objects , perf:heap_diff , dòng thời gian sự kiện và biểu đồ đối tượng được phân bổ trong AppSignal.
Tôi hy vọng bạn thấy bài đăng này, cùng với phần một, hữu ích trong việc chẩn đoán và phân loại rò rỉ bộ nhớ trong ứng dụng Ruby của bạn.
Đọc thêm về các công cụ chúng tôi đã sử dụng:
memory_profilerderailed_benchmarks- Ứng dụng Rails bị rò rỉ
Đọc thêm chi tiết:
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ẻ!
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!