异常创建(非catch)是性能瓶颈,需测throw耗时;连接类异常5–12μs,超时类25–48μs,ssl类gc压力大;应改用result结构化返回,仅意外失败才抛异常。

在高吞吐量日志上报流水线中,网络阻塞引发的异常(如 ConnectException、SocketTimeoutException、IOException)本身不是性能瓶颈的根源,真正拖慢系统的是每次抛出异常时触发的栈遍历与对象分配。要定量评估这部分损耗,关键不是“有没有 try-catch”,而是精准测量 异常创建环节 在真实链路中的开销。
锁定测量位置:只测 throw,不测 catch
try-catch 包裹本身无运行时开销,JIT 会完全优化掉未触发路径。因此不能在 catch 块里用 System.nanoTime() 测——那测的是日志处理或 fallback 逻辑。正确做法是:
- 在调用日志客户端 send() 方法前打时间戳:
long start = System.nanoTime(); - 确保异常在 send() 内部被抛出(例如模拟网络不可达),且该方法不捕获自身异常
- 在
throw new IOException("connect timeout")执行完成后的下一行结束计时(可用 JFR 的ThrowableAllocation事件交叉验证) - 压测时固定线程数、关闭 GC 日志干扰,并绑定 CPU 核心以减少抖动
区分异常类型,按场景建模
不同网络异常的代价差异极大,需分类测量:
-
连接建立失败类(如
ConnectException):栈深通常较浅(3–6 层),fillInStackTrace 耗时约 5–12μs -
写入超时类(如
SocketTimeoutException):常伴随 Netty 或 HttpClient 深层回调,栈深可达 15–25 层,实测 P95 耗时 25–48μs -
SSL 握手失败类(如
SSLHandshakeException):含证书解析与加密上下文,对象体积大,GC 压力明显上升
建议对每类异常单独压测 1000 次,取 P95 值作为 SLA 参考基准,而非平均值。
用 Result 替代异常做高频容错判断
如果日志服务集群存在周期性抖动(如某节点临时失联),不应依赖重试 + 抛异常来探测。应改为:
- 前置健康检查:维护节点心跳状态表,send() 前查表跳过已标记为 DOWN 的 endpoint
- 返回结构化结果:
Result<void senderror></void>,成功/失败都走同一控制流,避免任何throw - 仅当健康检查失效、且实际 write 失败时,才允许抛出异常——此时它已是真正的“意外”,而非可预期抖动
监控反哺:从日志链路反推异常成本
上线后通过可观测性工具持续验证:
- 用 JFR 开启
jdk.ThrowableAllocation事件,看单位时间内异常实例数是否突增 - 在日志采集端(如 Fluentd)配置采样率,对比「原始日志量」与「成功上报量」差值,定位是否因异常导致缓冲区堆积
- 若发现
fillInStackTrace占 CPU 时间 Top 3,说明异常已成性能热点,必须重构为防御式检查











