日志交接边界
LoggingConsumer 将 Collector 网络请求替换为本地 JSON Lines 写入,由独立 LogAgent 负责后续交付。该方式依赖磁盘、Agent、清理和 ACK 运维协议,吞吐、延迟与故障恢复需在目标文件系统和 Agent 上测试。
一、 HTTP 上传与本地日志交接
选择日志交接前,应先比较默认批量上传与本地磁盘通道的故障边界:
- 默认批量上传:SDK 有界内存队列会重试网络故障;Collector 长时间不可用时,队列可能满,宿主必须处理拒收结果。
- 日志交接:SDK 把事件写入本地文件,减少调用路径对 Collector 的直接依赖;磁盘满、写入失败或 Agent 停滞仍会导致拒收或交接延迟。
- 业务事务一致性:两种模式都不能把业务状态提交与埋点写入合成一个原子事务;订单、支付等权威事实应采用宿主事务 Outbox。
二、 LoggingConsumer 解耦架构
LoggingConsumer 将文件写入与网络上传拆成两个独立环节,接入方需部署并验证 Agent/Relay:
┌─────────────────────────────────────────────────────────────┐
│ 宿主机 / 容器 Pod │
│ │
│ 业务系统 │
│ │ │
│ ▼ (序列化 + 本地文件追加;可选 fsync) │
│ LoggingConsumer │
│ │ │
│ ▼ │
│ 写入中 events-*.open → 封存后 events-*.jsonl │
│ [UTF-8 JSON Lines;Agent 只读取封存文件] │
│ │ │
│ ▼ (独立常驻进程监听文件读取) │
│ 日志采集代理 + 批协议 Relay │
└───────┬─────────────────────────────────────────────────────┘
│
▼ (批量压缩聚合网络上传)
接收网关 (Collector / Identity Gateway)行为与限制
- SDK 写入路径不直接调用 Collector,但仍有 JSON 序列化、锁、文件写和可选
fsync成本;Java 和 Go 为同步调用。 - Collector 故障不会直接阻塞 SDK 的网络请求,但磁盘预算耗尽、文件系统错误或 Agent 积压仍会让写入失败。
- LogAgent 必须把每行中的
event转换为 Collector 批协议并保留eventId;读取文件成功不等于 Collector ACK。
三、 磁盘预算与文件管理
本地日志需要设置容量上限并定义文件交接和清理策略。LoggingConsumer 提供以下保护:
- 单文件大小轮转 (
maxFileBytes):- 日志文件达到
maxFileBytes后轮转,创建新的活动文件。
- 日志文件达到
- 单前缀文件预算 (
maxTotalBytes):- 为每个写者前缀设置
maxTotalBytes上限;SDK 统计该前缀的活动.open和已封存.jsonl,超出预算时拒绝新事件。 - 多个 Worker 共用目录时,各自预算相加可能超过目录可用空间。需要目录级上限时,为每个 Worker 使用独立目录,或另设文件系统配额和磁盘监控。
- 为每个写者前缀设置
- 单写者所有权与文件交接:
- 同一本地规范化目录和文件前缀由
.prefix.lock独占;第二 owner 在预算统计前失败。四端在每次accept/flush及close最终同步前复核 token,sidecar 被删除或替换时先进入终态失败,不继续追加或发布用户态缓冲。 - 干净关闭释放锁。异常退出会保留锁,只有确认旧进程终止后才可人工移走;不得按 PID 或时间自动偷锁。
- 同一宿主上的多个 Worker 可使用不同且稳定的文件前缀,例如
events-worker-1、events-worker-2;同前缀第二写者必须失败,不能依赖操作系统日志追加的偶然原子性。前缀只允许字母、数字、点、下划线和连字符,且首字符须为字母或数字。 - 前缀按完整文件名边界识别;例如
events不接管events-worker-1的文件。Agent 的events-*.jsonl通配符可以读取多个前缀,但 SDK 所有权和预算仍分别计算。 - 该 sidecar 不是 NFS/PVC 分布式锁。多 Pod 应使用各自独占卷/前缀,或采用具备分布式协调的交付系统。
- 活动文件以
.open结尾;轮转或正常close()时先同步并关闭,再在同一目录原子改名为.jsonl。Agent 仅匹配.jsonl。flush()只同步当前.open,不会把它交给 Agent。因此低流量常驻进程可能直到轮转或关闭才交接文件,应按业务时效和文件大小安排受控轮转/实例关闭,而不能假定flush()会立即交接。 - 异常退出后
.open保留为待检查文件。确认旧进程停止后移除残留锁,重新打开相同前缀仍会报recovery-required/RECOVERY_REQUIRED/ErrRecoveryRequired,直至操作员检查并隔离或修复.open。不得直接改名为.jsonl或自动补发;先核对完整行、尾部截断和下游去重状态。
- 同一本地规范化目录和文件前缀由
- 已交接文件释放预算:
- Agent 只能在下游已持久接收并完成自身 checkpoint 后删除已封存的
.jsonl文件,不得删除.open。预算同时统计两类文件。 - SDK 按各语言实现核对目录用量;误删活动文件会 fail-closed。具体复核时机见所用语言的接入说明。
- SDK 不知道 Collector 是否 ACK;删除策略属于 Agent/Relay 运维合同,不能由“已读取一行”替代。
- Agent 只能在下游已持久接收并完成自身 checkpoint 后删除已封存的
四、 各语言构造
按语言配置 LoggingConsumer:
五、 LogAgent 接入边界与 Filebeat 输入示例
落盘文件是单行 JSON。下面的模式只匹配已封存的 .jsonl;禁止改成 events-*,否则会读到写入中的 .open。Filebeat 当前官方支持的输出包括 Elasticsearch、Logstash、Kafka、Redis、File、Console 和 Discard,并没有通用 output.http;因此不能把 Filebeat 直接指向本项目 Collector 批接口。可使用 filestream + ndjson 读取,再输出到 Kafka/Logstash,由受控 Relay 组装 ServerBatch、保留事件 ID 并处理 ACK。支持的输出列表见 Elastic 官方文档,filestream 的清理状态约束见 官方输入文档。
filebeat.inputs:
- type: filestream
id: track-business-events
enabled: true
paths:
- /var/log/track-business/events-*.jsonl
parsers:
- ndjson:
target: ''
# 示例中转目的地;不是 Track Business Collector。
output.kafka:
hosts: ['kafka-1.internal:9092']
topic: track-business-sdk-handoff从旧版“活动文件直接使用 .jsonl”升级时,应先停止写者与 Agent,确认旧进程已完全关闭并对旧文件完成下游 checkpoint,再部署新版写者及只匹配 .jsonl 的 Agent 配置。旧文件可能含不完整末行,须单独检查;不要把升级当作自动无损迁移。接入方应验证缺锁、残留 .open、容量耗尽、同前缀冲突及文件篡改时均不会静默继续采集。
六、业务事务与 Transactional Outbox
事务一致性
SDK 内置的 FileEventStore 仅是针对单进程单机重启场景的轻量级快照恢复适配器,它不是分布式事务 WAL,不能防止数据库回滚导致的数据虚假上报。
对于订单支付、扣款和退款等必须与业务事务保持一致的事件,可使用宿主实现的 Transactional Outbox:
[业务服务 Web 接口]
│
▼ (开启数据库本地事务)
┌──────────────────────────────────────────────────┐
│ 本地数据库 (MySQL / PostgreSQL) │
│ │
│ 1. INSERT INTO orders (id, amount, status); │
│ 2. INSERT INTO outbox_events (id, payload); │
└──────────────────────────────────────────────────┘
│
▼ (提交本地事务,使业务更新与 outbox insert 同成同败)
[事务提交成功]
│
▼ (独立轮询服务或 CDC 监听 outbox_events)
[读取未发送事件并通过 SDK 发送] ──► [收到 ACK 后标记 outbox_events 为已发送]业务记录与 outbox insert 在同一个本地事务中,可以消除二者之间的提交窗口;Relay 仍可能重复投递,必须复用不可变 eventId,并只在 Collector 明确 ACK 后更新终态。数据库 Outbox 和 Relay 需由接入方实现与验证。