mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-08-30 12:45:32 +08:00
feat(nginx-log): 多节点适配(node_id 全链路 + 全局/按节点聚合 + 节点筛选 + v2.4.0 升级脚本)
- 6 张表加 node_id 字段 + position/stat 改复合唯一键(migration) - 6 个 Scheme 同步 node_id 注解 + 唯一键 - Reader 带 node_id 参数 + 懒加载接管(getLastPosition 回退查 node_id='') - Aggregator 全局行(node_id='')+ 按节点循环 insertAggregatesForScope - Stat 所有查询方法加 where node_id 条件 - Dashboard 控制器/视图加节点筛选下拉框 + AJAX 带 node_id - AccessLog 列表加节点列 + 采集进度卡片显示节点信息 - import 定时任务 run_type 改 all(每节点采集自己的日志) - v2.4.0 升级脚本追加 ALTER TABLE(幂等 check-then-alter) - 菜单调整:去掉 Nginx 顶级菜单 + 读取位置管理,改为系统管理下挂两个子菜单 - ulthon-timer 文档补充 run_type=all 多节点部署注意事项
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
namespace base\common\service;
|
||||
|
||||
use app\admin\model\NginxLogPosition;
|
||||
use app\common\service\HostService;
|
||||
|
||||
/**
|
||||
* Nginx 日志增量读取 service(Base 层).
|
||||
@@ -10,12 +11,17 @@ use app\admin\model\NginxLogPosition;
|
||||
* 职责:
|
||||
* - 按文件 offset 增量读取日志行(Generator,不读整个文件到内存)
|
||||
* - 检测日志轮转(inode 变化 OR filesize < offset,覆盖 rename+rebuild 与 copytruncate 两种模式)
|
||||
* - 将读取进度持久化到 NginxLogPosition 表(upsert)
|
||||
* - 将读取进度持久化到 NginxLogPosition 表(upsert,按 node_id 隔离)
|
||||
*
|
||||
* 多节点适配:position 表按 (node_id, file_path) 唯一定位读取进度。
|
||||
* 旧数据(node_id 为空)通过懒加载接管(见 getLastPosition):首个查询到它的本节点原子 UPDATE node_id,
|
||||
* 避免 migration 全表回填(多节点共享 DB 时 migration 只跑一次,只有懒加载能确保每节点各自接管)。
|
||||
*
|
||||
* 不负责:解析(T5 Parser)、聚合(T7 Aggregator)。
|
||||
*
|
||||
* 依赖倒置:内部 model 调用走 `app\admin\model\NginxLogPosition`(App 入口类),
|
||||
* 使用者可在 app/ 重写该 model 拦截行为;本类不直接 `new NginxLogPosition`(静态调用满足多态)。
|
||||
* 节点身份走 `app\common\service\HostService`(App 入口类)。
|
||||
*/
|
||||
class NginxLogReaderServiceBase
|
||||
{
|
||||
@@ -159,15 +165,27 @@ class NginxLogReaderServiceBase
|
||||
/**
|
||||
* 查询文件的上次读取位置.
|
||||
*
|
||||
* 多节点适配:where 条件带 node_id 隔离各节点进度。
|
||||
* 懒加载接管:多节点共享 DB 时 migration 只跑一次,旧记录 node_id 为空,
|
||||
* 首个查询到它的本节点原子 UPDATE node_id 接管,保证每节点各自有独立 position 记录。
|
||||
*
|
||||
* @param string $filePath 日志文件绝对路径
|
||||
*
|
||||
* @return array|null 命中时返回 [inode, offset, last_line_hash, parse_fail_count, parse_fail_samples],无记录返回 null
|
||||
*/
|
||||
public function getLastPosition(string $filePath): ?array
|
||||
{
|
||||
$row = NginxLogPosition::where('file_path', $filePath)->find();
|
||||
$nodeId = HostService::getNodeId();
|
||||
|
||||
$row = NginxLogPosition::where('file_path', $filePath)
|
||||
->where('node_id', $nodeId)
|
||||
->find();
|
||||
if ($row === null) {
|
||||
return null;
|
||||
// 懒加载接管:旧记录 node_id 为空,原子抢占接管到当前节点
|
||||
$row = $this->lazyAdoptPosition($filePath, $nodeId);
|
||||
if ($row === null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -179,6 +197,39 @@ class NginxLogReaderServiceBase
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 懒加载接管:把旧的无 node_id 记录原子接管到当前节点.
|
||||
*
|
||||
* 多节点共享 DB 时 migration 只跑一次,无法为每节点预置 position 记录。
|
||||
* 旧记录(node_id='')由首个查询到它的本节点通过原子条件 UPDATE 接管:
|
||||
* UPDATE ... SET node_id=current WHERE node_id='' AND file_path=X
|
||||
* 并发安全:WHERE node_id='' 保证只有一个节点能 affected=1,其他节点查询时已被接管。
|
||||
*
|
||||
* @return \think\Model|null 接管成功返回接管后的记录,无旧记录或被其他节点抢占返回 null
|
||||
*/
|
||||
protected function lazyAdoptPosition(string $filePath, string $nodeId)
|
||||
{
|
||||
$legacy = NginxLogPosition::where('file_path', $filePath)
|
||||
->where('node_id', '')
|
||||
->find();
|
||||
if ($legacy === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 原子条件 UPDATE:只有 node_id='' 时才能被接管(防并发多节点同时接管同一记录)
|
||||
$affected = NginxLogPosition::where('id', $legacy->getData('id'))
|
||||
->where('node_id', '')
|
||||
->update(['node_id' => $nodeId]);
|
||||
if ($affected === 0) {
|
||||
// 被其他节点抢先接管,本节点放弃(下次 savePosition 会创建新记录)
|
||||
return null;
|
||||
}
|
||||
|
||||
return NginxLogPosition::where('file_path', $filePath)
|
||||
->where('node_id', $nodeId)
|
||||
->find();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存读取位置(upsert).
|
||||
*
|
||||
@@ -196,8 +247,11 @@ class NginxLogReaderServiceBase
|
||||
public function savePosition(string $filePath, int $inode, int $offset, ?string $lastLineHash, int $failCount = 0, ?string $failSamples = null): void
|
||||
{
|
||||
$now = time();
|
||||
$nodeId = HostService::getNodeId();
|
||||
|
||||
$existing = NginxLogPosition::where('file_path', $filePath)->find();
|
||||
$existing = NginxLogPosition::where('file_path', $filePath)
|
||||
->where('node_id', $nodeId)
|
||||
->find();
|
||||
if ($existing !== null) {
|
||||
$existing->save([
|
||||
'inode' => $inode,
|
||||
@@ -212,6 +266,7 @@ class NginxLogReaderServiceBase
|
||||
}
|
||||
|
||||
NginxLogPosition::create([
|
||||
'node_id' => $nodeId,
|
||||
'file_path' => $filePath,
|
||||
'inode' => $inode,
|
||||
'offset' => $offset,
|
||||
|
||||
Reference in New Issue
Block a user