perf(syslog-consumer-rule): 非标日志改为单线程异步批量写入
- 非标写库结果不再参与 Kafka ACK - 使用独立单线程和有界队列处理非标日志 - 非标数据按固定大小分批写入达梦 - 增加异步成功、失败和队列拒绝指标 - 补充异步解耦及批量写入测试 perf(syslog-consumer-rule): 按批次字段并集优化达梦批量写入 - 普通日志和告警日志按非空字段并集生成批写模板 - 为同批记录补齐缺失字段并绑定 NULL - 使用 MyBatis ExecutorType.BATCH 复用 PreparedStatement - 避免固定写入全部 373 个候选字段 - 非标日志保留固定 24 列多行批量写入 - 批量大小和非标队列容量默认调整为 500 - 补充字段并集与动态 SQL 一致性测试
This commit is contained in:
+3
-8
@@ -15,21 +15,16 @@ import java.util.Map;
|
||||
public interface SyslogNormalAlarmMapper {
|
||||
|
||||
/**
|
||||
* 动态插入数据 - 使用 Map
|
||||
* 使用当前小批次的字段并集模板插入告警日志。调用方保证同批 Map 具有相同键集合,
|
||||
* 缺失值绑定为 NULL,使 MyBatis ExecutorType.BATCH 能复用 PreparedStatement。
|
||||
*/
|
||||
int insertDynamic(@Param("dataMap") Map<String, Object> dataMap);
|
||||
int insertBatchPlanned(@Param("dataMap") Map<String, Object> dataMap);
|
||||
|
||||
/**
|
||||
* 动态插入数据 - 使用实体类
|
||||
*/
|
||||
int insertByEntity(SyslogNormalAlarm entity);
|
||||
|
||||
/**
|
||||
* 批量插入数据
|
||||
*/
|
||||
int batchInsert(@Param("dataList") List<Map<String, Object>> dataList);
|
||||
|
||||
|
||||
/**
|
||||
* 统计24小时内分组数据量
|
||||
*/
|
||||
|
||||
+3
-8
@@ -12,21 +12,16 @@ import java.util.List;
|
||||
public interface SyslogNormalDataMapper {
|
||||
|
||||
/**
|
||||
* 动态插入数据 - 使用 Map
|
||||
* 使用当前小批次的字段并集模板插入普通日志。调用方保证同批 Map 具有相同键集合,
|
||||
* 缺失值绑定为 NULL,使 MyBatis ExecutorType.BATCH 能复用 PreparedStatement。
|
||||
*/
|
||||
int insertDynamic(@Param("dataMap") Map<String, Object> dataMap);
|
||||
int insertBatchPlanned(@Param("dataMap") Map<String, Object> dataMap);
|
||||
|
||||
/**
|
||||
* 动态插入数据 - 使用实体类
|
||||
*/
|
||||
int insertByEntity(SyslogNormalData entity);
|
||||
|
||||
/**
|
||||
* 批量插入数据
|
||||
*/
|
||||
int batchInsert(@Param("dataList") java.util.List<Map<String, Object>> dataList);
|
||||
|
||||
|
||||
/**
|
||||
* 统计24小时内分组数据量
|
||||
*/
|
||||
|
||||
+44
-5
@@ -13,13 +13,18 @@ import org.springframework.transaction.support.TransactionSynchronizationAdapter
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 在一个达梦小事务中执行真正的 MyBatis JDBC batch。
|
||||
*
|
||||
* <p>这个类只负责数据库执行,不负责错误分类、拆分或 Kafka 状态。上层写入器保证传入的所有行
|
||||
* 属于同一张表和同一个动态列签名,因此 MyBatis BatchExecutor 可以复用同一个 PreparedStatement。
|
||||
* 普通日志和告警日志分别使用当前小批次的非空字段并集模板,因此同一目标表、同一分区日期内
|
||||
* MyBatis BatchExecutor 可以复用同一个 PreparedStatement。
|
||||
* 每个 {@link #execute(boolean, List, int)} 调用都会创建独立事务;失败时整个小批次回滚,上层随后
|
||||
* 决定二分定位还是按系统故障停止。</p>
|
||||
*/
|
||||
@@ -63,7 +68,7 @@ public class DmStandardizedBatchExecutor {
|
||||
* 执行一个同构小批次。
|
||||
*
|
||||
* @param alarmTable true 写 syslog_normal_alarm,false 写 syslog_normal_data
|
||||
* @param rows 已按相同动态列集合分组的行
|
||||
* @param rows 已按相同目标表和分区日期分组的行
|
||||
* @param transactionTimeoutSeconds 当前小事务硬超时
|
||||
*/
|
||||
public void execute(boolean alarmTable, List<Map<String, Object>> rows,
|
||||
@@ -72,10 +77,11 @@ public class DmStandardizedBatchExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> alignedRows = alignBatchColumns(rows);
|
||||
int attempt = 0;
|
||||
while (true) {
|
||||
try {
|
||||
executeTransaction(alarmTable, rows, transactionTimeoutSeconds, attempt);
|
||||
executeTransaction(alarmTable, alignedRows, transactionTimeoutSeconds, attempt);
|
||||
return;
|
||||
} catch (DirectStatsProjectionWriter.ConcurrentStatsWriteException concurrentFailure) {
|
||||
if (attempt >= Math.max(0, maxConcurrencyRetries)) {
|
||||
@@ -89,6 +95,39 @@ public class DmStandardizedBatchExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算当前小批次所有非空字段的并集,并为每行建立相同键集合的浅拷贝。
|
||||
*
|
||||
* <p>Mapper XML 只允许声明过的列,额外业务键即使出现在 Map 中也不会进入 SQL。
|
||||
* 同一键在某行不存在时,Map#get 返回 null,最终由 JDBC 按 SQL NULL 绑定。</p>
|
||||
*/
|
||||
static List<Map<String, Object>> alignBatchColumns(List<Map<String, Object>> rows) {
|
||||
Set<String> columnUnion = new LinkedHashSet<>();
|
||||
for (Map<String, Object> row : rows) {
|
||||
if (row == null) {
|
||||
throw new IllegalArgumentException("达梦批写行不能为空");
|
||||
}
|
||||
for (Map.Entry<String, Object> entry : row.entrySet()) {
|
||||
if (entry.getKey() != null && entry.getValue() != null) {
|
||||
columnUnion.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (columnUnion.isEmpty()) {
|
||||
throw new IllegalArgumentException("达梦批写没有可插入的非空字段");
|
||||
}
|
||||
|
||||
List<Map<String, Object>> alignedRows = new ArrayList<>(rows.size());
|
||||
for (Map<String, Object> row : rows) {
|
||||
Map<String, Object> aligned = new LinkedHashMap<>(columnUnion.size());
|
||||
for (String column : columnUnion) {
|
||||
aligned.put(column, row.get(column));
|
||||
}
|
||||
alignedRows.add(aligned);
|
||||
}
|
||||
return alignedRows;
|
||||
}
|
||||
|
||||
private void executeTransaction(boolean alarmTable, List<Map<String, Object>> rows,
|
||||
int transactionTimeoutSeconds, int retries) {
|
||||
|
||||
@@ -112,12 +151,12 @@ public class DmStandardizedBatchExecutor {
|
||||
SyslogNormalAlarmMapper mapper = batchSqlSession.getMapper(SyslogNormalAlarmMapper.class);
|
||||
for (Map<String, Object> row : rows) {
|
||||
// BatchExecutor 下这里不会逐条提交;返回值也不是最终影响行数,不能据此判断成功。
|
||||
mapper.insertDynamic(row);
|
||||
mapper.insertBatchPlanned(row);
|
||||
}
|
||||
} else {
|
||||
SyslogNormalDataMapper mapper = batchSqlSession.getMapper(SyslogNormalDataMapper.class);
|
||||
for (Map<String, Object> row : rows) {
|
||||
mapper.insertDynamic(row);
|
||||
mapper.insertBatchPlanned(row);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-24
@@ -32,8 +32,6 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executor;
|
||||
@@ -48,7 +46,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
* <p>写入器遵守三条不能破坏的可靠性规则:</p>
|
||||
*
|
||||
* <ol>
|
||||
* <li>正常行按目标表、分区日期和动态列签名分组后批量提交;</li>
|
||||
* <li>普通日志和告警日志分别按目标表、分区日期计算批次字段并集后批量提交;</li>
|
||||
* <li>数据类错误回滚当前小批次并有限二分,最终只把坏行交给 Retry/DLT;</li>
|
||||
* <li>连接、超时、提交未知等系统故障绝不降级为大量单条请求,而是立即失败并保留 Kafka offset。</li>
|
||||
* </ol>
|
||||
@@ -67,7 +65,7 @@ public class DmStandardizedBatchWriter {
|
||||
@Value("${dm.standardized-write.mode:BATCH}")
|
||||
private String mode;
|
||||
|
||||
@Value("${dm.standardized-write.batch-size:100}")
|
||||
@Value("${dm.standardized-write.batch-size:500}")
|
||||
private int batchSize;
|
||||
|
||||
@Value("${dm.standardized-write.transaction-timeout-seconds:45}")
|
||||
@@ -161,7 +159,7 @@ public class DmStandardizedBatchWriter {
|
||||
}
|
||||
|
||||
/**
|
||||
* 相同目标表、log_time 日期和非空列集合才会进入同一批次。
|
||||
* 两张标准化表在每个小批次计算字段并集,只按目标表和 log_time 日期分组。
|
||||
* LinkedHashMap 保持稳定遍历顺序,便于测试和现场日志复现。
|
||||
*/
|
||||
private <T> void writeGroupedBatches(List<WriteRequest<T>> requests, WriteReport<T> report,
|
||||
@@ -173,7 +171,7 @@ public class DmStandardizedBatchWriter {
|
||||
groups.computeIfAbsent(key, ignored -> new ArrayList<>()).add(request);
|
||||
} catch (RuntimeException invalidGroupingKey) {
|
||||
report.recordFailure(request.getSource(), new PermanentRowWriteException(
|
||||
"标准化结果缺少稳定分区时间或列签名无效", invalidGroupingKey));
|
||||
"标准化结果缺少稳定分区时间", invalidGroupingKey));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,18 +292,7 @@ public class DmStandardizedBatchWriter {
|
||||
boolean alarmTable = normalDataService.isAlarmData(data);
|
||||
LocalDate partitionDate = partitionDate(data.get("log_time"));
|
||||
|
||||
// MyBatis XML 的动态列判断只包含非 null 值,所以签名也只记录非 null 列。
|
||||
// TreeSet 提供固定顺序,避免 HashMap 插入顺序不同却被错误拆成多个批次。
|
||||
Set<String> columns = new TreeSet<>();
|
||||
for (Map.Entry<String, Object> entry : data.entrySet()) {
|
||||
if (entry.getValue() != null) {
|
||||
columns.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
if (columns.isEmpty()) {
|
||||
throw new IllegalArgumentException("标准化结果没有可写入列");
|
||||
}
|
||||
return new BatchKey(alarmTable, partitionDate, String.join(",", columns));
|
||||
return new BatchKey(alarmTable, partitionDate);
|
||||
}
|
||||
|
||||
private LocalDate partitionDate(Object value) {
|
||||
@@ -499,12 +486,10 @@ public class DmStandardizedBatchWriter {
|
||||
private static final class BatchKey {
|
||||
private final boolean alarmTable;
|
||||
private final LocalDate partitionDate;
|
||||
private final String columnSignature;
|
||||
|
||||
private BatchKey(boolean alarmTable, LocalDate partitionDate, String columnSignature) {
|
||||
private BatchKey(boolean alarmTable, LocalDate partitionDate) {
|
||||
this.alarmTable = alarmTable;
|
||||
this.partitionDate = partitionDate;
|
||||
this.columnSignature = columnSignature;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -513,13 +498,12 @@ public class DmStandardizedBatchWriter {
|
||||
if (!(other instanceof BatchKey)) return false;
|
||||
BatchKey that = (BatchKey) other;
|
||||
return alarmTable == that.alarmTable
|
||||
&& Objects.equals(partitionDate, that.partitionDate)
|
||||
&& Objects.equals(columnSignature, that.columnSignature);
|
||||
&& Objects.equals(partitionDate, that.partitionDate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(alarmTable, partitionDate, columnSignature);
|
||||
return Objects.hash(alarmTable, partitionDate);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
-7
@@ -15,6 +15,7 @@ import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -35,21 +36,22 @@ public class SyslogNormalDataService {
|
||||
|
||||
|
||||
/**
|
||||
* 动态插入单条数据 - Map方式
|
||||
* 标准化单条兼容入口:按 event_level 路由后,两张目标表均使用受白名单约束的动态列模板。
|
||||
*/
|
||||
@Transactional
|
||||
public int insertDynamic(Map<String, Object> dataMap) {
|
||||
// [流程节点7] 根据标准化结果分类落库:安全事件写入 syslog_normal_alarm,访问事件写入 syslog_normal_data。
|
||||
// [SYSLOG-ALARM][DBBATCH] 每条标准化结果单独开启事务并执行动态 INSERT,会产生大量数据库往返、SQL 解析和提交开销;
|
||||
// 高峰期 DB 变慢又会放大 Kafka 批次超时。建议按目标表和固定列集合聚合批次写入,并用 event_id + 规则版本唯一键保证重试幂等。
|
||||
// 高峰期 DB 变慢又会放大 Kafka 批次超时。建议按目标表和批次字段并集聚合写入,并用 event_id + 规则版本唯一键保证重试幂等。
|
||||
prepareForInsert(dataMap);
|
||||
|
||||
// [流程节点7.2] event_level 非空走安全事件表;为空走访问事件表,这是两条后续分析链路的分叉点。
|
||||
boolean alarmData = isAlarmData(dataMap);
|
||||
Map<String, Object> plannedData = nonNullColumnMap(dataMap);
|
||||
try {
|
||||
int inserted = alarmData
|
||||
? syslogNormalAlarmMapper.insertDynamic(dataMap)
|
||||
: syslogNormalDataMapper.insertDynamic(dataMap);
|
||||
? syslogNormalAlarmMapper.insertBatchPlanned(plannedData)
|
||||
: syslogNormalDataMapper.insertBatchPlanned(plannedData);
|
||||
if (inserted > 0) {
|
||||
if (alarmData && lateAlarmBucketWriter != null) {
|
||||
lateAlarmBucketWriter.record(java.util.Collections.singletonList(dataMap));
|
||||
@@ -72,6 +74,17 @@ public class SyslogNormalDataService {
|
||||
}
|
||||
}
|
||||
|
||||
/** 单条兼容路径也只把实际非空字段交给列模板,避免原 Map 中的空键误触发列选择。 */
|
||||
private Map<String, Object> nonNullColumnMap(Map<String, Object> dataMap) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
|
||||
if (entry.getKey() != null && entry.getValue() != null) {
|
||||
result.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在单条和批量写入之前执行完全相同的数据准备与关键字段校验。
|
||||
*
|
||||
@@ -220,9 +233,8 @@ public class SyslogNormalDataService {
|
||||
data.put("log_time", new java.util.Date());
|
||||
}
|
||||
}
|
||||
// 旧 mapper 的 batchInsert 只写固定的少数字段,DIRECT 下会造成“统计有 device/rule,
|
||||
// 事实行却丢了 device/rule”的永久差异。主 Kafka 链路使用 DmStandardizedBatchExecutor;
|
||||
// 这个低频兼容 API 逐条复用完整动态 INSERT,但仍处于本方法的同一个 Spring 事务中。
|
||||
// 主 Kafka 链路使用 DmStandardizedBatchExecutor 执行真正的 JDBC batch;这个低频兼容
|
||||
// API 在同一个 Spring 事务内复用受白名单约束的列模板,不能再调用旧的五字段 batch SQL。
|
||||
int inserted = 0;
|
||||
for (Map<String, Object> data : dataList) {
|
||||
inserted += insertDynamic(data);
|
||||
|
||||
+19
-2
@@ -11,12 +11,14 @@ import com.common.service.StatsChangeCaptureService;
|
||||
import com.common.service.DirectStatsProjectionWriter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -38,6 +40,9 @@ public class SyslogNonNormalMessageServiceImpl
|
||||
@Autowired
|
||||
private DirectStatsProjectionWriter directStatsProjectionWriter;
|
||||
|
||||
@Value("${dm.non-normal-write.batch-size:500}")
|
||||
private int nonNormalBatchSize;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean saveMessage(SyslogNonNormalMessage message) {
|
||||
@@ -125,8 +130,20 @@ public class SyslogNonNormalMessageServiceImpl
|
||||
}
|
||||
}
|
||||
|
||||
// 批量插入
|
||||
int inserted = messageMapper.batchInsert(messages);
|
||||
// 同一个服务事务内分块执行固定列多行 INSERT,避免单条提交,也避免一个 Kafka poll
|
||||
// 直接生成过大的 SQL。所有分块成功后才统一写 DIRECT 统计并提交事务。
|
||||
int inserted = 0;
|
||||
int effectiveBatchSize = Math.max(1, nonNormalBatchSize);
|
||||
for (int start = 0; start < messages.size(); start += effectiveBatchSize) {
|
||||
int end = Math.min(start + effectiveBatchSize, messages.size());
|
||||
int affected = messageMapper.batchInsert(
|
||||
new ArrayList<>(messages.subList(start, end)));
|
||||
if (affected <= 0) {
|
||||
throw new IllegalStateException("非标日志批量 INSERT 未写入记录: start="
|
||||
+ start + ", end=" + end);
|
||||
}
|
||||
inserted += affected;
|
||||
}
|
||||
if (inserted > 0) {
|
||||
if (statsChangeCaptureService.isEnabled()) {
|
||||
for (SyslogNonNormalMessage message : messages) {
|
||||
|
||||
Reference in New Issue
Block a user