Files
FendxPHP/fendx-framework/fendx-service/src/LoadBalancer/Strategy/TrafficDistributionStrategy.php

858 lines
27 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
namespace Fendx\Service\LoadBalancer\Strategy;
use Fendx\Service\LoadBalancer\Strategy\Router\TrafficRouter;
use Fendx\Service\LoadBalancer\Strategy\Analyzer\TrafficAnalyzer;
use Fendx\Service\LoadBalancer\Strategy\Balancer\TrafficBalancer;
use Fendx\Service\LoadBalancer\Strategy\Monitor\TrafficMonitor;
class TrafficDistributionStrategy
{
protected TrafficRouter $router;
protected TrafficAnalyzer $analyzer;
protected TrafficBalancer $balancer;
protected TrafficMonitor $monitor;
protected array $config = [];
protected array $rules = [];
protected array $trafficStats = [];
protected array $distributionHistory = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->router = new TrafficRouter($this->config);
$this->analyzer = new TrafficAnalyzer($this->config);
$this->balancer = new TrafficBalancer($this->config);
$this->monitor = new TrafficMonitor($this->config);
$this->initialize();
}
/**
* Distribute traffic to services based on strategy.
*/
public function distributeTraffic(array $instances, array $request, string $strategy = 'weighted'): ?array
{
if (empty($instances)) {
return null;
}
// Analyze request
$requestAnalysis = $this->analyzer->analyzeRequest($request);
// Apply routing rules
$filteredInstances = $this->applyRoutingRules($instances, $requestAnalysis);
if (empty($filteredInstances)) {
return null;
}
// Select instance based on strategy
$selectedInstance = $this->selectInstance($filteredInstances, $requestAnalysis, $strategy);
if ($selectedInstance) {
// Record traffic distribution
$this->recordTrafficDistribution($selectedInstance['id'], $requestAnalysis, $strategy);
// Update monitoring
$this->monitor->recordRequest($selectedInstance['id'], $requestAnalysis);
}
return $selectedInstance;
}
/**
* Add traffic distribution rule.
*/
public function addRule(string $name, array $rule): void
{
$this->validateRule($rule);
$this->rules[$name] = array_merge([
'name' => $name,
'enabled' => true,
'priority' => 100,
'conditions' => [],
'actions' => [],
'weight' => 1,
'created_at' => time(),
'updated_at' => time()
], $rule);
// Sort rules by priority
uasort($this->rules, function($a, $b) {
return $a['priority'] <=> $b['priority'];
});
$this->logInfo("Traffic distribution rule added: {$name}");
}
/**
* Remove traffic distribution rule.
*/
public function removeRule(string $name): bool
{
if (!isset($this->rules[$name])) {
return false;
}
unset($this->rules[$name]);
$this->logInfo("Traffic distribution rule removed: {$name}");
return true;
}
/**
* Update traffic distribution rule.
*/
public function updateRule(string $name, array $updates): bool
{
if (!isset($this->rules[$name])) {
return false;
}
$this->rules[$name] = array_merge($this->rules[$name], $updates);
$this->rules[$name]['updated_at'] = time();
$this->logInfo("Traffic distribution rule updated: {$name}");
return true;
}
/**
* Get all rules.
*/
public function getRules(): array
{
return $this->rules;
}
/**
* Get rule by name.
*/
public function getRule(string $name): ?array
{
return $this->rules[$name] ?? null;
}
/**
* Enable/disable rule.
*/
public function setRuleEnabled(string $name, bool $enabled): bool
{
if (!isset($this->rules[$name])) {
return false;
}
$this->rules[$name]['enabled'] = $enabled;
$this->rules[$name]['updated_at'] = time();
$this->logInfo("Rule " . ($enabled ? 'enabled' : 'disabled') . ": {$name}");
return true;
}
/**
* Set traffic weights for services.
*/
public function setTrafficWeights(array $weights): void
{
$this->balancer->setWeights($weights);
$this->logInfo("Traffic weights updated for " . count($weights) . " services");
}
/**
* Get traffic weights.
*/
public function getTrafficWeights(): array
{
return $this->balancer->getWeights();
}
/**
* Distribute traffic by percentage.
*/
public function distributeByPercentage(array $instances, array $percentages): ?array
{
if (empty($instances) || empty($percentages)) {
return null;
}
// Validate percentages sum to 100
$totalPercentage = array_sum($percentages);
if (abs($totalPercentage - 100) > 0.01) {
throw new \InvalidArgumentException("Percentages must sum to 100, got: {$totalPercentage}");
}
// Select instance based on percentage distribution
return $this->balancer->selectByPercentage($instances, $percentages);
}
/**
* Distribute traffic by geographic location.
*/
public function distributeByGeography(array $instances, string $clientLocation): ?array
{
if (empty($instances)) {
return null;
}
// Find closest instances
$closestInstances = $this->findClosestInstances($instances, $clientLocation);
if (empty($closestInstances)) {
// Fallback to any instance
return $instances[0];
}
// Select from closest instances using weighted random
return $this->balancer->selectWeightedRandom($closestInstances);
}
/**
* Distribute traffic by user segment.
*/
public function distributeBySegment(array $instances, string $userSegment): ?array
{
if (empty($instances)) {
return null;
}
// Filter instances by segment
$segmentInstances = array_filter($instances, function($instance) use ($userSegment) {
$segments = $instance['segments'] ?? [];
return in_array($userSegment, $segments) || in_array('all', $segments);
});
if (empty($segmentInstances)) {
// Fallback to instances without segment restriction
$segmentInstances = array_filter($instances, function($instance) {
$segments = $instance['segments'] ?? [];
return empty($segments) || in_array('all', $segments);
});
}
if (empty($segmentInstances)) {
return $instances[0]; // Ultimate fallback
}
return $this->balancer->selectRoundRobin(array_values($segmentInstances));
}
/**
* Distribute traffic by time of day.
*/
public function distributeByTime(array $instances, \DateTime $dateTime = null): ?array
{
$dateTime = $dateTime ?? new \DateTime();
$hour = (int) $dateTime->format('H');
if (empty($instances)) {
return null;
}
// Filter instances by time availability
$availableInstances = array_filter($instances, function($instance) use ($hour) {
$availability = $instance['availability'] ?? [];
if (empty($availability)) {
return true; // Always available
}
foreach ($availability as $period) {
if ($hour >= $period['start'] && $hour < $period['end']) {
return true;
}
}
return false;
});
if (empty($availableInstances)) {
return $instances[0]; // Fallback
}
return $this->balancer->selectWeightedRandom(array_values($availableInstances));
}
/**
* Distribute traffic by load capacity.
*/
public function distributeByCapacity(array $instances): ?array
{
if (empty($instances)) {
return null;
}
// Calculate available capacity for each instance
$capacityScores = [];
foreach ($instances as $instance) {
$maxCapacity = $instance['max_capacity'] ?? 100;
$currentLoad = $instance['current_load'] ?? 0;
$availableCapacity = max(0, $maxCapacity - $currentLoad);
$capacityScores[$instance['id']] = $availableCapacity;
}
// Select instance with most available capacity
$maxCapacity = max($capacityScores);
$bestInstanceIds = array_keys($capacityScores, $maxCapacity);
if (count($bestInstanceIds) === 1) {
$bestInstanceId = $bestInstanceIds[0];
} else {
// Multiple instances with same capacity, use round-robin
$bestInstanceId = $bestInstanceIds[array_rand($bestInstanceIds)];
}
foreach ($instances as $instance) {
if ($instance['id'] === $bestInstanceId) {
return $instance;
}
}
return null;
}
/**
* Get traffic distribution statistics.
*/
public function getStatistics(): array
{
$totalRequests = 0;
$serviceStats = [];
$ruleStats = [];
// Calculate service statistics
foreach ($this->trafficStats as $serviceId => $stats) {
$requests = $stats['total_requests'] ?? 0;
$totalRequests += $requests;
$serviceStats[$serviceId] = [
'total_requests' => $requests,
'percentage' => 0, // Will be calculated below
'average_response_time' => $this->calculateAverageResponseTime($serviceId),
'error_rate' => $this->calculateErrorRate($serviceId),
'last_request' => $stats['last_request'] ?? null
];
}
// Calculate percentages
if ($totalRequests > 0) {
foreach ($serviceStats as $serviceId => &$stats) {
$stats['percentage'] = ($stats['total_requests'] / $totalRequests) * 100;
}
}
// Calculate rule statistics
foreach ($this->rules as $ruleName => $rule) {
$ruleStats[$ruleName] = [
'enabled' => $rule['enabled'],
'priority' => $rule['priority'],
'match_count' => $rule['match_count'] ?? 0,
'last_matched' => $rule['last_matched'] ?? null
];
}
return [
'total_requests' => $totalRequests,
'service_statistics' => $serviceStats,
'rule_statistics' => $ruleStats,
'active_rules' => count(array_filter($this->rules, fn($r) => $r['enabled'])),
'distribution_history' => array_slice($this->distributionHistory, -10)
];
}
/**
* Get traffic distribution history.
*/
public function getDistributionHistory(int $limit = 100): array
{
return array_slice($this->distributionHistory, -$limit);
}
/**
* Get real-time traffic metrics.
*/
public function getRealTimeMetrics(): array
{
return [
'current_rps' => $this->monitor->getCurrentRPS(),
'active_connections' => $this->monitor->getActiveConnections(),
'average_response_time' => $this->monitor->getAverageResponseTime(),
'error_rate' => $this->monitor->getErrorRate(),
'top_services' => $this->monitor->getTopServices(10),
'geographic_distribution' => $this->monitor->getGeographicDistribution(),
'user_segment_distribution' => $this->monitor->getSegmentDistribution()
];
}
/**
* Optimize traffic distribution.
*/
public function optimizeDistribution(): array
{
$recommendations = [];
$currentStats = $this->getStatistics();
// Analyze service performance
foreach ($currentStats['service_statistics'] as $serviceId => $stats) {
if ($stats['error_rate'] > 5) {
$recommendations[] = [
'type' => 'high_error_rate',
'service_id' => $serviceId,
'message' => "Service {$serviceId} has high error rate: {$stats['error_rate']}%",
'action' => 'consider_reducing_weight_or_failing_over'
];
}
if ($stats['average_response_time'] > 1000) { // 1 second
$recommendations[] = [
'type' => 'slow_response',
'service_id' => $serviceId,
'message' => "Service {$serviceId} has slow response time: {$stats['average_response_time']}ms",
'action' => 'consider_reducing_weight'
];
}
}
// Analyze rule effectiveness
foreach ($currentStats['rule_statistics'] as $ruleName => $stats) {
if ($stats['enabled'] && $stats['match_count'] === 0) {
$recommendations[] = [
'type' => 'unused_rule',
'rule_name' => $ruleName,
'message' => "Rule {$ruleName} is enabled but never matches",
'action' => 'review_or_disable_rule'
];
}
}
// Suggest weight adjustments
$weightSuggestions = $this->suggestWeightAdjustments();
$recommendations = array_merge($recommendations, $weightSuggestions);
return $recommendations;
}
/**
* Reset traffic statistics.
*/
public function resetStatistics(): void
{
$this->trafficStats = [];
$this->distributionHistory = [];
foreach ($this->rules as &$rule) {
$rule['match_count'] = 0;
$rule['last_matched'] = null;
}
$this->logInfo("Traffic statistics reset");
}
/**
* Export traffic configuration.
*/
public function exportConfiguration(string $format = 'json'): string
{
$data = [
'rules' => $this->rules,
'traffic_weights' => $this->getTrafficWeights(),
'statistics' => $this->getStatistics(),
'distribution_history' => $this->distributionHistory,
'exported_at' => date('Y-m-d H:i:s'),
'version' => $this->config['version'] ?? '1.0'
];
switch ($format) {
case 'json':
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
case 'php':
return '<?php return ' . var_export($data, true) . ';';
default:
throw new \InvalidArgumentException("Unsupported export format: {$format}");
}
}
/**
* Apply routing rules to instances.
*/
protected function applyRoutingRules(array $instances, array $requestAnalysis): array
{
$filteredInstances = $instances;
foreach ($this->rules as $rule) {
if (!$rule['enabled']) {
continue;
}
if ($this->matchesRuleConditions($rule['conditions'], $requestAnalysis)) {
$rule['match_count'] = ($rule['match_count'] ?? 0) + 1;
$rule['last_matched'] = time();
// Apply rule actions
$filteredInstances = $this->applyRuleActions($filteredInstances, $rule['actions']);
// If rule has stop_processing flag, break
if ($rule['stop_processing'] ?? false) {
break;
}
}
}
return $filteredInstances;
}
/**
* Check if request matches rule conditions.
*/
protected function matchesRuleConditions(array $conditions, array $requestAnalysis): bool
{
foreach ($conditions as $condition) {
if (!$this->matchesCondition($condition, $requestAnalysis)) {
return false;
}
}
return true;
}
/**
* Check if single condition matches.
*/
protected function matchesCondition(array $condition, array $requestAnalysis): bool
{
$field = $condition['field'];
$operator = $condition['operator'];
$value = $condition['value'];
$requestValue = $this->getRequestValue($requestAnalysis, $field);
switch ($operator) {
case 'equals':
return $requestValue === $value;
case 'not_equals':
return $requestValue !== $value;
case 'in':
return in_array($requestValue, (array) $value);
case 'not_in':
return !in_array($requestValue, (array) $value);
case 'contains':
return is_string($requestValue) && strpos($requestValue, $value) !== false;
case 'starts_with':
return is_string($requestValue) && strpos($requestValue, $value) === 0;
case 'ends_with':
return is_string($requestValue) && substr($requestValue, -strlen($value)) === $value;
case 'greater_than':
return $requestValue > $value;
case 'less_than':
return $requestValue < $value;
case 'between':
return $requestValue >= $value[0] && $requestValue <= $value[1];
case 'regex':
return preg_match($value, (string) $requestValue) === 1;
default:
return false;
}
}
/**
* Get value from request analysis.
*/
protected function getRequestValue(array $requestAnalysis, string $field)
{
$fields = explode('.', $field);
$value = $requestAnalysis;
foreach ($fields as $f) {
if (!is_array($value) || !array_key_exists($f, $value)) {
return null;
}
$value = $value[$f];
}
return $value;
}
/**
* Apply rule actions to instances.
*/
protected function applyRuleActions(array $instances, array $actions): array
{
$filteredInstances = $instances;
foreach ($actions as $action) {
switch ($action['type']) {
case 'filter':
$filteredInstances = $this->filterInstances($filteredInstances, $action['conditions']);
break;
case 'set_weight':
$filteredInstances = $this->setInstanceWeights($filteredInstances, $action['weights']);
break;
case 'prioritize':
$filteredInstances = $this->prioritizeInstances($filteredInstances, $action['criteria']);
break;
case 'exclude':
$filteredInstances = $this->excludeInstances($filteredInstances, $action['instances']);
break;
}
}
return $filteredInstances;
}
/**
* Select instance based on strategy.
*/
protected function selectInstance(array $instances, array $requestAnalysis, string $strategy): ?array
{
switch ($strategy) {
case 'weighted':
return $this->balancer->selectWeightedRandom($instances);
case 'round_robin':
return $this->balancer->selectRoundRobin($instances);
case 'least_connections':
return $this->balancer->selectLeastConnections($instances);
case 'response_time':
return $this->balancer->selectByResponseTime($instances);
case 'geographic':
$location = $requestAnalysis['client']['location'] ?? 'unknown';
return $this->distributeByGeography($instances, $location);
case 'segment':
$segment = $requestAnalysis['user']['segment'] ?? 'default';
return $this->distributeBySegment($instances, $segment);
case 'capacity':
return $this->distributeByCapacity($instances);
default:
return $instances[0];
}
}
/**
* Find closest instances by geography.
*/
protected function findClosestInstances(array $instances, string $clientLocation): array
{
// This would use a proper geolocation service in production
// For now, return instances with location matching
return array_filter($instances, function($instance) use ($clientLocation) {
$instanceLocation = $instance['location'] ?? 'unknown';
return $instanceLocation === $clientLocation || $instanceLocation === 'global';
});
}
/**
* Record traffic distribution.
*/
protected function recordTrafficDistribution(string $serviceId, array $requestAnalysis, string $strategy): void
{
// Update service stats
if (!isset($this->trafficStats[$serviceId])) {
$this->trafficStats[$serviceId] = [
'total_requests' => 0,
'requests_by_strategy' => [],
'response_times' => [],
'errors' => 0,
'last_request' => null
];
}
$this->trafficStats[$serviceId]['total_requests']++;
$this->trafficStats[$serviceId]['requests_by_strategy'][$strategy] =
($this->trafficStats[$serviceId]['requests_by_strategy'][$strategy] ?? 0) + 1;
$this->trafficStats[$serviceId]['last_request'] = time();
// Add to distribution history
$this->distributionHistory[] = [
'timestamp' => time(),
'service_id' => $serviceId,
'strategy' => $strategy,
'request_analysis' => $requestAnalysis
];
// Limit history size
if (count($this->distributionHistory) > 1000) {
$this->distributionHistory = array_slice($this->distributionHistory, -1000);
}
}
/**
* Calculate average response time for service.
*/
protected function calculateAverageResponseTime(string $serviceId): float
{
$stats = $this->trafficStats[$serviceId] ?? [];
$responseTimes = $stats['response_times'] ?? [];
if (empty($responseTimes)) {
return 0.0;
}
return array_sum($responseTimes) / count($responseTimes);
}
/**
* Calculate error rate for service.
*/
protected function calculateErrorRate(string $serviceId): float
{
$stats = $this->trafficStats[$serviceId] ?? [];
$totalRequests = $stats['total_requests'] ?? 0;
$errors = $stats['errors'] ?? 0;
if ($totalRequests === 0) {
return 0.0;
}
return ($errors / $totalRequests) * 100;
}
/**
* Suggest weight adjustments.
*/
protected function suggestWeightAdjustments(): array
{
$suggestions = [];
$stats = $this->getStatistics();
foreach ($stats['service_statistics'] as $serviceId => $serviceStats) {
if ($serviceStats['error_rate'] > 2) {
$suggestions[] = [
'type' => 'weight_adjustment',
'service_id' => $serviceId,
'current_weight' => $this->balancer->getWeight($serviceId),
'suggested_weight' => max(1, (int) ($this->balancer->getWeight($serviceId) * 0.5)),
'reason' => 'High error rate detected'
];
}
}
return $suggestions;
}
/**
* Validate rule configuration.
*/
protected function validateRule(array $rule): void
{
if (empty($rule['conditions'])) {
throw new \InvalidArgumentException("Rule must have at least one condition");
}
if (empty($rule['actions'])) {
throw new \InvalidArgumentException("Rule must have at least one action");
}
foreach ($rule['conditions'] as $condition) {
if (!isset($condition['field']) || !isset($condition['operator']) || !isset($condition['value'])) {
throw new \InvalidArgumentException("Condition must have field, operator, and value");
}
}
}
/**
* Initialize traffic distribution strategy.
*/
protected function initialize(): void
{
// Load configuration from storage
$this->loadConfiguration();
// Start monitoring
$this->monitor->start();
$this->logInfo("Traffic distribution strategy initialized");
}
/**
* Load configuration from storage.
*/
protected function loadConfiguration(): void
{
// This would load from persistent storage in production
$this->logInfo("Configuration loaded");
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[TrafficDistributionStrategy] {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'default_strategy' => 'weighted',
'enable_monitoring' => true,
'monitoring_interval' => 60,
'history_retention' => 1000,
'logging_enabled' => true,
'optimization_enabled' => true,
'optimization_interval' => 300,
'version' => '1.0'
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create traffic distribution strategy instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for high traffic.
*/
public static function forHighTraffic(): self
{
return new self([
'default_strategy' => 'capacity',
'enable_monitoring' => true,
'monitoring_interval' => 30,
'optimization_enabled' => true,
'optimization_interval' => 120
]);
}
/**
* Create for global distribution.
*/
public static function forGlobalDistribution(): self
{
return new self([
'default_strategy' => 'geographic',
'enable_monitoring' => true,
'logging_enabled' => true
]);
}
}