作者存档: 颜开 - 第2页

Linux大文件传输

我们经常需要在机器之间传输文件。比如备份,复制数据等等。这个是很常见,也是很简单的。用scp或者rsync就能很好的完成任务。但是如果文件很大,需要占用一些传输时间的时候,怎样又快又好地完成任务就很重要了。在我的测试用例中,一个最佳的方案比最差的方案,性能提高了10倍。

复制文件

如果我们是复制一个未压缩的文件。这里走如下步骤:
  1. 压缩数据
  2. 发送到另外一台机器上
  3. 数据解压缩
  4. 校验正确性
这样做会很有效率,数据压缩后可以更有效的利用带宽

使用ZIP+SCP

我们可以通过ZIP+SCP的组合实现这个功能。
gzip -c /home/yankay/data | ssh yankay01 "gunzip -c - > /home/yankay/data"

这条命令是将/home/yankay/data经过GZIP压缩,通过ssh传输到yankay01的机器上。

data文件的大小是1.1GB,经过Zip压缩后是183MB,执行上面的命令需要45.6s。平均吞吐量为24.7MB/s
我们会发现Scp也有压缩功能,所以上面的语句可以写成
scp -C -c blowfish /home/yankay/data yankay01:/home/yankay/data

这样运行效果是相同的,不通之处在于我使用了blowfish算法作为Scp的密匙算法,使用这个算法可以比默认的情况快很多。单单对与scp,使用了blowfish 吞吐量是62MB/s,不使用只有46MB/s。

可是我执行上面一条命令的时候,发现还是需要45s。平均吞吐量还为24MB/s。没有丝毫的提升,可见瓶颈不在网络上。
那瓶颈在哪里呢?

性能分析

我们先定义几个变量

  • 压缩工具的压缩比是 CompressRadio
  • 压缩工具的压缩吞吐是CompressSpeed MB/s
  • 网络传输的吞吐是 NetSpeed MB/s

由于使用了管道,管道的性能取决于管道中最慢的部分的性能,所以整体的性能是:

speed=min(NetSpeed/CompressRadio,CompressSpeed)

当压缩吞吐较网络传输慢的时候,压缩是瓶颈;但网络较慢的时候,网络传输/吞吐 是瓶颈。

根据现有的测试数据(纯文本),可以得到表格:

压缩比 吞吐量 千兆网卡(100MB/s)吞吐量 千兆网卡吞吐量,基于ssh(62MB/s) 百兆网卡(10MB/s)吞吐量
ZLIB 35.80% 9.6 9.6 9.6 9.6
LZO 54.40% 101.7 101.7 101.7 18.38235294
LIBLZF 54.60% 134.3 134.3 113.5531136 18.31501832
QUICKLZ 54.90% 183.4 182.1493625 112.9326047 18.21493625
FASTLZ 56.20% 134.4 134.4 110.3202847 17.79359431
SNAPPY 59.80% 189 167.2240803 103.6789298 16.72240803
NONE 100% 300 100 62 10

可以看出来。在千兆网卡下,使用QuickLZ作为压缩算法,可以达到最高的性能。如果使用SSH作为数据传输通道,则远远没有达到网卡可以达到的最佳性能。在百兆网卡的情况下,各个算法相近。对比下来QuickLZ是有优势的。

对于不同的数据和不同的机器,可以得出不同的最佳压缩算法。但有一点是肯定的,尽量把瓶颈压在网络上。对于较慢的网络环境,高压缩比的算法会比较有优势;相反对于较快的网络环境,低压缩比的算法会更好。

结论

根据上面的分析结果,我们不能是用SSH作为网络传输通道,可以使用NC这个基本网络工具,提高性能。同时使用qpress作为压缩算法。

scp /usr/bin/qpress yankay01:/usr/bin/qpress
ssh yankay01 "nc -l 12345 |  qpress -dio > /home/yankay/data" &
qpress -o /home/yankay/data |nc yankay01 12345

第一行是将gpress安装到远程机器上,第二行在远程机器上使用nc监听一个端口,第三行压缩并传送数据。

执行上面的命令需要2.8s。平均吞吐量为402MB/s,比使用Gzip+Scp快了16倍!!

根据上文的公式,和自己的数据,可以绘出上面的表格,就可以选择出最适合的压缩算法和传输方式。达到满意的效果。如果是一个长期运行的脚本的话,这么做是值得的。

内存究竟有多快?

一般来说。CPU需要0个周期来访问其寄存器,1-30个周期来访问高速缓存,50-200个周期来访问主存。

对于Intel Core i7来说。这个值可以很具体。Intel Core i7的主频约在2-3GHz。可以计算出。

L1—指令缓存 L1-数据缓存 L2-缓存 L3-缓存 内存
访问周期 4 4 11 30-40 50-200
缓存大小 32KB 32KB 256KB 8MB 若干GB
访问时间 2ns 2ns 5ns 14-18ns 24-93ns

也就是说,访问内存的时间是ns级别的。

再来看看磁盘。

磁盘的访问时间=寻道时间+旋转延迟+数据传输时间。对于普通的7200转STAT磁盘。这个值是:9ms+4ms+0.02ms=13.02ms。

也就是说,如果从磁盘随机访问一个字节,需要13.02ms,比从内存获取的时间24-93ns,至少要多14万倍。相差5个数据级,何其巨大的差距。

顺序读写磁盘会快一些。 假设一个盘片有1000个扇区,每个扇区512字节,7200转。顺序读可以忽略掉寻道的时间。所以吞吐量是 扇区数×扇区大小×转速=1000*512/(60/7200)=58MB/s。这个数据似乎不咋样。如果使用多盘系统。STAT II的接口,吞吐量可以达到300MB/s。追求极限性能可以mount裸盘直接操作多盘。

存储器山

《深入理解计算机系统》一书中提到了一个存储器山的概念。教授先生别出心裁的将存储器的吞吐量,画成了一座山。

 

存储器山的测试程序是这样的:

Kernel_loop(elems, stride):
for (i = 0; i < elems; i += stride)
    result = data[i];

X轴表示的是读取步长,Y轴是吞吐量,Z轴是数据总量的大小。

可以看出来步长越小,数据数据总量越小。性能越好。

很明显,山是不是平滑的,是成阶梯状。红色部分为L1缓存,绿色为L2缓存,浅蓝是L3缓存,深蓝是内存。我们可以得一些数据。

L1-数据缓存 L2-缓存 L3-缓存 内存 磁盘 SSD
缓存大小 32KB 256KB 8MB 十几GB 几TB 几百GB
访问时间 2ns 5ns 14-18ns 24-93ns 13.0ms 30-300us
吞吐量 6500MB/s 3300MB/s 2200MB/s 800MB/s 60MB/s 250MB/s

也就是说,去除高速缓存的内存,吞吐量性能只有800MB/s而已。比起磁盘的300MB/s,网络的100MB/s。也只是快了几倍。平时说内存比磁盘快许多,其实没有那么多,如果不好好操作内存,内存的频繁读写,也可以成为系统瓶颈。

总结

现在处理器的主频已经停止了增长。但是高速缓存仍然以摩尔定律的速度增长的。长久的看,高速缓存频率逐渐会追上处理器的性能,容量也会越来越大。但是内存则不容乐观,虽然容量增加了许多,但是性能确没有大的提升,磁盘的状况也是类似;SSD刚刚开始普及,趋势不明显。

但可以看到,SSD的吞吐量和内存的吞吐量相去并不大。也就是说在未来,当SSD完全替代了磁盘。我们要像现在操作磁盘一样小心翼翼地操作内存,才有可能写出符合那个时代计算机性能的程序。相比之下,SSD的使用比磁盘要轻松一些,毕竟随机读写的速度在一两个数量级上。

并发编程之巧用锁

背景

程序都是跑在机器上的,并行CPU包含几个部分。

  • 处理器核心。执行线程的设备。一个核心可以执行一个或多个线程(超线程)
  • 互连线。处理器和处理器,处理器和储存器之间的通信通道。一般CPU分两种架构,SMP对称多处理器和NUMA架构。SMP的通信是总线式的,核心增加后性能会下降。NUMA的结果相当于以太网,性能相对较好。理论上NUMA是没有cache的,不过商业产品都有。互连线的资源是有限的
  • 存储器。这里存储器值得是一级缓存,二级缓存和内存。一级缓存一般和处理器在一起,访问它需要1-2个时钟周期,访问二级缓存则需要数十个时钟周期。而访问内存,需要数百个时钟周期。
可以看出来,CPU访问内存的代价是很高的。如果一个处理器改变的一个volatile变量,为了保持一致性,需要将这个消息广播到其他的处理器,其他处理器废弃其一级缓存,重新加载。整个操作需要至少数十个时钟周期。而如果CPU仅仅访问其一级缓存的数据,性能就很高。所以多核编程和网络编程一样,性能的瓶颈就在于互连线的使用。
自转锁
自转锁是一种锁的实现方式。就是在while语句中不断监视一个变量,通过观察到变化,保证只有一个线程持有锁。
public void lock(){
  1.         while(state.getAndSet(true)){}
  2.     }

这里的GetAndSet方法是一个CPU的CAS硬件指令。我们可以看到,每次调用这个质量,CPU要保证缓存一致,丢弃该CPU持有该变量的缓存,重新加载。会消耗至少数十个时钟周期的时间。所以可以进行如下改进。

   public void lock(){
  1.         while(true){
  2.             while(state.get()){};
  3.             if(!state.getAndSet(true))
  4.                 return;
  5.     }

这个锁和上面的锁逻辑上是一样的。区别是处理器在自旋的时候,不再执行CAS,而是执行一个get操作,直接从一级缓存中取数据,不主动同步。让发现状态有所变化后,再通过CAS操作确认并加锁。这样可以大大减少CAS指令的数量,节约了处理器之间的信息流量。

当然这个锁还有很多可以改进的地方,不详述了。不过其中“乐观”的思想是值得借鉴的。下面总结了一个用锁时候的方法,可以提高性能。

细粒度同步

细粒度同步避免对整个数据结构上锁。比如Java语言中的ConcurrentHashMap相对普通的HashMap性能要高很多。下图是ConcurrentHashMap的结构。

《Java并发编程之ConcurrentHashMap》很好的解释了ConcurrentHashMap的实现。

读写锁

许多共享对象都是这样的,读可以并发进行,不会去修改对象。而写操作需要线性进行。如果读操作远远大于写操作,区分读写行为,可以提高程序的并发性。

乐观同步

上面的自旋锁,就是一个乐观的例子。乐观就是先取出变量,乐观的认为没有冲突,在最后再确认下没有冲突,如果有冲突,重新再执行一边。这样就把麻烦的事情放到了最后,可以减少整段程序中加锁的部分,提高并行性。

惰性同步

惰性同步和乐观思路是一样的。在修改变量的时候,分两个阶段,先修改好,再在最后确认没有冲突,完成修改。

非阻塞操作

非阻塞操作就是CAS操作。使用这些操作可以避免使用加锁的传统方法,但实现一个非阻塞的共享数据结构非常的复杂,很容易出问题。性能的提升不是没有成本的。关于CAS操作,有一个缺陷,即ABA问题。CAS(compare and swap)操作的语义是(先比较原来的值和新的值是否一致,如果一致则更新并返回True,不一致则返回False)。这个操作不是原子的,中途如果有其他的线程先将这个地址修改为另一个值在改回来,一样会返回True。所以要注意这个特性,否则就可能造成一些Bug.

性能的提升很不容易,多点复杂性,就多点Bug的可能。并发编程的Bug还不怎么好找出来。所以我觉得,善用现有的久经考验的数据结构,少自己操作底层的原语,等到发现性能问题的时候,再在性能瓶颈的部分履薄冰地编程,尽可能少的引入复杂度。话说Java的LinkedTransferQueue使用了更精致的并发编程可以极大的提高队列的性能,可到Java7才成为JDK的一部分。可见实现一个并发的队列有多难了。