这个故事是关于我们最近在Plumbr进行的容量优化任务。 一切始于将无害的要求添加到现有组合中。
如您所知,Plumbr监视解决方案作为连接到服务器的Java代理分发。 只需少量添加即可跟踪一段时间内所有已连接的代理,以便可以实时回答以下问题:
- 我们有多久没有收到这个特定JVM的消息了?
- 另一个JVM的最后一次已知停机时间是什么?
当每个代理每秒发送一次心跳时,我们在服务器端需要做的就是跟踪所有心跳。 由于每个心跳都附加有唯一的时间戳记,因此天真的解决方案就像将Set或Map中的所有心跳都扔掉一样容易。 那么-简单,完成,接下来,请?
但是,一些快速的数学运算表明,最初的想法可能行不通。 考虑到:
- 时间戳记的类型很长 ,需要8个字节才能容纳它自己
- 一年中有365 x 24 x 60 x 60 = 31,536,000秒
我们可以快速进行数学计算,发现单个JVM仅使用一年的原始数据就需要240MB 。 仅原始数据的大小就已经足够吓人了,但是当打包到HashSet时,结构的保留大小 激增至约2GB ,而所有开销java.util.Collection API实现都隐藏在它们的腹部 。
幼稚的解决方案已经无法解决,我们需要一个替代方案。 最初我们不必走得很远,因为在同一java.util包中,一个等待被发现的意外之举叫java.util.BitSet 。 根据该类的javadoc:
BitSet类实现一个按需增长的位向量。 位集合的每个分量都有一个布尔值。 BitSet的位由非负整数索引。 可以检查,设置或清除各个索引位。
那么,如果我们将从代理获取的心跳存储为由心跳时间戳记索引的布尔值,该怎么办? Java中的时间戳表示为当前时间与1970年1月1日UTC午夜之间的毫秒差。 知道这一点后,我们可以将UTC表示为2015年9月1日12:00 UTC,即数字1441108800。那么,如果当我们看到一个Agent在时间戳1441108800处向我们发送心跳信号时,我们会将带有索引1441108800的位设置为true ,否则被保留为默认false ?
解决方案的问题隐藏在一个事实中,即BitSet中的位是用整数而不是long索引的。 要继续执行此解决方案,我们将需要一种将整数映射到long而不丢失任何信息的方法。 如果似乎不可能,那么让我们回顾一下这样一个事实,即需要一秒而不是一毫秒的精度。 知道了这一点,我们可以将索引缩小1,000倍,并以秒而不是毫秒的精度标记时间。
但是仅使用整数就可以表示多少秒? 显然,Integer.MAX_VALUE足够大,可以表示从1970年1月1日到19.01.2038的每一秒。 除了制造2038年的问题外,它还应该足够好,对吗?
不幸的是,正如我们的餐巾纸计算所显示的那样,一年的数据价值仍需要约800MB的堆空间。 这是从原始HashSet的2GB向正确方向迈出的一小步,但对于实际使用而言仍然太多了。
为了克服该问题,可能需要重新阅读/重新考虑“足以代表1970年1月1日的每一秒”的部分。 (不幸的)先生。 直到1995年,高斯林才发明Java虚拟机。18年后,Plumbr看到了曙光。 因此,我们直到1970年才需要回顾历史,并且每个整数都有一堆零。 可以从01.01.2013开始,而不是从01.01.1970开始,并使用一个索引0对应于01.01.2013 00:00(UTC)。
重做我们的餐巾纸数学,并在实践中检查结果使我们成为赢家。 现在一年的数据量只能存储在20MB中 。 与原始2GB相比,我们将所需容量减少了100倍 。 由于现有的基础架构已经可以解决这个问题,因此已经处于舒适区域,因此我们没有在优化路径上走得更远。
故事的道德启示? 当您有需求时,请找出对应用程序性能的影响。 我的意思是性能的各个方面,因为不仅有延迟和吞吐量,还应该忘记容量。 并且–了解您的域名。 没有它,您将无法做出决策,如果仅仅配备了有关数据结构的智能书籍,这些决策就显得不安全且危险。
翻译自: https://www.javacodegeeks.com/2015/09/squeezing-data-into-the-data-structure.html