feat: add CDN cache hit rate for accurate bandwidth cost estimation
- Add cdn_enabled and cdn_cache_hit_rate API parameters - Use case별 기본 캐시 히트율 자동 적용 (video: 92%, blog: 90%, etc.) - 원본 서버 트래픽(origin_monthly_tb)과 절감 비용(cdn_savings_cost) 계산 - 응답에 CDN breakdown 필드 추가 (bandwidth_estimate, bandwidth_info) - 캐시 키에 CDN 옵션 포함하여 정확한 캐시 분리 - 4개 CDN 관련 테스트 추가 (총 59 tests) - CLAUDE.md 문서 업데이트 Cost impact example (10K video streaming): - Without CDN: $18,370 → With CDN 92%: $1,464 (92% savings) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,10 +22,19 @@ export function findUseCaseConfig(useCase: string): UseCaseConfig {
|
||||
category: 'default',
|
||||
patterns: /.*/,
|
||||
dauMultiplier: { min: 10, max: 14 },
|
||||
activeRatio: 0.5
|
||||
activeRatio: 0.5,
|
||||
cdnCacheHitRate: 0.50 // 기본값: 50%
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CDN cache hit rate based on use case
|
||||
* Returns default rate for the use case category
|
||||
*/
|
||||
export function getCdnCacheHitRate(useCase: string): number {
|
||||
return findUseCaseConfig(useCase).cdnCacheHitRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DAU multiplier based on use case (how many daily active users per concurrent user)
|
||||
*/
|
||||
@@ -40,10 +49,24 @@ export function getActiveUserRatio(useCase: string): number {
|
||||
return findUseCaseConfig(useCase).activeRatio;
|
||||
}
|
||||
|
||||
export interface CdnOptions {
|
||||
enabled?: boolean; // CDN 사용 여부 (기본: true)
|
||||
cacheHitRate?: number; // 캐시 히트율 override (0.0-1.0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate monthly bandwidth based on concurrent users and use case
|
||||
* @param concurrentUsers Expected concurrent users
|
||||
* @param useCase Use case description
|
||||
* @param trafficPattern Traffic pattern (steady, spiky, growing)
|
||||
* @param cdnOptions CDN configuration options
|
||||
*/
|
||||
export function estimateBandwidth(concurrentUsers: number, useCase: string, trafficPattern?: string): BandwidthEstimate {
|
||||
export function estimateBandwidth(
|
||||
concurrentUsers: number,
|
||||
useCase: string,
|
||||
trafficPattern?: string,
|
||||
cdnOptions?: CdnOptions
|
||||
): BandwidthEstimate {
|
||||
const useCaseLower = useCase.toLowerCase();
|
||||
|
||||
// Get use case configuration
|
||||
@@ -167,29 +190,54 @@ export function estimateBandwidth(concurrentUsers: number, useCase: string, traf
|
||||
}
|
||||
}
|
||||
|
||||
// CDN configuration
|
||||
const cdnEnabled = cdnOptions?.enabled !== false; // 기본값: true
|
||||
const cdnCacheHitRate = cdnOptions?.cacheHitRate ?? config.cdnCacheHitRate;
|
||||
|
||||
console.log(`[Bandwidth] Model: ${bandwidthModel}`);
|
||||
console.log(`[Bandwidth] DAU: ${dailyUniqueVisitors} (${dauMultiplier.min}-${dauMultiplier.max}x), Active: ${activeDau} (${(activeUserRatio * 100).toFixed(0)}%), Daily: ${dailyBandwidthGB.toFixed(1)} GB`);
|
||||
|
||||
// Monthly bandwidth
|
||||
const monthlyGB = dailyBandwidthGB * 30;
|
||||
const monthlyTB = monthlyGB / 1024;
|
||||
// Monthly bandwidth (gross - before CDN)
|
||||
const grossMonthlyGB = dailyBandwidthGB * 30;
|
||||
const grossMonthlyTB = grossMonthlyGB / 1024;
|
||||
|
||||
// Categorize
|
||||
// Origin bandwidth (after CDN cache hit)
|
||||
const originMonthlyTB = cdnEnabled
|
||||
? grossMonthlyTB * (1 - cdnCacheHitRate)
|
||||
: grossMonthlyTB;
|
||||
const originMonthlyGB = originMonthlyTB * 1024;
|
||||
|
||||
// Use origin traffic for categorization (actual server load)
|
||||
const monthlyGB = originMonthlyGB;
|
||||
const monthlyTB = originMonthlyTB;
|
||||
|
||||
console.log(`[Bandwidth] CDN: ${cdnEnabled ? 'enabled' : 'disabled'}, Hit Rate: ${(cdnCacheHitRate * 100).toFixed(0)}%`);
|
||||
console.log(`[Bandwidth] Gross: ${grossMonthlyTB.toFixed(1)} TB → Origin: ${originMonthlyTB.toFixed(1)} TB (${((1 - cdnCacheHitRate) * 100).toFixed(0)}%)`);
|
||||
|
||||
// Categorize based on ORIGIN traffic (actual server load)
|
||||
let category: 'light' | 'moderate' | 'heavy' | 'very_heavy';
|
||||
let description: string;
|
||||
|
||||
if (monthlyTB < 0.5) {
|
||||
category = 'light';
|
||||
description = `~${Math.round(monthlyGB)} GB/month - Most VPS plans include sufficient bandwidth`;
|
||||
description = cdnEnabled
|
||||
? `총 ${grossMonthlyTB.toFixed(1)}TB 중 원본 서버 ~${Math.round(monthlyGB)}GB/월 (CDN ${(cdnCacheHitRate * 100).toFixed(0)}% 캐시)`
|
||||
: `~${Math.round(monthlyGB)} GB/month - Most VPS plans include sufficient bandwidth`;
|
||||
} else if (monthlyTB < 2) {
|
||||
category = 'moderate';
|
||||
description = `~${monthlyTB.toFixed(1)} TB/month - Check provider bandwidth limits`;
|
||||
description = cdnEnabled
|
||||
? `총 ${grossMonthlyTB.toFixed(1)}TB 중 원본 서버 ~${monthlyTB.toFixed(1)}TB/월 (CDN ${(cdnCacheHitRate * 100).toFixed(0)}% 캐시)`
|
||||
: `~${monthlyTB.toFixed(1)} TB/month - Check provider bandwidth limits`;
|
||||
} else if (monthlyTB < 6) {
|
||||
category = 'heavy';
|
||||
description = `~${monthlyTB.toFixed(1)} TB/month - Prefer providers with generous bandwidth (Linode: 1-6TB included)`;
|
||||
description = cdnEnabled
|
||||
? `총 ${grossMonthlyTB.toFixed(1)}TB 중 원본 서버 ~${monthlyTB.toFixed(1)}TB/월 (CDN ${(cdnCacheHitRate * 100).toFixed(0)}% 캐시) - 대역폭 여유 있는 플랜 권장`
|
||||
: `~${monthlyTB.toFixed(1)} TB/month - Prefer providers with generous bandwidth (Linode: 1-6TB included)`;
|
||||
} else {
|
||||
category = 'very_heavy';
|
||||
description = `~${monthlyTB.toFixed(1)} TB/month - HIGH BANDWIDTH: Linode strongly recommended for cost savings`;
|
||||
description = cdnEnabled
|
||||
? `총 ${grossMonthlyTB.toFixed(1)}TB 중 원본 서버 ~${monthlyTB.toFixed(1)}TB/월 (CDN ${(cdnCacheHitRate * 100).toFixed(0)}% 캐시) - 고대역폭 플랜 필수`
|
||||
: `~${monthlyTB.toFixed(1)} TB/month - HIGH BANDWIDTH: Linode strongly recommended for cost savings`;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -200,7 +248,12 @@ export function estimateBandwidth(concurrentUsers: number, useCase: string, traf
|
||||
description,
|
||||
estimated_dau_min: estimatedDauMin,
|
||||
estimated_dau_max: estimatedDauMax,
|
||||
active_ratio: activeUserRatio
|
||||
active_ratio: activeUserRatio,
|
||||
// CDN breakdown
|
||||
cdn_enabled: cdnEnabled,
|
||||
cdn_cache_hit_rate: cdnCacheHitRate,
|
||||
gross_monthly_tb: Math.round(grossMonthlyTB * 10) / 10,
|
||||
origin_monthly_tb: Math.round(originMonthlyTB * 10) / 10
|
||||
};
|
||||
}
|
||||
|
||||
@@ -304,14 +357,24 @@ export function calculateBandwidthInfo(
|
||||
const overageCost = isKorean ? roundKrw100(overageCostUsd) : Math.round(overageCostUsd * 100) / 100;
|
||||
const totalCost = isKorean ? roundKrw100(totalCostUsd) : Math.round(totalCostUsd * 100) / 100;
|
||||
|
||||
// CDN savings calculation
|
||||
const cdnEnabled = bandwidthEstimate.cdn_enabled;
|
||||
const cdnCacheHitRate = bandwidthEstimate.cdn_cache_hit_rate;
|
||||
const grossMonthlyTb = bandwidthEstimate.gross_monthly_tb;
|
||||
const cdnSavingsTb = cdnEnabled ? grossMonthlyTb - estimatedTb : 0;
|
||||
const cdnSavingsCostUsd = cdnSavingsTb * overagePerTbUsd;
|
||||
const cdnSavingsCost = isKorean ? roundKrw100(cdnSavingsCostUsd) : Math.round(cdnSavingsCostUsd * 100) / 100;
|
||||
|
||||
let warning: string | undefined;
|
||||
if (overageTb > includedTb) {
|
||||
const costStr = isKorean ? `₩${overageCost.toLocaleString()}` : `$${overageCost.toFixed(0)}`;
|
||||
warning = `⚠️ 예상 트래픽(${estimatedTb.toFixed(1)}TB)이 기본 포함량(${includedTb}TB)의 2배 이상입니다. 상위 플랜을 고려하세요.`;
|
||||
warning = cdnEnabled
|
||||
? `⚠️ CDN 적용 후에도 원본 서버 트래픽(${estimatedTb.toFixed(1)}TB)이 기본 포함량(${includedTb}TB)의 2배 이상입니다. 상위 플랜을 고려하세요.`
|
||||
: `⚠️ 예상 트래픽(${estimatedTb.toFixed(1)}TB)이 기본 포함량(${includedTb}TB)의 2배 이상입니다. 상위 플랜을 고려하세요.`;
|
||||
} else if (overageTb > 0) {
|
||||
const costStr = isKorean ? `₩${overageCost.toLocaleString()}` : `$${overageCost.toFixed(0)}`;
|
||||
warning = isKorean
|
||||
? `예상 초과 트래픽: ${overageTb.toFixed(1)}TB (추가 비용 ~${costStr}/월)`
|
||||
warning = cdnEnabled
|
||||
? `예상 초과 트래픽: ${overageTb.toFixed(1)}TB (CDN ${(cdnCacheHitRate * 100).toFixed(0)}% 캐시 적용 후, 추가 비용 ~${costStr}/월)`
|
||||
: `예상 초과 트래픽: ${overageTb.toFixed(1)}TB (추가 비용 ~${costStr}/월)`;
|
||||
}
|
||||
|
||||
@@ -324,6 +387,12 @@ export function calculateBandwidthInfo(
|
||||
estimated_overage_cost: overageCost,
|
||||
total_estimated_cost: totalCost,
|
||||
currency,
|
||||
warning
|
||||
warning,
|
||||
// CDN breakdown
|
||||
cdn_enabled: cdnEnabled,
|
||||
cdn_cache_hit_rate: cdnCacheHitRate,
|
||||
gross_monthly_tb: Math.round(grossMonthlyTb * 10) / 10,
|
||||
cdn_savings_tb: Math.round(cdnSavingsTb * 10) / 10,
|
||||
cdn_savings_cost: cdnSavingsCost
|
||||
};
|
||||
}
|
||||
|
||||
@@ -69,6 +69,14 @@ export function generateCacheKey(req: RecommendRequest): string {
|
||||
parts.push(`lang:${req.lang}`);
|
||||
}
|
||||
|
||||
// Include CDN options in cache key
|
||||
if (req.cdn_enabled !== undefined) {
|
||||
parts.push(`cdn:${req.cdn_enabled}`);
|
||||
}
|
||||
if (req.cdn_cache_hit_rate !== undefined) {
|
||||
parts.push(`cdnrate:${req.cdn_cache_hit_rate}`);
|
||||
}
|
||||
|
||||
return `recommend:${parts.join('|')}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,10 +25,12 @@ export {
|
||||
findUseCaseConfig,
|
||||
getDauMultiplier,
|
||||
getActiveUserRatio,
|
||||
getCdnCacheHitRate,
|
||||
estimateBandwidth,
|
||||
getProviderBandwidthAllocation,
|
||||
calculateBandwidthInfo
|
||||
} from './bandwidth';
|
||||
export type { CdnOptions } from './bandwidth';
|
||||
|
||||
// Cache and rate limiting utilities
|
||||
export {
|
||||
|
||||
Reference in New Issue
Block a user