perf: add socket2 TCP_NODELAY + socket tuning for low-latency responses
- Replace basic TcpListener::bind with socket2 tuned socket - TCP_NODELAY: disable Nagle's algorithm (-5 to 40ms latency on small responses) - SO_REUSEADDR: port available immediately after server restart - SO_REUSEPORT: ready for multi-worker scaling (Linux) - TCP_KEEPALIVE: detect dead connections within 60s/10s interval - listen(2048): high backlog for WebDAV connection bursts - Eliminate redundant create_dir_all calls from upload hot path
This commit is contained in:
Generated
+1
@@ -1847,6 +1847,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
"socket2",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ infer = "0.19"
|
|||||||
async-compression = { version = "0.4", features = ["tokio", "gzip"] }
|
async-compression = { version = "0.4", features = ["tokio", "gzip"] }
|
||||||
async_zip = { version = "0.0.18", features = ["tokio", "deflate"] }
|
async_zip = { version = "0.0.18", features = ["tokio", "deflate"] }
|
||||||
dashmap = "6"
|
dashmap = "6"
|
||||||
|
socket2 = { version = "0.6.2", features = ["all"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|||||||
@@ -210,15 +210,7 @@ impl DedupService {
|
|||||||
// same filesystem; if it fails because the other writer won,
|
// same filesystem; if it fails because the other writer won,
|
||||||
// we just discard our temp file — the blob is already there.
|
// we just discard our temp file — the blob is already there.
|
||||||
if !blob_path.exists() {
|
if !blob_path.exists() {
|
||||||
if let Some(parent) = blob_path.parent() {
|
// Parent directory (xx/) guaranteed to exist — created by initialize()
|
||||||
fs::create_dir_all(parent).await.map_err(|e| {
|
|
||||||
DomainError::internal_error(
|
|
||||||
"Dedup",
|
|
||||||
format!("Failed to create blob directory: {}", e),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let temp_path = self.temp_root.join(format!("{}.tmp", uuid::Uuid::new_v4()));
|
let temp_path = self.temp_root.join(format!("{}.tmp", uuid::Uuid::new_v4()));
|
||||||
fs::write(&temp_path, content).await.map_err(|e| {
|
fs::write(&temp_path, content).await.map_err(|e| {
|
||||||
DomainError::internal_error("Dedup", format!("Failed to write temp blob: {}", e))
|
DomainError::internal_error("Dedup", format!("Failed to write temp blob: {}", e))
|
||||||
@@ -308,14 +300,7 @@ impl DedupService {
|
|||||||
// Blob already on disk — discard the source file
|
// Blob already on disk — discard the source file
|
||||||
let _ = fs::remove_file(source_path).await;
|
let _ = fs::remove_file(source_path).await;
|
||||||
} else {
|
} else {
|
||||||
if let Some(parent) = blob_path.parent() {
|
// Parent directory (xx/) guaranteed to exist — created by initialize()
|
||||||
fs::create_dir_all(parent).await.map_err(|e| {
|
|
||||||
DomainError::internal_error(
|
|
||||||
"Dedup",
|
|
||||||
format!("Failed to create blob directory: {}", e),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// rename is atomic on the same filesystem. If source and blob
|
// rename is atomic on the same filesystem. If source and blob
|
||||||
// dirs live on different filesystems (rare), this falls back to
|
// dirs live on different filesystems (rare), this falls back to
|
||||||
|
|||||||
@@ -119,8 +119,8 @@ impl FileHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Spool multipart field to temp file + hash-on-write ──
|
// ── Spool multipart field to temp file + hash-on-write ──
|
||||||
|
// .dedup_temp is created once by DedupService::initialize() at startup
|
||||||
let temp_dir = state.core.path_service.get_root_path().join(".dedup_temp");
|
let temp_dir = state.core.path_service.get_root_path().join(".dedup_temp");
|
||||||
let _ = tokio::fs::create_dir_all(&temp_dir).await;
|
|
||||||
let temp_path = temp_dir.join(format!("upload-{}", uuid::Uuid::new_v4()));
|
let temp_path = temp_dir.join(format!("upload-{}", uuid::Uuid::new_v4()));
|
||||||
|
|
||||||
let mut total_size: u64 = 0;
|
let mut total_size: u64 = 0;
|
||||||
|
|||||||
+27
-2
@@ -4,6 +4,9 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use socket2::{Domain, Protocol, Socket, TcpKeepalive, Type};
|
||||||
|
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use axum::extract::DefaultBodyLimit;
|
use axum::extract::DefaultBodyLimit;
|
||||||
@@ -269,15 +272,37 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// Without this Axum caps Multipart bodies at 2 MB.
|
// Without this Axum caps Multipart bodies at 2 MB.
|
||||||
app = app.layer(DefaultBodyLimit::max(10 * 1024 * 1024 * 1024));
|
app = app.layer(DefaultBodyLimit::max(10 * 1024 * 1024 * 1024));
|
||||||
|
|
||||||
// Start server
|
// Start server — tuned socket for low-latency responses
|
||||||
let addr = SocketAddr::from(([0, 0, 0, 0], 8086));
|
let addr = SocketAddr::from(([0, 0, 0, 0], 8086));
|
||||||
tracing::info!("Starting OxiCloud server on http://{}", addr);
|
tracing::info!("Starting OxiCloud server on http://{}", addr);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP))?;
|
||||||
|
socket.set_reuse_address(true)?;
|
||||||
|
// Allow multiple workers on the same port (future-ready)
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
socket.set_reuse_port(true)?;
|
||||||
|
// Disable Nagle's algorithm — send small responses (JSON, PROPFIND)
|
||||||
|
// immediately instead of waiting up to 40ms for coalescing.
|
||||||
|
socket.set_tcp_nodelay(true)?;
|
||||||
|
// Detect dead connections within 60s instead of hours
|
||||||
|
socket.set_keepalive(true)?;
|
||||||
|
socket.set_tcp_keepalive(
|
||||||
|
&TcpKeepalive::new()
|
||||||
|
.with_time(Duration::from_secs(60))
|
||||||
|
.with_interval(Duration::from_secs(10)),
|
||||||
|
)?;
|
||||||
|
socket.set_nonblocking(true)?;
|
||||||
|
socket.bind(&addr.into())?;
|
||||||
|
// High backlog for connection bursts (WebDAV clients open many parallel connections)
|
||||||
|
socket.listen(2048)?;
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::from_std(socket.into())?;
|
||||||
|
|
||||||
// Provide the fully-built state to the router
|
// Provide the fully-built state to the router
|
||||||
let app = app.with_state(app_state);
|
let app = app.with_state(app_state);
|
||||||
|
|
||||||
|
// TCP_NODELAY is inherited from the listening socket on Linux,
|
||||||
|
// so every accepted connection already has Nagle disabled.
|
||||||
axum::serve(listener, app).await?;
|
axum::serve(listener, app).await?;
|
||||||
tracing::info!("Server shutdown completed");
|
tracing::info!("Server shutdown completed");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user