feat(worker): 实现集群限流与自适应负载

保留平台模型 RPM、TPM 和并发策略语义,增加 PostgreSQL 集群级租约、饱和候选重选和多平台自动负载,避免突发任务固定等待首个平台。\n\n新增 Worker 实时负载采样、自适应 active/heavy 容量、心跳与管理端指标,并扩展本地 acceptance runner,覆盖三 Worker、同模型三平台 2/4/6 并发和 48 个带图视频突发任务。\n\n验证:go test ./...、go vet ./...、PostgreSQL 跨 Store 集成测试、gofmt、bash -n、ShellCheck 及本地集群 provider-burst 验收通过;48/48 成功,无越限、重复提交、重复计费、重复回调或终态资源泄漏。
This commit is contained in:
2026-08-03 00:13:46 +08:00
parent 9a01fd4657
commit c28bf74230
52 changed files with 3700 additions and 272 deletions
+16 -4
View File
@@ -37,6 +37,7 @@ import type {
UserGroupUpsertRequest,
UserGroup,
WalletRechargeRequest,
WorkerClusterRuntime,
} from '@easyai-ai-gateway/contracts';
import {
batchAccessRules,
@@ -67,6 +68,7 @@ import {
getRunnerPolicy,
getSecurityEventConnection,
getWalletSummary,
getWorkerClusterRuntime,
listAccessRules,
listAdminTasks,
listAuditLogs,
@@ -201,6 +203,7 @@ type DataKey =
| 'runtimePolicySets'
| 'rateLimitWindows'
| 'modelRateLimits'
| 'workerClusterRuntime'
| 'tenants'
| 'users'
| 'userGroups'
@@ -255,6 +258,7 @@ export function App() {
const [rateLimitWindows, setRateLimitWindows] = useState<RateLimitWindow[]>([]);
const [modelRateLimits, setModelRateLimits] = useState<ModelRateLimitStatus[]>([]);
const [modelRateLimitsUpdatedAt, setModelRateLimitsUpdatedAt] = useState<number | null>(null);
const [workerClusterRuntime, setWorkerClusterRuntime] = useState<WorkerClusterRuntime | null>(null);
const [tenants, setTenants] = useState<GatewayTenant[]>([]);
const [users, setUsers] = useState<GatewayUser[]>([]);
const [userGroups, setUserGroups] = useState<UserGroup[]>([]);
@@ -381,18 +385,21 @@ export function App() {
useEffect(() => {
if (!token || activePage !== 'admin' || adminSection !== 'realtimeLoad') return undefined;
const timer = window.setInterval(() => {
void Promise.all([listModelRateLimitStatuses(token), listPlatforms(token)])
.then(([rateLimitResponse, platformResponse]) => {
void Promise.all([listModelRateLimitStatuses(token), listPlatforms(token), getWorkerClusterRuntime(token)])
.then(([rateLimitResponse, platformResponse, workerRuntime]) => {
setModelRateLimits(rateLimitResponse.items);
setModelRateLimitsUpdatedAt(Date.now());
setPlatforms(platformResponse.items);
setWorkerClusterRuntime(workerRuntime);
loadedDataKeysRef.current.add('modelRateLimits');
loadedDataKeysRef.current.add('platforms');
loadedDataKeysRef.current.add('workerClusterRuntime');
})
.catch((err) => {
if (handleAuthExpired(err, token)) return;
loadedDataKeysRef.current.delete('modelRateLimits');
loadedDataKeysRef.current.delete('platforms');
loadedDataKeysRef.current.delete('workerClusterRuntime');
});
}, 3000);
return () => window.clearInterval(timer);
@@ -446,6 +453,7 @@ export function App() {
rateLimitWindows,
modelRateLimits,
modelRateLimitsUpdatedAt,
workerClusterRuntime,
runtimePolicySets,
securityEventConnection,
taskResult,
@@ -455,7 +463,7 @@ export function App() {
users,
walletAccounts,
walletTransactions,
}), [accessRules, adminTasks, apiKeys, auditLogs, baseModels, clientCustomizationSettings, currentUser, currentUserGroups, fileStorageChannels, fileStorageSettings, modelCatalog, modelRateLimits, modelRateLimitsUpdatedAt, models, networkProxyConfig, platforms, pricingRuleSets, pricingRules, providers, rateLimitWindows, runnerPolicy, runtimePolicySets, securityEventConnection, taskResult, tasks, tenants, userGroups, users, walletAccounts, walletTransactions]);
}), [accessRules, adminTasks, apiKeys, auditLogs, baseModels, clientCustomizationSettings, currentUser, currentUserGroups, fileStorageChannels, fileStorageSettings, modelCatalog, modelRateLimits, modelRateLimitsUpdatedAt, models, networkProxyConfig, platforms, pricingRuleSets, pricingRules, providers, rateLimitWindows, runnerPolicy, runtimePolicySets, securityEventConnection, taskResult, tasks, tenants, userGroups, users, walletAccounts, walletTransactions, workerClusterRuntime]);
async function refresh(nextToken = token) {
await ensureRouteData(nextToken, true);
@@ -593,6 +601,9 @@ export function App() {
setModelRateLimitsUpdatedAt(Date.now());
}
return;
case 'workerClusterRuntime':
setWorkerClusterRuntime(await getWorkerClusterRuntime(nextToken));
return;
case 'tenants':
setTenants((await listTenants(nextToken)).items);
return;
@@ -1255,6 +1266,7 @@ export function App() {
setAuditLogs([]);
setRateLimitWindows([]);
setModelRateLimits([]);
setWorkerClusterRuntime(null);
setTenants([]);
setUsers([]);
setUserGroups([]);
@@ -1744,7 +1756,7 @@ function dataKeysForRoute(
case 'platforms':
return ['platforms', 'models', 'providers', 'baseModels', 'pricingRuleSets', 'networkProxyConfig'];
case 'realtimeLoad':
return ['platforms', 'modelRateLimits'];
return ['platforms', 'modelRateLimits', 'workerClusterRuntime'];
case 'tasks':
return ['adminTasks', 'tenants', 'users', 'userGroups', 'platforms', 'models'];
case 'tenants':
+5
View File
@@ -59,6 +59,7 @@ import type {
WalletBalanceAdjustmentRequest,
WalletRechargeRequest,
WalletSummaryResponse,
WorkerClusterRuntime,
} from '@easyai-ai-gateway/contracts';
import type { AdminTaskQuery, PlatformCreateInput, PlatformModelBindingInput, WorkspaceTaskQuery } from './types';
@@ -1037,6 +1038,10 @@ export async function listModelRateLimitStatuses(token: string): Promise<ListRes
return request<ListResponse<ModelRateLimitStatus>>('/api/admin/runtime/model-rate-limits', { token });
}
export async function getWorkerClusterRuntime(token: string): Promise<WorkerClusterRuntime> {
return request<WorkerClusterRuntime>('/api/admin/runtime/workers', { token });
}
export async function restoreModelRuntimeStatus(token: string, platformModelId: string): Promise<ModelRateLimitStatus> {
return request<ModelRateLimitStatus>(`/api/admin/runtime/model-rate-limits/${platformModelId}/restore`, {
method: 'POST',
+2
View File
@@ -26,6 +26,7 @@ import type {
RuntimePolicySet,
SecurityEventConnectionResponse,
UserGroup,
WorkerClusterRuntime,
} from '@easyai-ai-gateway/contracts';
export interface ConsoleData {
@@ -50,6 +51,7 @@ export interface ConsoleData {
rateLimitWindows: RateLimitWindow[];
modelRateLimits: ModelRateLimitStatus[];
modelRateLimitsUpdatedAt: number | null;
workerClusterRuntime: WorkerClusterRuntime | null;
runtimePolicySets: RuntimePolicySet[];
securityEventConnection: SecurityEventConnectionResponse | null;
taskResult: GatewayTask | null;
+1 -1
View File
@@ -50,7 +50,7 @@ export const adminPages = [
{ title: '用户组策略', path: '/admin/user-groups', description: '用户组成员、充值折扣、调用折扣、TPM/RPM/并发和队列优先级。' },
{ title: '全局模型配置', path: '/admin/models/global', description: '基准模型库、能力 schema、基准定价和默认限流模板。' },
{ title: '平台管理', path: '/admin/platforms', description: '平台 CRUD、凭证、默认折扣、平台模型、限流和重试策略。' },
{ title: '实时负载', path: '/admin/realtime-load', description: '平台模型查看实时 RPM、TPM、并发、排队和冷却状态。' },
{ title: '实时负载', path: '/admin/realtime-load', description: '查看平台模型 RPM、TPM、并发,以及 Worker 自适应容量和压力状态。' },
{ title: '任务记录', path: '/admin/tasks', description: '跨租户查询任务、执行链路、参数转换、计费和原始详情。' },
{ title: '计费结算', path: '/admin/billing-settlements', description: '查询计费结算队列,批量处理等待重试和人工复核记录。' },
{ title: '运行与队列', path: '/admin/runtime/queues', description: 'TPM/RPM 窗口、并发 lease、cooldown、任务恢复和队列积压。' },
+1
View File
@@ -179,6 +179,7 @@ export function AdminPage(props: {
modelRateLimits={props.data.modelRateLimits}
modelRateLimitsUpdatedAt={props.data.modelRateLimitsUpdatedAt}
platforms={props.data.platforms}
workerClusterRuntime={props.data.workerClusterRuntime}
onSavePlatformDynamicPriority={props.onSavePlatformDynamicPriority}
onRestoreRuntimeModel={props.onRestoreRuntimeModel}
/>
+92 -1
View File
@@ -1,13 +1,14 @@
import { useEffect, useMemo, useState, type FormEvent } from 'react';
import { Popover as AntPopover } from 'antd';
import { CheckCircle2, History, RotateCcw, Search, SlidersHorizontal } from 'lucide-react';
import type { IntegrationPlatform, ModelRateLimitStatus, PlatformDynamicPriorityUpdateRequest, PlatformPolicyEvent, PriorityDemotionRecord } from '@easyai-ai-gateway/contracts';
import type { IntegrationPlatform, ModelRateLimitStatus, PlatformDynamicPriorityUpdateRequest, PlatformPolicyEvent, PriorityDemotionRecord, WorkerClusterRuntime, WorkerInstanceRuntime } from '@easyai-ai-gateway/contracts';
import { Badge, Button, Card, CardContent, EmptyState, FormDialog, Input, Label, Select, Table, TableCell, TableHead, TableRow } from '../../components/ui';
export function RealtimeLoadPanel(props: {
modelRateLimits: ModelRateLimitStatus[];
modelRateLimitsUpdatedAt: number | null;
platforms: IntegrationPlatform[];
workerClusterRuntime: WorkerClusterRuntime | null;
onSavePlatformDynamicPriority: (platformId: string, input: PlatformDynamicPriorityUpdateRequest) => Promise<void>;
onRestoreRuntimeModel: (platformModelId: string) => Promise<void>;
}) {
@@ -119,6 +120,7 @@ export function RealtimeLoadPanel(props: {
return (
<section className="pageStack">
<WorkerRuntimeTable runtime={props.workerClusterRuntime} />
<Card className="compactAdminTableCard">
<CardContent className="compactAdminTableContent">
<div className="compactAdminToolbar realtimeCompactToolbar">
@@ -241,6 +243,95 @@ export function RealtimeLoadPanel(props: {
);
}
function WorkerRuntimeTable(props: { runtime: WorkerClusterRuntime | null }) {
const workers = props.runtime?.workers ?? [];
const queue = props.runtime?.queue;
return (
<Card className="compactAdminTableCard">
<CardContent className="compactAdminTableContent">
<div className="compactAdminToolbar">
<span className="platformTableName">
<strong>Worker </strong>
<small>
{queue
? `共享队列 ${queue.queued},运行 ${queue.running},最老等待 ${Math.round(queue.oldestWaitSeconds)}`
: '等待集群负载快照'}
</small>
</span>
</div>
{!workers.length ? (
<EmptyState title="暂无活跃 Worker" description="Worker 心跳后会显示安全容量、阶段分布和压力状态。" />
) : (
<div className="platformLimitTableViewport">
<Table className="platformDataTable platformLimitTable" density="compact">
<TableRow className="shTableHeader">
<TableHead>Worker</TableHead>
<TableHead></TableHead>
<TableHead className="platformLimitNumberHead"> / / Hard</TableHead>
<TableHead className="platformLimitNumberHead"></TableHead>
<TableHead className="platformLimitNumberHead"> / / </TableHead>
<TableHead className="platformLimitNumberHead"> / </TableHead>
<TableHead></TableHead>
</TableRow>
{workers.map((worker) => (
<TableRow key={worker.instanceId}>
<TableCell>
<span className="platformTableName">
<strong>{worker.podName || worker.instanceId}</strong>
<small>{[worker.site, shortId(worker.revision)].filter(Boolean).join(' · ') || shortId(worker.instanceId)}</small>
</span>
</TableCell>
<TableCell>{workerPressureCell(worker)}</TableCell>
<TableCell className="platformLimitNumberCell">
<strong>{worker.allocatedCapacity} / {worker.safeCapacity} / {worker.hardCapacityLimit}</strong>
</TableCell>
<TableCell className="platformLimitNumberCell">{worker.heavyCapacity}</TableCell>
<TableCell className="platformLimitNumberCell">
<span className="rateMetricCell">
<strong>{worker.preparingTasks} / {worker.waitingUpstreamTasks} / {worker.finalizingTasks}</strong>
<small> {worker.reportedActiveTasks}</small>
</span>
</TableCell>
<TableCell className="platformLimitNumberCell">{worker.runningTasks} / {worker.activeLeases}</TableCell>
<TableCell>
<span className="platformTableName">
<strong>{formatDateTime(worker.loadSampledAt) || '-'}</strong>
<small> {formatDateTime(worker.heartbeatAt)}</small>
</span>
</TableCell>
</TableRow>
))}
</Table>
</div>
)}
</CardContent>
</Card>
);
}
function workerPressureCell(worker: WorkerInstanceRuntime) {
const variant = worker.pressureState === 'critical'
? 'destructive'
: worker.pressureState === 'busy'
? 'warning'
: worker.pressureState === 'normal'
? 'success'
: 'secondary';
const label = worker.pressureState === 'critical'
? '临界'
: worker.pressureState === 'busy'
? '繁忙'
: worker.pressureState === 'normal'
? '正常'
: '未知';
return (
<span className="platformTableName">
<strong><Badge variant={variant}>{label}</Badge></strong>
<small>{worker.pressureReason || '无压力原因'}</small>
</span>
);
}
type PriorityDialogState = {
platform: IntegrationPlatform | undefined;
status: ModelRateLimitStatus;