Console Login

Serverless Without the Lock-in: Building Event-Driven Architectures on KVM in 2016

Serverless Without the Lock-in: Building Event-Driven Architectures on KVM

Everyone is talking about "Serverless" right now. If you believe the marketing from Seattle, we're all supposed to delete our SSH keys, upload zip files to AWS Lambda, and forget how operating systems work. They promise infinite scalability and zero management.

I've been in ops long enough to know that "zero management" usually just means "zero control."

Here is the reality for those of us deploying systems in Norway: AWS eu-central-1 (Frankfurt) is the closest region. The latency is decent, but it's not local. Furthermore, with the recent invalidation of Safe Harbor and the brand new Privacy Shield agreement, trusting US-owned cloud giants with Norwegian user data is becoming a legal minefield. Datatilsynet isn't known for being lenient.

So, how do we get the benefits of Serverless—event-driven code, decoupling, and high efficiency—without the cold starts, the 5-minute execution limits, and the vendor lock-in? We build it ourselves on high-performance KVM infrastructure.

The "Worker Pattern": Serverless on Your Terms

At its core, Serverless is just an event loop. An event triggers a function. Instead of renting that event loop, you can build a robust one using RabbitMQ and Docker. This gives you sub-millisecond control over the environment.

We recently migrated a heavy image-processing workload off a public cloud FaaS provider. The costs were spiraling, and the cold starts were killing the user experience. By moving to a self-hosted worker queue on NVMe-backed VPS instances, we cut costs by 60%.

The Architecture

The setup is simple but battle-tested:

  • Ingest: Nginx accepting API requests.
  • Queue: RabbitMQ (AMQP) holding tasks.
  • Compute: Stateless Docker containers consuming tasks.

With Docker 1.12 released just this month, we now have Swarm Mode built-in, making orchestration of these workers trivial without needing the complexity of early Kubernetes setups.

Configuration: Optimizing the Queue

First, don't run RabbitMQ with default settings if you value your data. We need to ensure message durability without sacrificing I/O speed. This is where the underlying storage matters. On CoolVDS, we have direct access to NVMe storage, which prevents the message queue from becoming the bottleneck.

Here is a production-ready snippet for /etc/rabbitmq/rabbitmq.config optimized for a 4-core, 8GB RAM instance:

[
  {rabbit, [
    {vm_memory_high_watermark, 0.7},
    {disk_free_limit, {mem_relative, 1.0}},
    {hipe_compile, true},
    {tcp_listeners, [5672]},
    {default_vhost, <<"/">>},
    {default_user, <<"admin">>}
  ]}
].

Pro Tip: Enabling hipe_compile can boost performance by 20-50%, but it can cause issues on older Erlang versions. Ensure you are running Erlang R16B03 or newer.

The Code: Building an Async Worker

Let's look at the "function" part of our serverless setup. We'll use Python 2.7 (though Python 3.5 is gaining ground) and pika to consume messages. Unlike Lambda, this worker is persistent, meaning no cold start penalty.

import pika
import time
import json
import os

# Environment variables for flexibility (12-factor app style)
RABBIT_HOST = os.getenv('RABBIT_HOST', 'localhost')
QUEUE_NAME = os.getenv('QUEUE_NAME', 'task_queue')

def callback(ch, method, properties, body):
    payload = json.loads(body)
    print(" [x] Received %r" % payload)
    
    # Simulate heavy processing (e.g., resizing images)
    time.sleep(payload.get('duration', 1))
    
    print(" [x] Done")
    # Acknowledge completion manually to prevent data loss
    ch.basic_ack(delivery_tag=method.delivery_tag)

connection = pika.BlockingConnection(pika.ConnectionParameters(host=RABBIT_HOST))
channel = connection.channel()

# Durable queue to survive container restarts
channel.queue_declare(queue=QUEUE_NAME, durable=True)

# Fair dispatch: don't give a worker more than 1 task at a time
channel.basic_qos(prefetch_count=1)

channel.basic_consume(callback, queue=QUEUE_NAME)

print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()

This script runs inside a lightweight Docker container. Because it maintains a persistent connection to RabbitMQ, latency is effectively zero.

Orchestration with Docker Compose (v2)

To tie this together, we use Docker Compose. This allows us to spin up the entire stack on a single CoolVDS instance for testing, or scale it across a Swarm.

version: '2'
services:
  rabbitmq:
    image: rabbitmq:3-management
    ports:
      - "15672:15672"
      - "5672:5672"
    environment:
      RABBITMQ_DEFAULT_USER: admin
      RABBITMQ_DEFAULT_PASS: securepassword123
    ulimits:
      nofile:
        soft: 65536
        hard: 65536

  worker:
    build: .
    environment:
      RABBIT_HOST: rabbitmq
    depends_on:
      - rabbitmq
    restart: always
    # Scale this service to increase throughput
    # command: python worker.py

Scale Tip: With Docker Compose, you can simply run docker-compose scale worker=5 to instantly spin up 4 more consumers. If you are running on a CoolVDS High Frequency Compute plan, the CPU context switching overhead is negligible compared to shared hosting environments.

Why KVM Beats Public Cloud Functions

When you use a managed FaaS provider, you are running on shared infrastructure with "noisy neighbors." If another tenant on the physical host spikes their usage, your function might hang. This is unacceptable for real-time applications.

CoolVDS uses KVM (Kernel-based Virtual Machine) virtualization. This provides true hardware isolation. Your RAM is yours. Your CPU cycles are reserved. When you are processing sensitive Norwegian financial data or health records, this isolation is not just a performance feature; it's a compliance necessity.

Feature Public Cloud FaaS CoolVDS KVM Worker
Execution Time Limit 5 Minutes (Hard limit) Unlimited
Cold Start Latency 100ms - 2s 0ms (Persistent)
Data Location Frankfurt/Ireland Oslo (Low Latency)
Storage Speed Network Attached Local NVMe

Handling the Load Balancer

To feed the queue, you need a robust frontend. Nginx is the industry standard. Here is a snippet to ensure you don't drop connections during high load bursts before they hit the queue.

worker_processes auto;
events {
    worker_connections 4096;
    use epoll;
}

http {
    upstream backend_api {
        server 127.0.0.1:8000;
    }

    server {
        listen 80;
        server_name api.yourdomain.no;

        location / {
            proxy_pass http://backend_api;
            proxy_set_header X-Real-IP $remote_addr;
            
            # Buffer settings for large payloads
            client_body_buffer_size 128k;
            client_max_body_size 10m;
        }
    }
}

Conclusion

Serverless is a pattern, not a product. It's about event-driven architecture, not about billing per millisecond. By building your own worker clusters using Docker and RabbitMQ on CoolVDS, you gain total control over the timeout limits, memory allocation, and most importantly, the data sovereignty.

Don't let latency to Frankfurt kill your app's responsiveness. Build it local, build it robust.

Ready to deploy your worker cluster? Spin up a KVM instance with NVMe storage on CoolVDS today and see what real I/O performance feels like.