Architecting for Velocity: Unlocking Peak Performance with RoadRunner's rr.yaml Supervisor
Welcome back, engineers! Yesterday, we laid the groundwork for our High-Scale PHP CMS, recognizing the monumental shift from traditional shared-nothing PHP to a persistent runtime model. We saw how this paradigm allows PHP to finally shed its "bootload tax" – that constant, repetitive overhead of bootstrapping the entire application on every single request.
Today, we're diving deeper into the beating heart of this new architecture: RoadRunner's rr.yaml configuration file, specifically focusing on its role as a process supervisor. This isn't just a config file; it's the operational blueprint, the manifest that dictates how your PHP application workers will live, breathe, and perform under immense pressure. Understanding this file is the difference between a high-performance system and one that crumbles under load.
Agenda for Day 2:
Core Concepts: The Supervisor Pattern,
rr.yamlas an Orchestration Manifest, Worker Lifecycle Management.Architectural Fit: How
rr.yamlconfigures the RoadRunner server within our overall CMS.Control & Data Flow: Tracing a request through a supervised PHP worker.
Real-world Insights: Sizing and tuning for 100M+ requests per second.
Hands-on Implementation: Setting up a basic RoadRunner-managed PHP application.
Assignment & Solution: Practical exercises to solidify your understanding.
Core Concepts: The Unseen Hand of the Supervisor
At its essence, a supervisor is a guardian process that monitors and manages other processes. Think of it as a benevolent overseer ensuring that critical components of your system remain healthy and operational. In the context of RoadRunner and high-scale PHP, this concept is nothing short of revolutionary.
Why is a Supervisor Critical for High-Scale PHP?
Traditional PHP, with its "shared-nothing" architecture, was inherently resilient to memory leaks or stale state because every request started fresh. But this freshness came at a cost: the bootload tax. RoadRunner eliminates this by keeping PHP workers alive and ready to serve multiple requests – a "shared-everything" or "persistent runtime" model.
This persistence, while a performance boon, introduces new challenges:
Memory Leaks: Long-running processes are susceptible to gradual memory accumulation, leading to performance degradation and eventual crashes.
Stale State: Application-level caches or global variables might become outdated if not properly managed across requests.
Resource Exhaustion: A single runaway worker could consume excessive CPU or memory, impacting the entire pool.
The RoadRunner supervisor, configured via rr.yaml, is the elegant solution to these problems. It actively monitors your PHP workers and, crucially, recycles them gracefully before they become problematic.
rr.yaml: Your Application's Operational Manifest
The rr.yaml file is where you declare your intentions for RoadRunner. While it covers various services like http, rpc, jobs, our focus today is on the php section – this is where the supervisor magic happens.
Worker Lifecycle Management: The Art of Graceful Recycling
The num_workers, max_requests, max_memory, and max_lifetime parameters are your most potent weapons in the fight for stability and performance.
num_workers: Directly impacts concurrency. On a multi-core server, you'd typically set this toCPU_CORES * N, whereNis 1 or 2, depending on whether your application is CPU-bound or I/O-bound. Too many workers can lead to context switching overhead; too few, and you're leaving performance on the table.max_requests: This is the golden knob. By setting a reasonablemax_requests(e.g., 500-1000), you instruct RoadRunner to gracefully shut down and replace a worker after it has processed that many requests. This proactively mitigates memory leaks and ensures your workers are always starting from a relatively "fresh" state, without incurring the full bootload tax. The new worker spins up in the background, minimizing downtime.max_memory: A hard limit. If a worker exceeds this, it's restarted. Essential for catching unexpected memory spikes.max_lifetime: A time-based fallback. Even ifmax_requestsisn't hit, workers will eventually be recycled.
Component Architecture: rr.yaml in the Grand Scheme
In our High-Scale PHP CMS, rr.yaml defines how the RoadRunner server interacts with your PHP application. It's not a component itself, but rather the configuration for the central component: the RoadRunner server.
The RoadRunner server acts as a reverse proxy and process manager. It listens for HTTP requests, then dispatches them to an available PHP worker from its pool. The rr.yaml defines the size of this pool, the lifecycle rules for each worker, and the environment they operate within.
Control Flow and Data Flow
Request Ingress: An HTTP request hits our system (potentially via a load balancer).
RoadRunner Interception: The RoadRunner server, configured by
rr.yaml, receives the request.Worker Selection: RoadRunner selects an
IdlePHP worker from its pool.Data Ingestion: The HTTP request details (headers, body, method, path) are passed via
STDINto the selected PHP worker.PHP Application Execution: Your
src/public/index.php(or your framework's entry point) processes the request, interacting with databases, caches, etc.Response Egress: The PHP application writes its HTTP response (headers, body) to
STDOUT.RoadRunner Relay: RoadRunner captures the
STDOUTand sends the HTTP response back to the client.Supervisor Check: After each request, the supervisor checks if the worker has hit
max_requests,max_memory, ormax_lifetime. If so, it marks the worker for graceful shutdown and replacement, ensuring a fresh worker is always ready.
Sizing for 100 Million Requests Per Second: The Nuance of max_requests
Handling 100M RPS is not just about raw power; it's about surgical precision in resource management. For such extreme scale, max_requests is your most critical operational lever.
The Trade-off: A higher
max_requestsmeans fewer worker restarts, less overhead, potentially better throughput if your application is perfectly leak-free. A lowermax_requestsmeans more frequent restarts, slightly higher overhead, but significantly improved stability and memory footprint control.Real-world Insight: In systems handling massive traffic, even tiny memory leaks can accumulate rapidly across millions of requests. Setting
max_requeststo a conservative value (e.g., 500-1000) is often preferred for stability, even if it introduces a minuscule performance penalty. The cost of a few extra worker spawns is dwarfed by the cost of a memory-exhausted server or an unstable application.Dynamic Tuning: In very advanced setups,
max_requestsmight even be dynamically adjusted based on real-time memory profiling of workers. But for most high-scale systems, a well-chosen static value is sufficient.CPU/Memory Allocation: The
num_workersshould be carefully calibrated against your server's CPU cores and available memory. A good starting point isnum_workers = number_of_CPU_cores * 1.5to allow for some I/O wait. Then, monitor memory usage closely. If workers are frequently hittingmax_memory, either increase the limit (if resources allow) or reducemax_requeststo recycle them sooner.
Hands-on: Setting Up Our First Supervised PHP Application
Let's get our hands dirty. We'll set up a minimal PHP application and configure RoadRunner to supervise it using rr.yaml.
1. Project Structure
We'll create a simple src/public/index.php that will serve as our web entry point.
2. src/public/index.php
This script will simulate a basic web request and show us some memory usage, demonstrating the persistent nature.
3. rr.yaml
This will be our supervisor configuration.
Note: We've intentionally set max_requests to a very low 5 here. This is purely for demonstration purposes so you can quickly observe worker recycling. In a production system, this would typically be 500-1000.
Assignment: Master the Supervisor
Your mission, should you choose to accept it, is to experiment with the rr.yaml configuration and observe its impact.
Initial Setup: Follow the
start.shscript to get the application running.Observe Recycling: Hit your application (
curl http://localhost:8080) more than 5 times. Watch therequestCountin the response and RoadRunner's logs. You should see workers restarting after 5 requests.Tune
max_requests: Changemax_requeststo100. Restart RoadRunner. Observe how many requests a worker now handles before recycling.Tune
num_workers: Changenum_workersto4. Restart RoadRunner. Observe if your system feels more responsive under concurrent load (e.g., usingab -n 50 -c 10 http://localhost:8080).Simulate Memory Leak (Optional but insightful): In
src/public/index.php, add a line like$GLOBALS['leak_array'][] = str_repeat('A', 1024 * 10);inside the request processing loop. This will intentionally leak 10KB per request. Then, setmax_memoryto a tight value (e.g.,16777216for 16MB) andmax_requeststo a high value (e.g.,1000). Observe if workers are now recycled bymax_memoryinstead ofmax_requests. Remember to revert this change for production!
Solution Hints:
RoadRunner Logs: Pay close attention to the console output from RoadRunner when it starts and when you make requests. It will explicitly log when workers are "spawned," "recycled," or "terminated."
requestCount: Our simpleindex.php's$requestCountvariable, beingstatic, will persist within a single worker. When a worker is recycled, the new worker starts its$requestCountfrom1. This is your visual cue.memory_get_usage(): The memory reported will show the worker's current memory footprint. You'll see it reset when a worker recycles.Restarting RoadRunner: After any change to
rr.yaml, you must restart RoadRunner for the changes to take effect. Thestop.shandstart.shscripts will help with this.
By mastering rr.yaml and its supervisor capabilities, you're not just configuring a server; you're designing a resilient, self-healing system capable of handling the demands of a truly high-scale CMS. This understanding is foundational for building robust distributed systems, not just in PHP, but across any language where persistent processes are managed.