Skip to the content.

Enhancements and Future Improvements

In this section, we’ll explore potential enhancements to make the distributed rendering system production-ready. These improvements focus on performance, scalability, reliability, and operational excellence.

🎯 What You'll Learn

Overview

While the current implementation works for learning purposes, production deployments require additional considerations:

graph TB
    subgraph "Current System"
        A1[Simple HTTP APIs]
        A2[In-memory job storage]
        A3[Direct HTTP calls]
        A4[Manual scaling]
    end
    
    subgraph "Production System"
        B1[Rate limiting & validation]
        B2[Persistent storage]
        B3[Message queues]
        B4[Auto-scaling]
        B5[Monitoring & alerts]
        B6[Service discovery]
    end
    
    A1 -.->|Add| B1
    A2 -.->|Upgrade| B2
    A3 -.->|Replace| B3
    A4 -.->|Implement| B4
    
    style A1 fill:#ffebee
    style A2 fill:#ffebee
    style A3 fill:#ffebee
    style A4 fill:#ffebee
    style B1 fill:#e8f5e9
    style B2 fill:#e8f5e9
    style B3 fill:#e8f5e9
    style B4 fill:#e8f5e9
    style B5 fill:#e8f5e9
    style B6 fill:#e8f5e9

1. Rate Limiting (429 Too Many Requests)

Prevent server overload by limiting the number of requests.

Implementation with express-rate-limit

const rateLimit = require('express-rate-limit');

// Configure rate limiter
const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // Limit each IP to 100 requests per windowMs
    message: 'Too many requests, please try again later.',
    standardHeaders: true, // Return rate limit info in headers
    legacyHeaders: false,
});

// Apply to all routes
app.use('/job', limiter);

Response Headers

When rate limited, clients receive:

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1634567890
Retry-After: 60

Too many requests, please try again later.

📚 RFC 6585 - Additional HTTP Status Codes

2. Single Job Processing Per Server

Ensure each server processes only one job at a time to prevent resource exhaustion.

Server Implementation

let currentJob = null;

app.post('/job', (req, res) => {
    // Check if server is busy
    if (currentJob !== null) {
        return res.status(503)
            .header('Retry-After', 30)
            .send('Server busy, try again later');
    }
    
    const { from, to } = req.body;
    const jobProcess = exec(blenderCommand);
    
    currentJob = {
        pid: jobProcess.pid,
        process: jobProcess
    };
    
    jobProcess.on('close', () => {
        currentJob = null; // Mark server as available
    });
    
    res.status(202)
        .header('Location', `/job/${jobProcess.pid}`)
        .send({ pid: jobProcess.pid });
});

Orchestrator Job Queue

const queue = [];
let processing = false;

async function processQueue() {
    if (processing || queue.length === 0) return;
    
    processing = true;
    const job = queue.shift();
    
    try {
        await distributeJob(job);
    } catch (error) {
        // Retry logic
        if (error.response?.status === 503) {
            queue.push(job); // Re-queue
        }
    }
    
    processing = false;
    processQueue(); // Process next job
}

app.post('/render', (req, res) => {
    const job = { from: req.body.from, to: req.body.to };
    queue.push(job);
    processQueue();
    res.status(202).send({ status: 'queued' });
});

3. Production Deployment (Kubernetes/AKS)

Deploy to Azure Kubernetes Service for production-grade infrastructure.

AKS Architecture

graph TB
    subgraph "Azure Kubernetes Service"
        subgraph "Ingress"
            Ingress[Ingress Controller<br/>Load Balancer]
        end
        
        subgraph "Orchestrator Pods"
            Orch1[orchestrator-1]
            Orch2[orchestrator-2]
        end
        
        subgraph "Blender Server Pods"
            Server1[blender-server-1]
            Server2[blender-server-2]
            Server3[blender-server-3]
            ServerN[blender-server-n]
        end
        
        subgraph "Storage"
            PV[Persistent Volume<br/>Azure Files]
        end
    end
    
    Client[Client] -->|HTTPS| Ingress
    Ingress --> Orch1
    Ingress --> Orch2
    
    Orch1 --> Server1
    Orch1 --> Server2
    Orch2 --> Server3
    Orch2 --> ServerN
    
    Server1 -.-> PV
    Server2 -.-> PV
    Server3 -.-> PV
    ServerN -.-> PV
    
    style Client fill:#e1f5ff
    style Ingress fill:#fff4e1
    style Orch1 fill:#fff4e1
    style Orch2 fill:#fff4e1
    style Server1 fill:#e8f5e9
    style Server2 fill:#e8f5e9
    style Server3 fill:#e8f5e9
    style ServerN fill:#e8f5e9
    style PV fill:#f3e5f5

Kubernetes Deployment Manifest

apiVersion: apps/v1
kind: Deployment
metadata:
  name: blender-server
spec:
  replicas: 3
  selector:
    matchLabels:
      app: blender-server
  template:
    metadata:
      labels:
        app: blender-server
    spec:
      containers:
      - name: blender-server
        image: yourregistry.azurecr.io/blender-server:latest
        resources:
          requests:
            memory: "2Gi"
            cpu: "1000m"
          limits:
            memory: "4Gi"
            cpu: "2000m"
        ports:
        - containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: blender-server
spec:
  selector:
    app: blender-server
  ports:
  - port: 3000
    targetPort: 3000

Key Features

Cluster Autoscaler: Automatically scale nodes based on demand

az aks update \
  --resource-group myResourceGroup \
  --name myAKSCluster \
  --enable-cluster-autoscaler \
  --min-count 1 \
  --max-count 10

Horizontal Pod Autoscaler: Scale pods based on CPU/memory

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: blender-server-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: blender-server
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

📚 Azure Kubernetes Service Documentation

4. Service Discovery

Automatically detect available rendering nodes.

Kubernetes Service Discovery

Kubernetes provides built-in service discovery through DNS:

// Orchestrator code
const NODES = process.env.BLENDER_SERVICE_URL || 'http://blender-server:3000';

// Kubernetes creates DNS entries automatically
// Service name 'blender-server' resolves to all pod IPs

Using Consul (Alternative)

const consul = require('consul')();

// Register service
await consul.agent.service.register({
    name: 'blender-server',
    address: 'localhost',
    port: 3000,
    check: {
        http: 'http://localhost:3000/health',
        interval: '10s'
    }
});

// Discover services
const services = await consul.health.service('blender-server');
const NODES = services.map(s => `http://${s.Service.Address}:${s.Service.Port}`);

📚 Kubernetes Service Discovery

5. Message Queues

Decouple orchestrator and servers for better reliability.

Architecture with RabbitMQ

graph LR
    Client[Client]
    Orch[Orchestrator]
    Queue[(RabbitMQ<br/>Job Queue)]
    Server1[Server 1]
    Server2[Server 2]
    Server3[Server 3]
    
    Client -->|Submit Job| Orch
    Orch -->|Enqueue Batch| Queue
    Queue -->|Consume| Server1
    Queue -->|Consume| Server2
    Queue -->|Consume| Server3
    
    Server1 -.->|Result| Queue
    Server2 -.->|Result| Queue
    Server3 -.->|Result| Queue
    Queue -.->|Status| Orch
    
    style Client fill:#e1f5ff
    style Orch fill:#fff4e1
    style Queue fill:#ffe0b2
    style Server1 fill:#e8f5e9
    style Server2 fill:#e8f5e9
    style Server3 fill:#e8f5e9

Orchestrator with RabbitMQ

const amqp = require('amqplib');

// Connect to RabbitMQ
const connection = await amqp.connect('amqp://localhost');
const channel = await connection.createChannel();
await channel.assertQueue('render-jobs', { durable: true });

// Enqueue job
app.post('/render', async (req, res) => {
    const job = { from: req.body.from, to: req.body.to };
    const batches = splitFramesIntoChunks(job.from, job.to, BATCH_SIZE);
    
    for (const batch of batches) {
        channel.sendToQueue('render-jobs', 
            Buffer.from(JSON.stringify(batch)),
            { persistent: true }
        );
    }
    
    res.status(202).send({ status: 'queued' });
});

Server as Queue Consumer

const amqp = require('amqplib');

const connection = await amqp.connect('amqp://localhost');
const channel = await connection.createChannel();
await channel.assertQueue('render-jobs', { durable: true });

// Consume one job at a time
channel.prefetch(1);

channel.consume('render-jobs', async (msg) => {
    const job = JSON.parse(msg.content.toString());
    
    try {
        await renderFrames(job.from, job.to);
        channel.ack(msg); // Mark as processed
    } catch (error) {
        channel.nack(msg, false, true); // Requeue on failure
    }
}, { noAck: false });

Benefits:

📚 RabbitMQ Tutorials

6. Monitoring and Logging

Implement comprehensive observability.

Structured Logging

const winston = require('winston');

const logger = winston.createLogger({
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
    ),
    transports: [
        new winston.transports.Console(),
        new winston.transports.File({ filename: 'app.log' })
    ]
});

// Usage
logger.info('Job started', { 
    jobId: '123', 
    frames: { from: 1, to: 5 },
    node: 'server-1'
});

Distributed Tracing

const opentelemetry = require('@opentelemetry/api');
const tracer = opentelemetry.trace.getTracer('blender-app');

app.post('/render', async (req, res) => {
    const span = tracer.startSpan('render-job');
    
    try {
        await distributeJob(req.body);
        span.setStatus({ code: opentelemetry.SpanStatusCode.OK });
    } catch (error) {
        span.setStatus({ 
            code: opentelemetry.SpanStatusCode.ERROR,
            message: error.message 
        });
    } finally {
        span.end();
    }
});

Prometheus Metrics

const prometheus = require('prom-client');

const jobsTotal = new prometheus.Counter({
    name: 'jobs_total',
    help: 'Total number of render jobs',
    labelNames: ['status']
});

const jobDuration = new prometheus.Histogram({
    name: 'job_duration_seconds',
    help: 'Job duration in seconds',
    buckets: [1, 5, 15, 30, 60, 120]
});

// Track metrics
jobsTotal.inc({ status: 'started' });
const timer = jobDuration.startTimer();
// ... process job ...
timer();
📚 ELK Stack Azure Monitor

7. Additional Improvements

Load Balancing

Distribute requests evenly across server instances.

Kubernetes Service provides automatic load balancing:

apiVersion: v1
kind: Service
metadata:
  name: blender-server
spec:
  type: LoadBalancer
  selector:
    app: blender-server
  ports:
  - port: 3000

Fault Tolerance

Handle failures gracefully with retry logic and circuit breakers.

const CircuitBreaker = require('opossum');

const breaker = new CircuitBreaker(assignJobToNode, {
    timeout: 30000, // 30 seconds
    errorThresholdPercentage: 50,
    resetTimeout: 60000 // 1 minute
});

breaker.fallback(() => ({ error: 'Service temporarily unavailable' }));

Security Enhancements

Performance Optimization

Conclusion

This tutorial has taken you from a simple rendering node to a production-ready distributed system. You’ve learned:

Next Steps



🎉 Congratulations! You’ve completed the distributed systems tutorial!

Want to contribute? See our Contributing Guide

Having issues? Open an issue on GitHub