Bí ẩn của hồ sơ bậc cầu thang Gần đây, chúng tôi đã thấy , một dịch vụ , 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. một trong những dự án Rust của chúng tôi axum 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 . Tôi muốn đào sâu và tìm hiểu chuyện gì đang xảy ra. có hại cho doanh nghiệp 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à đố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. bình thường 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à: và . khối lượng yêu cầu lớn hơn khả 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 với một trình xử lý duy nhất nhận phần thân JSON. axum 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 để biết chi tiết về cách tôi chạy thử nghiệm tải của mình. README 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à 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. Heap Fragmentation Phân mảnh Heap là gì? Những người ở một độ tuổi nhất định có thể đã từng xem trên DOS hoặc 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í". tiện ích chống phân mảnh Windows 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 ). 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. wongarsu đã chỉnh sửa Điều gì đó tương tự có thể 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". xảy ra 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 , thứ gì đó ở cấp độ khung trong , thứ gì đó sâu hơn trong 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 ews) serde axum tokio nhận xét của chúng tôi về HackerN 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? để giải cứu Jemalloc tự mô tả là nhằm mục đích Đồ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. có vẻ như đó có thể là thứ tôi cần. jemalloc "[nhấn mạnh] tránh phân mảnh và hỗ trợ đồng thời có thể mở rộng." jemalloc Vì 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ớ. jemalloc 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 readme, rất ít công việc được yêu cầu để mang nó đi lái thử. https://github.com/tikv/jemallocator Đối vớ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 và chạy lại các thử nghiệm tải của mình. dự án đồ chơi jemalloc 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ó , chúng ta thấy mặt cắt bậc cầu thang quen thuộc. Với , 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 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ở. jemalloc jemalloc jemalloc 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 để 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ì có thể cho kết quả tốt hơn về tổng thể. jemalloc jemalloc Một cách riêng biệt, được bao gồm trong repo là một để sử dụng với công cụ kiểm tra tải . 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. dự án đồ chơi benchmark.yml https://github.com/fcsonline/drill Đố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 để biết thêm thông tin. trang nghề nghiệp của chúng tôi Để 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 , hoặc để có các bản cập nhật mới nhất cho hoặc tham gia thảo luận trên . Twitter Github RSS dịch vụ webhook Svix cộng đồng Slack của chúng tôi Cũng được xuất bản ở đây.