-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
Description
Summary
Optimize audio streaming to reduce Node.js CPU usage by offloading stream handling to Nginx.
Current Problem
- All audio data flows through Node.js:
MinIO → Node.js → Client - 59 MB/s passes through Node.js at 1000 VUs
- CPU reaches 84% (while PostgreSQL only 5%, MinIO 14%)
Options Analyzed
Option A: Presigned URLs (❌ Not Recommended)
Client → 302 redirect → MinIO presigned URL
| Pros | Cons |
|---|---|
| Simple implementation | Exposes MinIO endpoint |
| Zero Node.js CPU | CDN unfriendly - signed URLs expire, cache invalidation issues |
| Native Range support | Security concerns |
Option B: Nginx Reverse Proxy (✅ Recommended)
Client → Nginx → MinIO (for /stream/*)
→ Node.js (for /api/*)
| Pros | Cons |
|---|---|
| MinIO stays internal | Moderate implementation effort |
| CDN friendly - stable URLs | Nginx config changes |
| High security | |
| C-level stream handling |
Option C: Keep Current (⏸️ Acceptable for Now)
- Current performance is sufficient (1000 VUs, 0% error)
- Can defer optimization until CDN integration or higher load requirements
Recommended Solution: Option B (Nginx Proxy)
Architecture
Before:
Client → Node.js (:4000) → [stream processing] → MinIO
↑ bottleneck: 59MB/s through Node.js
After:
Client → Nginx (:80)
├── /api/* → Node.js (:4000) # API requests
└── /stream/* → MinIO (:9000) # Audio streams (bypass Node.js)
Authentication Flow
- Client requests
GET /api/songs/:id/streamwith JWT - Node.js validates JWT, checks user permission
- Returns 302 redirect to internal path
/stream/{filePath}?_token={internal_signature} - Nginx receives
/stream/*, proxies to MinIO - MinIO returns audio data directly to client via Nginx
Implementation Details
Backend code (dual mode support):
// backend/src/routes/songs.ts
app.get('/:id/stream', async (c) => {
// ... auth validation ...
if (process.env.STREAM_MODE === 'redirect') {
// Production: redirect to Nginx proxy path
const token = generateInternalToken(song.file.path);
return c.redirect(`/stream/${song.file.path}?_token=${token}`);
} else {
// Development: keep existing Node.js proxy (simple but slower)
return streamThroughNodeJS(c, song);
}
});Nginx config addition:
# Audio stream direct to MinIO (production only)
location /stream/ {
internal; # Only accept internal redirects
proxy_pass http://m3w-minio:9000/m3w-music/;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
}Environment control:
# Local dev (backend/.env) - no change needed
STREAM_MODE=proxy # default, keeps current behavior
# Docker production (.env.docker)
STREAM_MODE=redirect # enables Nginx direct streamingLocal Development Impact
| Scenario | Audio Path | Config Required |
|---|---|---|
npm run dev |
Node.js proxy | None |
docker-compose up (dev) |
Node.js proxy | None |
docker run m3w:prod |
Nginx direct | Auto-enabled |
Local development workflow unchanged.
CDN Integration Path (Future)
Client → CDN → Nginx → MinIO
- Stable URLs (
/api/songs/:id/stream) work well with CDN caching - Standard
Cache-Controlheaders apply - No signed URL expiration issues
Acceptance Criteria
- Nginx config supports
/stream/*proxy to MinIO - Backend supports
STREAM_MODEenvironment variable - Development mode (
STREAM_MODE=proxy) works unchanged - Production mode (
STREAM_MODE=redirect) reduces CPU significantly - Range requests (seeking) work correctly in both modes
- Load test confirms CPU < 30% at 1000 VUs (production mode)
Priority
P2 - Low priority. Current performance is acceptable (1000 VUs, 0% error).
Implement when:
- Preparing for CDN integration (Epic 3.4)
- Experiencing actual performance issues in production
- Vertical scaling becomes insufficient
References
- Bottleneck analysis: Epic 3.1 Epic 3.1: Establish Baseline #178
- Load test repository: https://github.com/test3207/m3w-load-test
Parent Issue: Epic 3.2 (#182)
Reactions are currently unavailable