集群支持

This commit is contained in:
iVampireSP.com 2023-02-10 03:04:39 +08:00
parent 35aeecd2c7
commit 50763da6dc
No known key found for this signature in database
GPG Key ID: 2F7B001CA27A8132
8 changed files with 365 additions and 4 deletions

View File

@ -48,17 +48,23 @@ public function handle(): int
return CommandAlias::SUCCESS;
}
private function format(string $event, array $message = [])
public function format(string $event, array $message = [], $stdout = true): ?string
{
$status = $this->switch($event, $message['data']);
if (! $status) {
return;
return null;
}
$message = "[{$message['node']['type']}] {$message['node']['id']}:$event: ".$status;
$this->info($message);
if ($stdout) {
$this->info($message);
return null;
} else {
return $message;
}
}
public function switch($event, $message = []): string|null

View File

@ -0,0 +1,101 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Console\Commands\Cluster\Monitor;
use App\Http\Controllers\Controller;
use App\Support\ClusterSupport;
use Illuminate\Contracts\View\View;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Symfony\Component\HttpFoundation\StreamedResponse;
class NodeController extends Controller
{
/**
* Display a listing of the resource.
*
* @return View
*/
public function index()
{
$nodes = ClusterSupport::nodes();
return view('admin.cluster.nodes', compact('nodes'));
}
/**
* Update the specified resource in storage.
*
* @param Request $request
* @param string $node
* @return JsonResponse
*/
public function update(Request $request, string $node)
{
$request->validate([
'weight' => 'sometimes|integer|min:0|max:100',
]);
ClusterSupport::update($node, [
'weight' => $request->input('weight'),
]);
return $this->success('Updated');
}
public function event(Request $request)
{
$request->validate([
'restart' => 'nullable|string|max:10|in:web,queue',
]);
if ($service = $request->input('restart')) {
if ($service === 'web') {
ClusterSupport::publish('cluster.restart.'.$service);
} elseif ($service === 'queue') {
Artisan::call('queue:restart');
}
}
return back()->with('success', '已经向集群广播命令。');
}
public function stream()
{
$response = new StreamedResponse(function () {
ClusterSupport::publish('monitor.started');
ClusterSupport::listen('*', function ($event, $message) {
$monitor = new Monitor();
if (connection_aborted()) {
return;
}
$msg = $monitor->format($event, $message, false);
echo 'data: '.$msg."\n\n";
ob_flush();
flush();
}, false);
});
$response->headers->set('Content-Type', 'text/event-stream');
$response->headers->set('X-Accel-Buffering', 'no');
$response->headers->set('Cache-Control', 'no-cache');
return $response;
}
// /**
// * Remove the specified resource from storage.
// *
// * @param int $id
// * @return Response
// */
// public function destroy($id)
// {
// //
// }
}

View File

@ -66,6 +66,11 @@ public static function updateThisNode($data = []): void
{
$node_id = config('settings.node.id');
self::update($node_id, $data);
}
public static function update($node_id, $data = []): void
{
$node = self::hget('nodes', $node_id, '[]');
$node = json_decode($node, true);

View File

@ -0,0 +1,24 @@
@extends('layouts.admin')
@section('title', '事件广播')
@section('content')
<h3>事件广播</h3>
<p>对集群广播命令。</p>
<h4>重启</h4>
<form method="POST" action="{{ route('admin.cluster.events.send') }}" class="d-inline">
@csrf
<input type="hidden" name="restart" value="web" />
<button type="submit" class="btn btn-primary">Web 服务</button>
</form>
<form method="POST" action="{{ route('admin.cluster.events.send') }}" class="d-inline">
@csrf
<input type="hidden" name="restart" value="queue" />
<button type="submit" class="btn btn-primary">队列服务</button>
</form>
@endsection

View File

@ -0,0 +1,53 @@
@php use Illuminate\Support\Carbon; @endphp
@extends('layouts.admin')
@section('title', '监视器')
@section('content')
<h3>集群日志监视器</h3>
<div id="logs"></div>
<script>
const eventSource = new EventSource('stream');
let line = 0;
let currentLine = 0;
let maxLine = 100;
const logs = document.getElementById('logs');
eventSource.onmessage = function (event) {
const text = event.data
if (text === '') {
return;
}
// 将 text 填充到 logs 中,如果超过 maxLine 行,则删除最早的一行
const lines = text.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line === '') {
continue;
}
const span = document.createElement('span');
const localDateTimeString = new Date().toLocaleString();
span.innerHTML = `[${localDateTimeString}]` + line + '<br/>';
logs.appendChild(span);
if (currentLine > maxLine) {
logs.removeChild(logs.firstChild);
currentLine--;
}
currentLine++;
window.scrollTo(0, document.body.scrollHeight);
}
}
</script>
@endsection

View File

@ -0,0 +1,139 @@
@php use Illuminate\Support\Carbon; @endphp
@extends('layouts.admin')
@section('title', '节点')
@section('content')
<h3>Cluster Ready!</h3>
<p>节点管理</p>
<table class="table table-hover">
<thead>
<th>
类型
</th>
<th>
标识
</th>
<th>
对端地址
</th>
<th>
权重(可更改)
</th>
<th>
上次心跳
</th>
{{-- <th>--}}
{{-- 管理--}}
{{-- </th>--}}
</thead>
<tbody>
@foreach($nodes as $node)
<tr>
<td>
@if ($node['type'] == 'master')
<span class="text-success">主节点</span>
@elseif ($node['type'] == 'slave')
<span class="text-secondary">工作节点</span>
@elseif ($node['type'] == 'edge')
<span class="text-warning">边缘节点</span>
@endif
</td>
<td>
{{ $node['id'] }}
</td>
<td>
{{ $node['ip'] }}
</td>
<td>
<span class="editable" node-id="{{ $node['id'] }}" value=" {{ $node['weight'] ?? '' }}"></span>
</td>
<td>
@php($time = Carbon::createFromTimestamp($node['last_heartbeat']))
@if ($time->diffInMinutes() > 1)
<span class="text-danger">{{ $time->diffForHumans() }}</span>
@else
<span class="text-success">{{ $time->diffForHumans() }}</span>
@endif
</td>
{{-- <td>--}}
{{-- <a>--}}
{{-- 清除数据--}}
{{-- </a>--}}
{{-- </td>--}}
</tr>
@endforeach
</tbody>
</table>
<p>注意: 权重为 <span class="text-danger">0</span> 则不调度。</p>
<script>
let editables = document.querySelectorAll('.editable');
editables.forEach(function (editable) {
// fill :value
editable.innerText = editable.getAttribute('value');
editable.addEventListener('click', function () {
let input = document.createElement('input');
input.value = editable.innerText;
input.classList.add('form-control')
editable.innerText = '';
editable.appendChild(input);
input.focus();
input.addEventListener('blur', function () {
editable.innerText = input.value;
input.remove();
// 不能为空,负数
if (input.value === '' || input.value < 0) {
editable.innerText = editable.getAttribute('value');
} else {
let node_id = editable.getAttribute('node-id');
axios.patch('nodes/' + node_id, {
weight: input.value
}).then(function () {
editable.setAttribute('value', input.value);
}).catch(function (error) {
editable.innerText = editable.getAttribute('value');
if (error.response.status === 422) {
let errors = error.response.data.errors;
let message = '';
for (let key in errors) {
message += errors[key][0] + '\n';
}
alert(message);
} else {
alert('服务器错误');
}
});
}
});
input.addEventListener('keyup', function (e) {
if (e.key === 'Enter') {
input.blur();
}
});
});
});
</script>
<style>
.editable {
cursor: pointer;
}
.editable input {
width: 10rem;
}
</style>
@endsection

View File

@ -87,7 +87,6 @@
<li>
<a class="dropdown-item" href="{{ route('admin.devices.index') }}">物联设备</a>
</li>
<li>
<a class="dropdown-item" href="{{ route('admin.applications.index') }}">应用程序</a>
</li>
@ -113,6 +112,31 @@
</li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownCluster" role="button"
data-bs-toggle="dropdown" aria-expanded="false">
集群
</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdownCluster">
<li>
<a class="dropdown-item" href="{{ route('admin.cluster.nodes') }}">
节点
</a>
</li>
<li>
<a class="dropdown-item" href="{{ route('admin.cluster.events') }}">
广播
</a>
</li>
<li>
<a class="dropdown-item" href="{{ route('admin.cluster.monitor') }}">
监视器
</a>
</li>
</ul>
</li>
</ul>
@endauth

View File

@ -7,6 +7,7 @@
use App\Http\Controllers\Admin\HomeController;
use App\Http\Controllers\Admin\HostController;
use App\Http\Controllers\Admin\ModuleController;
use App\Http\Controllers\Admin\NodeController;
use App\Http\Controllers\Admin\NotificationController;
use App\Http\Controllers\Admin\ReplyController;
use App\Http\Controllers\Admin\UserController;
@ -41,6 +42,13 @@
Route::get('devices', [DeviceController::class, 'index'])->name('devices.index');
Route::delete('devices', [DeviceController::class, 'destroy'])->name('devices.destroy');
Route::get('cluster/nodes', [NodeController::class, 'index'])->name('cluster.nodes');
Route::patch('cluster/nodes/{node}', [NodeController::class, 'update'])->name('cluster.nodes.update');
Route::view('cluster/events', 'admin.cluster.events')->name('cluster.events');
Route::post('cluster/events', [NodeController::class, 'event'])->name('cluster.events.send');
Route::view('cluster/monitor', 'admin.cluster.monitor')->name('cluster.monitor');
Route::get('cluster/stream', [NodeController::class, 'stream'])->name('cluster.stream');
Route::resource('notifications', NotificationController::class)->only(['create', 'store']);
Route::view('commands', 'admin.commands')->name('commands');
@ -51,4 +59,5 @@
Route::get('/login', [AuthController::class, 'index'])->name('login');
Route::post('/login', [AuthController::class, 'login']);
});
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');