diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/config/ThreadPoolConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/config/ThreadPoolConfig.java index d090f44f..99c0a0b1 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/config/ThreadPoolConfig.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/config/ThreadPoolConfig.java @@ -134,28 +134,45 @@ public class ThreadPoolConfig { } /** - * 审计事件处理线程池 - 使用虚拟线程(Java 21+) + * 审计事件处理线程池 - 使用传统线程池(更稳定可靠) * - * ⚠️ 为什么使用虚拟线程? - * 1. 审计事件处理是**I/O密集型**任务(写日志、写数据库) - * 2. 虚拟线程在I/O阻塞时不占用OS线程,资源消耗极低 - * 3. 审计事件量可能很大,虚拟线程支持高并发处理 - * 4. 独立线程池避免与业务线程池竞争资源 + * ⚠️ 为什么不使用虚拟线程? + * 1. 审计任务量不大,不需要虚拟线程的高并发能力 + * 2. 审计日志是关键数据,传统线程池的异常处理更成熟可靠 + * 3. 传统线程池有队列缓冲,可以防止突发流量 + * 4. 传统线程池的监控和调试工具更完善 + * 5. 虚拟线程在异常处理方面还不够成熟,可能导致异常被吞掉 * * 💡 场景: * - 异步记录用户操作审计日志 * - 写入审计数据库 * - 发送审计事件到消息队列 * - * 🎯 解决问题: - * - 修复 "More than one TaskExecutor bean found" 警告 - * - 确保审计事件处理不受其他业务线程池影响 + * 🎯 线程池配置: + * - 核心线程数:2(审计任务通常不多) + * - 最大线程数:4(足够处理突发流量) + * - 队列容量:1000(缓冲突发的审计事件) + * - 拒绝策略:CallerRunsPolicy(确保审计日志不丢失) + * - 优雅关闭:等待60秒让审计任务完成 */ @Bean("auditTaskExecutor") - public SimpleAsyncTaskExecutor auditTaskExecutor() { - SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor("audit-virtual-"); - executor.setVirtualThreads(true); - executor.setConcurrencyLimit(-1); // 无限制,支持大量并发审计事件 + public AsyncTaskExecutor auditTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); // 审计任务不多,2个核心线程足够 + executor.setMaxPoolSize(4); // 最大4个线程处理突发流量 + executor.setQueueCapacity(1000); // 较大的队列,缓冲突发审计事件 + executor.setThreadNamePrefix("audit-"); + executor.setKeepAliveSeconds(60); + + // ⚠️ 关键:使用CallerRunsPolicy确保审计日志不丢失 + // 如果线程池满了,由调用线程执行,保证审计日志一定被记录 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + + // ⚠️ 优雅关闭:等待审计任务完成 + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAwaitTerminationSeconds(60); + + executor.initialize(); return executor; } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/audit/listener/AuditEventListener.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/audit/listener/AuditEventListener.java index ee521fad..f8cd4d44 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/audit/listener/AuditEventListener.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/audit/listener/AuditEventListener.java @@ -13,14 +13,24 @@ public class AuditEventListener { @Async("auditTaskExecutor") @EventListener public void handleAuditEvent(AuditEvent event) { - // 这里可以将审计信息保存到数据库或发送到日志系统 - log.info("Audit: {} performed {} on {} (ID: {}) at {}, detail: {}", - event.getMetadata().getOperator(), - event.getMetadata().getAction(), - event.getMetadata().getEntityType(), - event.getMetadata().getEntityId(), - event.getMetadata().getTimestamp(), - event.getMetadata().getDetail() - ); + try { + // 这里可以将审计信息保存到数据库或发送到日志系统 + log.info("Audit: {} performed {} on {} (ID: {}) at {}, detail: {}", + event.getMetadata().getOperator(), + event.getMetadata().getAction(), + event.getMetadata().getEntityType(), + event.getMetadata().getEntityId(), + event.getMetadata().getTimestamp(), + event.getMetadata().getDetail() + ); + } catch (Exception e) { + // ⚠️ 关键修复:捕获并记录所有异常,防止异常被@Async吞掉 + log.error("Failed to handle audit event: operator={}, action={}, entityType={}, entityId={}", + event.getMetadata().getOperator(), + event.getMetadata().getAction(), + event.getMetadata().getEntityType(), + event.getMetadata().getEntityId(), + e); + } } } \ No newline at end of file diff --git a/backend/src/main/resources/db/changelog/sql/20251209141300-01.sql b/backend/src/main/resources/db/changelog/sql/20251209141300-01.sql index 7cab7bdd..8a06dd93 100644 --- a/backend/src/main/resources/db/changelog/sql/20251209141300-01.sql +++ b/backend/src/main/resources/db/changelog/sql/20251209141300-01.sql @@ -12,24 +12,15 @@ INSERT INTO system_release ( ) VALUES ( 'system', NOW(), 'system', NOW(), 1, 0, - 1.41, 'ALL', NOW(), + 1.42, 'ALL', NOW(), '【后端】 - -- ServerLogStreamStrategy: 修复tail -f命令重复问题,数据库logQueryCommand已包含完整命令,只需追加-n 行数参数 -- K8sLogStreamStrategy: 统一sessionId命名规范,修复日志输出不一致问题 -- DockerLogStreamStrategy: 统一sessionId命名规范 -- AbstractLogStreamWebSocketHandler: 在sendLogLine(), sendStatus(), sendError()方法中添加synchronized(session)保护 -- 说明:synchronized(session)锁粒度是每个session对象,不同用户的session互不影响,性能影响可忽略 -- Strategy实现中统一使用logStreamSessionId参数名(仅用于日志输出) -- 区分webSocketId(原始ID)和logStreamSessionId(增强ID) +- 优化cleanupSession()方法,先关闭Shell输入输出流强制中断阻塞的read操作 +- 增强readSSHOutput()和readSSHError()的异常处理和线程中断检查 【前端】 -- Monaco Editor替换XTerm.js -- 架构重构 - 移除LogViewerWindow中间层 -- LogViewerWindow违反单一职责原则 -- 后端协议对齐(LOG/STATUS/ERROR消息类型) -- K8S应用重复连接 -- 智能自动滚动 -- 新日志强制滚动影响用户体验 +- 修改类型定义(types.ts),将k8sNamespaceId: number和k8sDeploymentId: number改为k8sNamespaceName: string和k8sDeploymentName: string +- 更新K8sRuntimeConfig组件,改为基于name进行选择和匹配,通过namespaceName查找对应的namespace ID来加载Deployment列表 +- 调整RuntimeConfigSection和TeamApplicationDialog组件的props传递和表单数据结构,确保使用name字段进行数据绑定和提交 +- 同步更新TeamApplicationManageDialog的保存逻辑,向后端提交name字段而非ID ', 0, NULL, NULL, 0 );