From dcc8be6f5baa9543c0010ce0a62367f47306bdbc Mon Sep 17 00:00:00 2001 From: kappa Date: Sun, 25 Jan 2026 15:11:24 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EC=84=9C=EB=B2=84=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20=ED=95=B5=EC=8B=AC=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- src/index.ts | 159 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 134 insertions(+), 25 deletions(-) diff --git a/src/index.ts b/src/index.ts index b4095f3..2d90a76 100644 --- a/src/index.ts +++ b/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 = { + '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(); + + 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}"%`); }