有一个比较常见的文件传输需求,数据库打快照,快照文件压缩打包,上传/下载, 解压,对端数据库加载

如何缩短这个链路的时间,数据库打快照的时间控制以及加载数据库的时间控制,不沟通用,这里不讨论

这里的快指的是快速嗷

打包/压缩

打包压缩有个典型的用法

#压缩
tar cf - __filexx__ | lz4  > __filexx__
tar cf - __filexx__ | snzip  > __filexx__
#解压
lz4 -dc  __filexx__  | tar -xv -C __dirxx__
snzip -dc  __filexx__  | tar -xv -C __dirxx__

tar/lz4/snzip都有针对流的处理,unix这种设计非常的优雅,省拷贝

tar压缩是单线程,lz4的cli工具,用的也是单线程, snzip也是单线程

如何让这个压缩解压的流程更快?

针对多线程的lz4,我找到了一个实现

简单测试多线程版本的lz4 cli和原版lz4并没有速度上的差距?

解压是没法并行的

结合snzip,测了三个数据,确定是io到瓶颈了,三个数据相同

#6G数据,fasterkv生成

/usr/bin/time -p  tar cf - hlog | ./lz4  > h  #自己编的多线程lz4
real 42.96
user 0.10
sys 3.81


/usr/bin/time -p  tar cf - hlog |  lz4  > h  
real 42.42
user 0.09
sys 3.31


 /usr/bin/time -p  tar cf - hlog |  snzip  > h 
real 42.73
user 0.10
sys 3.46

查看iostat -x -y 1能看到util %100 dstat反而看不出来

找了个ssd机器,重新跑, 100g数据

# cpu 120%, 磁盘没跑满
# 100G压缩到4G
/usr/bin/time -p  tar cf - hlog | ./lz4  > h                                                                                
real 98.81
user 1.66
sys 68.08

# cpu 80%, 磁盘也没跑满
/usr/bin/time -p  tar cf - hlog | lz4  > h         
real 126.30
user 1.79
sys 63.79

# cpu 100%, 磁盘也没跑满
# 100G压缩到13G
/usr/bin/time -p  tar cf - hlog | ./snzip  > h  
real 105.23
user 2.15
sys 55.77

# 100G压缩到2.3G 多线程 gzip速度非常慢,放弃
/usr/bin/time -p  tar cf - hlog | pigz  > h
real 112.64
user 1.89
sys 108.99

解压速度

/usr/bin/time -p lz4 -dc   h | tar -xv -C temp
real 141.59
user 57.58
sys 36.63

/usr/bin/time -p ./snzip -dc  h | tar -xv -C temp
real 94.30
user 54.38
sys 29.29

 /usr/bin/time -p pigz -dc  h | tar -xv -C temp
real 161.14
user 209.26
sys 65.88

snappy解压要比lz4快的

gzip过于离谱,不测了

100G压缩/解压 snappy比lz4 快20 + 40 60 秒

但文件生成 100G-4G 100-13G

这个压缩比过于离谱,重新找一个数据来测

源数据 69G lz4 压缩到29G

#压缩
# lz4 69G - 29G
/usr/bin/time -p  tar cf - hlog | ./lz4  > h                                                                                
real 97.49
user 1.14
sys 50.86

/usr/bin/time -p  tar cf - hlog | lz4  > h         
real 165.71
user 1.11
sys 45.75

# 压缩
# snappy 69G - 31G
/usr/bin/time -p  tar cf - hlog | ./snzip  > h 
real 124.31
user 1.39
sys 38.66

# 解压
/usr/bin/time -p lz4 -dc h | tar -xv -C temp
real 123.29
user 51.14
sys 34.83

/usr/bin/time -p ./lz4 -dc h | tar -xv -C temp
real 113.17
user 38.79
sys 35.28

/usr/bin/time -p ./snzip -dc  h | tar -xv -C temp
real 77.99
user 40.26
sys 25.58

上传/下载

上传下载到时间需要确定

假设上传速度U m/s 下载速度D m/s

压缩比 Z

snappy压缩比 sn

lz4压缩比lz

文件总体积 T

Z(T/U + T/D) 带入上面的数据,上传下载假定30M 1G需要用 34s

snappy用时 13* 1024 / 30 *2 = 886s

lz4 用时间 4 * 1024 / 30 * 2 = 272s

考虑到压缩解压速度 snappy快60,并没有什么用

前面snappy节省的时间比在1以内,除非压缩比的压缩比在1以内,否则没有胜算

从上传下载速度考虑,除非差距在60以内,也就是说速度要在250M,才有优势

不同类型的文件有不同的压缩比,如果以当前的压缩比来考虑,snappy无胜算

第二组数据,其中压缩大小 69G - 29G(lz4) - 31G(snappy)

机器是32核

压缩算法\时间(s) 压缩时间 解压时间 总时间
lz4 165 124 289
lz4 multi-thread 98 114 212
snappy 124 78 202

压缩比的压缩比差距不大,体积差2G也就是68s,但是snappy的压缩解压并没有快68s,只快了十多秒

注意 lz4 multi-thread用的是std::thread::hardware_concurrency()决定线程数, 而在docker中,这个返回的是宿主机的核数

可以用 stress -c 4确认 –cpus=2能限定cpu使用200%,但是对于多线程的个数无能为力。

补充三组,分别是4核 8核,16核 64核

压缩算法\时间(s) 压缩时间 解压时间 总时间
Lz4 2线程 95.59 113.84 210
lz4 4线程 99.84 118.45 219
lz4 8线程 95.47 114.88 211
lz4 16线程 97.11 114.90 213
lz4 64线程 98.80 113.35 213

可以看到 ,并没有随着线程数增加而扩展性能

lz4多线程版的实现逻辑是一个队列,worker去干活压缩,然后队列中的数据排队写到文件

压缩最终是写成一个文件,多线程并没有加速写文件的过程,只是加速了分片压缩,最终压缩好的数据还是要逐个排队写到文件里

这个场景实际上有点类似于logger多消费者单生产者模型(MPSC),logger肯定有多线程写入同步问题,如何快速的多线程写文件。我的观察,这个代码是有修改空间的。这里挖一个坑,考虑一下如何把nanolog/spdlog的逻辑搬过来

结论

上传速度一般,要好的压缩率,选lz4 多线程版

上传速度非常快,压缩率在速度面前无关紧要,选snappy,解压速度快

多线程版lz4和snappy压缩速度差不多,解压速度snappy更快(压缩率低)

多线程版本lz4还是有改进空间的比如去掉锁竞争之类的

其他

看了篇文章 How to speed up the unloading of LZ4 in ClickHouse

结论部分介绍了解压速度提升的一些总结,在memcpy上能有点改进,不过不能在lz4库上改,而是自己实现一套RLE压缩算法。另外收益有,但是不是决定性的

一个查询,耗时不仅仅在解压上,收益界定类似本文

有一些特别久的查询,要大量的获取冷数据,那么冷数据的压缩率高更省IO(读),所以选用zstd而不是lz4

也有没有用mmap /o_direct之类的零拷贝技术,也没有用一些提高checksum计算速度的技巧,压缩率高这些优化收益不大(why?)

参考

  • 思路启发 https://www.codeproject.com/Questions/3689965/Multiple-threads-writing-to-a-single-file-in-C
  • https://stackoverflow.com/questions/7565034/can-multiple-threads-write-into-a-file-simultaneously-if-all-the-threads-are-wr 这里也讨论了实际上fd有状态不太可能多线程都持有fd来写,需要同步。实际上还是退化成乱序操作之后顺序写这个代价要最小化
  • 也考虑过把tar内嵌到应用里,这里有个简单实现 https://github.com/rxi/microtar