mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-07-01 15:32:48 +08:00
T5: TimerServiceBase.syncConfigToDatabase() - syncs task config to DB
T6: HostServiceBase - auto master election, stale node detection,
getMasterNode/setMasterNode methods
165 lines
5.3 KiB
PHP
165 lines
5.3 KiB
PHP
<?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) {
|
||
throw $e;
|
||
Log::error("节点 [{$nodeId}] 心跳更新失败: " . $e->getMessage());
|
||
Log::error($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;
|
||
}
|
||
}
|