mirror of
https://github.com/Kori1c/ecs-controller.git
synced 2026-06-03 06:49:30 +08:00
Handle invalid AK protection and tune local PHP-FPM
This commit is contained in:
@@ -478,6 +478,9 @@ class AliyunTrafficCheck
|
||||
$logPrefix = "[{$accountLabel}]";
|
||||
$actions = [];
|
||||
$forceRefresh = false;
|
||||
$protectionSuspended = !empty($account['protection_suspended']);
|
||||
$protectionSuspendReason = trim((string) ($account['protection_suspend_reason'] ?? ''));
|
||||
$protectionSuspendNotifiedAt = (int) ($account['protection_suspend_notified_at'] ?? 0);
|
||||
|
||||
// 1. 自适应心跳
|
||||
$lastUpdate = $account['updated_at'] ?? 0;
|
||||
@@ -506,6 +509,23 @@ class AliyunTrafficCheck
|
||||
'traffic_api_status' => $trafficResult['status'] ?? 'ok',
|
||||
'traffic_api_message' => $trafficResult['message'] ?? ''
|
||||
];
|
||||
$authInvalid = $this->isCredentialInvalidTrafficStatus($trafficResult['status'] ?? '');
|
||||
|
||||
if ($authInvalid) {
|
||||
$metadata['protection_suspended'] = 1;
|
||||
$metadata['protection_suspend_reason'] = 'credential_invalid';
|
||||
$metadata['protection_suspend_notified_at'] = $protectionSuspendNotifiedAt;
|
||||
$protectionSuspended = true;
|
||||
$protectionSuspendReason = 'credential_invalid';
|
||||
} elseif ($protectionSuspended && $protectionSuspendReason === 'credential_invalid') {
|
||||
$metadata['protection_suspended'] = 0;
|
||||
$metadata['protection_suspend_reason'] = '';
|
||||
$metadata['protection_suspend_notified_at'] = 0;
|
||||
$protectionSuspended = false;
|
||||
$protectionSuspendReason = '';
|
||||
$protectionSuspendNotifiedAt = 0;
|
||||
$this->db->addLog('info', "账号鉴权已恢复,自动停机保护已重新启用 [{$accountLabel}]");
|
||||
}
|
||||
|
||||
if (empty($trafficResult['success'])) {
|
||||
$traffic = $account['traffic_used'];
|
||||
@@ -548,20 +568,37 @@ class AliyunTrafficCheck
|
||||
$trafficDesc .= $isHardLimitExceeded ? "[已超出上限]" : "[接近上限]";
|
||||
|
||||
if ($thresholdAction === 'stop_and_notify') {
|
||||
$canAttemptStop = !in_array($status, ['Stopped', 'Stopping', 'Released'], true);
|
||||
|
||||
// 达到账号流量上限后必须立即保护,不再等待下一次接口刷新窗口。
|
||||
if ($canAttemptStop) {
|
||||
if ($this->safeControlInstance($account, 'stop', $shutdownMode)) {
|
||||
$previousStatus = $status;
|
||||
$actions[] = $isHardLimitExceeded ? "已超量自动停机" : "接近上限自动停机";
|
||||
$this->db->addLog('warning', "账号出口流量达到保护线,已自动停机 [{$accountLabel}] 当前使用率:{$usagePercent}%");
|
||||
$this->configManager->updateAccountStatus($account['id'], $traffic, 'Stopping', $currentTime);
|
||||
$this->notifyStatusChangeIfNeeded($account, $previousStatus, 'Stopping', '流量达到保护线,已自动停机。');
|
||||
$status = 'Stopping';
|
||||
if ($protectionSuspended && $protectionSuspendReason === 'credential_invalid') {
|
||||
if ($protectionSuspendNotifiedAt <= 0) {
|
||||
$actions[] = "账号密钥失效,已暂停自动停机";
|
||||
$notifyResult = $this->notificationService->notifyCredentialInvalid($accountLabel, $accountTraffic, $usagePercent, $threshold);
|
||||
$this->logNotificationResult($notifyResult, $accountLabel);
|
||||
$this->db->addLog('warning', "检测到账号鉴权失效,已暂停自动停机保护 [{$accountLabel}] 当前使用率:{$usagePercent}%");
|
||||
$protectionSuspendNotifiedAt = $currentTime;
|
||||
$this->configManager->updateAccountStatus($account['id'], $traffic, $status, $lastUpdate, [
|
||||
'protection_suspended' => 1,
|
||||
'protection_suspend_reason' => 'credential_invalid',
|
||||
'protection_suspend_notified_at' => $protectionSuspendNotifiedAt
|
||||
]);
|
||||
} else {
|
||||
$actions[] = "自动停机失败";
|
||||
$this->db->addLog('error', "账号出口流量达到保护线,但自动停机失败 [{$accountLabel}] 当前使用率:{$usagePercent}%");
|
||||
$apiStatusLog .= " [鉴权失效,已暂停自动停机]";
|
||||
}
|
||||
} else {
|
||||
$canAttemptStop = !in_array($status, ['Stopped', 'Stopping', 'Released'], true);
|
||||
|
||||
// 达到账号流量上限后必须立即保护,不再等待下一次接口刷新窗口。
|
||||
if ($canAttemptStop) {
|
||||
if ($this->safeControlInstance($account, 'stop', $shutdownMode)) {
|
||||
$previousStatus = $status;
|
||||
$actions[] = $isHardLimitExceeded ? "已超量自动停机" : "接近上限自动停机";
|
||||
$this->db->addLog('warning', "账号出口流量达到保护线,已自动停机 [{$accountLabel}] 当前使用率:{$usagePercent}%");
|
||||
$this->configManager->updateAccountStatus($account['id'], $traffic, 'Stopping', $currentTime);
|
||||
$this->notifyStatusChangeIfNeeded($account, $previousStatus, 'Stopping', '流量达到保护线,已自动停机。');
|
||||
$status = 'Stopping';
|
||||
} else {
|
||||
$actions[] = "自动停机失败";
|
||||
$this->db->addLog('error', "账号出口流量达到保护线,但自动停机失败 [{$accountLabel}] 当前使用率:{$usagePercent}%");
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($shouldCheckApi) {
|
||||
@@ -569,7 +606,7 @@ class AliyunTrafficCheck
|
||||
$this->db->addLog('warning', "账号出口流量超限触发提醒 [{$accountLabel}] 当前使用率:{$usagePercent}%");
|
||||
}
|
||||
|
||||
if (!empty($actions)) {
|
||||
if (!empty($actions) && !($protectionSuspended && $protectionSuspendReason === 'credential_invalid')) {
|
||||
$mailRes = $this->notificationService->sendTrafficWarning($accountLabel, $accountTraffic, $usagePercent, implode(',', $actions), $threshold);
|
||||
$this->logNotificationResult($mailRes, $accountLabel);
|
||||
}
|
||||
@@ -683,6 +720,14 @@ class AliyunTrafficCheck
|
||||
'traffic_api_status' => $trafficResult['status'] ?? 'ok',
|
||||
'traffic_api_message' => $trafficResult['message'] ?? ''
|
||||
];
|
||||
if ($this->isCredentialInvalidTrafficStatus($trafficResult['status'] ?? '')) {
|
||||
$metadata['protection_suspended'] = 1;
|
||||
$metadata['protection_suspend_reason'] = 'credential_invalid';
|
||||
} else {
|
||||
$metadata['protection_suspended'] = 0;
|
||||
$metadata['protection_suspend_reason'] = '';
|
||||
$metadata['protection_suspend_notified_at'] = 0;
|
||||
}
|
||||
|
||||
if (empty($trafficResult['success'])) {
|
||||
$traffic = $targetAccount['traffic_used'];
|
||||
@@ -1256,6 +1301,45 @@ class AliyunTrafficCheck
|
||||
return date('Y-m', (int) $timestamp) === date('Y-m', (int) $currentTime);
|
||||
}
|
||||
|
||||
private function isCredentialInvalidTrafficStatus($status)
|
||||
{
|
||||
return trim((string) $status) === 'auth_error';
|
||||
}
|
||||
|
||||
private function isCredentialInvalidError($code, $message = '')
|
||||
{
|
||||
$normalizedCode = strtolower(trim((string) $code));
|
||||
$normalizedMessage = strtolower(trim((string) $message));
|
||||
if ($normalizedCode === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$credentialErrorCodes = [
|
||||
'invalidaccesskeyid.notfound',
|
||||
'invalidaccesskeyid',
|
||||
'signaturedoesnotmatch',
|
||||
'incompletesignature',
|
||||
'forbidden.accesskeydisabled',
|
||||
'invalidsecuritytoken.expired',
|
||||
'invalidsecuritytoken.malformed',
|
||||
'missingsecuritytoken'
|
||||
];
|
||||
|
||||
if (in_array($normalizedCode, $credentialErrorCodes, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($normalizedMessage === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return strpos($normalizedMessage, 'access key is not found') !== false
|
||||
|| strpos($normalizedMessage, 'access key id does not exist') !== false
|
||||
|| strpos($normalizedMessage, 'signature does not match') !== false
|
||||
|| strpos($normalizedMessage, 'incomplete signature') !== false
|
||||
|| strpos($normalizedMessage, 'accesskeydisabled') !== false;
|
||||
}
|
||||
|
||||
private function safeGetTraffic($account)
|
||||
{
|
||||
try {
|
||||
@@ -1269,13 +1353,21 @@ class AliyunTrafficCheck
|
||||
$code = trim((string) $e->getErrorCode());
|
||||
$message = '缺少云监控权限';
|
||||
$status = 'permission_denied';
|
||||
if ($code !== '' && !in_array($code, ['403', 'NoPermission'], true)) {
|
||||
if ($this->isCredentialInvalidError($code, $e->getMessage())) {
|
||||
$message = '账号 AK 已失效';
|
||||
$status = 'auth_error';
|
||||
} elseif ($code !== '' && !in_array($code, ['403', 'NoPermission'], true)) {
|
||||
$message = '云监控鉴权失败';
|
||||
$status = 'auth_error';
|
||||
}
|
||||
$this->db->addLog('error', "公网出口流量查询配置错误 [{$this->getAccountLogLabel($account)}]: " . ($code ?: "鉴权失败") . ",请确认AK拥有云监控流量查询权限");
|
||||
return ['success' => false, 'value' => null, 'status' => $status, 'message' => $message];
|
||||
} catch (ServerException $e) {
|
||||
$code = trim((string) $e->getErrorCode());
|
||||
if ($this->isCredentialInvalidError($code, $e->getErrorMessage())) {
|
||||
$this->db->addLog('error', "公网出口流量查询失败 [{$this->getAccountLogLabel($account)}]: {$code} - " . $e->getErrorMessage());
|
||||
return ['success' => false, 'value' => null, 'status' => 'auth_error', 'message' => '账号 AK 已失效'];
|
||||
}
|
||||
$this->db->addLog('error', "公网出口流量查询失败 [{$this->getAccountLogLabel($account)}]: " . $e->getErrorCode() . " - " . $e->getErrorMessage());
|
||||
return ['success' => false, 'value' => null, 'status' => 'sync_error', 'message' => '云监控接口异常'];
|
||||
} catch (\Exception $e) {
|
||||
@@ -1951,6 +2043,14 @@ class AliyunTrafficCheck
|
||||
'traffic_api_status' => $trafficResult['status'] ?? 'ok',
|
||||
'traffic_api_message' => $trafficResult['message'] ?? ''
|
||||
];
|
||||
if ($this->isCredentialInvalidTrafficStatus($trafficResult['status'] ?? '')) {
|
||||
$metadata['protection_suspended'] = 1;
|
||||
$metadata['protection_suspend_reason'] = 'credential_invalid';
|
||||
} else {
|
||||
$metadata['protection_suspended'] = 0;
|
||||
$metadata['protection_suspend_reason'] = '';
|
||||
$metadata['protection_suspend_notified_at'] = 0;
|
||||
}
|
||||
$trafficApiStatus = $metadata['traffic_api_status'];
|
||||
$trafficApiMessage = $metadata['traffic_api_message'];
|
||||
|
||||
|
||||
@@ -877,6 +877,18 @@ class ConfigManager
|
||||
$sql .= ", traffic_api_message = ?";
|
||||
$params[] = $metadata['traffic_api_message'];
|
||||
}
|
||||
if (isset($metadata['protection_suspended'])) {
|
||||
$sql .= ", protection_suspended = ?";
|
||||
$params[] = $metadata['protection_suspended'] ? 1 : 0;
|
||||
}
|
||||
if (isset($metadata['protection_suspend_reason'])) {
|
||||
$sql .= ", protection_suspend_reason = ?";
|
||||
$params[] = (string) $metadata['protection_suspend_reason'];
|
||||
}
|
||||
if (isset($metadata['protection_suspend_notified_at'])) {
|
||||
$sql .= ", protection_suspend_notified_at = ?";
|
||||
$params[] = (int) $metadata['protection_suspend_notified_at'];
|
||||
}
|
||||
|
||||
$sql .= " WHERE id = ?";
|
||||
$params[] = $id;
|
||||
|
||||
@@ -210,6 +210,9 @@ class Database
|
||||
$this->ensureColumn('accounts', 'health_status', "TEXT DEFAULT 'Unknown'");
|
||||
$this->ensureColumn('accounts', 'traffic_api_status', "TEXT DEFAULT 'ok'");
|
||||
$this->ensureColumn('accounts', 'traffic_api_message', "TEXT DEFAULT ''");
|
||||
$this->ensureColumn('accounts', 'protection_suspended', 'INTEGER DEFAULT 0');
|
||||
$this->ensureColumn('accounts', 'protection_suspend_reason', "TEXT DEFAULT ''");
|
||||
$this->ensureColumn('accounts', 'protection_suspend_notified_at', 'INTEGER DEFAULT 0');
|
||||
$this->ensureColumn('ecs_create_tasks', 'public_ip_mode', "TEXT DEFAULT 'ecs_public_ip'");
|
||||
$this->ensureColumn('ecs_create_tasks', 'eip_allocation_id', "TEXT DEFAULT ''");
|
||||
$this->ensureColumn('ecs_create_tasks', 'eip_address', "TEXT DEFAULT ''");
|
||||
|
||||
@@ -70,6 +70,7 @@ WORKDIR /var/www/html
|
||||
|
||||
# 复制 Nginx 配置 (利用缓存,变更频率低)
|
||||
COPY docker/nginx.conf /etc/nginx/http.d/default.conf
|
||||
COPY docker/php-fpm-www.conf /usr/local/etc/php-fpm.d/zz-www-local.conf
|
||||
|
||||
# 复制并配置启动脚本
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
@@ -82,4 +83,4 @@ COPY --from=builder --chown=www-data:www-data /app /var/www/html
|
||||
EXPOSE 80
|
||||
|
||||
# 设置容器启动入口
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
@@ -71,6 +71,32 @@ class NotificationService
|
||||
return $this->dispatchNotifications($title, "检测到流量异常或达到阈值", $details, 'warning', $textMsg, $accessKeyId);
|
||||
}
|
||||
|
||||
public function notifyCredentialInvalid($accessKeyId, $traffic, $percentage, $threshold)
|
||||
{
|
||||
$title = "流量告警 - 账号密钥已失效";
|
||||
$trafficText = $this->formatTraffic((float) $traffic);
|
||||
$maskedAccount = substr($accessKeyId, 0, 7) . '***';
|
||||
$statusText = '检测到 AK 已失效,已暂停自动停机';
|
||||
$details = [
|
||||
['label' => '账号', 'value' => $maskedAccount],
|
||||
['label' => '当前流量', 'value' => $trafficText],
|
||||
['label' => '使用率', 'value' => $percentage . '%', 'highlight' => true],
|
||||
['label' => '设定阈值', 'value' => $threshold . '%'],
|
||||
['label' => '当前状态', 'value' => $statusText],
|
||||
['label' => '处理建议', 'value' => '请更新 AK 后再恢复自动停机保护。']
|
||||
];
|
||||
|
||||
$textMsg = "【ECS 服务器管家】{$title}\n" .
|
||||
"账号: {$maskedAccount}\n" .
|
||||
"当前流量: {$trafficText}\n" .
|
||||
"使用率: {$percentage}%\n" .
|
||||
"设定阈值: {$threshold}%\n" .
|
||||
"当前状态: {$statusText}\n" .
|
||||
"处理建议: 请更新 AK 后再恢复自动停机保护。";
|
||||
|
||||
return $this->dispatchNotifications($title, "检测到账号密钥失效,已暂停自动停机保护,避免重复失败通知。", $details, 'warning', $textMsg, $accessKeyId);
|
||||
}
|
||||
|
||||
public function notifyEcsCreated($accountLabel, array $result, array $preview = [])
|
||||
{
|
||||
$title = 'ECS 创建并启动成功';
|
||||
|
||||
7
docker/php-fpm-www.conf
Normal file
7
docker/php-fpm-www.conf
Normal file
@@ -0,0 +1,7 @@
|
||||
[www]
|
||||
pm = dynamic
|
||||
pm.max_children = 12
|
||||
pm.start_servers = 4
|
||||
pm.min_spare_servers = 2
|
||||
pm.max_spare_servers = 6
|
||||
pm.max_requests = 500
|
||||
Reference in New Issue
Block a user