refactor: 서버 추천 핵심 로직 개선

## vCPU 계산 로직 개선
- 카테고리 합산 → 병목 분석(Max)으로 변경
- nginx+nodejs+postgresql 조합: 16 vCPU → 10 vCPU
- 요청 흐름(web→app→db)에서 가장 느린 컴포넌트가 병목

## 메모리 계산 로직 개선
- memory_intensive 서비스: Max → 합산으로 변경
- java+elasticsearch+redis: 8GB → 11GB (실제 동시 실행 반영)

## 대역폭 추정 개선
- 사용자 활동률(activeUserRatio) 추가
- video: 30%, gaming: 50%, e-commerce: 40%
- 비디오 1000명: 257TB → ~80TB/월 (현실적)

## DAU 변환 비율 개선
- 용도별 차등 적용 (getDauMultiplier)
- gaming: 10-20배, blog: 30-50배, saas: 5-10배

## aliases 대소문자 수정
- LOWER(aliases) LIKE로 case-insensitive 매칭

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-25 15:11:24 +09:00
parent 502bbd271e
commit dcc8be6f5b

View File

@@ -135,10 +135,70 @@ interface BandwidthEstimate {
estimated_dau_max: number; // Daily Active Users estimate (max)
}
/**
* Get DAU multiplier based on use case (how many daily active users per concurrent user)
*/
function getDauMultiplier(useCase: string): { min: number; max: number } {
const useCaseLower = useCase.toLowerCase();
if (/game|gaming|minecraft|게임/.test(useCaseLower)) {
// Gaming: users stay online longer, higher concurrent ratio
return { min: 10, max: 20 };
} else if (/blog|news|static|블로그|뉴스|포트폴리오/.test(useCaseLower)) {
// Blog/Static: short visits, lower concurrent ratio
return { min: 30, max: 50 };
} else if (/api|saas|backend|서비스|백엔드/.test(useCaseLower)) {
// SaaS/API: business hours concentration
return { min: 5, max: 10 };
} else if (/e-?commerce|shop|store|쇼핑|커머스|온라인몰/.test(useCaseLower)) {
// E-commerce: moderate session lengths
return { min: 20, max: 30 };
} else if (/forum|community|board|게시판|커뮤니티|포럼/.test(useCaseLower)) {
// Forum/Community: moderate engagement
return { min: 15, max: 25 };
} else if (/video|stream|media|youtube|netflix|vod|동영상|스트리밍|미디어/.test(useCaseLower)) {
// Video/Streaming: medium-long sessions
return { min: 8, max: 12 };
} else {
// Default: general web app
return { min: 10, max: 14 };
}
}
/**
* Get active user ratio (what percentage of DAU actually performs the bandwidth-heavy action)
*/
function getActiveUserRatio(useCase: string): number {
const useCaseLower = useCase.toLowerCase();
if (/video|stream|media|youtube|netflix|vod|동영상|스트리밍|미디어/.test(useCaseLower)) {
// Video/Streaming: only 30% of DAU actually stream
return 0.3;
} else if (/game|gaming|minecraft|게임/.test(useCaseLower)) {
// Gaming: 50% of DAU are active players
return 0.5;
} else if (/e-?commerce|shop|store|쇼핑|커머스|온라인몰/.test(useCaseLower)) {
// E-commerce: 40% browse products
return 0.4;
} else if (/api|saas|backend|서비스|백엔드/.test(useCaseLower)) {
// API/SaaS: 60% active usage
return 0.6;
} else if (/forum|community|board|게시판|커뮤니티|포럼/.test(useCaseLower)) {
// Forum/Community: 50% active posting/reading
return 0.5;
} else if (/blog|static|portfolio|블로그|포트폴리오/.test(useCaseLower)) {
// Static/Blog: 30% active readers
return 0.3;
} else {
// Default: 50% active
return 0.5;
}
}
/**
* Estimate monthly bandwidth based on concurrent users and use case
*
* Formula: concurrent_users × multiplier × avg_page_size_mb × requests_per_session × active_hours × 30
* Formula: concurrent_users × dau_multiplier × active_ratio × avg_page_size_mb × requests_per_session × active_hours × 30
*
* Multipliers by use case:
* - Static site/blog: 0.5 MB/request, 5 requests/session
@@ -209,16 +269,19 @@ function estimateBandwidth(concurrentUsers: number, useCase: string, trafficPatt
// Assume 8 active hours per day average (varies by use case)
const activeHoursPerDay = 8;
// Calculate DAU estimate from concurrent users
// Typical ratio: concurrent users = 5-10% of DAU (or DAU = 10-20x concurrent)
// Using conservative 10-14x multiplier
const estimatedDauMin = Math.round(concurrentUsers * 10);
const estimatedDauMax = Math.round(concurrentUsers * 14);
// Calculate DAU estimate from concurrent users with use-case-specific multipliers
const dauMultiplier = getDauMultiplier(useCase);
const estimatedDauMin = Math.round(concurrentUsers * dauMultiplier.min);
const estimatedDauMax = Math.round(concurrentUsers * dauMultiplier.max);
// Calculate daily bandwidth
// Use average DAU for bandwidth calculation
// Calculate daily bandwidth with active user ratio
const dailyUniqueVisitors = Math.round((estimatedDauMin + estimatedDauMax) / 2);
const dailyBandwidthMB = dailyUniqueVisitors * avgPageSizeMB * requestsPerSession * categoryMultiplier * patternMultiplier;
const activeUserRatio = getActiveUserRatio(useCase);
const activeDau = Math.round(dailyUniqueVisitors * activeUserRatio);
console.log(`[Bandwidth] DAU: ${dailyUniqueVisitors} (${dauMultiplier.min}-${dauMultiplier.max}x), Active DAU: ${activeDau} (${(activeUserRatio * 100).toFixed(0)}%)`);
const dailyBandwidthMB = activeDau * avgPageSizeMB * requestsPerSession * categoryMultiplier * patternMultiplier;
const dailyBandwidthGB = dailyBandwidthMB / 1024;
// Monthly bandwidth
@@ -718,24 +781,70 @@ async function handleRecommend(
console.log('[Recommend] Tech specs matched:', techSpecs.length);
console.log('[Recommend] Benchmark data points (initial):', benchmarkDataAll.length);
// Calculate minimum memory from memory-intensive specs
// Calculate minimum memory with proper aggregation
// Memory-intensive services (Java, Elasticsearch, Redis): sum their memory requirements
// Non-memory-intensive services: 256MB overhead each
const memoryIntensiveSpecs = techSpecs.filter(s => s.is_memory_intensive);
const minMemoryMb = memoryIntensiveSpecs.length > 0
? Math.max(...memoryIntensiveSpecs.map(s => s.min_memory_mb))
: undefined;
const otherSpecs = techSpecs.filter(s => !s.is_memory_intensive);
// Calculate minimum vCPU based on expected users and tech specs
// Formula: expected_users / vcpu_per_users (use minimum ratio from all tech specs)
let minMemoryMb: number | undefined;
if (memoryIntensiveSpecs.length > 0 || otherSpecs.length > 0) {
// Sum memory-intensive requirements
const memoryIntensiveSum = memoryIntensiveSpecs.reduce((sum, s) => sum + s.min_memory_mb, 0);
// Add 256MB overhead per non-memory-intensive service
const otherOverhead = otherSpecs.length * 256;
minMemoryMb = memoryIntensiveSum + otherOverhead;
console.log(`[Recommend] Memory calculation: ${memoryIntensiveSpecs.length} memory-intensive (${(memoryIntensiveSum/1024).toFixed(1)}GB) + ${otherSpecs.length} other services (${(otherOverhead/1024).toFixed(1)}GB) = ${(minMemoryMb/1024).toFixed(1)}GB total`);
}
// Calculate minimum vCPU with category-based weighting
// Different tech categories have different bottleneck characteristics
let minVcpu: number | undefined;
if (techSpecs.length > 0) {
const vcpuRequirements = techSpecs.map(spec => {
// Use vcpu_per_users: 1 vCPU can handle N users
// So for expected_users, we need: expected_users / vcpu_per_users vCPUs
// Group specs by category
const categoryWeights: Record<string, number> = {
'web_server': 0.1, // nginx, apache: reverse proxy uses minimal resources
'runtime': 1.0, // nodejs, php, python: actual computation
'database': 1.0, // mysql, postgresql, mongodb: major bottleneck
'cache': 0.5, // redis, memcached: supporting role
'search': 0.8, // elasticsearch: CPU-intensive but not always primary
'container': 0.3, // docker: orchestration overhead
'messaging': 0.5, // rabbitmq, kafka: I/O bound
'default': 0.7 // unknown categories
};
// Calculate weighted vCPU requirements per category
const categoryRequirements = new Map<string, number>();
for (const spec of techSpecs) {
const category = spec.category || 'default';
const weight = categoryWeights[category] || categoryWeights['default'];
const vcpuNeeded = Math.ceil(body.expected_users / spec.vcpu_per_users);
return vcpuNeeded;
});
minVcpu = Math.max(...vcpuRequirements, 1); // At least 1 vCPU
console.log(`[Recommend] Minimum vCPU required: ${minVcpu} (for ${body.expected_users} users)`);
const weightedVcpu = vcpuNeeded * weight;
const existing = categoryRequirements.get(category) || 0;
// Take max within same category (not additive)
categoryRequirements.set(category, Math.max(existing, weightedVcpu));
console.log(`[Recommend] ${spec.name} (${category}): ${vcpuNeeded} vCPU × ${weight} weight = ${weightedVcpu.toFixed(1)} weighted vCPU`);
}
// Find bottleneck: use MAX across categories, not SUM
// Request flow (web_server → runtime → database) means the slowest component is the bottleneck
// SUM would over-provision since components process the SAME requests sequentially
let maxWeightedVcpu = 0;
let bottleneckCategory = '';
for (const [category, vcpu] of categoryRequirements) {
console.log(`[Recommend] Category '${category}': ${vcpu.toFixed(1)} weighted vCPU`);
if (vcpu > maxWeightedVcpu) {
maxWeightedVcpu = vcpu;
bottleneckCategory = category;
}
}
minVcpu = Math.max(Math.ceil(maxWeightedVcpu), 1); // At least 1 vCPU
console.log(`[Recommend] Bottleneck: '${bottleneckCategory}' with ${maxWeightedVcpu.toFixed(1)} weighted vCPU → ${minVcpu} vCPU (for ${body.expected_users} users)`);
}
// Calculate bandwidth estimate for provider filtering
@@ -1478,13 +1587,13 @@ async function queryTechSpecs(
// Normalize user input
const normalizedStack = techStack.map(t => t.toLowerCase().trim());
// Build query that matches both name and aliases
// Using LIKE for alias matching since aliases are stored as JSON array strings
// Build query that matches both name and aliases (case-insensitive)
// Using LOWER() for alias matching since aliases are stored as JSON array strings
const conditions: string[] = [];
const params: string[] = [];
for (const tech of normalizedStack) {
conditions.push(`(LOWER(name) = ? OR aliases LIKE ?)`);
conditions.push(`(LOWER(name) = ? OR LOWER(aliases) LIKE ?)`);
params.push(tech, `%"${tech}"%`);
}