feat(syslog): 增加积压采样免采样窗口

- 新增 start-lag-ms 配置,默认15分钟内全量保留
- 在15分钟至1小时之间按10级逐步降低保留率
- 延迟达到1小时后使用普通10%、非标20%的最低保留率
- 校验起始延迟必须小于最低保留率阈值
- 状态接口新增 start_lag_ms 配置展示
- 将采样策略版本升级为 v2
- 更新 Docker Compose 和 v4 升级配置说明
- 补充采样时间边界及非法配置单元测试
This commit is contained in:
王绍宏
2026-09-12 23:43:56 +08:00
parent c868e3befd
commit 87fee6fe69
27 changed files with 1777 additions and 94 deletions
@@ -0,0 +1,9 @@
package com.common.mapper;
import com.common.service.BacklogRetentionDelta;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface BacklogRetentionAuditMapper {
int upsert(BacklogRetentionDelta delta);
}
@@ -0,0 +1,118 @@
package com.common.service;
import java.time.LocalDateTime;
import java.util.Objects;
/** 一批积压消息按来源小时、Topic、Partition 聚合后的采样审计增量。 */
public final class BacklogRetentionDelta {
private final LocalDateTime sourceHour;
private final String sourceTopic;
private final int kafkaPartition;
private final String policyVersion;
private final int normalRetainBps;
private final int nonNormalRetainBps;
private long messageTotal;
private long alarmRowsTotal;
private long alarmRowsSaved;
private long normalRowsTotal;
private long normalRowsSaved;
private long normalRowsDropped;
private long nonNormalRowsTotal;
private long nonNormalRowsSaved;
private long nonNormalRowsDropped;
private long rawTotal;
private long rawSaved;
private long rawDropped;
private long minLagMs;
private long maxLagMs;
public BacklogRetentionDelta(LocalDateTime sourceHour, String sourceTopic, int kafkaPartition,
String policyVersion, int normalRetainBps, int nonNormalRetainBps,
long messageTotal, long alarmRowsTotal, long alarmRowsSaved,
long normalRowsTotal, long normalRowsSaved, long normalRowsDropped,
long nonNormalRowsTotal, long nonNormalRowsSaved,
long nonNormalRowsDropped, long rawTotal, long rawSaved,
long rawDropped, long minLagMs, long maxLagMs) {
this.sourceHour = Objects.requireNonNull(sourceHour, "sourceHour");
this.sourceTopic = Objects.requireNonNull(sourceTopic, "sourceTopic");
this.kafkaPartition = kafkaPartition;
this.policyVersion = Objects.requireNonNull(policyVersion, "policyVersion");
this.normalRetainBps = normalRetainBps;
this.nonNormalRetainBps = nonNormalRetainBps;
this.messageTotal = messageTotal;
this.alarmRowsTotal = alarmRowsTotal;
this.alarmRowsSaved = alarmRowsSaved;
this.normalRowsTotal = normalRowsTotal;
this.normalRowsSaved = normalRowsSaved;
this.normalRowsDropped = normalRowsDropped;
this.nonNormalRowsTotal = nonNormalRowsTotal;
this.nonNormalRowsSaved = nonNormalRowsSaved;
this.nonNormalRowsDropped = nonNormalRowsDropped;
this.rawTotal = rawTotal;
this.rawSaved = rawSaved;
this.rawDropped = rawDropped;
this.minLagMs = minLagMs;
this.maxLagMs = maxLagMs;
}
public void merge(BacklogRetentionDelta other) {
if (!sameKey(other)) throw new IllegalArgumentException("不能合并不同维度的采样审计增量");
messageTotal += other.messageTotal;
alarmRowsTotal += other.alarmRowsTotal;
alarmRowsSaved += other.alarmRowsSaved;
normalRowsTotal += other.normalRowsTotal;
normalRowsSaved += other.normalRowsSaved;
normalRowsDropped += other.normalRowsDropped;
nonNormalRowsTotal += other.nonNormalRowsTotal;
nonNormalRowsSaved += other.nonNormalRowsSaved;
nonNormalRowsDropped += other.nonNormalRowsDropped;
rawTotal += other.rawTotal;
rawSaved += other.rawSaved;
rawDropped += other.rawDropped;
minLagMs = Math.min(minLagMs, other.minLagMs);
maxLagMs = Math.max(maxLagMs, other.maxLagMs);
}
public BacklogRetentionDelta copy() {
return new BacklogRetentionDelta(sourceHour, sourceTopic, kafkaPartition, policyVersion,
normalRetainBps, nonNormalRetainBps, messageTotal, alarmRowsTotal,
alarmRowsSaved, normalRowsTotal, normalRowsSaved, normalRowsDropped,
nonNormalRowsTotal, nonNormalRowsSaved, nonNormalRowsDropped,
rawTotal, rawSaved, rawDropped, minLagMs, maxLagMs);
}
private boolean sameKey(BacklogRetentionDelta other) {
return other != null && kafkaPartition == other.kafkaPartition
&& normalRetainBps == other.normalRetainBps
&& nonNormalRetainBps == other.nonNormalRetainBps
&& sourceHour.equals(other.sourceHour)
&& sourceTopic.equals(other.sourceTopic)
&& policyVersion.equals(other.policyVersion);
}
public String aggregationKey() {
return sourceHour + "\u0000" + sourceTopic + "\u0000" + kafkaPartition + "\u0000"
+ policyVersion + "\u0000" + normalRetainBps + "\u0000" + nonNormalRetainBps;
}
public LocalDateTime getSourceHour() { return sourceHour; }
public String getSourceTopic() { return sourceTopic; }
public int getKafkaPartition() { return kafkaPartition; }
public String getPolicyVersion() { return policyVersion; }
public int getNormalRetainBps() { return normalRetainBps; }
public int getNonNormalRetainBps() { return nonNormalRetainBps; }
public long getMessageTotal() { return messageTotal; }
public long getAlarmRowsTotal() { return alarmRowsTotal; }
public long getAlarmRowsSaved() { return alarmRowsSaved; }
public long getNormalRowsTotal() { return normalRowsTotal; }
public long getNormalRowsSaved() { return normalRowsSaved; }
public long getNormalRowsDropped() { return normalRowsDropped; }
public long getNonNormalRowsTotal() { return nonNormalRowsTotal; }
public long getNonNormalRowsSaved() { return nonNormalRowsSaved; }
public long getNonNormalRowsDropped() { return nonNormalRowsDropped; }
public long getRawTotal() { return rawTotal; }
public long getRawSaved() { return rawSaved; }
public long getRawDropped() { return rawDropped; }
public long getMinLagMs() { return minLagMs; }
public long getMaxLagMs() { return maxLagMs; }
}
@@ -0,0 +1,81 @@
package com.common.service;
import com.common.mapper.BacklogRetentionAuditMapper;
import com.config.BacklogSamplingProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** 先提交小时审计,再允许调用方执行有损采样;审计失败时调用方必须回退全量保留。 */
@Slf4j
@Service
public class BacklogSamplingAuditService {
private final BacklogRetentionAuditMapper mapper;
private final BacklogSamplingProperties properties;
private final BacklogSamplingMetrics metrics;
private final PlatformTransactionManager transactionManager;
private volatile long retryAfterMs;
public BacklogSamplingAuditService(BacklogRetentionAuditMapper mapper,
BacklogSamplingProperties properties,
BacklogSamplingMetrics metrics,
PlatformTransactionManager transactionManager) {
this.mapper = mapper;
this.properties = properties;
this.metrics = metrics;
this.transactionManager = transactionManager;
}
/** 返回 false 表示不能安全采样,调用方应保留当前批次全部数据。 */
public boolean recordBeforeDropping(List<BacklogRetentionDelta> deltas) {
if (deltas == null || deltas.isEmpty()) return true;
long affectedMessages = messageCount(deltas);
long now = System.currentTimeMillis();
if (now < retryAfterMs) {
metrics.auditFailure(affectedMessages);
return false;
}
List<BacklogRetentionDelta> aggregated = aggregate(deltas);
try {
TransactionTemplate transaction = new TransactionTemplate(transactionManager);
transaction.setTimeout(Math.max(1, properties.getAuditTimeoutSeconds()));
transaction.execute(status -> {
for (BacklogRetentionDelta delta : aggregated) mapper.upsert(delta);
return null;
});
retryAfterMs = 0L;
metrics.record(aggregated);
return true;
} catch (RuntimeException failure) {
retryAfterMs = System.currentTimeMillis() + Math.max(1L, properties.getAuditRetryBackoffMs());
metrics.auditFailure(affectedMessages);
log.error("[BACKLOG_SAMPLING_AUDIT_FAILURE] 小时采样审计写入失败,当前批次自动回退全量保留: "
+ "messages={}, groups={}, retryBackoffMs={}",
affectedMessages, aggregated.size(), properties.getAuditRetryBackoffMs(), failure);
return false;
}
}
static List<BacklogRetentionDelta> aggregate(List<BacklogRetentionDelta> deltas) {
Map<String, BacklogRetentionDelta> grouped = new LinkedHashMap<>();
for (BacklogRetentionDelta delta : deltas) {
BacklogRetentionDelta existing = grouped.get(delta.aggregationKey());
if (existing == null) grouped.put(delta.aggregationKey(), delta.copy());
else existing.merge(delta);
}
return new ArrayList<>(grouped.values());
}
private long messageCount(List<BacklogRetentionDelta> deltas) {
long total = 0L;
for (BacklogRetentionDelta delta : deltas) total += delta.getMessageTotal();
return total;
}
}
@@ -0,0 +1,56 @@
package com.common.service;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.LongAdder;
/** 积压采样热路径指标;表审计失败时仍可从接口和日志发现。 */
@Component
public class BacklogSamplingMetrics {
private final LongAdder degradedMessages = new LongAdder();
private final LongAdder alarmRowsSaved = new LongAdder();
private final LongAdder normalRowsSaved = new LongAdder();
private final LongAdder normalRowsDropped = new LongAdder();
private final LongAdder nonNormalRowsSaved = new LongAdder();
private final LongAdder nonNormalRowsDropped = new LongAdder();
private final LongAdder rawSaved = new LongAdder();
private final LongAdder rawDropped = new LongAdder();
private final LongAdder auditWriteFailures = new LongAdder();
private final LongAdder auditFallbackMessages = new LongAdder();
public void record(List<BacklogRetentionDelta> deltas) {
for (BacklogRetentionDelta delta : deltas) {
degradedMessages.add(delta.getMessageTotal());
alarmRowsSaved.add(delta.getAlarmRowsSaved());
normalRowsSaved.add(delta.getNormalRowsSaved());
normalRowsDropped.add(delta.getNormalRowsDropped());
nonNormalRowsSaved.add(delta.getNonNormalRowsSaved());
nonNormalRowsDropped.add(delta.getNonNormalRowsDropped());
rawSaved.add(delta.getRawSaved());
rawDropped.add(delta.getRawDropped());
}
}
public void auditFailure(long affectedMessages) {
auditWriteFailures.increment();
auditFallbackMessages.add(Math.max(0L, affectedMessages));
}
public Map<String, Object> snapshot() {
Map<String, Object> values = new LinkedHashMap<>();
values.put("degraded_messages_total", degradedMessages.sum());
values.put("alarm_rows_saved_total", alarmRowsSaved.sum());
values.put("normal_rows_saved_total", normalRowsSaved.sum());
values.put("normal_rows_dropped_total", normalRowsDropped.sum());
values.put("non_normal_rows_saved_total", nonNormalRowsSaved.sum());
values.put("non_normal_rows_dropped_total", nonNormalRowsDropped.sum());
values.put("raw_saved_total", rawSaved.sum());
values.put("raw_dropped_total", rawDropped.sum());
values.put("audit_write_failures_total", auditWriteFailures.sum());
values.put("audit_fallback_messages_total", auditFallbackMessages.sum());
return values;
}
}