1 điểm bởi GN⁺ 2024-05-12 | 1 bình luận | Chia sẻ qua WhatsApp

Tổng hợp quá trình khắc phục memory leak liên quan đến ActiveSupport::Notifications

  • Tình huống phát sinh memory leak

    • Từ một thời điểm nhất định, mức sử dụng bộ nhớ của web Dyno bắt đầu tăng bất thường
    • Pager bắt đầu báo động, xuất hiện tình huống có vẻ là memory leak
  • Ứng phó ngay lập tức

    • Trên Heroku, khi nghi ngờ có memory leak thì có thể tạm thời xử lý bằng cách khởi động lại Dyno
    • Khởi động lại theo chu kỳ deploy thông thường hoặc thủ công khởi động lại các Dyno đang gần chạm giới hạn bộ nhớ
  • Rà soát mã đáng nghi để xác định nguyên nhân

    • Xem lại các thay đổi mã được triển khai ngay trước thời điểm memory spike
    • Triển khai lại từng đoạn mã bị nghi ngờ để kiểm tra có phát sinh memory leak hay không
    • Không thấy đoạn mã nào có vẻ là nguyên nhân nên tiếp tục rollback cả các thay đổi về tooling để kiểm tra. Nhưng memory leak vẫn tiếp diễn
  • Phân tích mẫu tăng bộ nhớ

    • Chỉ web Dyno bị leak. Sidekiq, Delayed::Job Dyno vẫn bình thường
    • Không phải mọi web Dyno đều luôn bị leak. Có lúc chạy bình thường vài giờ rồi một, hai hoặc tất cả Dyno mới bắt đầu leak
    • Nghi ngờ nguyên nhân đến từ một loại traffic cụ thể hơn là do lưu lượng tổng thể
    • Không phải tất cả Puma worker trong Dyno đều bị leak, mà chỉ một số ít worker đang dùng phần lớn tổng bộ nhớ
  • Thu thập và phân tích heap dump

    • Dùng rbtrace để thu thập heap dump của Ruby process đang bị leak
      • Dùng heroku ps:exec để SSH vào dyno đang bị leak
      • Dùng lệnh ps để chọn Ruby worker process đang dùng nhiều bộ nhớ nhất
      • Attach rbtrace vào pid đó rồi bắt đầu theo dõi phân bổ bộ nhớ (ObjectSpace.trace_object_allocations_start)
      • Thu thập heap dump bằng ObjectSpace.dump_all. Nếu dung lượng lớn thì nén bằng gzip
      • Dùng heroku ps:copy để tải file dump về máy local
    • Dùng reap để trực quan hóa heap dump thành flamegraph
      • Phát hiện một Thread đang tham chiếu tới 1.9GB bộ nhớ, và bên dưới là một Array tham chiếu tới 32.067 object
    • Dùng sheap để truy vết các object khả nghi
      • Xác định Thread đó là worker thread của Puma
      • Object ActiveSupport::SubscriberQueueRegistry đang tham chiếu tới Hash, và bên dưới có các object StringArray
      • Trong Array có hơn 32.000 object ActiveSupport::Notifications::Event bị tích tụ
  • Suy luận về nguyên nhân

    • Phỏng đoán rằng object Event của ActiveSupport::Notifications đang bị tích sai trong mảng #children
    • Nếu xảy ra lỗi bên trong block ActiveSupport::Notifications.instrument, có thể Event đó không bị xóa khỏi #children và gây ra rò rỉ bộ nhớ
  • Tái hiện trên local

    • Gửi request trên local với request path và parameter đáng nghi đã phát hiện trong production
    • Xác nhận xuất hiện 500 Internal Server Error cùng với URI::InvalidURIError
    • Xác nhận mức sử dụng bộ nhớ của production dyno đã xử lý request đó tăng vọt
  • Phân tích nguyên nhân cụ thể

    • Có một bug liên quan đến Event#children của ActiveSupport::Notifications đã được sửa trong Rails 7.1
    • Đồng thời có bug trong gem Bugsnag khiến quá trình làm sạch request url gọi URI.parse và phát sinh URI::InvalidURIError, từ đó dẫn đến memory leak
    • Lỗi được raise bên trong block ActiveSupport::Notifications.subscribe không được bắt, khiến Event tương ứng không bị xóa khỏi mảng #children và tiếp tục tích tụ, gây memory leak
  • Cách khắc phục

    • Ngắn hạn: nâng cấp phiên bản gem Bugsnag để khi phát sinh URI::InvalidURIError cũng không raise lỗi
    • Dài hạn: nâng cấp lên Rails 7.x nơi bug của ActiveSupport::Notifications đã được sửa

Ý kiến của GN⁺

  • Quá trình phát hiện vấn đề rồi lần lượt xác định nguyên nhân một cách có hệ thống rất ấn tượng. Bài viết cũng tổng hợp khá tốt quy trình phân tích cơ bản khi nghi ngờ có memory leak
  • Có vẻ các công cụ mã nguồn mở để thu thập, trực quan hóa và phân tích heap dump của Ruby như rbtrace, reap, sheap đang được phát triển rất tích cực. Không chỉ riêng Ruby, điều quan trọng là cần nắm được các công cụ phân tích bộ nhớ hữu ích theo từng ngôn ngữ và biết cách áp dụng vào vấn đề thực tế
  • Thực tế, nguyên nhân memory leak khá thường đến từ bug trong thư viện hoặc framework đang dùng, nhưng không phải lúc nào cũng có điều kiện tự phân tích, sửa và deploy bản vá, nên điều quan trọng là áp dụng cách né tránh hoặc giảm thiểu càng sớm càng tốt. Việc cung cấp bug report kèm theo phương án thay thế khả thi cũng là một cách hay
  • Điểm hay là bài viết không chỉ dừng ở việc xử lý memory leak mà còn đào sâu đến root cause của vấn đề. Tinh thần phân tích cẩn thận mã nguồn bên trong framework để lần ra nguyên nhân gốc là điều các developer rất cần
  • Cuối cùng, nguyên nhân của memory leak hóa ra lại nằm ở một lần nâng cấp phiên bản thư viện tưởng như chẳng liên quan. Đây là một ví dụ cho thấy tầm quan trọng của quản lý dependency và theo dõi thay đổi. Dù là thay đổi nhỏ cũng cần phân tích kỹ tác động và tiếp tục giám sát sau khi triển khai

1 bình luận

 
GN⁺ 2024-05-12
Ý kiến trên Hacker News

Có thể giải quyết bằng rèn luyện kỹ năng kỹ sư mà không cần sợ quản lý bộ nhớ thủ công

  • Chỉ cần RAII và các quy tắc sở hữu rõ ràng thì quản lý bộ nhớ là một công việc kỹ thuật dễ dàng
  • Ngược lại, những framework cố chấp dùng reference counting và shared pointer lại làm quyền sở hữu trở nên mơ hồ nên còn khó hơn
  • Đã tạo ra thì giải phóng, đã chuyển giao thì không cần bận tâm nữa, đó là một phần của kỷ luật kỹ thuật
  • Bug bộ nhớ về bản chất cũng không khác bug logic, nên việc sửa chúng là điều hiển nhiên
  • Tài nguyên của OS (handle, socket, v.v.) cũng được quản lý thủ công mà không cần trình quản lý tài nguyên tự động, nên bộ nhớ cũng có thể tiếp cận theo cách tương tự

Trường hợp thiệt hại 5 triệu USD do rò rỉ bộ nhớ

  • Giới thiệu một giai thoại về bug rò rỉ bộ nhớ trong driver máy in Solaris vào thập niên 90
  • Khi đó, ngân hàng xác nhận giao dịch bằng fax, in ra bằng máy in, rồi đọc lại qua điện thoại cho phía bên kia nghe và ghi âm để có xác nhận pháp lý
  • Do driver máy in bị sập vì rò rỉ bộ nhớ, giấy xác nhận không được in ra nên giao dịch bị hủy, gây thiệt hại 5 triệu USD
  • Cuối cùng, vì lời phàn nàn của CEO Sun mà các lập trình viên mới sửa bug

Công cụ gỡ lỗi rò rỉ bộ nhớ và hướng khắc phục

  • Dùng Valgrind thì có thể dễ dàng tìm ra rò rỉ trong C
  • Nếu thiết kế đúng cách thì phần lớn việc cấp phát và giải phóng diễn ra trong cùng một hàm nên khá dễ sửa
  • Giới thiệu trường hợp rò rỉ bộ nhớ ở máy chủ quảng cáo của Yahoo và giải pháp chắp vá tạm thời
  • Trích một câu đùa của người thiết kế PHP để cho thấy thái độ chọn chủ nghĩa thực dụng hơn là chủ nghĩa hoàn hảo
  • Trong Rails, người ta nói rằng việc giải quyết bằng phần cứng để đổi lấy năng suất là điều phổ biến

Lời khen về phong cách viết

  • Có bình luận rằng cách viết của tác giả tạo cảm giác thú vị, có thể là nhờ emoticon hoặc cách định dạng