perf(syslog): 统计异步化并按采样保留率自适应调整业务批量

- 入口统计改用独立 Kafka 消费组,移除主消费同步统计写库
- DIRECT 模式入库计数改为事务提交后异步投递,取消事实事务内统计 Outbox 写入
- 采样审计、告警分钟桶通知改为后台处理,异常不阻断主消费或回滚告警
- RAW 基础业务批量默认 500,按保留率动态扩大,上限 3000
- 限制业务块字节数及规则任务数量,保留告警全量处理和整次 poll 完成后提交
- 细化 SLOW_SYSLOG_BATCH,补充批次关联、线程排队、数据库执行及提交耗时
- 更新 v4 升级说明,明确异步统计损失风险及现场验证方法

验证:JDK 8 构建成功,232 项测试通过。
升级:复用现有表和 Kafka Topic,无新增建表或建 Topic 脚本要求。
This commit is contained in:
王绍宏
2026-09-13 06:11:12 +08:00
parent 44d327139b
commit 8173c2ff3f
39 changed files with 2400 additions and 216 deletions
@@ -84,6 +84,16 @@ syslog.raw-consumer.enabled=true
syslog.raw-listener.concurrency=${SYSLOG_RAW_LISTENER_CONCURRENCY:3}
# 端到端批次耗时达到该阈值时,打印业务各阶段及 Kafka offset 同步提交耗时;0 表示关闭。
syslog.slow-batch.threshold-ms=${SYSLOG_SLOW_BATCH_THRESHOLD_MS:500}
# 普通业务基础批量500(保留旧配置名);不再表示底层poll上限。已有同名环境变量仍覆盖基础值。
# 目标业务批量=ceil(基础条数*10000/max(普通保留万分比,非标保留万分比)),并受下面3000上限限制。
syslog.raw-listener.max-poll-records=${SYSLOG_RAW_LISTENER_MAX_POLL_RECORDS:500}
# 底层RAW max.poll.records固定为此值;无积压时也可能poll到3000,再按500分块,全部处理后才ACK。
# 基础值必须大于0且不超过上限;Retry/大日志/独立统计不采用此自适应策略。
syslog.raw-listener.adaptive-max-records=${SYSLOG_RAW_LISTENER_ADAPTIVE_MAX_RECORDS:3000}
# 大批次按 Kafka value 序列化字节拆成16MiB业务块,全部完成后提交本次poll;单条不会切断。
syslog.raw-listener.processing-batch-max-bytes=${SYSLOG_RAW_PROCESSING_BATCH_MAX_BYTES:16777216}
# 每个业务块只向共享规则线程池提交8个worker,避免2000条对应2000个排队任务。
syslog.normalization.workers-per-batch=${SYSLOG_NORMALIZATION_WORKERS_PER_BATCH:8}
# 连续性优先:特殊/系统性异常优先写 DLT;即使 DLT 失败也记录告警并推进原 offset,避免阻塞分区。
syslog.failure-continuity.enabled=${SYSLOG_FAILURE_CONTINUITY_ENABLED:true}
# DLT 故障时最多快速等待 1 秒;首次失败后整批直接记录并放行。
@@ -146,7 +156,7 @@ syslog.large-reliability.consumer-group-id=${spring.kafka.consumer.group-id}-lar
syslog.large-reliability.consumer.enabled=true
# 调试修改:普通 Kafka Consumer 单次最多拉取条数。如需改为2000,将 500 改为 2000。
# 通用消费者(如Retry)的上限;RAW底层上限用adaptive-max-records,业务基础值用syslog.raw-listener.max-poll-records。
# 使用 prod profile 时 application-prod.properties 中存在同名配置,必须同步修改,否则 prod 值会覆盖本项。
spring.kafka.consumer.max-poll-records=500
spring.kafka.consumer.properties.max.poll.interval.ms=900000
@@ -163,6 +173,8 @@ spring.task.scheduling.pool.size=10
# 标准化告警事实与迟到重放通知使用同一个达梦事务。
analysis.late-replay.outbox-enabled=${ANALYSIS_LATE_REPLAY_OUTBOX_ENABLED:true}
# 告警提交后后台写分钟桶的事务超时;不在主消费线程等待。JDBC/连接池自身超时仍独立生效。
analysis.late-replay.notify-timeout-seconds=${ANALYSIS_LATE_REPLAY_NOTIFY_TIMEOUT_SECONDS:5}
# 日志配置
logging.level.com.common.schedule=INFO
@@ -240,11 +252,18 @@ spring.datasource.hikari.validation-timeout=5000
spring.datasource.hikari.leak-detection-threshold=30000
spring.datasource.hikari.pool-name=HikariPool-SyslogConsumer-rule
spring.datasource.hikari.auto-commit=false
# DIRECT 不再写逐事实 change_log;默认只在事实事务追加一条批次 Outbox,日统计异步投影。
# DIRECT 连续消费优先:事实事务不再写统计Outbox/日统计表,提交后通过有界内存队列异步发布统计。
# 首次部署 DIRECT 前必须先执行 08/09/10/11 脚本并按设计文档排空旧 pending。
stats.write-mode=DIRECT
stats.incremental.capture-enabled=false
stats.ingress-metadata.enabled=true
# 入口统计独立消费原始Topic,直接聚合写统计表;主消费不再写入口Outbox。
# 各节点必须使用同一组名,且不得与业务主消费组相同。
stats.ingress-metadata.group-id=${STATS_INGRESS_GROUP_ID:${spring.kafka.consumer.group-id}-ingress-v1}
stats.ingress-metadata.large-group-id=${STATS_INGRESS_LARGE_GROUP_ID:${stats.ingress-metadata.group-id}-large}
stats.ingress-metadata.max-poll-records=${STATS_INGRESS_MAX_POLL_RECORDS:2000}
# 新组默认从Kafka仍保留的最早消息开始,确保不跳过历史接收时间;已有offset继续原位置。
stats.ingress-metadata.auto-offset-reset=${STATS_INGRESS_AUTO_OFFSET_RESET:earliest}
stats.incremental.read-enabled=true
stats.incremental.transaction-timeout-seconds=45
stats.device-receive.change-capture-enabled=false
@@ -275,10 +294,13 @@ stats.materialization.concurrency-retry-backoff-ms=${STATS_MATERIALIZATION_CONCU
stats.materialization.reconcile-initial-delay-ms=${STATS_MATERIALIZATION_RECONCILE_INITIAL_DELAY_MS:30000}
stats.materialization.reconcile-interval-ms=${STATS_MATERIALIZATION_RECONCILE_INTERVAL_MS:60000}
# Fact transactions append one aggregated durable event instead of locking the daily projection rows.
# A dedicated single-lane Kafka consumer applies device/rule/collector projection deltas and then
# emits the existing idempotent business-materialization dirty keys.
# 事实提交后只做内存offer,独立线程发送到统计Kafka;满队列/进程退出可能少统计,不阻碍事实消费。
# enabled=false 会跳过统计并记录损失,绝不回退为同步统计写库。正常保持true。
# 旧Outbox继续后台排空,applied表仍用于统计事件去重,不要删表。
stats.projection-async.enabled=${STATS_PROJECTION_ASYNC_ENABLED:true}
# 队列最多1000个批次聚合事件;含正在重试事件的聚合分组总数最多50000,限制内存占用。
stats.projection-async.memory-queue-capacity=${STATS_PROJECTION_MEMORY_QUEUE_CAPACITY:1000}
stats.projection-async.memory-max-pending-groups=${STATS_PROJECTION_MEMORY_MAX_PENDING_GROUPS:50000}
stats.projection-async.outbox-table=${STATS_PROJECTION_OUTBOX_TABLE:stats_projection_outbox}
stats.projection-async.applied-table=${STATS_PROJECTION_APPLIED_TABLE:stats_projection_applied_event}
stats.projection-async.topic=${STATS_PROJECTION_TOPIC:${spring.kafka.consumer.topic}.STATS.PROJECTION}