来源
在对 Dubbo 新版本做性能压测时, 无意中发现对用例中某个 TO(Transfer Object)类的一属性字段稍作修改, 由 Date 变成 LocalDateTime, 结果是吞吐量由近 5w 变成了 2w,RT 由 9ms 升指 90ms.
在线的系统, 拼的从来不仅仅是吞吐量,
而是在保证一定的 RT 基础上, 再去做其他文章的, 也就是说应用的 RT 是我们服务能力的基石所在, 拿压测来说, 我们能出的 qps/tps 容量, 必须是应用能接受的 RT 下的容量, 而不是纯理论的数据, 在集团云化的过程中计算过, 底层服务的 RT 每增加 0.1ms, 在应用层就会被放大,
整体的成本就会上升 10% 以上.
要走向异地, 首先要面对的阿喀琉斯之踵: 延时, 长距离来说每一百公里延时差不多在 1ms 左右, 杭州和上海来回的延迟就在 5ms 以上, 上海到深圳的延迟无疑会更大, 延时带来的直接影响也是响应 RT 变大,
用户体验下降, 成本直线上升. 如果一个请求在不同单元对同一行记录进行修改, 即使假定我们能做到一致性和完整性, 那么为此付出的代价也是非常高的, 想象一下如果一次请求需要访问
10 次以上的异地 HSF 服务或 10 次以上的异地 DB 调用, 服务再被服务调用, 延时就形成雪球, 越滚越大了.
普遍性
关于时间的处理应该是无处不在, 可以说离开了时间属性, 99.99% 的业务应用都无法支持其意义, 特别是像监控类的系统中更是面向时间做针对性的定制处理.
在 JDK8 以前, 基本是通过 java.util.Date 来描述日期和时刻, java.util.Calendar 来做时间相关的计算处理. JDK8 引入了更加方便的时间类, 包括 Instant,LocalDateTime,OffsetDateTime,ZonedDateTime 等等, 总的说来, 时间处理因为这些类的引入而更加直接方便.
Instant 存的是 UTC 的时间戳, 提供面向机器时间视图, 适合用于数据库存储, 业务逻辑, 数据交换, 序列化. LocalDateTime,OffsetDateTime,ZonedDateTime 等类结合了时区或时令信息, 提供了面向人类的时间视图, 用于向用户输入输出, 同一个时间面向不同用户时, 其值是不同的. 比如说订单的支付, 发货时间买卖双方都用本地时区显示. 可以把这 3 个类看作是一个面向外部的工具类, 而不是应用程序内部的工作部分.
简单说来, Instant 适用于后端服务和数据库存储, 而 LocalDateTime 等等适用于前台门面系统和前端展示, 二者可以自由转换. 这方面, 国际化业务的同学有相当多的体感和经验.
在 HSF/Dubbo 的服务集成中, 无论是 Date 属性还是 Instant 属性肯定是普遍的一种场景.
问题复现
Instant 等类的性能优势
以常见的格式化场景举例
- @Benchmark
- @BenchmarkMode(Mode.Throughput)
- public String date_format() {
- Date date = new Date();
- return new SimpleDateFormat("yyyyMMddhhmmss").format(date);
- }
- @Benchmark
- @BenchmarkMode(Mode.Throughput)
- public String instant_format() {
- return Instant.now().atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern(
- "yyyyMMddhhmmss"));
- }
在本地通过 4 个线程来并发运行 30 秒做压测, 结果如下.
- Benchmark Mode Cnt Score Error Units
- DateBenchmark.date_format thrpt 4101298.589 ops/s
- DateBenchmark.instant_format thrpt 6816922.578 ops/s
可见, Instant 在 format 时性能方面是有优势的, 事实上在其他操作方面 (包括日期时间相加减等) 都是有性能优势, 大家可以自行搜索或写代码测试来求解.
Instant 等类在序列化时的陷阱
针对 Java 自带, Hessian(淘宝优化版本)两种序列化方案, 压测序列化和反序列化的处理性能.
Hessian 是集团内应用的 HSF2.2 和开源的 Dubbo 中默认的序列化方案.
- @Benchmark
- @BenchmarkMode(Mode.Throughput)
- public Date date_Hessian() throws Exception {
- Date date = new Date();
- byte[] bytes = dateSerializer.serialize(date);
- return dateSerializer.deserialize(bytes);
- }
- @Benchmark
- @BenchmarkMode(Mode.Throughput)
- public Instant instant_Hessian() throws Exception {
- Instant instant = Instant.now();
- byte[] bytes = instantSerializer.serialize(instant);
- return instantSerializer.deserialize(bytes);
- }
- @Benchmark
- @BenchmarkMode(Mode.Throughput)
- public LocalDateTime localDate_Hessian() throws Exception {
- LocalDateTime date = LocalDateTime.now();
- byte[] bytes = localDateTimeSerializer.serialize(date);
- return localDateTimeSerializer.deserialize(bytes);
- }
结果如下. 可以看出, 在 Hessian 方案下, 无论还是 Instant 还是 LocalDateTime, 吞吐量相比较 Date, 都出现 "大跌眼镜" 的下滑, 相差 100 多倍; 通过通过分析, 每一次把 Date 序列化为字节流是 6 个字节, 而 LocalDateTime 则是 256 个字节, 这个放到网络带宽中的传输代价也是会被放大. 在 Java 内置的序列化方案下, 有稍微下滑, 但没有本质区别.
- Benchmark Mode Cnt Score Error Units
- DateBenchmark.date_Hessian thrpt 2084363.861 ops/s
- DateBenchmark.localDate_Hessian thrpt 17827.662 ops/s
- DateBenchmark.instant_Hessian thrpt 22492.539 ops/s
- DateBenchmark.instant_Java thrpt 1484884.452 ops/s
- DateBenchmark.date_Java thrpt 1500580.192 ops/s
- DateBenchmark.localDate_Java thrpt 1389041.578 ops/s
分析解释
Hession 中其实是有针对 Date 类做特殊处理, 遇到 Date 属性, 都是直接获取 long 类型的相对来做处理.
通过分析 Hessian 对 Instant 类的处理, 无论是序列化还是反序列化, 都需要 Class.forName 这个耗时的过程..., 怪不得 throughput 急剧下降.
延展思考
1) 可以通过扩展实现 Instant 等类的 com.alibaba.com.caucho.hessian.io.Serializer, 并注册到 SerializerFactory, 来升级优化 Hessian. 但会有前后兼容性上, 这个是大问题, 在集团内这种上下游依赖比较复杂的场景下, 极高的风险也会让此不可行. 从这个角度看, 只有建议大家都用 Date 来做个 TO 类的首选的时间属性.
2) HSF 的 RPC 协议从严格意义上讲是 Session 握手层的协议定义, 其中的版本识别也是这个层面的行为, 而业务数据的 presentation 展示层是通过 Hessian 等自描述的序列化框架来实现, 这一层其实是缺少版本识别, 从而导致升级起来就异常困难.
来源: https://yq.aliyun.com/articles/705413