周末看到我的男神——T神(trustno1)发的一道面试题(T神老师曾用名“恶魔吹着笛子来”,原名金尹。我学Python就是因为他N年前在《程序员》连载的《自由与繁荣的国度》,俺是看着他的书长大的。)
假设我们有一个数据中心,大约1000-5000台机器,我们要对这批机器的cpu进行采样,大概5秒一次,那么这些采样数据你觉得会是一个什么样规模的数据。对于这样规模的数据,如何有效的传输,存储,检索
面试一定要老实,所以不能一上来就说用XX技术来解决就行了,要本本分分的分析问题、提供解决思路然后再解决问题、优化解决方案。有时候理解面试官的出题意图要比做出题目更重要,理解意图往往能达到“心有灵犀一点通”,给面试官留下好印象完全不在话下。
这道题目看似很简单其实考察的非常全面里面涉及到简单的编码理论、数据压缩、网络协议、分布式系统、海量数据存储和检索,我们直接切入正题。
度量CPU的利用率一般用Load Average,这个值是一段时间内CPU正在处理、等待处理的、等待I/O的进程。关于如何用这个值度量并不是本文的目的而且网上有很多相关资料,所以不再赘述。
我希望数据越小越好,对于海量数据来说如果在原始数据层面压缩一个字节那么引起的就是质变。一天的数据量是17280条(24小时是86400秒,每5秒一条),如果一条数据用8 bytes表示时间,4 bytes表示CPU负载。那么一台服务器一天能产生207360 bytes的数据量,5000台就是989M。如果保留7天的数据,6.7G。
如果我们能把一条数据压缩到4 bytes,那么能省下来2/3的存储空间,5000台服务器一天的数据量是329M,保留7天则是2.2G。
海量数据的特点是“基数”大,随着“数量”和“时间”的增长而疯狂的增长。
如何压缩?常规的压缩算法有点投机取巧,我们还是要自己动脑筋看一下数据有什么特点。毕竟针对有特点的数据进行压缩得到的压缩比最高。我们期望能够把一条数据压缩到4 bytes(32 bits),看一下是否可行。
首先分析“时间”,17280条数据也就是说“时间”范围是1-17280。8 bytes能表示的范围远远大于这个数,太浪费,实际上2 bytes(16 bits)都用不完(2^16= 65536)。用15 bits保存时间(2^15= 32768,这个数字远远大于17280,足够的)就足够了。
再看Load Average是一个三位数字的小数,比如:0.12 , 1.64, 98.2(一般很难有超过10的,至少我没用过这么“牛B”的机器)。采集的时候我们只采集最近一分钟的数据。
如果我们忽略小数点那么这个数据范围是0-999,如果用二进制表示至少需要10 bits(2^10=1024,大于999,足够用的);因为要记录小数点的位置(有两种情况,小数点在个位后面或者十位后面)需要1 bits(2^1=2)。我们每次采集CPU Load Average只需要11 bits就够存放了。
那么一条数据实际上只需要15 bits+11 bits=26 bits,如果我们用4 bytes方案是完全可行(4 bytes=32 bits)甚至能多出bits用来表示“第几天”。表示格式如下:
前21 bits表示时间,其中前3 bits用于表示第几天(一共7天),后面15 bits用于表示第几组,3bits作为保留备用。后11 bits表示CPU Load Average,其中1 bits表示小数点位置,后面是“无浮点整数”。
性能数据中间丢失一部分不会影响整体的趋势,而且在内网环境UDP丢包的概率很少,所以一般用UDP作为性能采集的传输协议。以太网的MTU值1500 bytes——我们要避免IP重组,去掉IP头部(20bytes)+UDP头部( 8bytes),所以我们能传输的最大UDP数据包是1472bytes。
这么大的数据包完全可以满足现在的需求,考虑的以后协议的扩展,我们把数据包设计的“可扩展”一些。
数据头部由机器ID、指令类型组成。我们定义类型为1时表示主动上报数据,那么“变长数据”部分就由1 bytes的条目数和N个条目组成。
条目设计采用TLV(type、length、value)结构,1bytes表示类型,1bytes表示长度,剩下的是数据部分。例子中我们可以用0表示CPU类型的数据,长度是4(表示4 bytes),数据部分就是CPU的性能。
计算一下每个数据包能传递多少条目,1472 bytes是UDP的最大数据包,扣除采集协议头部的5 bytes,如果是上报数据那么会有一个固定的1 bytes开销表示长度。于是1472-5-1= 1466 bytes。针对我们的例子而言,CPU性能数据是固定的4 bytes,加上1 bytes长度开销,1bytes类型开销。那么一条CPU性能数据是6 bytes。1472/4= 244,大概一条UDP数据包能够承载244条的CPU性能数据,如果按5秒钟一条的速度那么大概需要20分钟。为了兼顾及时性和性能,不建议实时发送UDP数据包,最好批量发送(考虑实际情况很可能要采集内存、硬盘之类的数据,所以30-60秒应该是比较好的选择)。
存储的目的是为了方便检索,性能数据一般是按时间取的(主要是为了画图)那么我们可以按时间存放。
对于例子来说我们不需要考虑的太复杂,否则就是无穷无尽的苦恼。CPU数据是固定的,那么一天的数据量也是固定的,每天的数据量我们成为块,每条数据称为行,行大小是固定的,所以它在块中的偏移是固定的,所以读取的时候直接定位到偏移读取就可以了。
注意:如果中间有缺失数据,块不会因此变小,我们会把缺失的部分填充为0。只要这样才能保证偏移和时间是一一对应的。我怎么感觉省去了建索引的的步骤是一件非常投机取巧、猥琐无比的事情。。。。( ̄3 ̄)a
当然,现实中没有人会自己去写一套东西来实现这个目的,人最优秀的品质是利用工具,但是工具会“反噬”我们。提到监控大家说一大堆工具,这时候不再是我们利用工具而是工具在利用我们,我们解决问题的思路已经被工具化了或者说被“反噬”。套用“古老”的箴言——当你手上有一把锤子的时候,看所有的东西都是钉子。
欢迎关注公众账号了解更多信息“写程序的康德——思考、批判、理性”