mirror of
https://github.com/NetworkCats/rustyip.git
synced 2026-11-13 08:15:53 +00:00
Use mimalloc for request-path allocations
The production image is built against musl, whose allocator is slow and takes a global lock. Profiling /json on a 1 CPU / 1 GB Alpine container showed malloc/free at ~11% of total CPU, the largest user-space cost; the rest is mostly the kernel network stack, already at one recvfrom and one writev per request. mimalloc alone, however, kept the freed ~96 MB MMDB buffer resident after a database update (RSS stayed at ~197 MB in two of three swaps), and MIMALLOC_PURGE_DELAY=0 fixes that only by giving back most of the speedup. So blocks of 1 MiB or more, in practice only the MMDB file contents, go to the system allocator, which unmaps them on free; RSS returns to ~98 MB after every swap, as before. Median of three alternating 20 s runs: /json across random IPs rose from 131.6k to 142.4k rps (+8%), p50 latency 0.465ms -> 0.443ms; a single IP rose from 124.5k to 127.4k rps (+2%). Steady-state RSS grows by ~12 MB. Responses are byte-identical to before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c05b71e33b
commit
036a3fa061
Generated
+19
@@ -770,6 +770,15 @@ version = "0.2.182"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
||||
|
||||
[[package]]
|
||||
name = "libmimalloc-sys"
|
||||
version = "0.1.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
@@ -837,6 +846,15 @@ version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "mimalloc"
|
||||
version = "0.1.52"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862"
|
||||
dependencies = [
|
||||
"libmimalloc-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
@@ -1287,6 +1305,7 @@ dependencies = [
|
||||
"dotenvy",
|
||||
"http-body-util",
|
||||
"maxminddb",
|
||||
"mimalloc",
|
||||
"quick_cache",
|
||||
"reqwest",
|
||||
"serde",
|
||||
|
||||
@@ -20,6 +20,7 @@ urlencoding = "2"
|
||||
tokio-util = "0.7"
|
||||
static-files = "0.3.1"
|
||||
quick_cache = "0.7"
|
||||
mimalloc = "0.1.52"
|
||||
|
||||
[build-dependencies]
|
||||
static-files = "0.3.1"
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
//! Global allocator: mimalloc for request-path allocations, the system
|
||||
//! allocator for very large blocks.
|
||||
//!
|
||||
//! The production image is built against musl, whose allocator is slow and
|
||||
//! takes a global lock; it was the largest user-space cost of serving `/json`.
|
||||
//! mimalloc, however, keeps a freed ~100 MB MMDB buffer resident after a
|
||||
//! database update, while musl unmaps it immediately. Routing large blocks to
|
||||
//! the system allocator keeps the speed without holding a dead database.
|
||||
|
||||
use std::alloc::{GlobalAlloc, Layout, System};
|
||||
use std::ptr;
|
||||
|
||||
use mimalloc::MiMalloc;
|
||||
|
||||
/// Blocks at least this large go to the system allocator. Far above anything
|
||||
/// allocated per request, far below the MMDB file contents.
|
||||
const LARGE_ALLOC_BYTES: usize = 1 << 20;
|
||||
|
||||
pub struct Allocator;
|
||||
|
||||
fn is_large(size: usize) -> bool {
|
||||
size >= LARGE_ALLOC_BYTES
|
||||
}
|
||||
|
||||
// SAFETY: every block is freed or resized by the allocator that produced it,
|
||||
// since the choice depends only on the block size, which `dealloc` and
|
||||
// `realloc` receive unchanged from the caller.
|
||||
unsafe impl GlobalAlloc for Allocator {
|
||||
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
|
||||
unsafe {
|
||||
if is_large(layout.size()) {
|
||||
System.alloc(layout)
|
||||
} else {
|
||||
MiMalloc.alloc(layout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
|
||||
unsafe {
|
||||
if is_large(layout.size()) {
|
||||
System.alloc_zeroed(layout)
|
||||
} else {
|
||||
MiMalloc.alloc_zeroed(layout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
|
||||
unsafe {
|
||||
if is_large(layout.size()) {
|
||||
System.dealloc(ptr, layout);
|
||||
} else {
|
||||
MiMalloc.dealloc(ptr, layout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
|
||||
unsafe {
|
||||
match (is_large(layout.size()), is_large(new_size)) {
|
||||
(false, false) => MiMalloc.realloc(ptr, layout, new_size),
|
||||
(true, true) => System.realloc(ptr, layout, new_size),
|
||||
// Crossing the threshold moves the block to the other allocator.
|
||||
_ => {
|
||||
let new_layout = Layout::from_size_align_unchecked(new_size, layout.align());
|
||||
let new_ptr = self.alloc(new_layout);
|
||||
if !new_ptr.is_null() {
|
||||
ptr::copy_nonoverlapping(ptr, new_ptr, layout.size().min(new_size));
|
||||
self.dealloc(ptr, layout);
|
||||
}
|
||||
new_ptr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn realloc_across_threshold_preserves_contents() {
|
||||
let small = 64 * 1024;
|
||||
let large = 4 * LARGE_ALLOC_BYTES;
|
||||
let layout = Layout::from_size_align(small, 16).unwrap();
|
||||
unsafe {
|
||||
let p = Allocator.alloc(layout);
|
||||
assert!(!p.is_null());
|
||||
for i in 0..small {
|
||||
*p.add(i) = i as u8;
|
||||
}
|
||||
|
||||
let grown = Allocator.realloc(p, layout, large);
|
||||
assert!(!grown.is_null());
|
||||
assert_eq!(grown as usize % 16, 0);
|
||||
for i in 0..small {
|
||||
assert_eq!(*grown.add(i), i as u8);
|
||||
}
|
||||
|
||||
let large_layout = Layout::from_size_align(large, 16).unwrap();
|
||||
let shrunk = Allocator.realloc(grown, large_layout, small);
|
||||
assert!(!shrunk.is_null());
|
||||
for i in 0..small {
|
||||
assert_eq!(*shrunk.add(i), i as u8);
|
||||
}
|
||||
Allocator.dealloc(shrunk, layout);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alloc_zeroed_large_block_is_zeroed() {
|
||||
let layout = Layout::from_size_align(2 * LARGE_ALLOC_BYTES, 8).unwrap();
|
||||
unsafe {
|
||||
let p = Allocator.alloc_zeroed(layout);
|
||||
assert!(!p.is_null());
|
||||
assert!((0..layout.size()).step_by(4096).all(|i| *p.add(i) == 0));
|
||||
Allocator.dealloc(p, layout);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,11 @@ use tracing_subscriber::EnvFilter;
|
||||
|
||||
use rustyip::{config, db, handlers, routes, updater};
|
||||
|
||||
mod alloc;
|
||||
|
||||
#[global_allocator]
|
||||
static GLOBAL: alloc::Allocator = alloc::Allocator;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
Reference in New Issue
Block a user