问题的开始
我司某产品线有这么一个神奇接口 (https://host/path/customQuery)
该接口在预发或线上缓存正常的情况下TTFB为150ms左右(可以认为服务处理时间差不多就是TTFB),不过相比150ms的TTFB,显然数据资源下载时间过长的问题会更引人注意需要100ms左右(当然这也是网络条优秀的情况下,网络一般的话这个下载时间会更夸张) customQuery请求一次请求的数据响应大概为2.7MB, 压缩后也有超过300KB 下载时间过长看起来就是因为这个响应实体过大了(100Mb的带宽满速,300KB差不多也需要30ms),通过测试可以发现同样的网络条件同一个应用的其他接口,如果响应压缩后小于1KB,其ContentDownLoad时间可以忽略不计(通常都会小于2ms) 因为代理默认开启了gzip,其实数据已经被压缩了近10倍,但是压缩后的数据还是过大。 分析了customQuery响应实体的数据结构。 发现数据每个list中fields节点大量重复出现。
如上图其中field的描述是完全一致的(按一页50条计算,这些数据重复了50遍) 这些数据field描述数据单个都大小大概是50KB(重复50次可以看到2.7MB的数据几乎都是这些重复的数据)
开始秀了
既然已经明确了这些重复描述数据,服务端的同学很自然想到把这些field描述提取出来重新组装数据可以大幅度减小数据传输的大小。 不过自己恰好曾经“看过”DEFLATE压缩(http的gzip正好使用的是DEFLATE)其中使用到的LZ77是会匹配前文相同短语后面的相同短语都会被替换成“标记”。 那我“秀”的时候又到了,当即表示采用这种数据重组的方式并不会带来明显的实际提升,因为数据实际的信息量没有实际变化,只是手动去除了冗余,而之前冗余的数据其实已经被gzip处理过了,所以仅仅单纯去除重复描述数据片段并不能带来预期的收益。 因为我秀的时候如此自信,对方马上就自己不自信了,表示要回去先验证效果后在做打算。
看起来是失败了
果然后面的结果“居然”是我被打脸了
customQuery接口返回的实体大小直接变成了25kb,解压后189kb(之前是327kb,解压后2.7Mb)
那这差距太大了,实体大小减小到了之前的10%不到,当然下载速度ContentDownLoad也有了大幅度的降低。(基本上就是一个RTT的时间) 不过这完全跟我之前的认知不一样啊,一定是哪里出现了问题。(毕竟是以为自己懂了系列)
试图抢救下
为了挽回颜面,我把这2组原始数据下载下来,本地压缩进行分析(还不想承认自己错了,试图找到产生这种结果的其他解释)
如下图老的数据为customQuery_v1(2.7MB),新的为customQuery_v2(190KB),customQuery_v1中有大量重复的fields描述字段所以会明显大很多。
分别使用zip,gzip,rar对2组数据进行压缩 (gzip即为http默认使用的压缩算法,MAC上直接使用gzip命令可以对文件进行压缩) 可以发现RAR的压缩结果就与我最开始的想法差不多(即使原始数据差了超过10倍,而压缩的结果是几乎一致的,v1为19kb ;v2为17kb) 不过gzip对2组数据的压缩结果与在浏览器上看到的是一样的。(v1为329kb ;v2为25kb) 既然本地压缩也得到了同样的结果,看来真的是自己Too young too naive (大意了,没有闪,秀的时候应该先在本地验证一下的)
默默面对错误分析原因
但是为什么会有这样的结果,按我的理解压缩结果应该与rar一致才对。要搞清楚还要从压缩的方式入手。 一定是我以为的压缩行为与实际存在差异,gzip的基础是DEFLATE,DEFLATE是LZ77与哈夫曼编码的一个组合体( https://tools.ietf.org/html/rfc1951) Huffman Coding 只是单纯的字符编码,编码后的大小与编码前的大小直接正相关,肯定不是产生结果的原因。 那剩下就只有是LZ77,只能是LZ77一开始没有把那些重复的fields压缩掉,而为什么LZ77没有把原始数据里大量重复的描述“标记”起来。 LZ77整体是是使用已经出现过的相应匹配数据信息替换当前数据从而实现压缩功能,为了匹配数据需要用到了“滑动窗口”的概念 细细一品,LZ77并不是全文匹配,数据为了可以边发送边压缩会进行分块压缩。通过查阅RFC文档,大概可以明确块的大小被限制在64k内,最大滑动窗口就是64k/2=32k,并且还要求“标记”的最大长度为256字节(当然标记长度这个问题不大,大不了不多用几个标记)。这里的问题在于使用滑动窗口就要求重复的数据必须要“相邻” 而块大小最大为64K,如果重复的2段数据不能出现在一个窗口内是不能被标记的。但是窗口最多是块大小的一半32Kb(实际也不会用这么大的窗口),而我们之前就计算过我们重复的单个field描述就有50Kb,要出现有2个重复的内容,即使2个描述相邻那也至少上100Kb(他们甚至都无法在同一个块里),实际上窗口最大32Kb,所以LZ77根本不能标记出这些重复的field。
以下引至https://tools.ietf.org/html/rfc1951#section-2
Compressed representation overview A compressed data set consists of a series of blocks, correspondingto successive blocks of input data. The block sizes are arbitrary,except that non-compressible blocks are limited to 65,535 bytes. Each block is compressed using a combination of the LZ77 algorithmand Huffman coding. The Huffman trees for each block are independentof those for previous or subsequent blocks; the LZ77 algorithm mayuse a reference to a duplicated string occurring in a previous block,up to 32K input bytes before. Each block consists of two parts: a pair of Huffman code trees thatdescribe the representation of the compressed data part, and acompressed data part. (The Huffman trees themselves are compressedusing Huffman encoding.) The compressed data consists of a series ofelements of two types: literal bytes (of strings that have not beendetected as duplicated within the previous 32K input bytes), andpointers to duplicated strings, where a pointer is represented as apair <length, backward distance>. The representation used in the"deflate" format limits distances to 32K bytes and lengths to 258bytes, but does not limit the size of a block, except foruncompressible blocks, which are limited as noted above. Each type of value (literals, distances, and lengths) in thecompressed data is represented using a Huffman code, using one codetree for literals and lengths and a separate code tree for distances.The code trees for each block appear in a compact form just beforethe compressed data for that block. |
---|
总结
最终也还是自己错了,也没有什么好总结的
要是什么都不知道也不出问题,要是知道的很清楚也不会出问题,就是在“以为自己知道”的情况下就各种问题。