Scaling Guide
This guide covers strategies for scaling Armature applications to handle increased load.
Table of Contents
- Overview
- Horizontal Scaling
- Vertical Scaling
- Load Balancing
- Database Scaling
- Caching Strategies
- Async Processing
- Performance Optimization
- Monitoring for Scale
Overview
Armature applications can scale to handle millions of requests by:
- Horizontal Scaling - Adding more instances
- Vertical Scaling - Increasing instance resources
- Load Balancing - Distributing traffic evenly
- Caching - Reducing database load
- Async Processing - Offloading heavy operations
Horizontal Scaling
Stateless Design
Armature encourages stateless architecture for easy horizontal scaling:
// ❌ Bad: Server-side session storage
struct UserController {
sessions: HashMap<String, Session>, // Can't scale!
}
// ✅ Good: JWT-based stateless auth
struct UserController {
jwt_manager: JwtManager, // Stateless, can scale!
}
Using Ferron for Load Balancing
use armature_ferron::{FerronConfig, LoadBalancer, LoadBalanceStrategy, Backend};
let config = FerronConfig::builder()
.domain("api.example.com")
.load_balancer(
LoadBalancer::new()
.strategy(LoadBalanceStrategy::RoundRobin)
.backend(Backend::new("http://app1:3000"))
.backend(Backend::new("http://app2:3000"))
.backend(Backend::new("http://app3:3000"))
.backend(Backend::new("http://app4:3000"))
.health_check_interval(10)
.health_check_path("/health")
)
.build()?;
Service Discovery
Dynamic backend registration for auto-scaling:
use armature_ferron::ServiceRegistry;
let registry = ServiceRegistry::new();
// Register new instances as they come online
let id = registry.register("api-service", "http://new-instance:3000").await?;
// Deregister when scaling down
registry.deregister("api-service", &id).await?;
// Get current backends for load balancer
let backends = registry.get_healthy_urls("api-service").await;
Kubernetes HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: armature-api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: armature-api
minReplicas: 3
maxReplicas: 50
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
Vertical Scaling
Rust Performance Advantages
Armature benefits from Rust's efficiency:
- Low memory footprint - No garbage collector overhead
- High CPU utilization - Zero-cost abstractions
- Predictable latency - No GC pauses
Optimal Resource Allocation
# Kubernetes resource limits
resources:
limits:
cpu: "4"
memory: "2Gi"
requests:
cpu: "1"
memory: "512Mi"
Tokio Runtime Tuning
#[tokio::main(flavor = "multi_thread", worker_threads = 8)]
async fn main() {
// Configure based on CPU cores
}
Load Balancing
Load Balancing Strategies
use armature_ferron::LoadBalanceStrategy;
// Round Robin - Equal distribution
LoadBalanceStrategy::RoundRobin
// Least Connections - Route to least busy
LoadBalanceStrategy::LeastConnections
// IP Hash - Sticky sessions without state
LoadBalanceStrategy::IpHash
// Weighted - Based on server capacity
LoadBalanceStrategy::Weighted
Weighted Load Balancing
let lb = LoadBalancer::new()
.strategy(LoadBalanceStrategy::Weighted)
.backend(Backend::new("http://big-server:3000").weight(10))
.backend(Backend::new("http://small-server:3000").weight(5));
Database Scaling
Connection Pooling
use sqlx::postgres::PgPoolOptions;
let pool = PgPoolOptions::new()
.max_connections(100) // Max connections per instance
.min_connections(10) // Keep 10 warm
.acquire_timeout(Duration::from_secs(5))
.idle_timeout(Duration::from_secs(600))
.connect(&database_url)
.await?;
Read Replicas
// Write to primary
let primary = PgPool::connect("postgres://primary:5432/db").await?;
// Read from replicas
let replicas = vec![
PgPool::connect("postgres://replica1:5432/db").await?,
PgPool::connect("postgres://replica2:5432/db").await?,
];
// Route queries appropriately
async fn get_user(id: u32, replicas: &[PgPool]) -> Result<User, Error> {
let replica = &replicas[id as usize % replicas.len()];
sqlx::query_as("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch_one(replica)
.await
}
Caching Strategies
Multi-Tier Caching
use armature_cache::{TieredCache, InMemoryCache, RedisCache};
let cache = TieredCache::new()
.l1(InMemoryCache::new(1000)) // Fast, local
.l2(RedisCache::new(redis_url)) // Shared, persistent
.build();
// Automatic fallthrough
let user = cache.get_or_set("user:123", || {
fetch_user_from_db(123)
}).await?;
Cache Invalidation
use armature_cache::TaggedCache;
let cache = TaggedCache::new(redis_url);
// Set with tags
cache.set_with_tags(
"user:123",
user,
&["users", "user:123"],
Duration::from_secs(3600)
).await?;
// Invalidate by tag
cache.invalidate_tag("users").await?; // Clears all user caches
Async Processing
Job Queues
Offload heavy operations to background workers:
use armature_queue::{Queue, Job};
// Enqueue job instead of processing synchronously
queue.enqueue("send_email", EmailJob {
to: user.email,
subject: "Welcome!",
template: "welcome",
}).await?;
// Return immediately to client
HttpResponse::accepted()
Worker Scaling
# Scale workers independently
apiVersion: apps/v1
kind: Deployment
metadata:
name: email-worker
spec:
replicas: 5 # Scale workers based on queue depth
Performance Optimization
Response Compression
use armature_framework::compression::CompressionConfig;
let config = CompressionConfig::default()
.gzip(true)
.brotli(true)
.min_size(1024); // Only compress > 1KB
Connection Keep-Alive
// Ferron handles this automatically
let config = FerronConfig::builder()
.keepalive(true)
.keepalive_timeout(60)
.build()?;
Request Batching
// GraphQL DataLoader pattern
#[routes]
impl UserController {
#[post("/users/batch")]
async fn batch_get(req: HttpRequest) -> Result<HttpResponse, Error> {
let ids: Vec<u32> = req.json()?;
// Single query for all users
let users = sqlx::query_as("SELECT * FROM users WHERE id = ANY($1)")
.bind(&ids)
.fetch_all(&pool)
.await?;
HttpResponse::json(&users)
}
}
Monitoring for Scale
Key Metrics
use armature_framework::metrics::{counter, histogram};
// Track request rate
counter!("http_requests_total", "endpoint" => path);
// Track latency
histogram!("http_request_duration_seconds", duration);
// Track errors
counter!("http_errors_total", "status" => status.to_string());
Alerting Thresholds
# Prometheus alerting rules
groups:
- name: scaling
rules:
- alert: HighRequestRate
expr: rate(http_requests_total[5m]) > 10000
for: 5m
labels:
severity: warning
annotations:
summary: "Consider scaling up"
- alert: HighLatency
expr: histogram_quantile(0.95, http_request_duration_seconds) > 1
for: 5m
labels:
severity: critical
Load Testing
# Using oha (Ohayou HTTP load generator)
oha -n 100000 -c 100 http://localhost:3000/api/users
# Using wrk
wrk -t12 -c400 -d30s http://localhost:3000/api/users
Summary
Scaling checklist:
- Design stateless - Enable horizontal scaling
- Use Ferron - High-performance load balancing
- Enable service discovery - Dynamic backend management
- Configure HPA - Automatic pod scaling
- Pool connections - Efficient database usage
- Implement caching - Reduce database load
- Use job queues - Offload heavy operations
- Monitor metrics - Data-driven scaling decisions