mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-13 16:46:55 +00:00
feat(admin): Add admin v3 API routes and profiling endpoints for RustFS (#774)
* add Jemalloc * feat: optimize AI rules with unified .rules.md (#401) * feat: optimize AI rules with unified .rules.md and entry points - Create .rules.md as the central AI coding rules file - Add .copilot-rules.md as GitHub Copilot entry point - Add CLAUDE.md as Claude AI entry point - Incorporate principles from rustfs.com project - Add three critical rules: 1. Use English for all code comments and documentation 2. Clean up temporary scripts after use 3. Only make confident modifications * Update CLAUDE.md --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> * feat: translate chinese to english (#402) * Checkpoint before follow-up message Co-authored-by: anzhengchao <anzhengchao@gmail.com> * Translate project documentation and comments from Chinese to English Co-authored-by: anzhengchao <anzhengchao@gmail.com> * Fix typo: "unparseable" to "unparsable" in version test comment Co-authored-by: anzhengchao <anzhengchao@gmail.com> * Refactor compression test code with minor syntax improvements Co-authored-by: anzhengchao <anzhengchao@gmail.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> * fix: the automatic logout issue and user list display failure on Windows systems (#353) (#343) (#403) Co-authored-by: 安正超 <anzhengchao@gmail.com> * upgrade version * improve code for profiling * fix * Initial plan * feat: Implement layered DNS resolver with caching and validation Co-authored-by: houseme <4829346+houseme@users.noreply.github.com> * feat: Integrate DNS resolver into main application and fix formatting Co-authored-by: houseme <4829346+houseme@users.noreply.github.com> * feat: Implement enhanced DNS resolver with Moka cache and layered fallback Co-authored-by: houseme <4829346+houseme@users.noreply.github.com> * feat: Implement hickory-resolver with TLS support for enhanced DNS resolution Co-authored-by: houseme <4829346+houseme@users.noreply.github.com> * upgrade * add .gitignore config * fix * add * add * up * improve linux profiling * fix * fix * fix * feat(admin): Refactor profiling endpoints Replaces the existing pprof profiling endpoints with new trigger-based APIs for CPU and memory profiling. This change simplifies the handler logic by moving the profiling implementation to a dedicated module. A new handler file `admin/handlers/profile.rs` is created to contain the logic for these new endpoints. The core profiling functions are now expected to be in the `profiling` module, which the new handlers call to generate and save profile data. * cargo shear --fix * fix * fix * fix --------- Co-authored-by: 安正超 <anzhengchao@gmail.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: shiro.lee <69624924+shiroleeee@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
This commit is contained in:
+7
-9
@@ -123,8 +123,6 @@ zip = { workspace = true }
|
||||
|
||||
# Observability and Metrics
|
||||
metrics = { workspace = true }
|
||||
opentelemetry = { workspace = true }
|
||||
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies]
|
||||
sysctl = { workspace = true }
|
||||
@@ -132,14 +130,14 @@ sysctl = { workspace = true }
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libsystemd.workspace = true
|
||||
|
||||
[target.'cfg(all(target_os = "linux", target_env = "gnu"))'.dependencies]
|
||||
tikv-jemallocator = "0.6.1"
|
||||
|
||||
[target.'cfg(all(target_os = "linux", target_env = "musl"))'.dependencies]
|
||||
mimalloc = "0.1"
|
||||
|
||||
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
||||
pprof = { version = "0.15.0", features = ["flamegraph", "protobuf-codec"] }
|
||||
mimalloc = { workspace = true }
|
||||
[target.'cfg(all(target_os = "linux", target_env = "gnu"))'.dependencies]
|
||||
tikv-jemallocator = { workspace = true }
|
||||
[target.'cfg(all(not(target_env = "msvc"), not(target_os = "windows")))'.dependencies]
|
||||
tikv-jemalloc-ctl = { workspace = true }
|
||||
jemalloc_pprof = { workspace = true }
|
||||
pprof = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
uuid = { workspace = true, features = ["v4"] }
|
||||
|
||||
@@ -81,14 +81,13 @@ pub mod kms_dynamic;
|
||||
pub mod kms_keys;
|
||||
pub mod policies;
|
||||
pub mod pools;
|
||||
pub mod profile;
|
||||
pub mod rebalance;
|
||||
pub mod service_account;
|
||||
pub mod sts;
|
||||
pub mod tier;
|
||||
pub mod trace;
|
||||
pub mod user;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use pprof::protos::Message;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Serialize, Default)]
|
||||
@@ -1264,14 +1263,8 @@ impl Operation for ProfileHandler {
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
use crate::profiling;
|
||||
|
||||
if !profiling::is_profiler_enabled() {
|
||||
return Ok(S3Response::new((
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
Body::from("Profiler not enabled. Set RUSTFS_ENABLE_PROFILING=true to enable profiling".to_string()),
|
||||
)));
|
||||
}
|
||||
use rustfs_config::{DEFAULT_CPU_FREQ, ENV_CPU_FREQ};
|
||||
use rustfs_utils::get_env_usize;
|
||||
|
||||
let queries = extract_query_params(&req.uri);
|
||||
let seconds = queries.get("seconds").and_then(|s| s.parse::<u64>().ok()).unwrap_or(30);
|
||||
@@ -1284,73 +1277,56 @@ impl Operation for ProfileHandler {
|
||||
)));
|
||||
}
|
||||
|
||||
let guard = match profiling::get_profiler_guard() {
|
||||
Some(guard) => guard,
|
||||
None => {
|
||||
return Ok(S3Response::new((
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
Body::from("Profiler not initialized".to_string()),
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
info!("Starting CPU profile collection for {} seconds", seconds);
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_secs(seconds)).await;
|
||||
|
||||
let guard_lock = match guard.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(_) => {
|
||||
error!("Failed to acquire profiler guard lock");
|
||||
return Ok(S3Response::new((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Body::from("Failed to acquire profiler lock".to_string()),
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let report = match guard_lock.report().build() {
|
||||
Ok(report) => report,
|
||||
Err(e) => {
|
||||
error!("Failed to build profiler report: {}", e);
|
||||
return Ok(S3Response::new((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Body::from(format!("Failed to build profile report: {e}")),
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
info!("CPU profile collection completed");
|
||||
|
||||
match format.as_str() {
|
||||
"protobuf" | "pb" => {
|
||||
let profile = report.pprof().unwrap();
|
||||
let mut body = Vec::new();
|
||||
if let Err(e) = profile.write_to_vec(&mut body) {
|
||||
error!("Failed to serialize protobuf profile: {}", e);
|
||||
return Ok(S3Response::new((
|
||||
"protobuf" | "pb" => match crate::profiling::dump_cpu_pprof_for(std::time::Duration::from_secs(seconds)).await {
|
||||
Ok(path) => match tokio::fs::read(&path).await {
|
||||
Ok(bytes) => {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(CONTENT_TYPE, "application/octet-stream".parse().unwrap());
|
||||
Ok(S3Response::with_headers((StatusCode::OK, Body::from(bytes)), headers))
|
||||
}
|
||||
Err(e) => Ok(S3Response::new((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Body::from("Failed to serialize profile".to_string()),
|
||||
)));
|
||||
}
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(CONTENT_TYPE, "application/octet-stream".parse().unwrap());
|
||||
Ok(S3Response::with_headers((StatusCode::OK, Body::from(body)), headers))
|
||||
}
|
||||
Body::from(format!("Failed to read profile file: {e}")),
|
||||
))),
|
||||
},
|
||||
Err(e) => Ok(S3Response::new((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Body::from(format!("Failed to collect CPU profile: {e}")),
|
||||
))),
|
||||
},
|
||||
"flamegraph" | "svg" => {
|
||||
let mut flamegraph_buf = Vec::new();
|
||||
match report.flamegraph(&mut flamegraph_buf) {
|
||||
Ok(()) => (),
|
||||
let freq = get_env_usize(ENV_CPU_FREQ, DEFAULT_CPU_FREQ) as i32;
|
||||
let guard = match pprof::ProfilerGuard::new(freq) {
|
||||
Ok(g) => g,
|
||||
Err(e) => {
|
||||
error!("Failed to generate flamegraph: {}", e);
|
||||
return Ok(S3Response::new((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Body::from(format!("Failed to generate flamegraph: {e}")),
|
||||
Body::from(format!("Failed to create profiler: {e}")),
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_secs(seconds)).await;
|
||||
|
||||
let report = match guard.report().build() {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Ok(S3Response::new((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Body::from(format!("Failed to build profile report: {e}")),
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let mut flamegraph_buf = Vec::new();
|
||||
if let Err(e) = report.flamegraph(&mut flamegraph_buf) {
|
||||
return Ok(S3Response::new((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Body::from(format!("Failed to generate flamegraph: {e}")),
|
||||
)));
|
||||
}
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(CONTENT_TYPE, "image/svg+xml".parse().unwrap());
|
||||
Ok(S3Response::with_headers((StatusCode::OK, Body::from(flamegraph_buf)), headers))
|
||||
@@ -1380,9 +1356,11 @@ impl Operation for ProfileStatusHandler {
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let status = {
|
||||
use crate::profiling;
|
||||
use rustfs_config::{DEFAULT_ENABLE_PROFILING, ENV_ENABLE_PROFILING};
|
||||
use rustfs_utils::get_env_bool;
|
||||
|
||||
if profiling::is_profiler_enabled() {
|
||||
let enabled = get_env_bool(ENV_ENABLE_PROFILING, DEFAULT_ENABLE_PROFILING);
|
||||
if enabled {
|
||||
HashMap::from([
|
||||
("enabled", "true"),
|
||||
("status", "running"),
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::admin::router::Operation;
|
||||
use http::header::CONTENT_TYPE;
|
||||
use http::{HeaderMap, StatusCode};
|
||||
use matchit::Params;
|
||||
use s3s::{Body, S3Request, S3Response, S3Result, s3_error};
|
||||
use tracing::info;
|
||||
|
||||
pub struct TriggerProfileCPU {}
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for TriggerProfileCPU {
|
||||
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
info!("Triggering CPU profile dump via S3 request...");
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let mut header = HeaderMap::new();
|
||||
header.insert(CONTENT_TYPE, "text/plain".parse().unwrap());
|
||||
return Ok(S3Response::with_headers(
|
||||
(StatusCode::NOT_IMPLEMENTED, Body::from("CPU profiling is not supported on Windows")),
|
||||
header,
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let dur = std::time::Duration::from_secs(60);
|
||||
match crate::profiling::dump_cpu_pprof_for(dur).await {
|
||||
Ok(path) => {
|
||||
let mut header = HeaderMap::new();
|
||||
header.insert(CONTENT_TYPE, "text/html".parse().unwrap());
|
||||
Ok(S3Response::with_headers((StatusCode::OK, Body::from(path.display().to_string())), header))
|
||||
}
|
||||
Err(e) => Err(s3_error!(InternalError, "{}", format!("Failed to dump CPU profile: {e}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TriggerProfileMemory {}
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for TriggerProfileMemory {
|
||||
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
info!("Triggering Memory profile dump via S3 request...");
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let mut header = HeaderMap::new();
|
||||
header.insert(CONTENT_TYPE, "text/plain".parse().unwrap());
|
||||
return Ok(S3Response::with_headers(
|
||||
(StatusCode::NOT_IMPLEMENTED, Body::from("Memory profiling is not supported on Windows")),
|
||||
header,
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
match crate::profiling::dump_memory_pprof_now().await {
|
||||
Ok(path) => {
|
||||
let mut header = HeaderMap::new();
|
||||
header.insert(CONTENT_TYPE, "text/html".parse().unwrap());
|
||||
Ok(S3Response::with_headers((StatusCode::OK, Body::from(path.display().to_string())), header))
|
||||
}
|
||||
Err(e) => Err(s3_error!(InternalError, "{}", format!("Failed to dump Memory profile: {e}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,12 +22,13 @@ pub mod utils;
|
||||
#[cfg(test)]
|
||||
mod console_test;
|
||||
|
||||
// use ecstore::global::{is_dist_erasure, is_erasure};
|
||||
use handlers::{
|
||||
GetReplicationMetricsHandler, HealthCheckHandler, ListRemoteTargetHandler, RemoveRemoteTargetHandler, SetRemoteTargetHandler,
|
||||
bucket_meta,
|
||||
event::{ListNotificationTargets, ListTargetsArns, NotificationTarget, RemoveNotificationTarget},
|
||||
group, kms, kms_dynamic, kms_keys, policies, pools, rebalance,
|
||||
group, kms, kms_dynamic, kms_keys, policies, pools,
|
||||
profile::{TriggerProfileCPU, TriggerProfileMemory},
|
||||
rebalance,
|
||||
service_account::{AddServiceAccount, DeleteServiceAccount, InfoServiceAccount, ListServiceAccount, UpdateServiceAccount},
|
||||
sts, tier, user,
|
||||
};
|
||||
@@ -44,6 +45,8 @@ pub fn make_admin_route(console_enabled: bool) -> std::io::Result<impl S3Route>
|
||||
|
||||
// Health check endpoint for monitoring and orchestration
|
||||
r.insert(Method::GET, "/health", AdminOperation(&HealthCheckHandler {}))?;
|
||||
r.insert(Method::GET, "/profile/cpu", AdminOperation(&TriggerProfileCPU {}))?;
|
||||
r.insert(Method::GET, "/profile/memory", AdminOperation(&TriggerProfileMemory {}))?;
|
||||
|
||||
// 1
|
||||
r.insert(Method::POST, "/", AdminOperation(&sts::AssumeRoleHandle {}))?;
|
||||
@@ -515,7 +518,8 @@ fn register_user_route(r: &mut S3Router<AdminOperation>) -> std::io::Result<()>
|
||||
format!("{}{}", ADMIN_PREFIX, "/v3/target/{target_type}/{target_name}/reset").as_str(),
|
||||
AdminOperation(&RemoveNotificationTarget {}),
|
||||
)?;
|
||||
// arns
|
||||
|
||||
// arns list
|
||||
r.insert(
|
||||
Method::GET,
|
||||
format!("{}{}", ADMIN_PREFIX, "/v3/target/arns").as_str(),
|
||||
|
||||
+11
-10
@@ -12,6 +12,10 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::admin::ADMIN_PREFIX;
|
||||
use crate::admin::console::is_console_path;
|
||||
use crate::admin::console::make_console_server;
|
||||
use crate::admin::rpc::RPC_PREFIX;
|
||||
use hyper::HeaderMap;
|
||||
use hyper::Method;
|
||||
use hyper::StatusCode;
|
||||
@@ -30,11 +34,6 @@ use s3s::s3_error;
|
||||
use tower::Service;
|
||||
use tracing::error;
|
||||
|
||||
use crate::admin::ADMIN_PREFIX;
|
||||
use crate::admin::console::is_console_path;
|
||||
use crate::admin::console::make_console_server;
|
||||
use crate::admin::rpc::RPC_PREFIX;
|
||||
|
||||
pub struct S3Router<T> {
|
||||
router: Router<T>,
|
||||
console_enabled: bool,
|
||||
@@ -85,12 +84,13 @@ where
|
||||
T: Operation,
|
||||
{
|
||||
fn is_match(&self, method: &Method, uri: &Uri, headers: &HeaderMap, _: &mut Extensions) -> bool {
|
||||
if method == Method::GET && uri.path() == "/health" {
|
||||
let path = uri.path();
|
||||
if method == Method::GET && (path == "/health" || path == "/profile/cpu" || path == "/profile/memory") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// AssumeRole
|
||||
if method == Method::POST && uri.path() == "/" {
|
||||
if method == Method::POST && path == "/" {
|
||||
if let Some(val) = headers.get(header::CONTENT_TYPE) {
|
||||
if val.as_bytes() == b"application/x-www-form-urlencoded" {
|
||||
return true;
|
||||
@@ -98,17 +98,18 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
uri.path().starts_with(ADMIN_PREFIX) || uri.path().starts_with(RPC_PREFIX) || is_console_path(uri.path())
|
||||
path.starts_with(ADMIN_PREFIX) || path.starts_with(RPC_PREFIX) || is_console_path(path)
|
||||
}
|
||||
|
||||
// check_access before call
|
||||
async fn check_access(&self, req: &mut S3Request<Body>) -> S3Result<()> {
|
||||
// Allow unauthenticated access to health check
|
||||
if req.method == Method::GET && req.uri.path() == "/health" {
|
||||
let path = req.uri.path();
|
||||
if req.method == Method::GET && (path == "/health" || path == "/profile/cpu" || path == "/profile/memory") {
|
||||
return Ok(());
|
||||
}
|
||||
// Allow unauthenticated access to console static files if console is enabled
|
||||
if self.console_enabled && is_console_path(req.uri.path()) {
|
||||
if self.console_enabled && is_console_path(path) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -25,6 +25,7 @@ mod storage;
|
||||
mod update;
|
||||
mod version;
|
||||
|
||||
// Ensure the correct path for parse_license is imported
|
||||
use crate::server::{
|
||||
SHUTDOWN_TIMEOUT, ServiceState, ServiceStateManager, ShutdownSignal, init_event_notifier, shutdown_event_notifier,
|
||||
start_audit_system, start_http_server, stop_audit_system, wait_for_shutdown,
|
||||
@@ -62,6 +63,7 @@ use rustfs_obs::{init_obs, set_global_guard};
|
||||
use rustfs_targets::arn::TargetID;
|
||||
use rustfs_utils::net::parse_and_resolve_address;
|
||||
use s3s::s3_error;
|
||||
use std::env;
|
||||
use std::io::{Error, Result};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
@@ -126,7 +128,7 @@ async fn async_main() -> Result<()> {
|
||||
|
||||
// Initialize performance profiling if enabled
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
profiling::start_profiling_if_enabled();
|
||||
profiling::init_from_env().await;
|
||||
|
||||
// Run parameters
|
||||
match run(opt).await {
|
||||
@@ -349,7 +351,7 @@ async fn run(opt: config::Opt) -> Result<()> {
|
||||
/// Returns true if the environment variable is not set or set to true/1/yes/on/enabled,
|
||||
/// false if set to false/0/no/off/disabled
|
||||
fn parse_bool_env_var(var_name: &str, default: bool) -> bool {
|
||||
std::env::var(var_name)
|
||||
env::var(var_name)
|
||||
.unwrap_or_else(|_| default.to_string())
|
||||
.parse::<bool>()
|
||||
.unwrap_or(default)
|
||||
@@ -436,7 +438,7 @@ async fn handle_shutdown(
|
||||
}
|
||||
|
||||
fn init_update_check() {
|
||||
let update_check_enable = std::env::var(ENV_UPDATE_CHECK)
|
||||
let update_check_enable = env::var(ENV_UPDATE_CHECK)
|
||||
.unwrap_or_else(|_| DEFAULT_UPDATE_CHECK.to_string())
|
||||
.parse::<bool>()
|
||||
.unwrap_or(DEFAULT_UPDATE_CHECK);
|
||||
|
||||
+256
-36
@@ -12,52 +12,272 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use pprof::ProfilerGuard;
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
use tracing::info;
|
||||
use chrono::Utc;
|
||||
use jemalloc_pprof::PROF_CTL;
|
||||
use pprof::protos::Message;
|
||||
use rustfs_config::{
|
||||
DEFAULT_CPU_DURATION_SECS, DEFAULT_CPU_FREQ, DEFAULT_CPU_INTERVAL_SECS, DEFAULT_CPU_MODE, DEFAULT_ENABLE_PROFILING,
|
||||
DEFAULT_MEM_INTERVAL_SECS, DEFAULT_MEM_PERIODIC, DEFAULT_OUTPUT_DIR, ENV_CPU_DURATION_SECS, ENV_CPU_FREQ,
|
||||
ENV_CPU_INTERVAL_SECS, ENV_CPU_MODE, ENV_ENABLE_PROFILING, ENV_MEM_INTERVAL_SECS, ENV_MEM_PERIODIC, ENV_OUTPUT_DIR,
|
||||
};
|
||||
use rustfs_utils::{get_env_bool, get_env_str, get_env_u64, get_env_usize};
|
||||
use std::fs::{File, create_dir_all};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
static PROFILER_GUARD: OnceLock<Arc<Mutex<ProfilerGuard<'static>>>> = OnceLock::new();
|
||||
static CPU_CONT_GUARD: OnceLock<Arc<Mutex<Option<pprof::ProfilerGuard<'static>>>>> = OnceLock::new();
|
||||
|
||||
pub fn init_profiler() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let guard = pprof::ProfilerGuardBuilder::default()
|
||||
.frequency(1000)
|
||||
.blocklist(&["libc", "libgcc", "pthread", "vdso"])
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build profiler guard: {e}"))?;
|
||||
/// CPU profiling mode
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum CpuMode {
|
||||
Off,
|
||||
Continuous,
|
||||
Periodic,
|
||||
}
|
||||
|
||||
PROFILER_GUARD
|
||||
.set(Arc::new(Mutex::new(guard)))
|
||||
.map_err(|_| "Failed to set profiler guard (already initialized)")?;
|
||||
/// Get or create output directory
|
||||
fn output_dir() -> PathBuf {
|
||||
let dir = get_env_str(ENV_OUTPUT_DIR, DEFAULT_OUTPUT_DIR);
|
||||
let p = PathBuf::from(dir);
|
||||
if let Err(e) = create_dir_all(&p) {
|
||||
warn!("profiling: create output dir {} failed: {}, fallback to current dir", p.display(), e);
|
||||
return PathBuf::from(".");
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
info!("Performance profiler initialized");
|
||||
/// Read CPU profiling mode from env
|
||||
fn read_cpu_mode() -> CpuMode {
|
||||
match get_env_str(ENV_CPU_MODE, DEFAULT_CPU_MODE).to_lowercase().as_str() {
|
||||
"continuous" => CpuMode::Continuous,
|
||||
"periodic" => CpuMode::Periodic,
|
||||
_ => CpuMode::Off,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate timestamp string for filenames
|
||||
fn ts() -> String {
|
||||
Utc::now().format("%Y%m%dT%H%M%S").to_string()
|
||||
}
|
||||
|
||||
/// Write pprof report to file in protobuf format
|
||||
fn write_pprof_report_pb(report: &pprof::Report, path: &Path) -> Result<(), String> {
|
||||
let profile = report.pprof().map_err(|e| format!("pprof() failed: {e}"))?;
|
||||
let mut buf = Vec::with_capacity(512 * 1024);
|
||||
profile.write_to_vec(&mut buf).map_err(|e| format!("encode failed: {e}"))?;
|
||||
let mut f = File::create(path).map_err(|e| format!("create file failed: {e}"))?;
|
||||
f.write_all(&buf).map_err(|e| format!("write file failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_profiler_enabled() -> bool {
|
||||
PROFILER_GUARD.get().is_some()
|
||||
/// Internal: dump CPU pprof from existing guard
|
||||
async fn dump_cpu_with_guard(guard: &pprof::ProfilerGuard<'_>) -> Result<PathBuf, String> {
|
||||
let report = guard.report().build().map_err(|e| format!("build report failed: {e}"))?;
|
||||
let out = output_dir().join(format!("cpu_profile_{}.pb", ts()));
|
||||
write_pprof_report_pb(&report, &out)?;
|
||||
info!("CPU profile exported: {}", out.display());
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn get_profiler_guard() -> Option<Arc<Mutex<ProfilerGuard<'static>>>> {
|
||||
PROFILER_GUARD.get().cloned()
|
||||
}
|
||||
|
||||
pub fn start_profiling_if_enabled() {
|
||||
let enable_profiling = std::env::var("RUSTFS_ENABLE_PROFILING")
|
||||
.unwrap_or_else(|_| "false".to_string())
|
||||
.parse::<bool>()
|
||||
.unwrap_or(false);
|
||||
|
||||
if enable_profiling {
|
||||
match init_profiler() {
|
||||
Ok(()) => {
|
||||
info!("Performance profiling enabled via RUSTFS_ENABLE_PROFILING environment variable");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to initialize profiler: {}", e);
|
||||
info!("Performance profiling disabled due to initialization error");
|
||||
}
|
||||
// Public API: dump CPU for a duration; if continuous guard exists, snapshot immediately.
|
||||
pub async fn dump_cpu_pprof_for(duration: Duration) -> Result<PathBuf, String> {
|
||||
if let Some(cell) = CPU_CONT_GUARD.get() {
|
||||
let guard_slot = cell.lock().await;
|
||||
if let Some(ref guard) = *guard_slot {
|
||||
debug!("profiling: using continuous profiler guard for CPU dump");
|
||||
return dump_cpu_with_guard(guard).await;
|
||||
}
|
||||
}
|
||||
|
||||
let freq = get_env_usize(ENV_CPU_FREQ, DEFAULT_CPU_FREQ) as i32;
|
||||
let guard = pprof::ProfilerGuard::new(freq).map_err(|e| format!("create profiler failed: {e}"))?;
|
||||
sleep(duration).await;
|
||||
|
||||
dump_cpu_with_guard(&guard).await
|
||||
}
|
||||
|
||||
// Public API: dump memory pprof now (jemalloc)
|
||||
pub async fn dump_memory_pprof_now() -> Result<PathBuf, String> {
|
||||
let out = output_dir().join(format!("mem_profile_{}.pb", ts()));
|
||||
let mut f = File::create(&out).map_err(|e| format!("create file failed: {e}"))?;
|
||||
|
||||
let prof_ctl_cell = PROF_CTL
|
||||
.as_ref()
|
||||
.ok_or_else(|| "jemalloc profiling control not available".to_string())?;
|
||||
let mut prof_ctl = prof_ctl_cell.lock().await;
|
||||
|
||||
if !prof_ctl.activated() {
|
||||
return Err("jemalloc profiling is not active".to_string());
|
||||
}
|
||||
|
||||
let bytes = prof_ctl.dump_pprof().map_err(|e| format!("dump pprof failed: {e}"))?;
|
||||
f.write_all(&bytes).map_err(|e| format!("write file failed: {e}"))?;
|
||||
info!("Memory profile exported: {}", out.display());
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
// Jemalloc status check (No forced placement, only status observation)
|
||||
pub async fn check_jemalloc_profiling() {
|
||||
use tikv_jemalloc_ctl::{config, epoch, stats};
|
||||
|
||||
if let Err(e) = epoch::advance() {
|
||||
warn!("jemalloc epoch advance failed: {e}");
|
||||
}
|
||||
|
||||
match config::malloc_conf::read() {
|
||||
Ok(conf) => debug!("jemalloc malloc_conf: {}", conf),
|
||||
Err(e) => debug!("jemalloc read malloc_conf failed: {e}"),
|
||||
}
|
||||
|
||||
match std::env::var("MALLOC_CONF") {
|
||||
Ok(v) => debug!("MALLOC_CONF={}", v),
|
||||
Err(_) => debug!("MALLOC_CONF is not set"),
|
||||
}
|
||||
|
||||
if let Some(lock) = PROF_CTL.as_ref() {
|
||||
let ctl = lock.lock().await;
|
||||
info!(activated = ctl.activated(), "jemalloc profiling status");
|
||||
} else {
|
||||
info!("Performance profiling disabled. Set RUSTFS_ENABLE_PROFILING=true to enable");
|
||||
info!("jemalloc profiling controller is NOT available");
|
||||
}
|
||||
|
||||
let _ = epoch::advance();
|
||||
macro_rules! show {
|
||||
($name:literal, $reader:expr) => {
|
||||
match $reader {
|
||||
Ok(v) => debug!(concat!($name, "={}"), v),
|
||||
Err(e) => debug!(concat!($name, " read failed: {}"), e),
|
||||
}
|
||||
};
|
||||
}
|
||||
show!("allocated", stats::allocated::read());
|
||||
show!("resident", stats::resident::read());
|
||||
show!("mapped", stats::mapped::read());
|
||||
show!("metadata", stats::metadata::read());
|
||||
show!("active", stats::active::read());
|
||||
}
|
||||
|
||||
// Internal: start continuous CPU profiling
|
||||
async fn start_cpu_continuous(freq_hz: i32) {
|
||||
let cell = CPU_CONT_GUARD.get_or_init(|| Arc::new(Mutex::new(None))).clone();
|
||||
let mut slot = cell.lock().await;
|
||||
if slot.is_some() {
|
||||
warn!("profiling: continuous CPU guard already running");
|
||||
return;
|
||||
}
|
||||
match pprof::ProfilerGuardBuilder::default()
|
||||
.frequency(freq_hz)
|
||||
.blocklist(&["libc", "libgcc", "pthread", "vdso"])
|
||||
.build()
|
||||
{
|
||||
Ok(guard) => {
|
||||
*slot = Some(guard);
|
||||
info!(freq = freq_hz, "start continuous CPU profiling");
|
||||
}
|
||||
Err(e) => warn!("start continuous CPU profiling failed: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
// Internal: start periodic CPU sampling loop
|
||||
async fn start_cpu_periodic(freq_hz: i32, interval: Duration, duration: Duration) {
|
||||
info!(freq = freq_hz, ?interval, ?duration, "start periodic CPU profiling");
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
sleep(interval).await;
|
||||
let guard = match pprof::ProfilerGuard::new(freq_hz) {
|
||||
Ok(g) => g,
|
||||
Err(e) => {
|
||||
warn!("periodic CPU profiler create failed: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
sleep(duration).await;
|
||||
match guard.report().build() {
|
||||
Ok(report) => {
|
||||
let out = output_dir().join(format!("cpu_profile_{}.pb", ts()));
|
||||
if let Err(e) = write_pprof_report_pb(&report, &out) {
|
||||
warn!("write periodic CPU pprof failed: {e}");
|
||||
} else {
|
||||
info!("periodic CPU profile exported: {}", out.display());
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("periodic CPU report build failed: {e}"),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Internal: start periodic memory dump when jemalloc profiling is active
|
||||
async fn start_memory_periodic(interval: Duration) {
|
||||
info!(?interval, "start periodic memory pprof dump");
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
sleep(interval).await;
|
||||
|
||||
let Some(lock) = PROF_CTL.as_ref() else {
|
||||
debug!("skip memory dump: PROF_CTL not available");
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut ctl = lock.lock().await;
|
||||
if !ctl.activated() {
|
||||
debug!("skip memory dump: jemalloc profiling not active");
|
||||
continue;
|
||||
}
|
||||
|
||||
let out = output_dir().join(format!("mem_profile_periodic_{}.pb", ts()));
|
||||
match File::create(&out) {
|
||||
Err(e) => {
|
||||
error!("periodic mem dump create file failed: {}", e);
|
||||
continue;
|
||||
}
|
||||
Ok(mut f) => match ctl.dump_pprof() {
|
||||
Ok(bytes) => {
|
||||
if let Err(e) = f.write_all(&bytes) {
|
||||
error!("periodic mem dump write failed: {}", e);
|
||||
} else {
|
||||
info!("periodic memory profile dumped to {}", out.display());
|
||||
}
|
||||
}
|
||||
Err(e) => error!("periodic mem dump failed: {}", e),
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Public: unified init entry, avoid duplication/conflict
|
||||
pub async fn init_from_env() {
|
||||
let enabled = get_env_bool(ENV_ENABLE_PROFILING, DEFAULT_ENABLE_PROFILING);
|
||||
if !enabled {
|
||||
debug!("profiling: disabled by env");
|
||||
return;
|
||||
}
|
||||
|
||||
// Jemalloc state check once (no dump)
|
||||
check_jemalloc_profiling().await;
|
||||
|
||||
// CPU
|
||||
let cpu_mode = read_cpu_mode();
|
||||
let cpu_freq = get_env_usize(ENV_CPU_FREQ, DEFAULT_CPU_FREQ) as i32;
|
||||
let cpu_interval = Duration::from_secs(get_env_u64(ENV_CPU_INTERVAL_SECS, DEFAULT_CPU_INTERVAL_SECS));
|
||||
let cpu_duration = Duration::from_secs(get_env_u64(ENV_CPU_DURATION_SECS, DEFAULT_CPU_DURATION_SECS));
|
||||
|
||||
match cpu_mode {
|
||||
CpuMode::Off => debug!("profiling: CPU mode off"),
|
||||
CpuMode::Continuous => start_cpu_continuous(cpu_freq).await,
|
||||
CpuMode::Periodic => start_cpu_periodic(cpu_freq, cpu_interval, cpu_duration).await,
|
||||
}
|
||||
|
||||
// Memory
|
||||
let mem_periodic = get_env_bool(ENV_MEM_PERIODIC, DEFAULT_MEM_PERIODIC);
|
||||
let mem_interval = Duration::from_secs(get_env_u64(ENV_MEM_INTERVAL_SECS, DEFAULT_MEM_INTERVAL_SECS));
|
||||
if mem_periodic {
|
||||
start_memory_periodic(mem_interval).await;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user