From cea29bed7f6604ccc8b96c90e8d0e38f2ebf5845 Mon Sep 17 00:00:00 2001 From: Yuansheng Date: Wed, 12 Aug 2026 00:40:25 +0800 Subject: [PATCH 1/2] perf(server): jemalloc global allocator + fat LTO release profile Two compile-time changes, measured together on the c=128 saturation grid (same-rig before/after in the PR): - tikv-jemallocator 0.6 as the global allocator, gated to the targets we ship and bench (Linux glibc). Flamegraphs put glibc malloc/free at 15.5% of request CPU under saturation; jemalloc runs the same load at ~6%. - release profile thin -> fat LTO. codegen-units was already 1 and opt-level stays at the release default of 3. Other targets (macOS dev builds, musl) keep the system allocator. strip stays at "debuginfo" so shipped binaries remain profileable in the field (#847), and panic stays unwind for per-request isolation. --- Cargo.lock | 21 +++++++++++++++++++++ Cargo.toml | 5 ++++- crates/aisix-server/Cargo.toml | 7 +++++++ crates/aisix-server/src/main.rs | 12 ++++++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 379292ac..c8bf9748 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -453,6 +453,7 @@ dependencies = [ "serde_json", "socket2 0.5.10", "tempfile", + "tikv-jemallocator", "tokio", "tracing", "uuid", @@ -4860,6 +4861,26 @@ dependencies = [ "rustc-hash", ] +[[package]] +name = "tikv-jemalloc-sys" +version = "0.6.1+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd8aa5b2ab86a2cefa406d889139c162cbb230092f7d1d7cbc1716405d852a3b" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "tikv-jemallocator" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0359b4327f954e0567e69fb191cf1436617748813819c94b8cd4a431422d053a" +dependencies = [ + "libc", + "tikv-jemalloc-sys", +] + [[package]] name = "time" version = "0.3.47" diff --git a/Cargo.toml b/Cargo.toml index 83ec4804..4b884a3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -189,7 +189,10 @@ tempfile = "3.13" # CARGO_PROFILE_RELEASE_STRIP=none cargo build --release # See issue #847. [profile.release] -lto = "thin" +# Fat LTO over thin: cross-crate inlining across the whole graph is a +# measured per-request win on the saturation grid, paid for once per +# release build in link time. Numbers in the PR that flipped it. +lto = "fat" codegen-units = 1 strip = "debuginfo" diff --git a/crates/aisix-server/Cargo.toml b/crates/aisix-server/Cargo.toml index 99472d9b..fb7d48fc 100644 --- a/crates/aisix-server/Cargo.toml +++ b/crates/aisix-server/Cargo.toml @@ -56,6 +56,13 @@ socket2 = { version = "0.5", features = ["all"] } # axum-server itself depends on, so the builder types unify. hyper-util = { version = "0.1", features = ["server-auto", "tokio"] } +# jemalloc as the global allocator, only on the targets we ship and +# bench (Linux glibc — the Docker image and both supported production +# arches). Other targets (macOS dev builds, musl) keep the system +# allocator rather than carry an allocator we never run in production. +[target.'cfg(all(target_os = "linux", target_env = "gnu"))'.dependencies] +tikv-jemallocator = "0.6" + [dev-dependencies] tempfile = "3" wiremock = "0.6" diff --git a/crates/aisix-server/src/main.rs b/crates/aisix-server/src/main.rs index 41996c25..1e4edaf8 100644 --- a/crates/aisix-server/src/main.rs +++ b/crates/aisix-server/src/main.rs @@ -16,6 +16,18 @@ use std::error::Error as StdError; use std::path::{Path, PathBuf}; use std::sync::Arc; +// jemalloc as the global allocator on the shipped/benched targets +// (Linux glibc): under the thread-per-core saturation load, allocator +// time drops from ~15% of request CPU (glibc malloc) to ~6%. Other +// targets keep the system allocator. One deploy caveat: jemalloc bakes +// the build host's page size into the binary, so an aarch64 binary +// built on a 4K-page host aborts at startup on a 64K-page kernel — +// cross-building for such kernels needs JEMALLOC_SYS_WITH_LG_PAGE=16, +// which runs on both page sizes. +#[cfg(all(target_os = "linux", target_env = "gnu"))] +#[global_allocator] +static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; + mod cert_bundle; mod export; mod heartbeat; From fb97c9cf45650d4122584ec7cbb177d6b582990d Mon Sep 17 00:00:00 2001 From: Yuansheng Date: Wed, 12 Aug 2026 01:54:49 +0800 Subject: [PATCH 2/2] ci(docker): assert jemalloc is linked into the shipped binary Audit follow-ups for the allocator PR: - The #847 symbol-table step now also requires prefixed _rjem_ symbols, so a future drift of the cfg gates in aisix-server cannot silently fall back to glibc malloc (the regression would cost the throughput this change bought with no other signal). - Dockerfile: note the JEMALLOC_SYS_WITH_LG_PAGE=16 requirement next to the release build RUN, where a future linux/arm64 buildx target would otherwise bake QEMU's 4K page size into the image. --- .github/workflows/docker-image.yml | 5 +++++ Dockerfile | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 6dde9aa8..1cf71caa 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -219,6 +219,11 @@ jobs: ls -l /tmp/aisix-shipped test "$(nm /tmp/aisix-shipped | grep -c ' [tT] ')" -gt 1000 readelf -S /tmp/aisix-shipped | grep -E '\.symtab|\.eh_frame' + # jemalloc must actually be linked (prefixed _rjem_ symbols): + # if the cfg gates in aisix-server drift, the binary silently + # falls back to glibc malloc with no other signal. + nm /tmp/aisix-shipped | grep -q ' [tT] _rjem_' \ + || { echo "::error::jemalloc symbols missing from shipped binary"; exit 1; } - name: Install cosign if: github.event_name != 'pull_request' diff --git a/Dockerfile b/Dockerfile index 509e90dc..80e69ed3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -71,6 +71,11 @@ COPY schemas ./schemas # `--locked` forces the build to use the exact versions in Cargo.lock — # fails fast if the lockfile is stale rather than silently resolving # fresh deps in CI. +# +# If this ever builds for linux/arm64: jemalloc bakes the build host's +# page size into the binary, and QEMU reports 4K — set +# JEMALLOC_SYS_WITH_LG_PAGE=16 here or the image aborts at startup on +# 64K-page kernels (see crates/aisix-server/src/main.rs). RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/src/target \ cargo build --locked --release --bin aisix \