Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
4587940f38 | |||
82ca0381e9 |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "19.6.11",
|
"version": "19.6.12",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
|
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
|
@ -11,10 +11,11 @@
|
|||||||
- Hour 2: 2GB total / 60s = 34 MB/s ✗ (appears doubled!)
|
- Hour 2: 2GB total / 60s = 34 MB/s ✗ (appears doubled!)
|
||||||
- Hour 3: 3GB total / 60s = 50 MB/s ✗ (keeps rising!)
|
- Hour 3: 3GB total / 60s = 50 MB/s ✗ (keeps rising!)
|
||||||
|
|
||||||
**Solution**: Implemented snapshot-based byte tracking that calculates actual bytes transferred within each time window:
|
**Solution**: Implemented dedicated ThroughputTracker instances for each route and IP address:
|
||||||
- Store periodic snapshots of byte counts with timestamps
|
- Each route and IP gets its own throughput tracker with per-second sampling
|
||||||
- Calculate delta between window start and end snapshots
|
- Samples are taken every second and stored in a circular buffer
|
||||||
- Divide delta by window duration for accurate throughput
|
- Rate calculations use actual samples within the requested window
|
||||||
|
- Default window is now 1 second for real-time accuracy
|
||||||
|
|
||||||
### What Gets Counted (Network Interface Throughput)
|
### What Gets Counted (Network Interface Throughput)
|
||||||
|
|
||||||
@ -53,15 +54,19 @@ The byte tracking is designed to match network interface throughput (what Unifi/
|
|||||||
|
|
||||||
### Metrics Architecture
|
### Metrics Architecture
|
||||||
|
|
||||||
The metrics system has three layers:
|
The metrics system has multiple layers:
|
||||||
1. **Connection Records** (`record.bytesReceived/bytesSent`): Track total bytes per connection
|
1. **Connection Records** (`record.bytesReceived/bytesSent`): Track total bytes per connection
|
||||||
2. **ThroughputTracker**: Accumulates bytes between samples for global rate calculations (resets each second)
|
2. **Global ThroughputTracker**: Accumulates bytes between samples for overall rate calculations
|
||||||
3. **connectionByteTrackers**: Track bytes per connection with snapshots for accurate windowed per-route/IP metrics
|
3. **Per-Route ThroughputTrackers**: Dedicated tracker for each route with per-second sampling
|
||||||
|
4. **Per-IP ThroughputTrackers**: Dedicated tracker for each IP with per-second sampling
|
||||||
|
5. **connectionByteTrackers**: Track cumulative bytes and metadata for active connections
|
||||||
|
|
||||||
Key features:
|
Key features:
|
||||||
- Global throughput uses sampling with accumulator reset (accurate)
|
- All throughput trackers sample every second (1Hz)
|
||||||
- Per-route/IP throughput uses snapshots to calculate window-specific deltas (accurate)
|
- Each tracker maintains a circular buffer of samples (default: 1 hour retention)
|
||||||
|
- Rate calculations are accurate for any requested window (default: 1 second)
|
||||||
- All byte counting happens exactly once at the data flow point
|
- All byte counting happens exactly once at the data flow point
|
||||||
|
- Unused route/IP trackers are automatically cleaned up when connections close
|
||||||
|
|
||||||
### Understanding "High" Byte Counts
|
### Understanding "High" Byte Counts
|
||||||
|
|
||||||
|
@ -15,6 +15,8 @@ import { logger } from '../../core/utils/logger.js';
|
|||||||
export class MetricsCollector implements IMetrics {
|
export class MetricsCollector implements IMetrics {
|
||||||
// Throughput tracking
|
// Throughput tracking
|
||||||
private throughputTracker: ThroughputTracker;
|
private throughputTracker: ThroughputTracker;
|
||||||
|
private routeThroughputTrackers = new Map<string, ThroughputTracker>();
|
||||||
|
private ipThroughputTrackers = new Map<string, ThroughputTracker>();
|
||||||
|
|
||||||
// Request tracking
|
// Request tracking
|
||||||
private requestTimestamps: number[] = [];
|
private requestTimestamps: number[] = [];
|
||||||
@ -119,109 +121,31 @@ export class MetricsCollector implements IMetrics {
|
|||||||
return this.throughputTracker.getHistory(seconds);
|
return this.throughputTracker.getHistory(seconds);
|
||||||
},
|
},
|
||||||
|
|
||||||
byRoute: (windowSeconds: number = 60): Map<string, IThroughputData> => {
|
byRoute: (windowSeconds: number = 1): Map<string, IThroughputData> => {
|
||||||
const routeThroughput = new Map<string, IThroughputData>();
|
const routeThroughput = new Map<string, IThroughputData>();
|
||||||
const now = Date.now();
|
|
||||||
const windowStart = now - (windowSeconds * 1000);
|
|
||||||
|
|
||||||
// Aggregate bytes by route - calculate actual bytes transferred in window
|
// Get throughput from each route's dedicated tracker
|
||||||
const routeData = new Map<string, { bytesIn: number; bytesOut: number }>();
|
for (const [route, tracker] of this.routeThroughputTrackers) {
|
||||||
|
const rate = tracker.getRate(windowSeconds);
|
||||||
for (const [_, tracker] of this.connectionByteTrackers) {
|
if (rate.in > 0 || rate.out > 0) {
|
||||||
// Only include connections that were active within the window
|
routeThroughput.set(route, rate);
|
||||||
if (tracker.lastUpdate > windowStart) {
|
|
||||||
let windowBytesIn = 0;
|
|
||||||
let windowBytesOut = 0;
|
|
||||||
|
|
||||||
if (tracker.windowSnapshots && tracker.windowSnapshots.length > 0) {
|
|
||||||
// Find the earliest snapshot within or just before the window
|
|
||||||
let startSnapshot = { timestamp: tracker.startTime, bytesIn: 0, bytesOut: 0 };
|
|
||||||
for (const snapshot of tracker.windowSnapshots) {
|
|
||||||
if (snapshot.timestamp <= windowStart) {
|
|
||||||
startSnapshot = snapshot;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate bytes transferred since window start
|
|
||||||
windowBytesIn = tracker.bytesIn - startSnapshot.bytesIn;
|
|
||||||
windowBytesOut = tracker.bytesOut - startSnapshot.bytesOut;
|
|
||||||
} else if (tracker.startTime > windowStart) {
|
|
||||||
// Connection started within window, use all its bytes
|
|
||||||
windowBytesIn = tracker.bytesIn;
|
|
||||||
windowBytesOut = tracker.bytesOut;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to route totals
|
|
||||||
const current = routeData.get(tracker.routeName) || { bytesIn: 0, bytesOut: 0 };
|
|
||||||
current.bytesIn += windowBytesIn;
|
|
||||||
current.bytesOut += windowBytesOut;
|
|
||||||
routeData.set(tracker.routeName, current);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to rates (bytes per second)
|
|
||||||
for (const [route, data] of routeData) {
|
|
||||||
routeThroughput.set(route, {
|
|
||||||
in: Math.round(data.bytesIn / windowSeconds),
|
|
||||||
out: Math.round(data.bytesOut / windowSeconds)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return routeThroughput;
|
return routeThroughput;
|
||||||
},
|
},
|
||||||
|
|
||||||
byIP: (windowSeconds: number = 60): Map<string, IThroughputData> => {
|
byIP: (windowSeconds: number = 1): Map<string, IThroughputData> => {
|
||||||
const ipThroughput = new Map<string, IThroughputData>();
|
const ipThroughput = new Map<string, IThroughputData>();
|
||||||
const now = Date.now();
|
|
||||||
const windowStart = now - (windowSeconds * 1000);
|
|
||||||
|
|
||||||
// Aggregate bytes by IP - calculate actual bytes transferred in window
|
// Get throughput from each IP's dedicated tracker
|
||||||
const ipData = new Map<string, { bytesIn: number; bytesOut: number }>();
|
for (const [ip, tracker] of this.ipThroughputTrackers) {
|
||||||
|
const rate = tracker.getRate(windowSeconds);
|
||||||
for (const [_, tracker] of this.connectionByteTrackers) {
|
if (rate.in > 0 || rate.out > 0) {
|
||||||
// Only include connections that were active within the window
|
ipThroughput.set(ip, rate);
|
||||||
if (tracker.lastUpdate > windowStart) {
|
|
||||||
let windowBytesIn = 0;
|
|
||||||
let windowBytesOut = 0;
|
|
||||||
|
|
||||||
if (tracker.windowSnapshots && tracker.windowSnapshots.length > 0) {
|
|
||||||
// Find the earliest snapshot within or just before the window
|
|
||||||
let startSnapshot = { timestamp: tracker.startTime, bytesIn: 0, bytesOut: 0 };
|
|
||||||
for (const snapshot of tracker.windowSnapshots) {
|
|
||||||
if (snapshot.timestamp <= windowStart) {
|
|
||||||
startSnapshot = snapshot;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate bytes transferred since window start
|
|
||||||
windowBytesIn = tracker.bytesIn - startSnapshot.bytesIn;
|
|
||||||
windowBytesOut = tracker.bytesOut - startSnapshot.bytesOut;
|
|
||||||
} else if (tracker.startTime > windowStart) {
|
|
||||||
// Connection started within window, use all its bytes
|
|
||||||
windowBytesIn = tracker.bytesIn;
|
|
||||||
windowBytesOut = tracker.bytesOut;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to IP totals
|
|
||||||
const current = ipData.get(tracker.remoteIP) || { bytesIn: 0, bytesOut: 0 };
|
|
||||||
current.bytesIn += windowBytesIn;
|
|
||||||
current.bytesOut += windowBytesOut;
|
|
||||||
ipData.set(tracker.remoteIP, current);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to rates (bytes per second)
|
|
||||||
for (const [ip, data] of ipData) {
|
|
||||||
ipThroughput.set(ip, {
|
|
||||||
in: Math.round(data.bytesIn / windowSeconds),
|
|
||||||
out: Math.round(data.bytesOut / windowSeconds)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return ipThroughput;
|
return ipThroughput;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -322,8 +246,7 @@ export class MetricsCollector implements IMetrics {
|
|||||||
bytesIn: 0,
|
bytesIn: 0,
|
||||||
bytesOut: 0,
|
bytesOut: 0,
|
||||||
startTime: now,
|
startTime: now,
|
||||||
lastUpdate: now,
|
lastUpdate: now
|
||||||
windowSnapshots: [] // Initialize empty snapshots array
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cleanup old request timestamps
|
// Cleanup old request timestamps
|
||||||
@ -353,21 +276,21 @@ export class MetricsCollector implements IMetrics {
|
|||||||
tracker.bytesOut += bytesOut;
|
tracker.bytesOut += bytesOut;
|
||||||
tracker.lastUpdate = Date.now();
|
tracker.lastUpdate = Date.now();
|
||||||
|
|
||||||
// Initialize snapshots array if not present
|
// Update per-route throughput tracker
|
||||||
if (!tracker.windowSnapshots) {
|
let routeTracker = this.routeThroughputTrackers.get(tracker.routeName);
|
||||||
tracker.windowSnapshots = [];
|
if (!routeTracker) {
|
||||||
|
routeTracker = new ThroughputTracker(this.retentionSeconds);
|
||||||
|
this.routeThroughputTrackers.set(tracker.routeName, routeTracker);
|
||||||
}
|
}
|
||||||
|
routeTracker.recordBytes(bytesIn, bytesOut);
|
||||||
|
|
||||||
// Add current snapshot - we'll use these for accurate windowed calculations
|
// Update per-IP throughput tracker
|
||||||
tracker.windowSnapshots.push({
|
let ipTracker = this.ipThroughputTrackers.get(tracker.remoteIP);
|
||||||
timestamp: Date.now(),
|
if (!ipTracker) {
|
||||||
bytesIn: tracker.bytesIn,
|
ipTracker = new ThroughputTracker(this.retentionSeconds);
|
||||||
bytesOut: tracker.bytesOut
|
this.ipThroughputTrackers.set(tracker.remoteIP, ipTracker);
|
||||||
});
|
}
|
||||||
|
ipTracker.recordBytes(bytesIn, bytesOut);
|
||||||
// Keep only snapshots from last 5 minutes to prevent memory growth
|
|
||||||
const fiveMinutesAgo = Date.now() - 300000;
|
|
||||||
tracker.windowSnapshots = tracker.windowSnapshots.filter(s => s.timestamp > fiveMinutesAgo);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -388,8 +311,19 @@ export class MetricsCollector implements IMetrics {
|
|||||||
|
|
||||||
// Start periodic sampling
|
// Start periodic sampling
|
||||||
this.samplingInterval = setInterval(() => {
|
this.samplingInterval = setInterval(() => {
|
||||||
|
// Sample global throughput
|
||||||
this.throughputTracker.takeSample();
|
this.throughputTracker.takeSample();
|
||||||
|
|
||||||
|
// Sample per-route throughput
|
||||||
|
for (const [_, tracker] of this.routeThroughputTrackers) {
|
||||||
|
tracker.takeSample();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample per-IP throughput
|
||||||
|
for (const [_, tracker] of this.ipThroughputTrackers) {
|
||||||
|
tracker.takeSample();
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up old connection trackers (connections closed more than 5 minutes ago)
|
// Clean up old connection trackers (connections closed more than 5 minutes ago)
|
||||||
const cutoff = Date.now() - 300000;
|
const cutoff = Date.now() - 300000;
|
||||||
for (const [id, tracker] of this.connectionByteTrackers) {
|
for (const [id, tracker] of this.connectionByteTrackers) {
|
||||||
@ -397,6 +331,22 @@ export class MetricsCollector implements IMetrics {
|
|||||||
this.connectionByteTrackers.delete(id);
|
this.connectionByteTrackers.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up unused route trackers
|
||||||
|
const activeRoutes = new Set(Array.from(this.connectionByteTrackers.values()).map(t => t.routeName));
|
||||||
|
for (const [route, _] of this.routeThroughputTrackers) {
|
||||||
|
if (!activeRoutes.has(route)) {
|
||||||
|
this.routeThroughputTrackers.delete(route);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up unused IP trackers
|
||||||
|
const activeIPs = new Set(Array.from(this.connectionByteTrackers.values()).map(t => t.remoteIP));
|
||||||
|
for (const [ip, _] of this.ipThroughputTrackers) {
|
||||||
|
if (!activeIPs.has(ip)) {
|
||||||
|
this.ipThroughputTrackers.delete(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
}, this.sampleIntervalMs);
|
}, this.sampleIntervalMs);
|
||||||
|
|
||||||
// Subscribe to new connections
|
// Subscribe to new connections
|
||||||
|
@ -49,8 +49,8 @@ export interface IMetrics {
|
|||||||
average(): IThroughputData; // Last 60 seconds
|
average(): IThroughputData; // Last 60 seconds
|
||||||
custom(seconds: number): IThroughputData;
|
custom(seconds: number): IThroughputData;
|
||||||
history(seconds: number): Array<IThroughputHistoryPoint>;
|
history(seconds: number): Array<IThroughputHistoryPoint>;
|
||||||
byRoute(windowSeconds?: number): Map<string, IThroughputData>;
|
byRoute(windowSeconds?: number): Map<string, IThroughputData>; // Default: 1 second
|
||||||
byIP(windowSeconds?: number): Map<string, IThroughputData>;
|
byIP(windowSeconds?: number): Map<string, IThroughputData>; // Default: 1 second
|
||||||
};
|
};
|
||||||
|
|
||||||
// Request metrics
|
// Request metrics
|
||||||
@ -109,10 +109,4 @@ export interface IByteTracker {
|
|||||||
bytesOut: number;
|
bytesOut: number;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
lastUpdate: number;
|
lastUpdate: number;
|
||||||
// Track bytes at window boundaries for rate calculation
|
|
||||||
windowSnapshots?: Array<{
|
|
||||||
timestamp: number;
|
|
||||||
bytesIn: number;
|
|
||||||
bytesOut: number;
|
|
||||||
}>;
|
|
||||||
}
|
}
|
Reference in New Issue
Block a user