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:
73
CLAUDE.md
73
CLAUDE.md
@@ -56,7 +56,7 @@ src/
|
|||||||
│ └── report.ts # GET /api/recommend/report
|
│ └── report.ts # GET /api/recommend/report
|
||||||
└── __tests__/
|
└── __tests__/
|
||||||
├── utils.test.ts # Validation & security tests (27 tests)
|
├── utils.test.ts # Validation & security tests (27 tests)
|
||||||
└── bandwidth.test.ts # Bandwidth estimation tests (28 tests)
|
└── bandwidth.test.ts # Bandwidth estimation tests (32 tests, including CDN)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Data Flow
|
### Key Data Flow
|
||||||
@@ -115,6 +115,44 @@ Estimates monthly bandwidth based on use_case patterns:
|
|||||||
|
|
||||||
Heavy bandwidth (>1TB/month) triggers warning about overage costs.
|
Heavy bandwidth (>1TB/month) triggers warning about overage costs.
|
||||||
|
|
||||||
|
### CDN Cache Hit Rate (`bandwidth.ts`)
|
||||||
|
|
||||||
|
CDN 사용 시 원본 서버 트래픽을 자동 계산하여 실제 비용을 추정합니다.
|
||||||
|
|
||||||
|
**API Parameters**:
|
||||||
|
- `cdn_enabled`: CDN 사용 여부 (기본: `true`)
|
||||||
|
- `cdn_cache_hit_rate`: 캐시 히트율 override (0.0-1.0, 기본: use_case별 자동)
|
||||||
|
|
||||||
|
**Use Case별 기본 캐시 히트율**:
|
||||||
|
|
||||||
|
| Use Case | 캐시 히트율 | 설명 |
|
||||||
|
|----------|-------------|------|
|
||||||
|
| video/streaming | 92% | 동일 영상 반복 시청 |
|
||||||
|
| blog/static | 90% | 대부분 정적 콘텐츠 |
|
||||||
|
| file/download | 85% | 소프트웨어/에셋 다운로드 |
|
||||||
|
| ecommerce | 70% | 상품 이미지 캐시 가능 |
|
||||||
|
| forum | 60% | 정적/동적 혼합 |
|
||||||
|
| api/saas | 30% | 동적 응답 위주 |
|
||||||
|
| gaming | 20% | 실시간 통신 |
|
||||||
|
| chat | 10% | 실시간 메시지 |
|
||||||
|
|
||||||
|
**응답 필드** (`bandwidth_estimate`):
|
||||||
|
- `cdn_enabled`: CDN 사용 여부
|
||||||
|
- `cdn_cache_hit_rate`: 적용된 캐시 히트율
|
||||||
|
- `gross_monthly_tb`: CDN 적용 전 총 트래픽
|
||||||
|
- `origin_monthly_tb`: CDN 적용 후 원본 서버 트래픽
|
||||||
|
|
||||||
|
**응답 필드** (`bandwidth_info`):
|
||||||
|
- `cdn_savings_tb`: CDN으로 절감된 트래픽 (TB)
|
||||||
|
- `cdn_savings_cost`: CDN으로 절감된 비용 ($)
|
||||||
|
|
||||||
|
**비용 절감 예시** (10,000 동시접속 video streaming):
|
||||||
|
```
|
||||||
|
CDN 없음: 총 2,966TB → 원본 2,966TB → 초과비용 $18,370
|
||||||
|
CDN 92%: 총 2,966TB → 원본 237TB → 초과비용 $1,464
|
||||||
|
절감: 2,729TB 절감, $16,906 절감 (92%)
|
||||||
|
```
|
||||||
|
|
||||||
### Flexible Region Matching
|
### Flexible Region Matching
|
||||||
|
|
||||||
Both `/api/recommend` and `/api/servers` use `buildFlexibleRegionConditionsAnvil()` for anvil_regions:
|
Both `/api/recommend` and `/api/servers` use `buildFlexibleRegionConditionsAnvil()` for anvil_regions:
|
||||||
@@ -266,6 +304,15 @@ curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/recommend -
|
|||||||
# Server list with filters (supports flexible region: korea, seoul, tokyo, etc.)
|
# Server list with filters (supports flexible region: korea, seoul, tokyo, etc.)
|
||||||
curl -s "https://cloud-orchestrator.kappa-d8e.workers.dev/api/servers?region=korea&minCpu=4" | jq .
|
curl -s "https://cloud-orchestrator.kappa-d8e.workers.dev/api/servers?region=korea&minCpu=4" | jq .
|
||||||
|
|
||||||
|
# CDN cache hit rate - default (use_case별 자동 적용)
|
||||||
|
curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/recommend -H "Content-Type: application/json" -d '{"tech_stack":["nginx"],"expected_users":10000,"use_case":"video streaming","region_preference":["japan"]}' | jq '.bandwidth_estimate | {cdn_enabled, cdn_cache_hit_rate, gross_monthly_tb, origin_monthly_tb}'
|
||||||
|
|
||||||
|
# CDN disabled (원본 서버 전체 트래픽)
|
||||||
|
curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/recommend -H "Content-Type: application/json" -d '{"tech_stack":["nginx"],"expected_users":10000,"use_case":"video streaming","region_preference":["japan"],"cdn_enabled":false}' | jq '.bandwidth_estimate'
|
||||||
|
|
||||||
|
# Custom CDN cache hit rate (80%)
|
||||||
|
curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/recommend -H "Content-Type: application/json" -d '{"tech_stack":["nginx"],"expected_users":10000,"use_case":"video streaming","region_preference":["japan"],"cdn_cache_hit_rate":0.80}' | jq '.recommendations[0].bandwidth_info | {cdn_cache_hit_rate, cdn_savings_tb, cdn_savings_cost}'
|
||||||
|
|
||||||
# HTML Report (encode recommendation result as base64)
|
# HTML Report (encode recommendation result as base64)
|
||||||
# 1. Get recommendation and save to variable
|
# 1. Get recommendation and save to variable
|
||||||
RESULT=$(curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/recommend -H "Content-Type: application/json" -d '{"tech_stack":["nodejs"],"expected_users":500,"use_case":"simple api","lang":"ko"}')
|
RESULT=$(curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/recommend -H "Content-Type: application/json" -d '{"tech_stack":["nodejs"],"expected_users":500,"use_case":"simple api","lang":"ko"}')
|
||||||
@@ -277,7 +324,29 @@ echo $REPORT_URL
|
|||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
|
|
||||||
### Major Architecture Refactoring (Latest)
|
### CDN Cache Hit Rate (Latest)
|
||||||
|
|
||||||
|
**New Feature**: CDN 캐시 히트율 기반 원본 서버 트래픽 및 비용 계산
|
||||||
|
|
||||||
|
**API Parameters Added**:
|
||||||
|
- `cdn_enabled`: CDN 사용 여부 (기본: true)
|
||||||
|
- `cdn_cache_hit_rate`: 캐시 히트율 override (0.0-1.0)
|
||||||
|
|
||||||
|
**Use Case별 기본 캐시 히트율**:
|
||||||
|
- video/streaming: 92%, blog/static: 90%, file: 85%
|
||||||
|
- ecommerce: 70%, forum: 60%, api: 30%, gaming: 20%, chat: 10%
|
||||||
|
|
||||||
|
**Response Fields Added**:
|
||||||
|
- `bandwidth_estimate`: cdn_enabled, cdn_cache_hit_rate, gross_monthly_tb, origin_monthly_tb
|
||||||
|
- `bandwidth_info`: cdn_savings_tb, cdn_savings_cost
|
||||||
|
|
||||||
|
**Cost Impact Example** (10K concurrent video streaming):
|
||||||
|
- Without CDN: $18,370 overage
|
||||||
|
- With CDN 92%: $1,464 overage (92% savings)
|
||||||
|
|
||||||
|
**Tests Added**: 4 new CDN-specific tests (total 59 tests)
|
||||||
|
|
||||||
|
### Major Architecture Refactoring
|
||||||
|
|
||||||
**Security Hardening**:
|
**Security Hardening**:
|
||||||
- Fixed XSS vulnerability in HTML reports with `escapeHtml()`
|
- Fixed XSS vulnerability in HTML reports with `escapeHtml()`
|
||||||
|
|||||||
@@ -3,19 +3,61 @@ import { estimateBandwidth } from '../utils';
|
|||||||
|
|
||||||
describe('estimateBandwidth', () => {
|
describe('estimateBandwidth', () => {
|
||||||
describe('Video streaming use cases', () => {
|
describe('Video streaming use cases', () => {
|
||||||
it('should estimate bandwidth for video streaming', () => {
|
it('should estimate bandwidth for video streaming with CDN', () => {
|
||||||
const result = estimateBandwidth(100, 'video streaming platform');
|
const result = estimateBandwidth(100, 'video streaming platform');
|
||||||
// Video streaming produces very_heavy bandwidth (>6TB/month)
|
// Video streaming with CDN (92% cache) reduces origin traffic significantly
|
||||||
expect(result.category).toBe('very_heavy');
|
expect(result.cdn_enabled).toBe(true);
|
||||||
expect(result.monthly_tb).toBeGreaterThan(6);
|
expect(result.cdn_cache_hit_rate).toBe(0.92);
|
||||||
|
expect(result.gross_monthly_tb).toBeGreaterThan(6); // Gross traffic is very_heavy
|
||||||
|
expect(result.origin_monthly_tb).toBeLessThan(result.gross_monthly_tb); // Origin is reduced
|
||||||
|
expect(result.category).toBe('heavy'); // Origin traffic category
|
||||||
expect(result.estimated_dau_min).toBeGreaterThan(0);
|
expect(result.estimated_dau_min).toBeGreaterThan(0);
|
||||||
expect(result.estimated_dau_max).toBeGreaterThan(result.estimated_dau_min);
|
expect(result.estimated_dau_max).toBeGreaterThan(result.estimated_dau_min);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should estimate very_heavy bandwidth without CDN', () => {
|
||||||
|
const result = estimateBandwidth(100, 'video streaming platform', undefined, { enabled: false });
|
||||||
|
// Without CDN, video streaming is very_heavy
|
||||||
|
expect(result.cdn_enabled).toBe(false);
|
||||||
|
expect(result.category).toBe('very_heavy');
|
||||||
|
expect(result.monthly_tb).toBeGreaterThan(6);
|
||||||
|
expect(result.gross_monthly_tb).toBe(result.origin_monthly_tb); // No CDN reduction
|
||||||
|
});
|
||||||
|
|
||||||
it('should estimate higher bandwidth for 4K streaming', () => {
|
it('should estimate higher bandwidth for 4K streaming', () => {
|
||||||
const hd = estimateBandwidth(100, 'HD video streaming');
|
const hd = estimateBandwidth(100, 'HD video streaming');
|
||||||
const fourK = estimateBandwidth(100, '4K UHD streaming');
|
const fourK = estimateBandwidth(100, '4K UHD streaming');
|
||||||
expect(fourK.monthly_tb).toBeGreaterThan(hd.monthly_tb);
|
expect(fourK.gross_monthly_tb).toBeGreaterThan(hd.gross_monthly_tb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CDN cache hit rate', () => {
|
||||||
|
it('should apply use-case specific CDN cache hit rates', () => {
|
||||||
|
const video = estimateBandwidth(100, 'video streaming');
|
||||||
|
const blog = estimateBandwidth(100, 'personal blog');
|
||||||
|
const api = estimateBandwidth(100, 'REST API');
|
||||||
|
const gaming = estimateBandwidth(100, 'game server');
|
||||||
|
|
||||||
|
expect(video.cdn_cache_hit_rate).toBe(0.92); // Video: high cache
|
||||||
|
expect(blog.cdn_cache_hit_rate).toBe(0.90); // Blog: high cache
|
||||||
|
expect(api.cdn_cache_hit_rate).toBe(0.30); // API: low cache
|
||||||
|
expect(gaming.cdn_cache_hit_rate).toBe(0.20); // Gaming: very low cache
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow CDN cache hit rate override', () => {
|
||||||
|
const result = estimateBandwidth(100, 'video streaming', undefined, { cacheHitRate: 0.50 });
|
||||||
|
expect(result.cdn_cache_hit_rate).toBe(0.50);
|
||||||
|
expect(result.origin_monthly_tb).toBeCloseTo(result.gross_monthly_tb * 0.50, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate CDN savings correctly', () => {
|
||||||
|
const withCdn = estimateBandwidth(100, 'video streaming');
|
||||||
|
const withoutCdn = estimateBandwidth(100, 'video streaming', undefined, { enabled: false });
|
||||||
|
|
||||||
|
// CDN should reduce origin traffic significantly
|
||||||
|
expect(withCdn.origin_monthly_tb).toBeLessThan(withoutCdn.origin_monthly_tb);
|
||||||
|
// Gross should be the same
|
||||||
|
expect(withCdn.gross_monthly_tb).toBe(withoutCdn.gross_monthly_tb);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -24,49 +24,57 @@ export const USE_CASE_CONFIGS: UseCaseConfig[] = [
|
|||||||
category: 'video',
|
category: 'video',
|
||||||
patterns: /video|stream|media|youtube|netflix|vod|동영상|스트리밍|미디어/i,
|
patterns: /video|stream|media|youtube|netflix|vod|동영상|스트리밍|미디어/i,
|
||||||
dauMultiplier: { min: 8, max: 12 },
|
dauMultiplier: { min: 8, max: 12 },
|
||||||
activeRatio: 0.3
|
activeRatio: 0.3,
|
||||||
|
cdnCacheHitRate: 0.92 // 높은 캐시 히트율 (동일 영상 반복 시청)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: 'file',
|
category: 'file',
|
||||||
patterns: /download|file|storage|cdn|파일|다운로드|저장소/i,
|
patterns: /download|file|storage|cdn|파일|다운로드|저장소/i,
|
||||||
dauMultiplier: { min: 10, max: 14 },
|
dauMultiplier: { min: 10, max: 14 },
|
||||||
activeRatio: 0.5
|
activeRatio: 0.5,
|
||||||
|
cdnCacheHitRate: 0.85 // 소프트웨어/에셋 다운로드
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: 'gaming',
|
category: 'gaming',
|
||||||
patterns: /game|gaming|minecraft|게임/i,
|
patterns: /game|gaming|minecraft|게임/i,
|
||||||
dauMultiplier: { min: 10, max: 20 },
|
dauMultiplier: { min: 10, max: 20 },
|
||||||
activeRatio: 0.5
|
activeRatio: 0.5,
|
||||||
|
cdnCacheHitRate: 0.20 // 실시간 통신 위주, 캐시 어려움
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: 'api',
|
category: 'api',
|
||||||
patterns: /api|saas|backend|서비스|백엔드/i,
|
patterns: /api|saas|backend|서비스|백엔드/i,
|
||||||
dauMultiplier: { min: 5, max: 10 },
|
dauMultiplier: { min: 5, max: 10 },
|
||||||
activeRatio: 0.6
|
activeRatio: 0.6,
|
||||||
|
cdnCacheHitRate: 0.30 // 동적 응답 위주, 일부만 캐시 가능
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: 'ecommerce',
|
category: 'ecommerce',
|
||||||
patterns: /e-?commerce|shop|store|쇼핑|커머스|온라인몰/i,
|
patterns: /e-?commerce|shop|store|쇼핑|커머스|온라인몰/i,
|
||||||
dauMultiplier: { min: 20, max: 30 },
|
dauMultiplier: { min: 20, max: 30 },
|
||||||
activeRatio: 0.4
|
activeRatio: 0.4,
|
||||||
|
cdnCacheHitRate: 0.70 // 상품 이미지 캐시 가능, 동적 페이지는 낮음
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: 'forum',
|
category: 'forum',
|
||||||
patterns: /forum|community|board|게시판|커뮤니티|포럼/i,
|
patterns: /forum|community|board|게시판|커뮤니티|포럼/i,
|
||||||
dauMultiplier: { min: 15, max: 25 },
|
dauMultiplier: { min: 15, max: 25 },
|
||||||
activeRatio: 0.5
|
activeRatio: 0.5,
|
||||||
|
cdnCacheHitRate: 0.60 // 정적/동적 콘텐츠 혼합
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: 'blog',
|
category: 'blog',
|
||||||
patterns: /blog|news|static|portfolio|블로그|뉴스|포트폴리오|landing/i,
|
patterns: /blog|news|static|portfolio|블로그|뉴스|포트폴리오|landing/i,
|
||||||
dauMultiplier: { min: 30, max: 50 },
|
dauMultiplier: { min: 30, max: 50 },
|
||||||
activeRatio: 0.3
|
activeRatio: 0.3,
|
||||||
|
cdnCacheHitRate: 0.90 // 대부분 정적 콘텐츠
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: 'chat',
|
category: 'chat',
|
||||||
patterns: /chat|messaging|slack|discord|채팅|메신저/i,
|
patterns: /chat|messaging|slack|discord|채팅|메신저/i,
|
||||||
dauMultiplier: { min: 10, max: 14 },
|
dauMultiplier: { min: 10, max: 14 },
|
||||||
activeRatio: 0.7
|
activeRatio: 0.7,
|
||||||
|
cdnCacheHitRate: 0.10 // 실시간 메시지, 거의 캐시 불가
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -245,9 +245,14 @@ export async function handleRecommend(
|
|||||||
console.log(`[Recommend] Bottleneck: '${bottleneckCategory}' with ${maxWeightedVcpu.toFixed(1)} weighted vCPU → ${minVcpu} vCPU (for ${body.expected_users} users)`);
|
console.log(`[Recommend] Bottleneck: '${bottleneckCategory}' with ${maxWeightedVcpu.toFixed(1)} weighted vCPU → ${minVcpu} vCPU (for ${body.expected_users} users)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate bandwidth estimate for provider filtering
|
// Calculate bandwidth estimate for provider filtering (with CDN options)
|
||||||
const bandwidthEstimate = estimateBandwidth(body.expected_users, body.use_case, body.traffic_pattern);
|
// cdn_enabled가 명시적으로 false면 비활성화, 그 외에는 기본 활성화
|
||||||
console.log(`[Recommend] Bandwidth estimate: ${bandwidthEstimate.monthly_tb >= 1 ? bandwidthEstimate.monthly_tb + ' TB' : bandwidthEstimate.monthly_gb + ' GB'}/month (${bandwidthEstimate.category})`);
|
const cdnOptions = {
|
||||||
|
enabled: body.cdn_enabled !== false, // undefined면 true (기본값), false면 false
|
||||||
|
cacheHitRate: body.cdn_cache_hit_rate
|
||||||
|
};
|
||||||
|
const bandwidthEstimate = estimateBandwidth(body.expected_users, body.use_case, body.traffic_pattern, cdnOptions);
|
||||||
|
console.log(`[Recommend] Bandwidth estimate: Gross ${bandwidthEstimate.gross_monthly_tb}TB → Origin ${bandwidthEstimate.monthly_tb >= 1 ? bandwidthEstimate.monthly_tb + ' TB' : bandwidthEstimate.monthly_gb + ' GB'}/month (${bandwidthEstimate.category}, CDN: ${bandwidthEstimate.cdn_enabled ? `${(bandwidthEstimate.cdn_cache_hit_rate * 100).toFixed(0)}%` : 'disabled'})`);
|
||||||
|
|
||||||
// Estimate specs for VPS benchmark query (doesn't need exact candidates)
|
// Estimate specs for VPS benchmark query (doesn't need exact candidates)
|
||||||
const estimatedCores = minVcpu || 2;
|
const estimatedCores = minVcpu || 2;
|
||||||
@@ -358,6 +363,11 @@ export async function handleRecommend(
|
|||||||
description: bandwidthEstimate.description,
|
description: bandwidthEstimate.description,
|
||||||
active_ratio: bandwidthEstimate.active_ratio,
|
active_ratio: bandwidthEstimate.active_ratio,
|
||||||
calculation_note: `Based on ${body.expected_users} concurrent users with ${Math.round(bandwidthEstimate.active_ratio * 100)}% active ratio`,
|
calculation_note: `Based on ${body.expected_users} concurrent users with ${Math.round(bandwidthEstimate.active_ratio * 100)}% active ratio`,
|
||||||
|
// CDN breakdown
|
||||||
|
cdn_enabled: bandwidthEstimate.cdn_enabled,
|
||||||
|
cdn_cache_hit_rate: bandwidthEstimate.cdn_cache_hit_rate,
|
||||||
|
gross_monthly_tb: bandwidthEstimate.gross_monthly_tb,
|
||||||
|
origin_monthly_tb: bandwidthEstimate.origin_monthly_tb,
|
||||||
},
|
},
|
||||||
total_candidates: candidates.length,
|
total_candidates: candidates.length,
|
||||||
cached: false,
|
cached: false,
|
||||||
|
|||||||
17
src/types.ts
17
src/types.ts
@@ -26,6 +26,9 @@ export interface RecommendRequest {
|
|||||||
region_preference?: string[];
|
region_preference?: string[];
|
||||||
budget_limit?: number;
|
budget_limit?: number;
|
||||||
lang?: 'en' | 'zh' | 'ja' | 'ko'; // Response language
|
lang?: 'en' | 'zh' | 'ja' | 'ko'; // Response language
|
||||||
|
// CDN options
|
||||||
|
cdn_enabled?: boolean; // CDN 사용 여부 (기본: true - use_case별 자동 추정)
|
||||||
|
cdn_cache_hit_rate?: number; // 캐시 히트율 (0.0-1.0, 기본: use_case별 자동 추정)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExchangeRateCache {
|
export interface ExchangeRateCache {
|
||||||
@@ -59,12 +62,18 @@ export interface BandwidthInfo {
|
|||||||
included_transfer_tb: number; // 기본 포함 트래픽 (TB/월)
|
included_transfer_tb: number; // 기본 포함 트래픽 (TB/월)
|
||||||
overage_cost_per_gb: number; // 초과 비용 ($/GB 또는 ₩/GB)
|
overage_cost_per_gb: number; // 초과 비용 ($/GB 또는 ₩/GB)
|
||||||
overage_cost_per_tb: number; // 초과 비용 ($/TB 또는 ₩/TB)
|
overage_cost_per_tb: number; // 초과 비용 ($/TB 또는 ₩/TB)
|
||||||
estimated_monthly_tb: number; // 예상 월간 사용량 (TB)
|
estimated_monthly_tb: number; // 예상 월간 사용량 (TB) - CDN 적용 후 원본 서버
|
||||||
estimated_overage_tb: number; // 예상 초과량 (TB)
|
estimated_overage_tb: number; // 예상 초과량 (TB)
|
||||||
estimated_overage_cost: number; // 예상 초과 비용
|
estimated_overage_cost: number; // 예상 초과 비용
|
||||||
total_estimated_cost: number; // 총 예상 비용 (서버 + 트래픽)
|
total_estimated_cost: number; // 총 예상 비용 (서버 + 트래픽)
|
||||||
currency: 'USD' | 'KRW'; // 통화
|
currency: 'USD' | 'KRW'; // 통화
|
||||||
warning?: string; // 트래픽 관련 경고
|
warning?: string; // 트래픽 관련 경고
|
||||||
|
// CDN breakdown
|
||||||
|
cdn_enabled?: boolean; // CDN 사용 여부
|
||||||
|
cdn_cache_hit_rate?: number; // CDN 캐시 히트율 (0.0-1.0)
|
||||||
|
gross_monthly_tb?: number; // CDN 적용 전 총 트래픽 (TB)
|
||||||
|
cdn_savings_tb?: number; // CDN으로 절감된 트래픽 (TB)
|
||||||
|
cdn_savings_cost?: number; // CDN으로 절감된 비용
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AvailableRegion {
|
export interface AvailableRegion {
|
||||||
@@ -160,6 +169,11 @@ export interface BandwidthEstimate {
|
|||||||
estimated_dau_min: number; // Daily Active Users estimate (min)
|
estimated_dau_min: number; // Daily Active Users estimate (min)
|
||||||
estimated_dau_max: number; // Daily Active Users estimate (max)
|
estimated_dau_max: number; // Daily Active Users estimate (max)
|
||||||
active_ratio: number; // Active user ratio (0.0-1.0)
|
active_ratio: number; // Active user ratio (0.0-1.0)
|
||||||
|
// CDN traffic breakdown
|
||||||
|
cdn_enabled: boolean; // CDN 사용 여부
|
||||||
|
cdn_cache_hit_rate: number; // 캐시 히트율 (0.0-1.0)
|
||||||
|
gross_monthly_tb: number; // CDN 적용 전 총 트래픽 (TB)
|
||||||
|
origin_monthly_tb: number; // CDN 적용 후 원본 서버 트래픽 (TB)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use case configuration for bandwidth estimation and user metrics
|
// Use case configuration for bandwidth estimation and user metrics
|
||||||
@@ -168,6 +182,7 @@ export interface UseCaseConfig {
|
|||||||
patterns: RegExp;
|
patterns: RegExp;
|
||||||
dauMultiplier: { min: number; max: number };
|
dauMultiplier: { min: number; max: number };
|
||||||
activeRatio: number;
|
activeRatio: number;
|
||||||
|
cdnCacheHitRate: number; // 기본 CDN 캐시 히트율 (0.0-1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AIRecommendationResponse {
|
export interface AIRecommendationResponse {
|
||||||
|
|||||||
@@ -22,10 +22,19 @@ export function findUseCaseConfig(useCase: string): UseCaseConfig {
|
|||||||
category: 'default',
|
category: 'default',
|
||||||
patterns: /.*/,
|
patterns: /.*/,
|
||||||
dauMultiplier: { min: 10, max: 14 },
|
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)
|
* 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;
|
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
|
* 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();
|
const useCaseLower = useCase.toLowerCase();
|
||||||
|
|
||||||
// Get use case configuration
|
// 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] Model: ${bandwidthModel}`);
|
||||||
console.log(`[Bandwidth] DAU: ${dailyUniqueVisitors} (${dauMultiplier.min}-${dauMultiplier.max}x), Active: ${activeDau} (${(activeUserRatio * 100).toFixed(0)}%), Daily: ${dailyBandwidthGB.toFixed(1)} GB`);
|
console.log(`[Bandwidth] DAU: ${dailyUniqueVisitors} (${dauMultiplier.min}-${dauMultiplier.max}x), Active: ${activeDau} (${(activeUserRatio * 100).toFixed(0)}%), Daily: ${dailyBandwidthGB.toFixed(1)} GB`);
|
||||||
|
|
||||||
// Monthly bandwidth
|
// Monthly bandwidth (gross - before CDN)
|
||||||
const monthlyGB = dailyBandwidthGB * 30;
|
const grossMonthlyGB = dailyBandwidthGB * 30;
|
||||||
const monthlyTB = monthlyGB / 1024;
|
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 category: 'light' | 'moderate' | 'heavy' | 'very_heavy';
|
||||||
let description: string;
|
let description: string;
|
||||||
|
|
||||||
if (monthlyTB < 0.5) {
|
if (monthlyTB < 0.5) {
|
||||||
category = 'light';
|
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) {
|
} else if (monthlyTB < 2) {
|
||||||
category = 'moderate';
|
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) {
|
} else if (monthlyTB < 6) {
|
||||||
category = 'heavy';
|
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 {
|
} else {
|
||||||
category = 'very_heavy';
|
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 {
|
return {
|
||||||
@@ -200,7 +248,12 @@ export function estimateBandwidth(concurrentUsers: number, useCase: string, traf
|
|||||||
description,
|
description,
|
||||||
estimated_dau_min: estimatedDauMin,
|
estimated_dau_min: estimatedDauMin,
|
||||||
estimated_dau_max: estimatedDauMax,
|
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 overageCost = isKorean ? roundKrw100(overageCostUsd) : Math.round(overageCostUsd * 100) / 100;
|
||||||
const totalCost = isKorean ? roundKrw100(totalCostUsd) : Math.round(totalCostUsd * 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;
|
let warning: string | undefined;
|
||||||
if (overageTb > includedTb) {
|
if (overageTb > includedTb) {
|
||||||
const costStr = isKorean ? `₩${overageCost.toLocaleString()}` : `$${overageCost.toFixed(0)}`;
|
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) {
|
} else if (overageTb > 0) {
|
||||||
const costStr = isKorean ? `₩${overageCost.toLocaleString()}` : `$${overageCost.toFixed(0)}`;
|
const costStr = isKorean ? `₩${overageCost.toLocaleString()}` : `$${overageCost.toFixed(0)}`;
|
||||||
warning = isKorean
|
warning = cdnEnabled
|
||||||
? `예상 초과 트래픽: ${overageTb.toFixed(1)}TB (추가 비용 ~${costStr}/월)`
|
? `예상 초과 트래픽: ${overageTb.toFixed(1)}TB (CDN ${(cdnCacheHitRate * 100).toFixed(0)}% 캐시 적용 후, 추가 비용 ~${costStr}/월)`
|
||||||
: `예상 초과 트래픽: ${overageTb.toFixed(1)}TB (추가 비용 ~${costStr}/월)`;
|
: `예상 초과 트래픽: ${overageTb.toFixed(1)}TB (추가 비용 ~${costStr}/월)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,6 +387,12 @@ export function calculateBandwidthInfo(
|
|||||||
estimated_overage_cost: overageCost,
|
estimated_overage_cost: overageCost,
|
||||||
total_estimated_cost: totalCost,
|
total_estimated_cost: totalCost,
|
||||||
currency,
|
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}`);
|
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('|')}`;
|
return `recommend:${parts.join('|')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,10 +25,12 @@ export {
|
|||||||
findUseCaseConfig,
|
findUseCaseConfig,
|
||||||
getDauMultiplier,
|
getDauMultiplier,
|
||||||
getActiveUserRatio,
|
getActiveUserRatio,
|
||||||
|
getCdnCacheHitRate,
|
||||||
estimateBandwidth,
|
estimateBandwidth,
|
||||||
getProviderBandwidthAllocation,
|
getProviderBandwidthAllocation,
|
||||||
calculateBandwidthInfo
|
calculateBandwidthInfo
|
||||||
} from './bandwidth';
|
} from './bandwidth';
|
||||||
|
export type { CdnOptions } from './bandwidth';
|
||||||
|
|
||||||
// Cache and rate limiting utilities
|
// Cache and rate limiting utilities
|
||||||
export {
|
export {
|
||||||
|
|||||||
Reference in New Issue
Block a user