系统架构设计:SDK 与审计系统的关系、模块化设计、可扩展性考虑

做审计 SDK 之前,很多团队都踩过同一个坑 —— 把 SDK 做得像个“嵌入式监控”。功能倒是全,但每次业务系统升级,SDK 就得跟着改。改着改着,代码就成了一团乱麻。我在刚带项目时也犯过这毛病,后来才明白,好的架构不是堆功能,而是划边界。

这一章咱们就来拆开揉碎,看看审计 SDK 和审计系统到底是什么关系,怎么设计模块,以及怎么留好后路。你想想看,一个 SDK 如果写死了,等业务量上来,想扩展却得改底层,那得多痛苦。

2.1 审计 SDK 与审计系统:边界与通信

说白了,SDK 是“侦察兵”,审计系统是“指挥部”。侦察兵只负责收集情报、简单预处理,然后发给指挥部。指挥部再分析、存储、告警。这个职责必须分清楚,不然两边打架。

核心原则:SDK 只做“采集+缓冲+发送”,不做“分析+存储+决策”。分析放服务端。

我曾经在一个项目里看到有团队在 SDK 里直接写 MySQL 连接存审计日志,后来业务峰值一来,SDK 把数据库拖垮了。血的教训啊。

常用的通信方式有这几种:

方式适用场景注意点
HTTP/HTTPS 异步推送大多数业务,延迟容忍做好重试与幂等
消息队列(Kafka / RabbitMQ)高吞吐、可削峰客户端要管理连接和序列化
本地日志 + 采集器拉取合规严格、网络隔离落地文件要轮转和加密

提示:我个人习惯在 SDK 里使用异步非阻塞发送,用缓冲区队列 + 定时 flush。这样可以避免阻塞业务主线程。

2.2 模块化设计:让 SDK 像乐高一样拼装

一个模块化 SDK 应该长什么样?我把它拆成五个核心模块,每个模块只干一件事:

  • 采集模块 —— 从不同数据源(代码埋点、框架拦截、文件日志)获取审计事件。
  • 过滤模块 —— 根据白名单/黑名单、敏感字段脱敏,剔除无效事件。
  • 缓冲模块 —— 内存队列 + 磁盘缓存,防止突发流量丢事件。
  • 编码模块 —— 将事件序列化为 Protobuf 或 JSON,加上元数据。
  • 发送模块 —— 网络发送 + 重试 + 退避算法。

每个模块都通过接口定义,可以单独替换。比如你想把 Protobuf 改成 Avro,只换编码模块就行,不影响别的。

警告:模块之间的依赖必须单向!采集模块不能直接调用发送模块,必须经过缓冲。否则并发一高,互相阻塞,SDK 就把业务进程拖死了。

看一下这个模块关系图吧,我画了一个简化版的架构:

审计 SDK 模块化架构 应用埋点 框架拦截 文件监听 采集模块 统一事件接口 过滤模块 脱敏 / 白名单 缓冲模块 队列 + 磁盘缓存 编码模块 Protobuf / JSON 发送模块 重试 / 退避 审计服务端 分析引擎 存储 告警 可视化 (不属于SDK)

上图中虚线表示从缓冲模块到编码再到发送是异步流水线。我实际项目中,缓冲模块用的 RingBuffer + 本地文件缓存,编码和发送由独立线程池处理,保证业务线程永远不卡住。

2.3 可扩展性考虑:预留哪些扩展点

好的 SDK 不止能用,还要能“长”。我总结了四个必须预留的扩展点:

  1. 自定义数据源 —— 提供 SPI 接口,让用户自己实现从 RPC 框架、数据库连接池等来源采集审计事件。
  2. 过滤器链 —— 支持插入多个过滤器,比如脱敏器、合规检查器,按顺序执行。
  3. 序列化策略 —— 内置 JSON 和 Protobuf,但允许用户添加 Thrift、FlatBuffers 等。
  4. 传输渠道 —— HTTP、Kafka、gRPC 都要支持,且用户可以注册自己的 Transport。

扩展点设计原则:用接口 + 工厂模式,配置文件决定激活哪些实现。千万别用硬编码 if-else,否则扩展就是灾难。

我自己有个习惯:在 SDK 初始化时加一个 PluginManager,支持通过 classpath 扫描自动发现扩展。用户只需要把 jar 包丢进去,配置文件里写一行 audit.transport=com.example.MyTransport 就完事。

2.4 配置与降级:架构里的“安全气囊”

架构再漂亮,运行中也可能出问题。所以必须考虑降级策略。

我给你列一个生产中必配的降级开关:

开关行为触发条件
采样率只采集 N% 的事件服务端压力过高
缓冲区丢弃丢弃最旧事件本地磁盘写满
静默模式完全停止发送网络故障持续超时
本地落盘审计日志写本地文件审计服务端宕机

注意:我曾经遇到过某个 SDK 没有降级开关,服务端挂了之后 SDK 疯狂重试,结果把业务线程池也占满了。后来加了“熔断”机制 —— 连续失败超过阈值,直接进入静默模式,等健康检查恢复后再自动重连。

要实现熔断,你可以在发送模块里加一个 CircuitBreaker 状态机。错误次数累积到阈值,状态变为 OPEN,直接短路;过一段时间 HALF_OPEN 试发送,成功就 CLOSED。

这里给个简单的伪代码思路:

if circuitBreaker.isOpen():
    event.enqueueToLocalFile()
    return
try:
    send(event)
    circuitBreaker.recordSuccess()
except Exception as e:
    circuitBreaker.recordFailure()
    event.retryLater()

嗯,很朴素,但管用。实际项目里还可以把本地文件定期回传到服务端,保证不丢数据。

2.5 总结:一张架构清单

到现在你应该清楚了:审计 SDK 的架构不是堆功能,而是定边界、拆模块、留扩展、保降级。我习惯把最终的设计画成一张检查表,部署前逐条确认:

  • SDK 是否只做采集和发送,不做分析?
  • 各模块是否通过接口解耦?
  • 是否存在循环依赖?
  • 是否预留了至少 4 种扩展点?
  • 降级策略是否可配置且能自动恢复?
  • 异步发送是否使用独立线程池?

把这些搞明白,你的 SDK 架构才不会变成“屎山”。后面我们会用代码一步步把这些模块实现出来,到时候你再回看这一章,会发现每张图每行字都是工程经验换来的。