Table of Contents
- Understanding Performance Bottlenecks
- Real-World Tuning Examples
- General Best Practices for Linux Performance Tuning
- References
1. Understanding Performance Bottlenecks
Before diving into examples, it’s critical to recognize common performance bottlenecks. Linux server performance is typically constrained by one or more of the “big four” resources:
- CPU: High usage (e.g., >80% sustained) can cause slow response times, especially for CPU-bound workloads (e.g., video encoding, complex calculations).
- Memory (RAM): Insufficient memory leads to swapping (using disk as “virtual memory”), which drastically slows down the system (measured via
swapon -sorvmstat). - Disk I/O: Slow read/write speeds (e.g., high
%iowaitiniostat) bottleneck databases, file servers, or applications with heavy disk access. - Network: Limited bandwidth, high latency, or connection mismanagement (e.g., too many
TIME_WAITsockets) can cripple web servers or distributed systems.
Tuning starts with diagnosis: using tools like htop, iostat, netstat, or application-specific metrics (e.g., Nginx access logs) to identify which resource is constrained.
2. Real-World Tuning Examples
Let’s explore four common scenarios where Linux server performance was degraded—and how targeted tuning resolved the issues.
Example 1: Web Server (Nginx) – Fixing Latency During Traffic Spikes
Scenario
A small e-commerce website running on Nginx (serving static assets and proxying to a Node.js backend) began experiencing slow page loads (3–5 seconds) and intermittent 502/504 errors during peak traffic (e.g., sales events). The server had 4 CPU cores and 8GB RAM—resources that should have handled the load.
Problem Identification
- Symptoms: Slow page loads, high 5xx error rates (15% of requests), and user complaints.
- Tools Used:
top,netstat,nginx -V, and Nginx’s built-instub_statusmodule. - Data Collected:
topshowed Nginx worker processes consuming 90%+ CPU.netstat -nat | grep TIME_WAIT | wc -lrevealed 10,000+TIME_WAITconnections (idle TCP connections tying up resources).stub_statusshowed:Active connections: 5000,Requests per second: 200(low for the traffic volume).
Diagnosis
The bottlenecks were:
- Too many Nginx worker processes: Default Nginx config used
worker_processes auto;(4 workers, matching CPU cores), but each worker was overloaded with 1,250+ connections. - No keepalive connections: Each request opened a new TCP connection, leading to excessive
TIME_WAITsockets. - Missing caching: Static assets (images, CSS) were being re-fetched on every request, increasing backend load.
Tuning Steps
-
Optimize Nginx Worker Processes/Connections:
Nginx usesworker_processes(number of CPU cores) andworker_connections(max connections per worker). The formulaworker_processes * worker_connectionsshould exceed expected concurrent connections.
Updated/etc/nginx/nginx.conf:worker_processes auto; # Use all 4 CPU cores (no change here) events { worker_connections 1024; # Increased from default 1024 to 2048 (tested stability) multi_accept on; # Accept multiple connections at once } -
Enable Keepalive Connections:
Reduce TCP handshake overhead by reusing connections. Added to thehttpblock:keepalive_timeout 65; # Keep connections alive for 65s (default was 0) keepalive_requests 100; # Allow 100 requests per connection (default 100) -
Cache Static Assets:
Cache images, CSS, and JS to reduce backend requests. Added to theserverblock for static files:location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ { proxy_cache my_cache; # Define cache zone in http block proxy_cache_valid 200 302 1h; # Cache 200/302 responses for 1 hour proxy_cache_valid 404 1m; # Cache 404s for 1 minute expires 7d; # Add "Cache-Control: max-age=604800" header } # Define cache zone in http block: http { proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off; }
Results
- CPU Usage: Dropped from 90% to 40% during peak traffic.
- Response Time: Page load time decreased from 3–5s to 200–300ms.
- Error Rate: 5xx errors fell from 15% to 0.1%.
- Connections:
TIME_WAITsockets reduced from 10,000+ to <500.
Example 2: Database Server (MySQL) – Speeding Up Slow Queries & Connections
Scenario
A SaaS platform using MySQL 8.0 (on a server with 8 CPU cores and 32GB RAM) faced slow query responses (5–10 seconds for common user reports) and frequent “Too many connections” errors during business hours. The database had ~500k daily queries.
Problem Identification
- Symptoms: Slow report generation, “Too many connections” errors, and backend timeouts.
- Tools Used:
mysqltuner,show processlist;,slow_query_log, andpg_stat_statements(for query analytics). - Data Collected:
mysqltunerreport showed:innodb_buffer_pool_size = 1G(only 3% of available RAM).max_connections = 151(default, but 200+ concurrent connections were being requested).
slow_query_logrevealed 20+ queries withfull table scans(no indexes) taking 5–10s each.
Diagnosis
The bottlenecks were:
- Underprovisioned InnoDB Buffer Pool: MySQL was reading from disk instead of caching frequently accessed data in RAM.
- Insufficient Connections:
max_connectionswas too low, causing connection failures. - Unoptimized Queries: Full table scans on large tables (10M+ rows) hogged CPU and I/O.
Tuning Steps
-
Increase InnoDB Buffer Pool Size:
The InnoDB buffer pool caches table data and indexes. For a dedicated database server, allocate 50–70% of RAM to it. With 32GB RAM, we set:# In /etc/mysql/my.cnf: [mysqld] innodb_buffer_pool_size = 20G # 62.5% of 32GB RAM innodb_buffer_pool_instances = 4 # 1 instance per 4–8GB of buffer pool (avoids contention) -
Raise Connection Limits:
Increasemax_connectionsand add a connection pool (via PgBouncer) to reuse idle connections:[mysqld] max_connections = 500 # Higher than peak concurrent requests (300) wait_timeout = 60 # Close idle connections after 60s (default 28800s)Deployed PgBouncer to pool connections, reducing the number of new TCP connections to MySQL.
-
Optimize Slow Queries:
- Added indexes to columns used in
WHERE,JOIN, andORDER BYclauses (e.g.,CREATE INDEX idx_user_id ON orders(user_id);). - Rewrote a slow report query from:
To:SELECT * FROM orders WHERE created_at > '2023-01-01' AND total > 1000; # Full table scanSELECT id, user_id, total FROM orders WHERE created_at > '2023-01-01' AND total > 1000; # Uses covering index on (created_at, total)
- Added indexes to columns used in
Results
- Query Latency: Slow queries (5–10s) reduced to 100–300ms.
- Connections: “Too many connections” errors eliminated; PgBouncer reduced active MySQL connections from 300+ to 50–60.
- I/O Usage: Disk reads (measured via
iostat) dropped by 70% (thanks to the buffer pool).
Example 3: File Server (NFS) – Accelerating Large File Transfers
Scenario
A media production team used an NFS server (running on Ubuntu 22.04) to share large video files (10–50GB) between workstations. Transfers were slow (10–15 MB/s), despite a 1Gbps network. The server had a 4TB HDD (7200 RPM) and 16GB RAM.
Problem Identification
- Symptoms: Slow file transfers, long wait times for video exports, and workstation timeouts.
- Tools Used:
iostat,sar,nfsstat, andmount. - Data Collected:
iostat -x 5showed%iowait(disk I/O wait time) at 85% during transfers.mountrevealed the NFS export was mounted with default settings:rsize=4096,wsize=4096(small read/write blocks).sar -d 5showed disk read/write speeds of only 15–20 MB/s (well below HDD limits of ~100 MB/s).
Diagnosis
The bottlenecks were:
- Small NFS Block Sizes: Default
rsize/wsize(4KB) caused excessive round-trips between client and server. - Unoptimized Filesystem Mount: The underlying ext4 filesystem was mounted with
atime(updates access time on every read), adding unnecessary disk writes.
Tuning Steps
-
Increase NFS Block Sizes:
Updated the NFS export and client mounts to use larger block sizes (1MB) for better throughput:# On NFS server: Edit /etc/exports to set rsize/wsize: /media/share 192.168.1.0/24(rw,sync,rsize=1048576,wsize=1048576,no_subtree_check) # On NFS clients: Remount with new sizes: mount -o remount,rsize=1048576,wsize=1048576 192.168.1.100:/media/share /mnt/share -
Optimize Filesystem Mount:
Disabledatime(no need to track access times for media files) and enabledbarrier=0(reduces write overhead for non-critical data):# Edit /etc/fstab on the NFS server: UUID=abc123 /media/share ext4 defaults,noatime,barrier=0 0 2 # Remount the filesystem: mount -o remount /media/share -
Enable Read Caching:
Increased Linux’svm.dirty_background_ratioto allow more data to be cached in RAM before writing to disk:# Edit /etc/sysctl.conf: vm.dirty_background_ratio = 10 # Start writing cached data to disk when 10% of RAM is dirty vm.dirty_ratio = 20 # Force write when 20% of RAM is dirty # Apply changes: sysctl -p
Results
- Transfer Speed: Increased from 10–15 MB/s to 80–90 MB/s (near the 1Gbps network limit).
- %iowait: Dropped from 85% to 15% during transfers.
- User Productivity: Video exports that took 2 hours now completed in 20–30 minutes.
Example 4: Containerized Environment (Docker/Kubernetes) – Resolving Resource Contention
Scenario
A DevOps team ran 50+ Docker containers (microservices, databases, and monitoring tools) on a single Kubernetes node (8 CPU cores, 32GB RAM). Containers frequently froze, and dmesg showed Out Of Memory (OOM) kills.
Problem Identification
- Symptoms: Container crashes, slow response from microservices, and
kubectl top podsshowed some pods using 200%+ of their requested CPU. - Tools Used:
docker stats,kubectl top pods,dmesg, andcAdvisor. - Data Collected:
docker statsrevealed 3 containers (a Redis instance, a log processor, and a CI runner) using 8–10GB RAM each (no limits set).kubectl top podsshowed CPU throttling (pods hittingcpu.cfs.throttled_seconds).dmesg | grep -i oomconfirmed OOM kills for the log processor pod.
Diagnosis
The bottlenecks were:
- Unlimited Resources: Containers had no
--memoryor--cpulimits, leading to resource hoarding. - Poor CPU/Memory Allocation: Critical services (e.g., Redis) competed with non-critical ones (e.g., CI runners) for resources.
- Inefficient Docker Storage Driver: The server used
devicemapper(slow for containers);overlay2is recommended for better performance.
Tuning Steps
-
Set Resource Limits:
Defined CPU/memory limits in Docker Compose/Kubernetes manifests to prevent hoarding:# Kubernetes Pod example (log processor): apiVersion: v1 kind: Pod metadata: name: log-processor spec: containers: - name: log-processor image: my-log-processor:latest resources: requests: cpu: 100m # 0.1 CPU cores memory: 512Mi limits: cpu: 1000m # 1 CPU core memory: 2Gi # Hard limit of 2GB RAM -
Prioritize Critical Services:
Assigned higher CPU shares to critical pods (e.g., Redis) to ensure they get resources during contention:# Kubernetes Pod for Redis: resources: limits: cpu: 2000m memory: 4Gi requests: cpu: 1000m memory: 2Gi -
Switch to Overlay2 Storage Driver:
Reconfigured Docker to useoverlay2(faster, more efficient thandevicemapper):# Stop Docker: systemctl stop docker # Edit /etc/docker/daemon.json: { "storage-driver": "overlay2" } # Start Docker and verify: systemctl start docker docker info | grep "Storage Driver" # Should show "overlay2"
Results
- OOM Kills: Eliminated entirely (no more
dmesgOOM entries). - CPU Throttling: Reduced by 90% (critical pods now get priority).
- Container Start Time: Decreased from 30–60s to 5–10s (thanks to
overlay2).
3. General Best Practices for Linux Performance Tuning
Beyond the examples above, these practices will help you avoid bottlenecks and maintain performance:
- Monitor Continuously: Use tools like Prometheus+Grafana, Nagios, or
sarto track baseline metrics (CPU, memory, I/O) and detect anomalies early. - Test Incrementally: Change one setting at a time, then measure impact. This avoids breaking things and isolates what works.
- Document Changes: Log every tuning step, config file, and result (e.g., “Increased Nginx worker_connections to 2048; CPU usage dropped by 50%”).
- Leverage Automation: Use Ansible/Chef to apply tuning consistently across servers, and tools like
tuned-adm(RHEL) to apply prebuilt profiles (e.g.,throughput-performance).
4. References
- Tools:
mysqltuner: https://github.com/major/MySQLTuner-perlhtop: https://htop.dev/iostat(part ofsysstat): https://github.com/sysstat/sysstat
- Official Documentation:
- Nginx Tuning: https://nginx.org/en/docs/ngx_core_module.html
- MySQL InnoDB Buffer Pool: https://dev.mysql.com/doc/refman/8.0/en/innodb-buffer-pool.html
- Docker Resource Limits: https://docs.docker.com/config/containers/resource_constraints/
- Guides:
- Linux Performance Tuning Guide: https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/performance_tuning_guide/
By applying these examples and best practices, you can transform underperforming Linux servers into efficient, scalable systems that handle real-world workloads with ease.