paint-brush
Cách phát hiện và tránh phân mảnh đống trong ứng dụng Rusttừ tác giả@tomhacohen
1,007 lượt đọc
1,007 lượt đọc

Cách phát hiện và tránh phân mảnh đống trong ứng dụng Rust

từ tác giả Tom Hacohen8m2023/06/27
Read on Terminal Reader

dài quá đọc không nổi

Dự án Rust chứng kiến sự tăng trưởng bộ nhớ ngoài dự kiến. Sử dụng bộ nhớ tăng không tương xứng. Tăng trưởng bộ nhớ không giới hạn có thể dẫn đến các dịch vụ buộc phải thoát. Sự kết hợp kỳ diệu đối với tôi là: *cơ thể yêu cầu lớn hơn* và *đồng thời cao hơn. Các chi tiết của "bậc thang" cụ thể này là dành riêng cho chính ứng dụng đó.
featured image - Cách phát hiện và tránh phân mảnh đống trong ứng dụng Rust
Tom Hacohen HackerNoon profile picture
0-item
1-item
2-item

Bí ẩn của hồ sơ bậc cầu thang

Gần đây, chúng tôi đã thấy một trong những dự án Rust của chúng tôi , một dịch vụ axum , thể hiện một số hành vi kỳ quặc khi nói đến việc sử dụng bộ nhớ. Một hồ sơ bộ nhớ trông kỳ quặc là điều cuối cùng tôi mong đợi từ một chương trình Rust, nhưng chúng ta đang ở đây.



Dịch vụ sẽ chạy với bộ nhớ "phẳng" trong một khoảng thời gian, sau đó đột ngột nhảy lên một cao nguyên mới. Mô hình này sẽ lặp lại trong nhiều giờ, đôi khi dưới tải, nhưng không phải lúc nào cũng vậy. Điều đáng lo ngại là một khi chúng tôi thấy mức tăng mạnh, rất hiếm khi bộ nhớ giảm trở lại. Cứ như thể ký ức bị mất, hay nói cách khác là lâu lâu lại bị "rò rỉ".


Trong những trường hợp bình thường, cấu hình "bậc thang" này trông có vẻ kỳ lạ, nhưng tại một thời điểm, mức sử dụng bộ nhớ tăng lên một cách không tương xứng. Tăng trưởng bộ nhớ không giới hạn có thể dẫn đến các dịch vụ buộc phải thoát. Khi các dịch vụ thoát đột ngột, điều này có thể làm giảm tính khả dụng... điều này có hại cho doanh nghiệp . Tôi muốn đào sâu và tìm hiểu chuyện gì đang xảy ra.


Thông thường, khi tôi nghĩ về sự tăng trưởng bộ nhớ bất ngờ trong một chương trình, tôi nghĩ đến rò rỉ. Tuy nhiên, điều này có vẻ khác nhau. Khi bị rò rỉ, bạn có xu hướng thấy mô hình tăng trưởng đều đặn, ổn định hơn.


Thường thì đường này trông giống như một đường dốc lên và sang phải. Vì vậy, nếu dịch vụ của chúng tôi không bị rò rỉ, thì nó đang làm gì?


Nếu tôi có thể xác định các điều kiện gây ra sự tăng vọt về mức sử dụng bộ nhớ, có lẽ tôi có thể giảm thiểu bất cứ điều gì đang xảy ra.

Đào trong

Tôi có hai câu hỏi hóc búa:


  • Có điều gì đó thay đổi trong mã của chúng tôi để thúc đẩy hành vi này không?
  • Mặt khác, một mô hình lưu lượng truy cập mới đã xuất hiện?


Nhìn vào các số liệu lịch sử, tôi có thể thấy các mô hình tăng mạnh tương tự giữa các khoảng thời gian dài không thay đổi, nhưng chúng tôi chưa bao giờ có mức tăng trưởng như vậy trước đây. Để biết liệu sự tăng trưởng có phải là mới hay không (mặc dù mô hình "bậc thang" là bình thường đối với chúng tôi), tôi cần một cách đáng tin cậy để tái tạo hành vi này.


Nếu tôi có thể buộc "bước" tự hiển thị, thì tôi sẽ có cách để xác minh sự thay đổi trong hành vi khi tôi thực hiện các bước để hạn chế sự gia tăng trí nhớ. Tôi cũng có thể quay lại lịch sử git của chúng tôi và tìm kiếm một thời điểm khi dịch vụ không thể hiện sự tăng trưởng dường như không giới hạn.

Kích thước tôi đã sử dụng khi chạy thử nghiệm tải của mình là:


  • Kích thước của nội dung POST được gửi đến dịch vụ.

  • Tỷ lệ yêu cầu (nghĩa là yêu cầu mỗi giây).

  • Số lượng kết nối máy khách đồng thời.


Sự kết hợp kỳ diệu đối với tôi là: khối lượng yêu cầu lớn hơnkhả năng xử lý đồng thời cao hơn .


Khi chạy kiểm tra tải trên một hệ thống cục bộ, có đủ loại yếu tố hạn chế, bao gồm số lượng hữu hạn bộ xử lý có sẵn để chạy cả máy khách và chính máy chủ. Tuy nhiên, tôi vẫn có thể nhìn thấy "bậc thang" trong bộ nhớ trên máy cục bộ của mình trong những trường hợp phù hợp, ngay cả ở tỷ lệ yêu cầu tổng thể thấp hơn.


Sử dụng tải trọng có kích thước cố định và gửi yêu cầu theo đợt, với thời gian nghỉ ngắn giữa các đợt, tôi có thể tăng bộ nhớ của dịch vụ nhiều lần, mỗi lần một bước.


Tôi thấy thú vị là mặc dù tôi có thể phát triển trí nhớ theo thời gian, nhưng cuối cùng tôi cũng đạt đến điểm hiệu suất giảm dần. Cuối cùng, sẽ có một mức trần nào đó (vẫn cao hơn nhiều so với dự kiến). Chơi xung quanh thêm một chút, tôi nhận thấy mình có thể đạt đến mức trần cao hơn nữa bằng cách gửi yêu cầu với các kích thước tải trọng khác nhau.


Khi tôi đã xác định được thông tin đầu vào của mình, tôi có thể xem lại lịch sử git của chúng tôi, cuối cùng biết được rằng nỗi sợ sản xuất của chúng tôi không có khả năng là kết quả của những thay đổi gần đây từ phía chúng tôi.

Chi tiết về khối lượng công việc để kích hoạt "bậc thang" này dành riêng cho chính ứng dụng, mặc dù tôi có thể buộc một biểu đồ tương tự xảy ra với một dự án đồ chơi .


 #[derive(serde::Deserialize, Clone)] struct Widget { payload: serde_json::Value, } #[derive(serde::Serialize)] struct WidgetCreateResponse { id: String, size: usize, } async fn create_widget(Json(widget): Json<Widget>) -> Response { ( StatusCode::CREATED, Json(process_widget(widget.clone()).await), ) .into_response() } async fn process_widget(widget: Widget) -> WidgetCreateResponse { let widget_id = uuid::Uuid::new_v4(); let bytes = serde_json::to_vec(&widget.payload).unwrap_or_default(); // An arbitrary sleep to pad the handler latency as a stand-in for a more // complex code path. // Tweak the duration by setting the `SLEEP_MS` env var. tokio::time::sleep(std::time::Duration::from_millis( std::env::var("SLEEP_MS") .as_deref() .unwrap_or("150") .parse() .expect("invalid SLEEP_MS"), )) .await; WidgetCreateResponse { id: widget_id.to_string(), size: bytes.len(), } }

Hóa ra bạn không cần nhiều để đạt được điều đó. Tôi đã quản lý để thấy mức tăng mạnh tương tự (nhưng trong trường hợp này nhỏ hơn nhiều) từ ứng dụng axum với một trình xử lý duy nhất nhận phần thân JSON.


Mặc dù khả năng tăng trí nhớ trong dự án đồ chơi của tôi không ấn tượng như chúng ta đã thấy trong dịch vụ sản xuất, nhưng nó đủ để giúp tôi so sánh và đối chiếu trong giai đoạn tiếp theo của cuộc điều tra. Nó cũng giúp tôi có vòng lặp chặt chẽ hơn của một cơ sở mã nhỏ hơn trong khi tôi thử nghiệm các khối lượng công việc khác nhau. Xem README để biết chi tiết về cách tôi chạy thử nghiệm tải của mình.

Tôi đã dành thời gian tìm kiếm trên web các báo cáo lỗi hoặc thảo luận có thể mô tả một hành vi tương tự. Một thuật ngữ xuất hiện nhiều lần là Heap Fragmentation và sau khi đọc thêm một chút về chủ đề này, có vẻ như nó phù hợp với những gì tôi đang thấy.

Phân mảnh Heap là gì?

Những người ở một độ tuổi nhất định có thể đã từng xem tiện ích chống phân mảnh trên DOS hoặc Windows di chuyển các khối trên đĩa cứng để hợp nhất các vùng "đã sử dụng" và "miễn phí".



Trong trường hợp ổ cứng PC cũ này, các tệp có kích thước khác nhau được ghi vào đĩa, sau đó được di chuyển hoặc xóa, để lại một "lỗ hổng" không gian trống giữa các vùng được sử dụng khác. Khi đĩa bắt đầu đầy, bạn có thể thử tạo một tệp mới không vừa với một trong những vùng nhỏ hơn đó. Trong kịch bản phân mảnh heap, điều đó sẽ dẫn đến lỗi phân bổ, mặc dù chế độ lỗi phân mảnh đĩa sẽ ít nghiêm trọng hơn một chút. Trên đĩa, tệp sau đó sẽ cần được chia thành các phần nhỏ hơn, điều này làm cho việc truy cập kém hiệu quả hơn nhiều (cảm ơn wongarsu đã chỉnh sửa ). Giải pháp cho ổ đĩa là "chống phân mảnh" (khử phân mảnh) ổ đĩa để sắp xếp lại các khối mở đó thành các khoảng trống liên tục.


Điều gì đó tương tự có thể xảy ra khi bộ cấp phát (thứ chịu trách nhiệm quản lý cấp phát bộ nhớ trong chương trình của bạn) thêm và xóa các giá trị có kích thước khác nhau trong một khoảng thời gian. Các khoảng trống quá nhỏ và nằm rải rác trong heap có thể dẫn đến các khối bộ nhớ "mới" được phân bổ để chứa một giá trị mới không phù hợp nếu không. Mặc dù không may là do cách quản lý bộ nhớ hoạt động nên không thể "chống phân mảnh".


Nguyên nhân cụ thể của sự phân mảnh có thể là do bất kỳ thứ gì: phân tích cú pháp JSON bằng serde , thứ gì đó ở cấp độ khung trong axum , thứ gì đó sâu hơn trong tokio hoặc thậm chí chỉ là một cách giải quyết của việc triển khai bộ cấp phát cụ thể cho hệ thống nhất định. Ngay cả khi không biết nguyên nhân gốc rễ (nếu có), hành vi vẫn có thể quan sát được trong môi trường của chúng ta và phần nào có thể tái tạo được trong một ứng dụng cơ bản. (Cập nhật: cần điều tra thêm, nhưng chúng tôi khá chắc chắn rằng đó là phân tích cú pháp JSON, hãy xem nhận xét của chúng tôi về HackerN ews)


Nếu đây là điều đang xảy ra với bộ nhớ tiến trình, thì có thể làm gì với nó? Có vẻ như sẽ khó thay đổi khối lượng công việc để tránh bị phân mảnh. Có vẻ như thật khó để giải phóng tất cả các phụ thuộc trong dự án của tôi để có thể tìm ra nguyên nhân gốc rễ trong mã về cách các sự kiện phân mảnh đang xảy ra. Vậy thì cái gì có thể làm được?

Jemalloc để giải cứu

jemalloc tự mô tả là nhằm mục đích "[nhấn mạnh] tránh phân mảnh và hỗ trợ đồng thời có thể mở rộng." Đồng thời thực sự là một phần của vấn đề đối với chương trình của tôi và tránh phân mảnh là tên của trò chơi. jemalloc có vẻ như đó có thể là thứ tôi cần.

jemalloc là một bộ cấp phát hoạt động theo cách của nó ngay từ đầu để tránh bị phân mảnh, nên chúng tôi hy vọng rằng dịch vụ của chúng tôi có thể chạy lâu hơn mà không cần tăng dần bộ nhớ.


Việc thay đổi đầu vào cho chương trình của tôi hoặc đống phụ thuộc ứng dụng không phải là chuyện nhỏ. Tuy nhiên, việc hoán đổi bộ cấp phát là chuyện nhỏ.


Theo các ví dụ trong https://github.com/tikv/jemallocator readme, rất ít công việc được yêu cầu để mang nó đi lái thử.


Đối với dự án đồ chơi của tôi, tôi đã thêm một tính năng vận chuyển hàng hóa để tùy ý hoán đổi bộ phân bổ mặc định cho jemalloc và chạy lại các thử nghiệm tải của mình.


Việc ghi lại bộ nhớ thường trú trong quá trình tải mô phỏng của tôi cho thấy hai cấu hình bộ nhớ riêng biệt.

Không có jemalloc , chúng ta thấy mặt cắt bậc cầu thang quen thuộc. Với jemalloc , chúng tôi thấy bộ nhớ tăng giảm liên tục khi chạy thử nghiệm. Quan trọng hơn, mặc dù có sự khác biệt đáng kể giữa mức sử dụng bộ nhớ với jemalloc trong thời gian tải so với thời gian không hoạt động, nhưng chúng tôi không "mất điểm" như đã làm trước đây vì bộ nhớ luôn quay trở lại đường cơ sở.

kết thúc

Nếu bạn tình cờ nhìn thấy hồ sơ "bậc thang" trên dịch vụ Rust, hãy cân nhắc sử dụng jemalloc để lái thử. Nếu bạn tình cờ có một khối lượng công việc thúc đẩy phân mảnh heap, thì jemalloc có thể cho kết quả tốt hơn về tổng thể.


Một cách riêng biệt, được bao gồm trong repo dự án đồ chơi là một benchmark.yml để sử dụng với công cụ kiểm tra tải https://github.com/fcsonline/drill . Hãy thử thay đổi đồng thời, kích thước nội dung (và thời lượng ngủ của trình xử lý tùy ý trong chính dịch vụ), v.v. để xem sự thay đổi trong trình cấp phát tác động đến cấu hình bộ nhớ như thế nào.


Đối với tác động trong thế giới thực, bạn có thể thấy rõ sự thay đổi trong hồ sơ khi chúng tôi triển khai chuyển sang jemalloc .


Trường hợp dịch vụ được sử dụng để hiển thị các đường phẳng và các bước lớn, thường không phụ thuộc vào tải, giờ đây chúng tôi thấy một đường gồ ghề hơn theo sát khối lượng công việc đang hoạt động hơn. Ngoài lợi ích giúp dịch vụ tránh tăng bộ nhớ không cần thiết, thay đổi này giúp chúng tôi hiểu rõ hơn về cách dịch vụ của chúng tôi phản hồi tải, vì vậy nhìn chung, đây là một kết quả tích cực.


Nếu bạn quan tâm đến việc xây dựng một dịch vụ mạnh mẽ và có thể mở rộng bằng Rust, chúng tôi đang tuyển dụng! Kiểm tra trang nghề nghiệp của chúng tôi để biết thêm thông tin.


Để biết thêm nội dung như thế này, hãy đảm bảo theo dõi chúng tôi trên Twitter , Github hoặc RSS để có các bản cập nhật mới nhất cho dịch vụ webhook Svix hoặc tham gia thảo luận trên cộng đồng Slack của chúng tôi .


Cũng được xuất bản ở đây.