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)
|
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
|
* 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:
|
* Multipliers by use case:
|
||||||
* - Static site/blog: 0.5 MB/request, 5 requests/session
|
* - 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)
|
// Assume 8 active hours per day average (varies by use case)
|
||||||
const activeHoursPerDay = 8;
|
const activeHoursPerDay = 8;
|
||||||
|
|
||||||
// Calculate DAU estimate from concurrent users
|
// Calculate DAU estimate from concurrent users with use-case-specific multipliers
|
||||||
// Typical ratio: concurrent users = 5-10% of DAU (or DAU = 10-20x concurrent)
|
const dauMultiplier = getDauMultiplier(useCase);
|
||||||
// Using conservative 10-14x multiplier
|
const estimatedDauMin = Math.round(concurrentUsers * dauMultiplier.min);
|
||||||
const estimatedDauMin = Math.round(concurrentUsers * 10);
|
const estimatedDauMax = Math.round(concurrentUsers * dauMultiplier.max);
|
||||||
const estimatedDauMax = Math.round(concurrentUsers * 14);
|
|
||||||
|
|
||||||
// Calculate daily bandwidth
|
// Calculate daily bandwidth with active user ratio
|
||||||
// Use average DAU for bandwidth calculation
|
|
||||||
const dailyUniqueVisitors = Math.round((estimatedDauMin + estimatedDauMax) / 2);
|
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;
|
const dailyBandwidthGB = dailyBandwidthMB / 1024;
|
||||||
|
|
||||||
// Monthly bandwidth
|
// Monthly bandwidth
|
||||||
@@ -718,24 +781,70 @@ async function handleRecommend(
|
|||||||
console.log('[Recommend] Tech specs matched:', techSpecs.length);
|
console.log('[Recommend] Tech specs matched:', techSpecs.length);
|
||||||
console.log('[Recommend] Benchmark data points (initial):', benchmarkDataAll.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 memoryIntensiveSpecs = techSpecs.filter(s => s.is_memory_intensive);
|
||||||
const minMemoryMb = memoryIntensiveSpecs.length > 0
|
const otherSpecs = techSpecs.filter(s => !s.is_memory_intensive);
|
||||||
? Math.max(...memoryIntensiveSpecs.map(s => s.min_memory_mb))
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
// Calculate minimum vCPU based on expected users and tech specs
|
let minMemoryMb: number | undefined;
|
||||||
// Formula: expected_users / vcpu_per_users (use minimum ratio from all tech specs)
|
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;
|
let minVcpu: number | undefined;
|
||||||
if (techSpecs.length > 0) {
|
if (techSpecs.length > 0) {
|
||||||
const vcpuRequirements = techSpecs.map(spec => {
|
// Group specs by category
|
||||||
// Use vcpu_per_users: 1 vCPU can handle N users
|
const categoryWeights: Record<string, number> = {
|
||||||
// So for expected_users, we need: expected_users / vcpu_per_users vCPUs
|
'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);
|
const vcpuNeeded = Math.ceil(body.expected_users / spec.vcpu_per_users);
|
||||||
return vcpuNeeded;
|
const weightedVcpu = vcpuNeeded * weight;
|
||||||
});
|
|
||||||
minVcpu = Math.max(...vcpuRequirements, 1); // At least 1 vCPU
|
const existing = categoryRequirements.get(category) || 0;
|
||||||
console.log(`[Recommend] Minimum vCPU required: ${minVcpu} (for ${body.expected_users} users)`);
|
// 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
|
// Calculate bandwidth estimate for provider filtering
|
||||||
@@ -1478,13 +1587,13 @@ async function queryTechSpecs(
|
|||||||
// Normalize user input
|
// Normalize user input
|
||||||
const normalizedStack = techStack.map(t => t.toLowerCase().trim());
|
const normalizedStack = techStack.map(t => t.toLowerCase().trim());
|
||||||
|
|
||||||
// Build query that matches both name and aliases
|
// Build query that matches both name and aliases (case-insensitive)
|
||||||
// Using LIKE for alias matching since aliases are stored as JSON array strings
|
// Using LOWER() for alias matching since aliases are stored as JSON array strings
|
||||||
const conditions: string[] = [];
|
const conditions: string[] = [];
|
||||||
const params: string[] = [];
|
const params: string[] = [];
|
||||||
|
|
||||||
for (const tech of normalizedStack) {
|
for (const tech of normalizedStack) {
|
||||||
conditions.push(`(LOWER(name) = ? OR aliases LIKE ?)`);
|
conditions.push(`(LOWER(name) = ? OR LOWER(aliases) LIKE ?)`);
|
||||||
params.push(tech, `%"${tech}"%`);
|
params.push(tech, `%"${tech}"%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user