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:
159
src/index.ts
159
src/index.ts
@@ -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}"%`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user