Baseline: pr1 HYC下载站 v2.3 before security/functional fixes
This commit is contained in:
@@ -0,0 +1,426 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
镜像源健康检查模块
|
||||
自动检测上游镜像源的可用性,支持故障切换
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import threading
|
||||
import logging
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Callable
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HealthStatus(Enum):
|
||||
"""健康状态"""
|
||||
UNKNOWN = "unknown"
|
||||
HEALTHY = "healthy"
|
||||
DEGRADED = "degraded"
|
||||
UNHEALTHY = "unhealthy"
|
||||
|
||||
|
||||
@dataclass
|
||||
class HealthCheckResult:
|
||||
"""健康检查结果"""
|
||||
source_name: str
|
||||
status: HealthStatus
|
||||
response_time: float # 毫秒
|
||||
http_status: Optional[int] = None
|
||||
error_message: Optional[str] = None
|
||||
last_check: Optional[datetime] = None
|
||||
consecutive_failures: int = 0
|
||||
total_checks: int = 0
|
||||
success_rate: float = 100.0
|
||||
details: Dict = field(default_factory=dict)
|
||||
|
||||
|
||||
class HealthChecker:
|
||||
"""健康检查器"""
|
||||
|
||||
def __init__(self, config: dict = None):
|
||||
"""
|
||||
初始化健康检查器
|
||||
|
||||
Args:
|
||||
config: 健康检查配置
|
||||
"""
|
||||
self.config = config or {}
|
||||
self.default_timeout = self.config.get('timeout', 10) # 秒
|
||||
self.default_interval = self.config.get('interval', 60) # 秒
|
||||
self.max_retries = self.config.get('max_retries', 3)
|
||||
self.failure_threshold = self.config.get('failure_threshold', 3) # 连续失败次数阈值
|
||||
|
||||
# 检查结果存储
|
||||
self._results: Dict[str, HealthCheckResult] = {}
|
||||
self._lock = threading.Lock()
|
||||
|
||||
# 状态变化回调
|
||||
self._on_status_change: Optional[Callable] = None
|
||||
|
||||
def set_status_change_callback(self, callback: Callable):
|
||||
"""设置状态变化回调"""
|
||||
self._on_status_change = callback
|
||||
|
||||
def check_source(self, source_name: str, source_config: dict) -> HealthCheckResult:
|
||||
"""
|
||||
检查单个镜像源的健康状态
|
||||
|
||||
Args:
|
||||
source_name: 镜像源名称
|
||||
source_config: 镜像源配置
|
||||
|
||||
Returns:
|
||||
HealthCheckResult: 检查结果
|
||||
"""
|
||||
url = source_config.get('url', '')
|
||||
if not url:
|
||||
return HealthCheckResult(
|
||||
source_name=source_name,
|
||||
status=HealthStatus.UNHEALTHY,
|
||||
response_time=0,
|
||||
error_message="No URL configured",
|
||||
last_check=datetime.now()
|
||||
)
|
||||
|
||||
start_time = time.time()
|
||||
http_status = None
|
||||
error_message = None
|
||||
details = {}
|
||||
|
||||
try:
|
||||
# 发送 HEAD 请求(更轻量)
|
||||
req = urllib.request.Request(
|
||||
url.rstrip('/') + '/',
|
||||
method='HEAD',
|
||||
headers={
|
||||
'User-Agent': 'HYC-Mirror-HealthCheck/1.0',
|
||||
'Accept': '*/*'
|
||||
}
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req, timeout=self.default_timeout) as response:
|
||||
http_status = response.status
|
||||
details['headers'] = dict(response.headers)
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
http_status = e.code
|
||||
error_message = f"HTTP {e.code}"
|
||||
except urllib.error.URLError as e:
|
||||
error_message = f"Connection error: {str(e.reason)}"
|
||||
except Exception as e:
|
||||
error_message = str(e)
|
||||
|
||||
response_time = (time.time() - start_time) * 1000 # 转换为毫秒
|
||||
|
||||
# 判断健康状态
|
||||
if error_message:
|
||||
if http_status and 400 <= http_status < 500:
|
||||
status = HealthStatus.DEGRADED # 客户端错误,可能暂时
|
||||
else:
|
||||
status = HealthStatus.UNHEALTHY
|
||||
elif http_status and 200 <= http_status < 400:
|
||||
status = HealthStatus.HEALTHY
|
||||
elif http_status:
|
||||
status = HealthStatus.DEGRADED
|
||||
else:
|
||||
status = HealthStatus.UNHEALTHY
|
||||
|
||||
# 计算统计数据
|
||||
with self._lock:
|
||||
if source_name not in self._results:
|
||||
self._results[source_name] = HealthCheckResult(
|
||||
source_name=source_name,
|
||||
status=status,
|
||||
response_time=response_time,
|
||||
last_check=datetime.now(),
|
||||
consecutive_failures=0,
|
||||
total_checks=1
|
||||
)
|
||||
else:
|
||||
old_result = self._results[source_name]
|
||||
consecutive_failures = old_result.consecutive_failures + (1 if status == HealthStatus.UNHEALTHY else 0)
|
||||
total_checks = old_result.total_checks + 1
|
||||
success_rate = ((total_checks - consecutive_failures) / total_checks) * 100
|
||||
|
||||
self._results[source_name] = HealthCheckResult(
|
||||
source_name=source_name,
|
||||
status=status,
|
||||
response_time=response_time,
|
||||
http_status=http_status,
|
||||
error_message=error_message,
|
||||
last_check=datetime.now(),
|
||||
consecutive_failures=consecutive_failures,
|
||||
total_checks=total_checks,
|
||||
success_rate=success_rate,
|
||||
details=details
|
||||
)
|
||||
|
||||
# 检查状态变化,触发回调
|
||||
if self._on_status_change and source_name in self._results:
|
||||
old_status = self._results[source_name].status
|
||||
if old_status != status:
|
||||
try:
|
||||
self._on_status_change(source_name, old_status, status)
|
||||
except Exception as e:
|
||||
logger.error(f"状态变化回调执行失败: {e}")
|
||||
|
||||
return self._results[source_name]
|
||||
|
||||
def get_all_results(self) -> List[HealthCheckResult]:
|
||||
"""获取所有检查结果"""
|
||||
with self._lock:
|
||||
return list(self._results.values())
|
||||
|
||||
def get_result(self, source_name: str) -> Optional[HealthCheckResult]:
|
||||
"""获取指定源的结果"""
|
||||
with self._lock:
|
||||
return self._results.get(source_name)
|
||||
|
||||
def is_healthy(self, source_name: str) -> bool:
|
||||
"""检查源是否健康"""
|
||||
result = self.get_result(source_name)
|
||||
if result is None:
|
||||
return True # 未检查过的默认健康
|
||||
return result.status == HealthStatus.HEALTHY
|
||||
|
||||
def get_unhealthy_sources(self) -> List[str]:
|
||||
"""获取不健康的源列表"""
|
||||
with self._lock:
|
||||
return [name for name, result in self._results.items()
|
||||
if result.status == HealthStatus.UNHEALTHY]
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
"""获取健康检查统计"""
|
||||
with self._lock:
|
||||
total = len(self._results)
|
||||
healthy = sum(1 for r in self._results.values() if r.status == HealthStatus.HEALTHY)
|
||||
degraded = sum(1 for r in self._results.values() if r.status == HealthStatus.DEGRADED)
|
||||
unhealthy = sum(1 for r in self._results.values() if r.status == HealthStatus.UNHEALTHY)
|
||||
|
||||
avg_response_time = 0
|
||||
if total > 0:
|
||||
avg_response_time = sum(r.response_time for r in self._results.values()) / total
|
||||
|
||||
return {
|
||||
'total_sources': total,
|
||||
'healthy': healthy,
|
||||
'degraded': degraded,
|
||||
'unhealthy': unhealthy,
|
||||
'avg_response_time_ms': round(avg_response_time, 2),
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
|
||||
class MirrorFailoverManager:
|
||||
"""镜像源故障切换管理器"""
|
||||
|
||||
def __init__(self, config: dict = None):
|
||||
"""
|
||||
初始化故障切换管理器
|
||||
|
||||
Args:
|
||||
config: 配置,包含镜像源列表
|
||||
"""
|
||||
self.config = config or {}
|
||||
self.mirrors: Dict[str, Dict] = self.config.get('mirrors', {})
|
||||
|
||||
# 启用故障切换
|
||||
self.failover_enabled = self.config.get('failover_enabled', True)
|
||||
self.failover_threshold = self.config.get('failover_threshold', 3) # 连续失败次数
|
||||
|
||||
# 健康检查器
|
||||
self.health_checker = HealthChecker(self.config.get('health_check', {}))
|
||||
|
||||
# 当前活跃源
|
||||
self._active_source: Dict[str, str] = {} # mirror_type -> source_name
|
||||
self._source_priority: Dict[str, List[str]] = {} # mirror_type -> [优先列表]
|
||||
|
||||
# 故障切换历史
|
||||
self._failover_history: List[dict] = []
|
||||
|
||||
# 回调
|
||||
self._on_failover: Optional[Callable] = None
|
||||
|
||||
def set_failover_callback(self, callback: Callable):
|
||||
"""设置故障切换回调"""
|
||||
self._on_failover = callback
|
||||
|
||||
def initialize(self):
|
||||
"""初始化,确定各镜像类型的首选源"""
|
||||
for mirror_type, mirror_config in self.mirrors.items():
|
||||
if not isinstance(mirror_config, dict):
|
||||
continue
|
||||
|
||||
sources = mirror_config.get('sources', [])
|
||||
if sources:
|
||||
# 使用配置的优先列表
|
||||
self._source_priority[mirror_type] = sources
|
||||
else:
|
||||
# 使用内置的默认优先列表
|
||||
self._source_priority[mirror_type] = self._get_default_priority(mirror_type)
|
||||
|
||||
# 选择首选源
|
||||
if self._source_priority[mirror_type]:
|
||||
self._active_source[mirror_type] = self._source_priority[mirror_type][0]
|
||||
|
||||
def _get_default_priority(self, mirror_type: str) -> List[str]:
|
||||
"""获取默认的镜像源优先列表"""
|
||||
priorities = {
|
||||
'docker': ['docker.io', 'docker.mirrors.aliyun.com', 'dockerhub.azk8s.cn'],
|
||||
'apt': ['archive.ubuntu.com', 'mirrors.aliyun.com', 'security.ubuntu.com'],
|
||||
'yum': ['mirror.centos.org', 'mirrors.aliyun.com'],
|
||||
'pypi': ['pypi.org', 'pypi.mirrors.aliyun.com'],
|
||||
'npm': ['registry.npmjs.org', 'registry.npmmirror.com'],
|
||||
'go': ['proxy.golang.org', 'goproxy.cn']
|
||||
}
|
||||
return priorities.get(mirror_type, [])
|
||||
|
||||
def check_all(self) -> Dict[str, HealthCheckResult]:
|
||||
"""检查所有镜像源"""
|
||||
results = {}
|
||||
|
||||
for mirror_type, mirror_config in self.mirrors.items():
|
||||
if not isinstance(mirror_config, dict):
|
||||
continue
|
||||
|
||||
sources = mirror_config.get('sources', [])
|
||||
for source_name in sources:
|
||||
if source_name not in results:
|
||||
result = self.health_checker.check_source(source_name, {'url': self._get_source_url(mirror_type, source_name)})
|
||||
results[source_name] = result
|
||||
|
||||
return results
|
||||
|
||||
def _get_source_url(self, mirror_type: str, source_name: str) -> str:
|
||||
"""获取源 URL"""
|
||||
# 从配置中获取
|
||||
sources_config = self.mirrors.get(mirror_type, {}).get('sources_config', {})
|
||||
if source_name in sources_config:
|
||||
return sources_config[source_name].get('url', '')
|
||||
|
||||
# 从 URL 模板生成
|
||||
url_template = self.mirrors.get(mirror_type, {}).get('url_template', '')
|
||||
if url_template and '{mirror}' in url_template:
|
||||
return url_template.replace('{mirror}', source_name)
|
||||
|
||||
return ''
|
||||
|
||||
def get_active_source(self, mirror_type: str) -> Optional[str]:
|
||||
"""获取当前活跃的镜像源"""
|
||||
return self._active_source.get(mirror_type)
|
||||
|
||||
def get_source_for_request(self, mirror_type: str, original_url: str) -> str:
|
||||
"""
|
||||
根据故障切换策略获取请求的源 URL
|
||||
|
||||
Args:
|
||||
mirror_type: 镜像类型
|
||||
original_url: 原始 URL
|
||||
|
||||
Returns:
|
||||
str: 实际请求的 URL
|
||||
"""
|
||||
if not self.failover_enabled:
|
||||
return original_url
|
||||
|
||||
active_source = self._active_source.get(mirror_type)
|
||||
if not active_source:
|
||||
return original_url
|
||||
|
||||
source_url = self._get_source_url(mirror_type, active_source)
|
||||
if not source_url:
|
||||
return original_url
|
||||
|
||||
# 替换 URL 中的主机部分
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(original_url)
|
||||
|
||||
# 构建新 URL
|
||||
new_url = f"{parsed.scheme}://{source_url}{parsed.path}"
|
||||
if parsed.query:
|
||||
new_url += f"?{parsed.query}"
|
||||
|
||||
return new_url
|
||||
except Exception:
|
||||
return original_url
|
||||
|
||||
def perform_failover(self, mirror_type: str) -> bool:
|
||||
"""
|
||||
对指定镜像类型执行故障切换
|
||||
|
||||
Args:
|
||||
mirror_type: 镜像类型
|
||||
|
||||
Returns:
|
||||
bool: 是否成功切换
|
||||
"""
|
||||
priority_list = self._source_priority.get(mirror_type, [])
|
||||
if not priority_list:
|
||||
return False
|
||||
|
||||
current_index = 0
|
||||
if mirror_type in self._active_source:
|
||||
try:
|
||||
current_index = priority_list.index(self._active_source[mirror_type])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 查找下一个健康的源
|
||||
old_source = self._active_source.get(mirror_type)
|
||||
|
||||
for i in range(current_index + 1, len(priority_list)):
|
||||
source_name = priority_list[i]
|
||||
result = self.health_checker.get_result(source_name)
|
||||
|
||||
if result and result.status == HealthStatus.HEALTHY:
|
||||
self._active_source[mirror_type] = source_name
|
||||
|
||||
# 记录故障切换
|
||||
failover_record = {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'mirror_type': mirror_type,
|
||||
'old_source': old_source,
|
||||
'new_source': source_name,
|
||||
'reason': 'Health check failed'
|
||||
}
|
||||
self._failover_history.append(failover_record)
|
||||
|
||||
# 保持历史记录在合理范围内
|
||||
if len(self._failover_history) > 100:
|
||||
self._failover_history = self._failover_history[-50:]
|
||||
|
||||
# 触发回调
|
||||
if self._on_failover:
|
||||
try:
|
||||
self._on_failover(mirror_type, old_source, source_name)
|
||||
except Exception as e:
|
||||
logger.error(f"故障切换回调执行失败: {e}")
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_failover_history(self, limit: int = 10) -> List[dict]:
|
||||
"""获取故障切换历史"""
|
||||
return self._failover_history[-limit:]
|
||||
|
||||
def get_health_summary(self) -> dict:
|
||||
"""获取健康状态摘要"""
|
||||
return {
|
||||
'failover_enabled': self.failover_enabled,
|
||||
'health': self.health_checker.get_stats(),
|
||||
'active_sources': self._active_source.copy(),
|
||||
'failover_history_count': len(self._failover_history)
|
||||
}
|
||||
Reference in New Issue
Block a user