Files
ulthon_admin/extend/base/common/service/HostServiceBase.php
augushong fd89a60425 fix(timer): 修复 F2 代码质量问题
- HostServiceBase: move Log::error before throw so it actually executes
- TimerBase: remove empty comment block
2026-05-26 18:33:44 +08:00

164 lines
5.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace base\common\service;
use app\admin\model\SystemHost;
use app\common\tools\PathTools;
use think\facade\App;
use think\facade\Log;
/**
* 主机(节点)服务 - 基础类
* 用于注册、上报主机心跳及性能指标.
*/
class HostServiceBase
{
/**
* 获取当前节点的唯一ID。
* 该方法确保即使在多个具有相同hostname的隔离网络中ID也是唯一的。
* 它会在首次运行时生成一个ID并持久化到本地文件中以保证重启后ID不变。
*
* @return string
*/
public static function getNodeId(): string
{
// 定义一个持久化存储ID的文件路径 (runtime目录在部署时通常是可写的)
$idFilePath = App::getRootPath(). 'runtime/node_id.lock';
PathTools::intiDir($idFilePath);
// 1. 如果ID文件已存在直接读取并返回
if (file_exists($idFilePath)) {
$nodeId = file_get_contents($idFilePath);
if (!empty($nodeId)) {
return trim($nodeId);
}
}
// 2. 如果文件不存在或为空生成新的唯一ID
// 格式: hostname-8位随机字符串
$hostname = gethostname() ?: 'unknown_host';
$uniqueSuffix = substr(md5(uniqid((string) mt_rand(), true)), 0, 8);
$newNodeId = "{$hostname}-{$uniqueSuffix}";
// 3. 将新ID写入文件以便下次启动时使用
try {
file_put_contents($idFilePath, $newNodeId);
} catch (\Exception $e) {
Log::error('无法写入节点ID文件: ' . $idFilePath . ' - ' . $e->getMessage());
}
return $newNodeId;
}
/**
* 节点注册与心跳更新
* 这是一个原子操作,如果节点不存在则创建,如果存在则更新。
*/
public static function heartbeat()
{
$nodeId = static::getNodeId();
if (empty($nodeId)) {
Log::error('无法获取当前节点ID心跳更新失败');
return;
}
try {
$host = SystemHost::where('node_id', $nodeId)->find();
$data = static::collectHostInfo();
if (empty($host)) {
$host = new SystemHost();
// 首次注册
$data['node_id'] = $nodeId;
Log::info("节点 [{$nodeId}] 已成功注册并上线。");
}
$host->save($data);
// 主节点自动选举:若无在线主节点,当前节点自动成为主节点
$master = SystemHost::where('is_master', 1)->where('status', 1)->find();
if (empty($master)) {
$host->is_master = 1;
$host->save();
Log::info("节点 [{$nodeId}] 自动当选为主节点。");
}
// 过期节点检测超过90秒无心跳的节点标记为离线
$staleTime = date('Y-m-d H:i:s', time() - 90);
$staleCount = SystemHost::where('status', 1)
->where('last_heartbeat_at', '<', $staleTime)
->update(['status' => 0]);
if ($staleCount > 0) {
Log::info("已将 {$staleCount} 个过期节点标记为离线。");
}
} catch (\Exception $e) {
Log::error("节点 [{$nodeId}] 心跳更新失败: " . $e->getMessage());
throw $e;
}
}
/**
* 收集主机的性能指标和静态信息 (不依赖任何外部系统命令).
* @return array
*/
public static function collectHostInfo(): array
{
// 获取CPU平均负载 (如果函数存在且未被禁用)
$cpuLoad = null;
if (function_exists('sys_getloadavg')) {
$load = sys_getloadavg();
$cpuLoad = is_array($load) ? implode(',', array_map(fn ($l) => round($l, 2), $load)) : null;
}
return [
// 动态指标
'status' => 1,
'last_heartbeat_at' => date('Y-m-d H:i:s'),
'ip_address' => gethostbyname(gethostname()),
'cpu_load' => $cpuLoad,
'memory_usage' => memory_get_usage(), // false: 获取脚本自身内存占用
'disk_free' => disk_free_space(App::getRootPath()),
'disk_total' => disk_total_space(App::getRootPath()),
// 静态信息
'os_info' => php_uname(),
'php_version' => PHP_VERSION,
];
}
/**
* 获取当前主节点的node_id.
*
* @return string|null 返回主节点ID无主节点时返回null
*/
public static function getMasterNode(): ?string
{
$master = SystemHost::where('is_master', 1)->where('status', 1)->find();
return $master ? $master->node_id : null;
}
/**
* 手动切换主节点.
*
* @param string $nodeId 目标节点的node_id
* @return bool 切换成功返回true节点不存在返回false
*/
public static function setMasterNode(string $nodeId): bool
{
// 先清除所有主节点标记
SystemHost::where('is_master', 1)->update(['is_master' => 0]);
// 设置新主节点
$host = SystemHost::where('node_id', $nodeId)->find();
if ($host) {
$host->is_master = 1;
$host->save();
return true;
}
return false;
}
}