MidПрактика36 min

Оптимизация затрат (FinOps)

Cost optimization, right-sizing, spot instances, reserved capacity -- управление затратами на облачную инфраструктуру

Что такое FinOps

FinOps (Financial Operations) -- практика управления затратами на облачную инфраструктуру. Объединяет инженеров, финансистов и бизнес для принятия обоснованных решений о расходах.

Три фазы FinOps

Фаза Цель Действия
Inform Понять затраты Тегирование, отчёты, аллокация
Optimize Снизить затраты Right-sizing, Reserved, Spot
Operate Контролировать Бюджеты, алерты, governance

Ключевые метрики

Метрика Описание Формула
Unit Cost Стоимость на единицу бизнеса Total cost / requests (or users)
Coverage % покрытия Reserved/Savings Plans Reserved hours / total hours
Utilization Использование купленных Reserved Used reserved / purchased reserved
Waste Неиспользуемые ресурсы Idle resources cost / total cost

Анализ затрат

<?php

declare(strict_types=1);

/**
 * Cloud cost analyzer: collect and categorize spending
 */
final class CloudCostAnalyzer
{
    /** @var array<string, float> */
    private array $costs = [];

    /**
     * Categorize costs by service and environment
     *
     * @param array<int, array{service: string, environment: string, cost: float, usage: float, capacity: float}> $resources
     */
    public function analyze(array $resources): array
    {
        $byService = [];
        $byEnvironment = [];
        $waste = [];

        foreach ($resources as $resource) {
            $service = $resource['service'];
            $env = $resource['environment'];
            $cost = $resource['cost'];
            $utilization = $resource['capacity'] > 0
                ? $resource['usage'] / $resource['capacity']
                : 0;

            // Aggregate by service
            $byService[$service] = ($byService[$service] ?? 0) + $cost;

            // Aggregate by environment
            $byEnvironment[$env] = ($byEnvironment[$env] ?? 0) + $cost;

            // Detect waste: utilization < 20%
            if ($utilization < 0.2 && $cost > 0) {
                $waste[] = [
                    'service' => $service,
                    'environment' => $env,
                    'cost' => $cost,
                    'utilization' => round($utilization * 100, 1),
                    'potential_savings' => round($cost * (1 - $utilization), 2),
                ];
            }
        }

        arsort($byService);
        arsort($byEnvironment);

        return [
            'total_cost' => array_sum(array_column($resources, 'cost')),
            'by_service' => $byService,
            'by_environment' => $byEnvironment,
            'waste' => $waste,
            'total_waste' => array_sum(array_column($waste, 'potential_savings')),
        ];
    }

    /**
     * Calculate unit economics
     */
    public function unitCost(float $totalCost, int $totalRequests, int $totalUsers): array
    {
        return [
            'cost_per_million_requests' => $totalRequests > 0
                ? round($totalCost / ($totalRequests / 1_000_000), 2)
                : 0,
            'cost_per_user' => $totalUsers > 0
                ? round($totalCost / $totalUsers, 4)
                : 0,
            'total_cost' => $totalCost,
        ];
    }
}
## Right-Sizing

Right-sizing -- подбор оптимального размера ресурсов на основе реального потребления.

Стратегия Right-Sizing

Ресурс Метрика Действие
CPU avg < 20% Уменьшить instance type
Memory avg < 30% Уменьшить instance type
Disk used < 40% Уменьшить volume size
Network avg < 10% capacity Использовать меньший тип
<?php

declare(strict_types=1);

/**
 * Right-sizing recommendation engine
 */
final class RightSizingAdvisor
{
    private const CPU_THRESHOLD = 0.40;      // 40% average CPU
    private const MEMORY_THRESHOLD = 0.50;   // 50% average memory
    private const DISK_THRESHOLD = 0.60;     // 60% disk usage
    private const OBSERVATION_DAYS = 14;     // Minimum observation period

    /**
     * Generate right-sizing recommendations
     *
     * @param array<int, array{
     *     instance_id: string,
     *     instance_type: string,
     *     avg_cpu: float,
     *     max_cpu: float,
     *     avg_memory: float,
     *     max_memory: float,
     *     disk_usage: float,
     *     monthly_cost: float,
     *     observation_days: int,
     * }> $instances
     */
    public function recommend(array $instances): array
    {
        $recommendations = [];

        foreach ($instances as $instance) {
            // Skip instances without enough observation data
            if ($instance['observation_days'] < self::OBSERVATION_DAYS) {
                continue;
            }

            $actions = [];

            // CPU underutilized
            if ($instance['avg_cpu'] < self::CPU_THRESHOLD
                && $instance['max_cpu'] < 0.80) {
                $actions[] = [
                    'type' => 'downsize_cpu',
                    'reason' => sprintf(
                        'Average CPU %.1f%%, max %.1f%%',
                        $instance['avg_cpu'] * 100,
                        $instance['max_cpu'] * 100,
                    ),
                    'estimated_savings' => round($instance['monthly_cost'] * 0.30, 2),
                ];
            }

            // Memory underutilized
            if ($instance['avg_memory'] < self::MEMORY_THRESHOLD
                && $instance['max_memory'] < 0.80) {
                $actions[] = [
                    'type' => 'downsize_memory',
                    'reason' => sprintf(
                        'Average memory %.1f%%, max %.1f%%',
                        $instance['avg_memory'] * 100,
                        $instance['max_memory'] * 100,
                    ),
                    'estimated_savings' => round($instance['monthly_cost'] * 0.25, 2),
                ];
            }

            // Disk underutilized
            if ($instance['disk_usage'] < self::DISK_THRESHOLD) {
                $actions[] = [
                    'type' => 'downsize_disk',
                    'reason' => sprintf(
                        'Disk usage %.1f%%',
                        $instance['disk_usage'] * 100,
                    ),
                    'estimated_savings' => round($instance['monthly_cost'] * 0.10, 2),
                ];
            }

            // Completely idle
            if ($instance['avg_cpu'] < 0.05 && $instance['avg_memory'] < 0.10) {
                $actions = [[
                    'type' => 'terminate',
                    'reason' => 'Instance appears idle (CPU < 5%, Memory < 10%)',
                    'estimated_savings' => $instance['monthly_cost'],
                ]];
            }

            if (!empty($actions)) {
                $recommendations[] = [
                    'instance_id' => $instance['instance_id'],
                    'instance_type' => $instance['instance_type'],
                    'current_cost' => $instance['monthly_cost'],
                    'actions' => $actions,
                    'total_savings' => array_sum(array_column($actions, 'estimated_savings')),
                ];
            }
        }

        // Sort by potential savings (highest first)
        usort($recommendations, fn(array $a, array $b) =>
            $b['total_savings'] <=> $a['total_savings']
        );

        return $recommendations;
    }
}
## Модели ценообразования

On-Demand vs Reserved vs Spot

Модель Скидка Обязательство Риск прерывания
On-Demand 0% Нет Нет
Reserved (1 год) ~30-40% 1 год Нет
Reserved (3 года) ~50-60% 3 года Нет
Savings Plans ~30-50% $/час обязательство Нет
Spot Instances ~60-90% Нет Да (2 мин warning)

Spot Instances

<?php

declare(strict_types=1);

/**
 * Spot instance management for fault-tolerant workloads
 */
final class SpotInstanceManager
{
    /**
     * Determine if workload is suitable for spot instances
     */
    public function isSpotSuitable(array $workload): array
    {
        $suitable = true;
        $reasons = [];

        // Stateless check
        if ($workload['stateful'] ?? false) {
            $suitable = false;
            $reasons[] = 'Stateful workloads risk data loss on interruption';
        }

        // Duration check
        if (($workload['avg_duration_minutes'] ?? 0) > 60) {
            $reasons[] = 'Long-running tasks may be interrupted; use checkpointing';
        }

        // Redundancy check
        if (($workload['min_instances'] ?? 1) < 2) {
            $reasons[] = 'Single instance -- no redundancy for interruptions';
        }

        // SLA check
        if (($workload['sla_percent'] ?? 99) > 99.9) {
            $suitable = false;
            $reasons[] = 'High SLA requirement incompatible with spot interruptions';
        }

        return [
            'suitable' => $suitable,
            'reasons' => $reasons,
            'recommendation' => $suitable
                ? 'Use spot with diversified instance types and AZs'
                : 'Use on-demand or reserved instances',
        ];
    }

    /**
     * Calculate optimal spot fleet configuration
     *
     * Diversify across instance types and AZs to reduce interruption risk
     */
    public function calculateFleet(
        int $requiredCapacity,
        array $availableTypes,
        array $availableAzs,
    ): array {
        $fleet = [];

        // Spread across at least 3 instance types and 2 AZs
        $typesCount = min(count($availableTypes), max(3, count($availableTypes)));
        $azsCount = min(count($availableAzs), 2);

        $capacityPerGroup = (int) ceil(
            $requiredCapacity / ($typesCount * $azsCount)
        );

        for ($t = 0; $t < $typesCount; $t++) {
            for ($a = 0; $a < $azsCount; $a++) {
                $fleet[] = [
                    'instance_type' => $availableTypes[$t],
                    'az' => $availableAzs[$a],
                    'count' => $capacityPerGroup,
                    'allocation_strategy' => 'capacity-optimized',
                ];
            }
        }

        return [
            'fleet' => $fleet,
            'total_capacity' => $capacityPerGroup * $typesCount * $azsCount,
            'diversification' => [
                'instance_types' => $typesCount,
                'availability_zones' => $azsCount,
            ],
        ];
    }
}
### Reserved Capacity Planning
<?php

declare(strict_types=1);

/**
 * Reserved instance coverage optimizer
 */
final class ReservedCapacityPlanner
{
    /**
     * Recommend reserved instance purchases based on historical usage
     *
     * @param array<int, array{
     *     instance_type: string,
     *     hours_used: int,
     *     on_demand_rate: float,
     *     reserved_1yr_rate: float,
     *     reserved_3yr_rate: float,
     * }> $usageHistory  Monthly usage data
     */
    public function recommend(array $usageHistory, int $monthsOfData): array
    {
        $recommendations = [];

        foreach ($usageHistory as $usage) {
            $monthlyHours = $usage['hours_used'] / $monthsOfData;
            $hoursInMonth = 730; // Average hours per month

            // Calculate utilization percentage
            $utilization = $monthlyHours / $hoursInMonth;

            // Only recommend reserved if consistently used > 70%
            if ($utilization < 0.70) {
                continue;
            }

            $onDemandMonthlyCost = $monthlyHours * $usage['on_demand_rate'];
            $reserved1yrMonthlyCost = $hoursInMonth * $usage['reserved_1yr_rate'];
            $reserved3yrMonthlyCost = $hoursInMonth * $usage['reserved_3yr_rate'];

            $savings1yr = $onDemandMonthlyCost - $reserved1yrMonthlyCost;
            $savings3yr = $onDemandMonthlyCost - $reserved3yrMonthlyCost;

            if ($savings1yr > 0) {
                $recommendations[] = [
                    'instance_type' => $usage['instance_type'],
                    'utilization' => round($utilization * 100, 1),
                    'on_demand_monthly' => round($onDemandMonthlyCost, 2),
                    'options' => [
                        [
                            'term' => '1 year',
                            'monthly_cost' => round($reserved1yrMonthlyCost, 2),
                            'monthly_savings' => round($savings1yr, 2),
                            'annual_savings' => round($savings1yr * 12, 2),
                            'break_even_months' => 0, // No upfront
                        ],
                        [
                            'term' => '3 years',
                            'monthly_cost' => round($reserved3yrMonthlyCost, 2),
                            'monthly_savings' => round($savings3yr, 2),
                            'annual_savings' => round($savings3yr * 12, 2),
                        ],
                    ],
                    'recommendation' => $utilization > 0.90
                        ? '3-year reserved (high confidence)'
                        : '1-year reserved (moderate confidence)',
                ];
            }
        }

        usort($recommendations, fn(array $a, array $b) =>
            $b['options'][0]['annual_savings'] <=> $a['options'][0]['annual_savings']
        );

        return $recommendations;
    }
}
## Автоматическое управление затратами

Бюджеты и алерты

<?php

declare(strict_types=1);

/**
 * Budget monitoring and alerting
 */
final class BudgetMonitor
{
    public function __construct(
        private readonly CostApiClient $costApi,
        private readonly AlertService $alerts,
    ) {}

    /**
     * Check budgets and send alerts
     *
     * @param array<string, array{monthly_budget: float, alert_thresholds: array<float>}> $budgets
     */
    public function checkBudgets(array $budgets): array
    {
        $results = [];
        $today = new \DateTimeImmutable();
        $daysInMonth = (int) $today->format('t');
        $dayOfMonth = (int) $today->format('j');
        $monthProgress = $dayOfMonth / $daysInMonth;

        foreach ($budgets as $team => $budget) {
            $currentSpend = $this->costApi->getMonthToDateCost($team);
            $spendPercent = $currentSpend / $budget['monthly_budget'];

            // Forecast based on current spend rate
            $forecastedSpend = ($currentSpend / $monthProgress);
            $forecastPercent = $forecastedSpend / $budget['monthly_budget'];

            $status = 'ok';

            // Check against thresholds (e.g., [0.50, 0.80, 1.00])
            foreach ($budget['alert_thresholds'] as $threshold) {
                if ($spendPercent >= $threshold) {
                    $status = match (true) {
                        $threshold >= 1.0 => 'critical',
                        $threshold >= 0.8 => 'warning',
                        default => 'info',
                    };

                    $this->alerts->send(
                        level: $status,
                        message: sprintf(
                            'Team %s: %.0f%% of budget used (%.0f%% of month elapsed). Forecast: $%.2f / $%.2f',
                            $team,
                            $spendPercent * 100,
                            $monthProgress * 100,
                            $forecastedSpend,
                            $budget['monthly_budget'],
                        ),
                    );
                }
            }

            $results[$team] = [
                'budget' => $budget['monthly_budget'],
                'current_spend' => round($currentSpend, 2),
                'spend_percent' => round($spendPercent * 100, 1),
                'forecasted_spend' => round($forecastedSpend, 2),
                'forecast_percent' => round($forecastPercent * 100, 1),
                'status' => $status,
                'month_progress' => round($monthProgress * 100, 1),
            ];
        }

        return $results;
    }
}
### Автоматическое выключение Dev/Staging
<?php

declare(strict_types=1);

/**
 * Schedule-based resource management for non-production environments
 *
 * Dev/staging environments don't need to run 24/7.
 * Running only during business hours saves ~65% of compute costs.
 */
final class EnvironmentScheduler
{
    /**
     * Business hours: Mon-Fri, 08:00-20:00 local time
     */
    private const BUSINESS_HOURS_START = 8;
    private const BUSINESS_HOURS_END = 20;

    public function __construct(
        private readonly CloudApiClient $cloudApi,
        private readonly CostTracker $costTracker,
    ) {}

    /**
     * Determine if environment should be running right now
     */
    public function shouldBeRunning(string $environment, string $timezone): bool
    {
        if ($environment === 'production') {
            return true; // Production always runs
        }

        $now = new \DateTimeImmutable('now', new \DateTimeZone($timezone));
        $hour = (int) $now->format('G');
        $dayOfWeek = (int) $now->format('N'); // 1 (Mon) - 7 (Sun)

        // Weekends: off
        if ($dayOfWeek >= 6) {
            return false;
        }

        // Outside business hours: off
        return $hour >= self::BUSINESS_HOURS_START
            && $hour < self::BUSINESS_HOURS_END;
    }

    /**
     * Apply schedule to all non-production environments
     *
     * @param array<int, array{name: string, timezone: string, resources: array<string>}> $environments
     */
    public function applySchedule(array $environments): array
    {
        $actions = [];

        foreach ($environments as $env) {
            $shouldRun = $this->shouldBeRunning($env['name'], $env['timezone']);

            foreach ($env['resources'] as $resourceId) {
                $isRunning = $this->cloudApi->isRunning($resourceId);

                if ($shouldRun && !$isRunning) {
                    $this->cloudApi->start($resourceId);
                    $actions[] = [
                        'resource' => $resourceId,
                        'action' => 'started',
                        'environment' => $env['name'],
                    ];
                } elseif (!$shouldRun && $isRunning) {
                    $this->cloudApi->stop($resourceId);
                    $actions[] = [
                        'resource' => $resourceId,
                        'action' => 'stopped',
                        'environment' => $env['name'],
                    ];

                    $this->costTracker->recordSaving(
                        resourceId: $resourceId,
                        hoursSaved: self::BUSINESS_HOURS_END - self::BUSINESS_HOURS_START,
                    );
                }
            }
        }

        return $actions;
    }

    /**
     * Calculate monthly savings from scheduling
     */
    public function estimateSavings(float $monthlyOnDemandCost): array
    {
        $totalHoursInMonth = 730;
        $businessHoursPerDay = self::BUSINESS_HOURS_END - self::BUSINESS_HOURS_START; // 12
        $businessDaysPerMonth = 22;
        $runningHours = $businessHoursPerDay * $businessDaysPerMonth; // 264

        $utilizationPercent = $runningHours / $totalHoursInMonth;
        $savings = $monthlyOnDemandCost * (1 - $utilizationPercent);

        return [
            'running_hours' => $runningHours,
            'total_hours' => $totalHoursInMonth,
            'utilization' => round($utilizationPercent * 100, 1),
            'monthly_savings' => round($savings, 2),
            'savings_percent' => round((1 - $utilizationPercent) * 100, 1),
        ];
    }
}
## Тегирование и аллокация

Стратегия тегирования

Тег Назначение Пример
team Аллокация на команду backend, data, platform
environment Разделение по среде production, staging, dev
service Привязка к сервису api, worker, scheduler
cost-center Финансовый центр engineering, marketing
managed-by IaC или ручное terraform, manual

Правило: Тегирование -- фундамент FinOps. Без тегов невозможно понять, кто и за что платит. Установите mandatory tags policy и блокируйте создание ресурсов без обязательных тегов.

Чек-лист оптимизации

Категория Действие Типичная экономия
Compute Right-sizing по метрикам 20-40%
Compute Spot для stateless workloads 60-90%
Compute Reserved для baseline нагрузки 30-60%
Compute Выключение Dev/Staging ночью 65%
Storage Lifecycle policies (S3 → Glacier) 50-80%
Storage Удаление неиспользуемых EBS volumes 100%
Database Right-sizing RDS instances 20-40%
Database Reserved instances для production DB 30-50%
Network VPC endpoints вместо NAT Gateway 50-70%
Network CloudFront для static content 30-50%
Containers Kubernetes cluster autoscaler 30-50%
Containers Karpenter для node provisioning 20-40%

Итоги

  • FinOps объединяет инженеров и финансы для управления облачными затратами
  • Right-sizing на основе метрик -- первый и самый простой шаг оптимизации
  • Spot instances дают максимальную экономию для fault-tolerant workloads
  • Reserved capacity выгодна при стабильной baseline-нагрузке (> 70% utilization)
  • Автоматическое выключение Dev/Staging экономит ~65% compute-затрат
  • Тегирование ресурсов обязательно для аллокации и анализа затрат
  • Unit economics (стоимость на запрос/пользователя) -- ключевая метрика для бизнеса