|
| 1 | +# Gunicorn Architecture Hierarchy |
| 2 | +## Understanding Workers, Threads, and Connections |
| 3 | + |
| 4 | +## Visual Hierarchy |
| 5 | + |
| 6 | +``` |
| 7 | +┌─────────────────────────────────────────────────────────────────┐ |
| 8 | +│ GUNICORN MASTER PROCESS │ |
| 9 | +│ (Manages everything, listens on port, handles signals) │ |
| 10 | +└─────────────────────┬───────────────────────────────────────────┘ |
| 11 | + │ |
| 12 | + │ Forks/Creates |
| 13 | + │ |
| 14 | + ┌─────────────┴─────────────┐ |
| 15 | + │ │ |
| 16 | + ┌────▼─────┐ ┌─────▼──────┐ |
| 17 | + │ WORKER 1 │ │ WORKER 2 │ ← Separate Processes |
| 18 | + │ (Process)│ │ (Process) │ Each has own Python |
| 19 | + └────┬─────┘ └─────┬──────┘ interpreter & GIL |
| 20 | + │ │ |
| 21 | + │ Creates Threads │ Creates Threads |
| 22 | + │ │ |
| 23 | + ┌────┴────┐ ┌────┴─────┐ |
| 24 | + │ THREAD 1│ │ THREAD 1 │ |
| 25 | + │ THREAD 2│ │ THREAD 2 │ ← Threads within process |
| 26 | + └────┬────┘ └────┬─────┘ |
| 27 | + │ │ |
| 28 | + │ Handles Requests │ Handles Requests |
| 29 | + │ │ |
| 30 | + ┌────┴──────────────┐ ┌────┴──────────────┐ |
| 31 | + │ CONNECTION POOL │ │ CONNECTION POOL │ |
| 32 | + │ (Database/Redis) │ │ (Database/Redis) │ |
| 33 | + └───────────────────┘ └───────────────────┘ |
| 34 | +``` |
| 35 | + |
| 36 | +## Detailed Breakdown |
| 37 | + |
| 38 | +### Level 1: Gunicorn Master Process |
| 39 | + |
| 40 | +**What it does:** |
| 41 | +- Listens on the network port (e.g., 9092) |
| 42 | +- Accepts incoming HTTP connections |
| 43 | +- Distributes connections to workers |
| 44 | +- Manages worker lifecycle (start, restart, kill) |
| 45 | +- Handles signals (SIGTERM, SIGHUP, etc.) |
| 46 | + |
| 47 | +**Properties:** |
| 48 | +- Single process |
| 49 | +- Doesn't handle request processing directly |
| 50 | +- Coordinates all workers |
| 51 | + |
| 52 | +### Level 2: Worker Processes |
| 53 | + |
| 54 | +**What they are:** |
| 55 | +- Separate OS processes (forked from master) |
| 56 | +- Each has its own Python interpreter |
| 57 | +- Each has its own Global Interpreter Lock (GIL) |
| 58 | +- Each has its own memory space |
| 59 | + |
| 60 | +**Configuration:** |
| 61 | +```python |
| 62 | +workers = 4 # Creates 4 worker processes |
| 63 | +``` |
| 64 | + |
| 65 | +**Why separate processes:** |
| 66 | +- **True parallelism**: Multiple processes can run on multiple CPU cores |
| 67 | +- **Isolation**: If one worker crashes, others continue |
| 68 | +- **GIL bypass**: Each process has its own GIL, so they can truly run in parallel |
| 69 | + |
| 70 | +**Example with 4 workers:** |
| 71 | +``` |
| 72 | +Master Process |
| 73 | + ├── Worker Process #1 (PID 1001) - Python interpreter #1 |
| 74 | + ├── Worker Process #2 (PID 1002) - Python interpreter #2 |
| 75 | + ├── Worker Process #3 (PID 1003) - Python interpreter #3 |
| 76 | + └── Worker Process #4 (PID 1004) - Python interpreter #4 |
| 77 | +``` |
| 78 | + |
| 79 | +### Level 3: Threads (within each worker) |
| 80 | + |
| 81 | +**What they are:** |
| 82 | +- Lightweight execution units within a process |
| 83 | +- Share the same memory space as their parent worker |
| 84 | +- Share the same GIL (Global Interpreter Lock) |
| 85 | + |
| 86 | +**Configuration:** |
| 87 | +```python |
| 88 | +threads = 2 # 2 threads per worker |
| 89 | +``` |
| 90 | + |
| 91 | +**Why threads:** |
| 92 | +- **I/O concurrency**: While one thread waits for DB/network, another can process |
| 93 | +- **Efficiency**: Threads are lighter than processes |
| 94 | +- **Limited by GIL**: In CPU-bound tasks, only one thread runs at a time |
| 95 | + |
| 96 | +**Example within Worker #1:** |
| 97 | +``` |
| 98 | +Worker Process #1 |
| 99 | + ├── Thread #1 (handles request A) |
| 100 | + └── Thread #2 (handles request B) |
| 101 | + # Both threads share Worker #1's memory and GIL |
| 102 | +``` |
| 103 | + |
| 104 | +**GIL Impact:** |
| 105 | +- Python's GIL allows only one thread to execute Python bytecode at a time |
| 106 | +- However, threads can still be useful for I/O-bound operations (DB queries, network calls) |
| 107 | +- While Thread #1 waits for database response, Thread #2 can handle another request |
| 108 | + |
| 109 | +### Level 4: Connection Pools (per worker) |
| 110 | + |
| 111 | +**What they are:** |
| 112 | +- Pool of reusable connections to external resources |
| 113 | +- Database connections (PostgreSQL) |
| 114 | +- Redis connections |
| 115 | +- HTTP client connections |
| 116 | + |
| 117 | +**Configuration:** |
| 118 | +```python |
| 119 | +POOL_SIZE = 5 # 5 connections in pool |
| 120 | +MAX_OVERFLOW = 5 # 5 additional when pool exhausted |
| 121 | +``` |
| 122 | + |
| 123 | +**Why connection pools:** |
| 124 | +- Creating connections is expensive (network overhead, authentication) |
| 125 | +- Reusing connections is much faster |
| 126 | +- Limits number of connections to prevent resource exhaustion |
| 127 | + |
| 128 | +**Example within Worker #1:** |
| 129 | +``` |
| 130 | +Worker Process #1 |
| 131 | + ├── Thread #1 |
| 132 | + │ └── Uses Connection Pool #1 |
| 133 | + │ ├── DB Connection 1 |
| 134 | + │ ├── DB Connection 2 |
| 135 | + │ ├── DB Connection 3 |
| 136 | + │ └── Redis Connection |
| 137 | + └── Thread #2 |
| 138 | + └── Uses Connection Pool #1 (same pool, different connections) |
| 139 | + ├── DB Connection 4 |
| 140 | + ├── DB Connection 5 |
| 141 | + └── Redis Connection (shared) |
| 142 | +``` |
| 143 | + |
| 144 | +## Complete Example: 4 Workers × 2 Threads |
| 145 | + |
| 146 | +``` |
| 147 | +Gunicorn Master (Port 9092) |
| 148 | +│ |
| 149 | +├── Worker 1 (Process PID 1001) |
| 150 | +│ ├── Thread 1 → Can handle 1 request |
| 151 | +│ │ └── Connection Pool 1 |
| 152 | +│ │ ├── DB Conn 1-5 |
| 153 | +│ │ └── Redis Conn |
| 154 | +│ └── Thread 2 → Can handle 1 request |
| 155 | +│ └── Connection Pool 1 (shared) |
| 156 | +│ ├── DB Conn 1-5 (reused) |
| 157 | +│ └── Redis Conn (shared) |
| 158 | +│ |
| 159 | +├── Worker 2 (Process PID 1002) |
| 160 | +│ ├── Thread 1 → Can handle 1 request |
| 161 | +│ │ └── Connection Pool 2 |
| 162 | +│ │ ├── DB Conn 1-5 (separate from Worker 1) |
| 163 | +│ │ └── Redis Conn |
| 164 | +│ └── Thread 2 → Can handle 1 request |
| 165 | +│ └── Connection Pool 2 (shared) |
| 166 | +│ |
| 167 | +├── Worker 3 (Process PID 1003) |
| 168 | +│ ├── Thread 1 → Can handle 1 request |
| 169 | +│ └── Thread 2 → Can handle 1 request |
| 170 | +│ |
| 171 | +└── Worker 4 (Process PID 1004) |
| 172 | + ├── Thread 1 → Can handle 1 request |
| 173 | + └── Thread 2 → Can handle 1 request |
| 174 | +
|
| 175 | +Total Concurrent Requests: 4 workers × 2 threads = 8 requests simultaneously |
| 176 | +``` |
| 177 | + |
| 178 | +## Request Flow Example |
| 179 | + |
| 180 | +``` |
| 181 | +1. HTTP Request arrives at Port 9092 |
| 182 | + ↓ |
| 183 | +2. Gunicorn Master accepts connection |
| 184 | + ↓ |
| 185 | +3. Master distributes to Worker 2 (round-robin or least busy) |
| 186 | + ↓ |
| 187 | +4. Worker 2 assigns to Thread 1 (available thread) |
| 188 | + ↓ |
| 189 | +5. Thread 1 processes request: |
| 190 | + - Gets DB connection from Connection Pool 2 |
| 191 | + - Executes database query |
| 192 | + - Gets Redis connection from pool |
| 193 | + - Caches result |
| 194 | + - Returns response |
| 195 | + ↓ |
| 196 | +6. Thread 1 releases connections back to pool |
| 197 | + ↓ |
| 198 | +7. Thread 1 ready for next request |
| 199 | +``` |
| 200 | + |
| 201 | +## Connection Pool Hierarchy |
| 202 | + |
| 203 | +### Database Connection Pool (per worker process) |
| 204 | + |
| 205 | +``` |
| 206 | +Worker Process #1 |
| 207 | + └── SQLAlchemy Connection Pool |
| 208 | + ├── Pool Size: 5 connections |
| 209 | + ├── Max Overflow: 5 connections |
| 210 | + └── Total Max: 10 connections |
| 211 | +
|
| 212 | +Worker Process #2 |
| 213 | + └── SQLAlchemy Connection Pool (separate) |
| 214 | + ├── Pool Size: 5 connections |
| 215 | + ├── Max Overflow: 5 connections |
| 216 | + └── Total Max: 10 connections |
| 217 | +
|
| 218 | +Total across all workers: 4 workers × 10 connections = 40 max DB connections |
| 219 | +``` |
| 220 | + |
| 221 | +**Important:** |
| 222 | +- Each worker has its own connection pool |
| 223 | +- Threads within a worker share the pool |
| 224 | +- Connections are reused across requests |
| 225 | +- When pool exhausted, new connections created (up to MAX_OVERFLOW) |
| 226 | + |
| 227 | +### Redis Connection Pool (shared or per worker) |
| 228 | + |
| 229 | +``` |
| 230 | +Option 1: Per Worker (current setup) |
| 231 | +Worker 1 → Redis Connection Pool 1 |
| 232 | +Worker 2 → Redis Connection Pool 2 |
| 233 | +Worker 3 → Redis Connection Pool 3 |
| 234 | +Worker 4 → Redis Connection Pool 4 |
| 235 | +
|
| 236 | +Option 2: Shared (if using connection pooling library) |
| 237 | +All Workers → Single Redis Connection Pool (thread-safe) |
| 238 | +``` |
| 239 | + |
| 240 | +## Mathematical Relationship |
| 241 | + |
| 242 | +### Capacity Calculation |
| 243 | + |
| 244 | +``` |
| 245 | +Concurrent Requests = Workers × Threads |
| 246 | +
|
| 247 | +Example: |
| 248 | +- Workers = 4 |
| 249 | +- Threads = 2 |
| 250 | +- Concurrent Requests = 4 × 2 = 8 |
| 251 | +
|
| 252 | +Throughput Calculation: |
| 253 | +Throughput (req/sec) = Concurrent Requests × (1000ms / Average Response Time) |
| 254 | +
|
| 255 | +Example: |
| 256 | +- 8 concurrent requests |
| 257 | +- Average response time = 100ms |
| 258 | +- Throughput = 8 × (1000 / 100) = 80 req/sec |
| 259 | +
|
| 260 | +But with pipelining and request queuing: |
| 261 | +Actual throughput can be higher (1000-2000 req/sec) |
| 262 | +``` |
| 263 | + |
| 264 | +### Connection Pool Sizing |
| 265 | + |
| 266 | +``` |
| 267 | +Recommended Pool Size per Worker = (Workers × Threads) / Workers |
| 268 | +
|
| 269 | +For 4 workers, 2 threads: |
| 270 | +Pool Size = (4 × 2) / 4 = 2 connections per worker minimum |
| 271 | +
|
| 272 | +Better: Pool Size = Threads × 2-3 |
| 273 | +Pool Size = 2 × 3 = 6 connections per worker |
| 274 | +
|
| 275 | +Total DB Connections = Workers × Pool Size |
| 276 | +Total DB Connections = 4 × 6 = 24 connections (reasonable) |
| 277 | +``` |
| 278 | + |
| 279 | +## Key Differences |
| 280 | + |
| 281 | +| Aspect | Workers | Threads | Connections | |
| 282 | +|--------|---------|---------|-------------| |
| 283 | +| **Level** | Process-level | Thread-level | Resource-level | |
| 284 | +| **Isolation** | Separate memory | Shared memory | External resource | |
| 285 | +| **Parallelism** | True (multi-core) | Limited (GIL) | N/A | |
| 286 | +| **Communication** | IPC/message passing | Shared variables | Network protocol | |
| 287 | +| **Failure Impact** | Isolated (others continue) | Affects process | Recoverable | |
| 288 | +| **Memory** | Separate heap | Shared heap | External | |
| 289 | +| **Creation Cost** | High (fork) | Low (lightweight) | Medium (network) | |
| 290 | + |
| 291 | +## Best Practices |
| 292 | + |
| 293 | +### 1. Worker Count |
| 294 | +- **Formula**: (2 × CPU cores) + 1 |
| 295 | +- **For I/O-bound**: More workers (4-8) |
| 296 | +- **For CPU-bound**: Fewer workers (2-4) |
| 297 | +- **Your setup**: 4 workers (good for mixed workload) |
| 298 | + |
| 299 | +### 2. Thread Count |
| 300 | +- **For I/O-bound**: 2-4 threads per worker |
| 301 | +- **For CPU-bound**: 1 thread per worker (threads don't help with GIL) |
| 302 | +- **Your setup**: 2 threads (good for database/network I/O) |
| 303 | + |
| 304 | +### 3. Connection Pool Size |
| 305 | +- **Per worker**: Threads × 2-3 |
| 306 | +- **Example**: 2 threads × 3 = 6 connections per worker |
| 307 | +- **Total**: Workers × Pool Size |
| 308 | +- **Database limit**: Don't exceed database max_connections |
| 309 | + |
| 310 | +## Real-World Example |
| 311 | + |
| 312 | +**Your Current Configuration:** |
| 313 | +``` |
| 314 | +Gunicorn Master |
| 315 | +├── Worker 1 (handles requests 1, 9, 17, ...) |
| 316 | +│ ├── Thread 1 (handles requests 1, 5, 9, ...) |
| 317 | +│ └── Thread 2 (handles requests 2, 6, 10, ...) |
| 318 | +├── Worker 2 (handles requests 2, 10, 18, ...) |
| 319 | +│ ├── Thread 1 (handles requests 3, 7, 11, ...) |
| 320 | +│ └── Thread 2 (handles requests 4, 8, 12, ...) |
| 321 | +├── Worker 3 |
| 322 | +└── Worker 4 |
| 323 | +
|
| 324 | +At any moment: |
| 325 | +- 8 requests can be processed simultaneously |
| 326 | +- Each worker has its own connection pool |
| 327 | +- Threads share connection pool within worker |
| 328 | +- Connections are reused across requests |
| 329 | +``` |
| 330 | + |
| 331 | +## Summary |
| 332 | + |
| 333 | +**Hierarchy Order:** |
| 334 | +1. **Gunicorn Master** - Coordinates everything |
| 335 | +2. **Workers** (Processes) - Handle request processing (4 workers) |
| 336 | +3. **Threads** (within workers) - Enable concurrency (2 threads each) |
| 337 | +4. **Connection Pools** (per worker) - Reuse expensive connections |
| 338 | + |
| 339 | +**Key Takeaway:** |
| 340 | +- **Workers** = True parallelism (different CPU cores) |
| 341 | +- **Threads** = I/O concurrency (waiting for DB/network) |
| 342 | +- **Connections** = Resource efficiency (reuse expensive connections) |
| 343 | + |
| 344 | +**Your Setup:** |
| 345 | +- 4 workers × 2 threads = 8 concurrent requests |
| 346 | +- Each worker maintains its own DB/Redis connection pool |
| 347 | +- Can handle 1000-2000 req/sec with proper caching and optimization |
| 348 | + |
0 commit comments