分类存档: 软件技术
Amazon EBS架构猜想
Amazon EBS是专门为Amazon EC2 虚拟机设计的弹性块存储服务。Amazon EBS可以为Amazon EC2的虚拟机创建卷volumes,Amazon EBS卷类似没有格式化的外部卷设备。卷有设备名称,同时也提供了块设备接口。你可以在Amazon EBS 卷上驻留自己的文件系统,或者直接作为卷设备使用。也就是说EBS就是一个基于集群的完美的“大磁盘”,可以随机读写,较高性能,完美的一致性和高可靠。本来以为这只是幻象,十分不好做。杨公一席话让我茅塞顿开。所以猜测EBS的架构如下,内部人士不要笑话我。
架构
- EBS虚拟磁盘驱动器:EBS客户端构件,和EC2部署在一起,同生共死。作为操作系统的磁盘驱动器存在。作用是管理该磁盘的块,接受磁盘请求。
- 缓冲存储:我猜测,如果要想保证高性能,同时保证数据不丢失,需要使用一个本地的持久化存储作为缓冲和缓存。直接对集群进行大量的细碎的操作,延迟是不可接受的。如果使用长连接,多网卡,可以让延迟变得可以接受,那么这个组件就不是必须的。
- 类S3:类似S3的Key-Value存储。有高可靠,高延时,高吞吐的特点。肯定不是S3,也许也不是Key-value,但是大致是类似的。不可修改和按块存储的特性是相似的。
- 类Mysql:类似Mysql的DB,用来存储块的信息,必须高可靠,容量不比太大,压力也不大。
- Slave:可选组件,Slave会记录磁盘驱动器的每个操作,同步其日志。如果EC2那台机器的缓冲存储损坏,可以使用Slave上面的来恢复最近这段时间没有同步到S3的数据。
存储方式
存储的基本单位是块。每个块由Key和Value组成。块的Key分三部分:diskNo-blockNo-version,Value就是Block的内容。块存储在”S3″中,每个块都是不可以修改的,逻辑上的修改通过增加版本来实现。同时不是每一次修改都必须增加版本的。具体方式下面说。块的大小估计在4M左右,要综合”S3″的性能来决定。
一个“盘”由若干个“块”构成,需要记录每一个”块版本号”,blockNo和块的个数在盘创建的时候就已经决定了。“块版本号”信息持久化存储在Mysql中。
这个架构分两层,S3是底层,负责不断存储“盘”的快照。本地缓冲提供低延迟读写。
实现
下面分几种情况,分别来讨论如何实现。
创建,挂载盘
当创建一个盘的时候,只要在Mysql生成一个新的DiskNo。根据盘的大小和Block的大小,计算出Block数量,在Mysql中初始化元信息,将每个Block的版本标记为0(Block在物理上还不存在)。
然后“磁盘驱动器”挂载他,将Mysql中的源信息,加载到内存中。如果上次非正常关闭,可以通过缓冲存储中的数据,执行恢复操作。因为这个盘不是共享的只有该EC2可以使用。所以挂载后不需要再读Mysql.所以对元数据的操作都发生在内存中,每隔一段时间(比如10分钟),将元数据增量添加到Mysql。
有一点需要注意的是,如果一个块在元数据中有,这个块的数据可能在本地缓冲也可能在S3上。但是如果在Mysql的元数据中有,S3上必须存在有该块。
读块
根据blockId,可以得之其最近版本,并且是在本地缓冲存储还是在”S3″,直接访问即可。读过的块可以放入缓冲。
写块
写块比较复杂。当发起一个写操作的时候,如果本地不存在或者正在同步,本地会先写入一个临时块,写入成功就返回成功。然后会找到”S3″上的块,下载合并修改。如果本地存在,并且该块不在同步中,就直接修改。
每隔一段时间,系统会建立一个check point,将修改的块增加一个版本号,同步到“S3”中去。这里的同步是异步的。全部完成算完成,不存在中间状态。如果操作系统对一个块修改10次,但这些修改操作在两次同步之间,只增加一个版本,避免重复。
缓冲存储损坏
如果缓冲存储损坏,如果没有Slave。由于S3和Mysql中有上一次的快照信息,所以可以恢复到最近的快照状态。不会出现一致性问题。但Check Point之间的时间间隔可能比较长,如果无法忍受的话,可以建立一个Slave用来记录所有的写操作,但缓冲存储损坏的时候,可以通过Slave上的数据恢复到最近的点。
总结
分布式的虚拟磁盘,可以通过两层存储架构,同时满足严格的一致性和分区要求,也有好的随机读写性能。之所以可以采取两层存储,是因为一块“盘”只能一台机器独享,不要求共享,相当于在可分区的特性上打了个折扣,所以这样应该是没问题的。
亚马逊的测试报告也是写的性能远远大于读性能,和这个架构的特点也是相似的。暂时没有发现什么冲突的地方。当然这个架构只是我的猜想,做不得数的。
好像除了亚马逊,没有其他公司对这种磁盘系统感兴趣,也许是没有必要吧。这样虚拟出来的磁盘性能有限,而且系统层次越多越不稳定。但“云计算”招摇撞骗,大行其道,探索一下也好。
Linux大文件传输
复制文件
- 压缩数据
- 发送到另外一台机器上
- 数据解压缩
- 校验正确性
使用ZIP+SCP
gzip -c /home/yankay/data | ssh yankay01 "gunzip -c - > /home/yankay/data"
这条命令是将/home/yankay/data经过GZIP压缩,通过ssh传输到yankay01的机器上。
scp -C -c blowfish /home/yankay/data yankay01:/home/yankay/data
这样运行效果是相同的,不通之处在于我使用了blowfish算法作为Scp的密匙算法,使用这个算法可以比默认的情况快很多。单单对与scp,使用了blowfish 吞吐量是62MB/s,不使用只有46MB/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转SATA磁盘。这个值是:9ms+4ms+0.02ms=13.02ms。
也就是说,如果从磁盘随机访问一个字节,需要13.02ms,比从内存获取的时间24-93ns,至少要多14万倍。相差5个数据级,何其巨大的差距。
顺序读写磁盘会快一些。 假设一个盘片有1000个扇区,每个扇区512字节,7200转。顺序读可以忽略掉寻道的时间。所以吞吐量是 扇区数×扇区大小×转速=1000*512/(60/7200)=58MB/s。这个数据似乎不咋样。如果使用多盘系统。SATA 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个时钟周期,访问二级缓存则需要数十个时钟周期。而访问内存,需要数百个时钟周期。
public void lock(){
while(state.getAndSet(true)){}
}
这里的GetAndSet方法是一个CPU的CAS硬件指令。我们可以看到,每次调用这个质量,CPU要保证缓存一致,丢弃该CPU持有该变量的缓存,重新加载。会消耗至少数十个时钟周期的时间。所以可以进行如下改进。
public void lock(){
while(true){
while(state.get()){};
if(!state.getAndSet(true))
return;
}
这个锁和上面的锁逻辑上是一样的。区别是处理器在自旋的时候,不再执行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的一部分。可见实现一个并发的队列有多难了。
查询利器-bloom-filter详解
布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。本文着重于在实现Bloom Filter的时候会使用到的一些技巧。
布隆过滤器的原理不难理解。相对于一个精简的HashMap的数据结构,存入数据的时候,不存入数据本身,只保存其Hash的值。可以用于判断该数据是否存在。其本质是用Hash对数据进行”有损压缩”的位图索引。详细参见。
错误率
如果用来存放Hash值的槽位足够多,那么碰撞的概率就会比较小。但是所占用的空间就会比较大。所以当分配空间的时候,需要通过你能容忍的错误率和需要存放的Key的数量来指定。如果所需存储的Key数量是n,错误率是p,所需要的槽位是m。有计算槽位的公式$$m=-frac{nln p}{(ln 2)^2}.$$,也有计算概率的公式$$p = left( 1-e^{-(m/nln 2) n/m} right)^{(m/nln 2)}$$。这些公式当然不是我推导出来的,想来也不太难,就不赘述推导过程了。下面这张图可以很好的表示n和m取不同的值的时候,p的值。
根据这张图。我们可以计算出所需要的内存使用量。如果把错误率控制在1%以下的话。
保存key数 | 占用空间 |
---|---|
1万 | 64KB |
10万 | 1MB |
100万 | 16MB |
1000万 | 256MB |
1亿 | <4GB |
可见占用的空间在key的数量在百万级别还是很划算的,但到了上亿的级别就不那么划算了。
Bloom Filter的插入和查询都是常数级别的,所以最大的问题就是占用内存过大。而初次分配内存的时候,如果没有能够确认槽位的个数。如果分配过多会导致内存浪费,太少就会倒是错误率过高。下面提到的两个改进方案可以分别解决这两个问题。
折叠
折叠是指当你初始化一个Bloom Filter的时候,可以分配足够大的槽位,等到Key导入完毕后,可以对使用的槽位进行合并操作。具体方法是将槽位切成两半,一边完全叠加到另一边上。减少内存的使用量。检查key的代码要做稍许改变。例:
通过这个操作,可以使实际使用的内存量减半。多执行几次,能减少更多。
动态扩展
通过折叠操作,可以解决分配过大的问题,但是如果一开始分配过小,就需要扩展槽位才行。如何扩展呢?只要按原尺寸再建立一个Bloom Filter数组。原来的那个保存起来,不再写入。有新的写请求的时候,就将数据写入到新的那个Bloom Filter数组里面去。等到新的也写满了,就再建立一个,以此类推。查询的时候,就需要遍历每一个Bloom Filter数组才行。但因为查询一个Bloom Filter数组的速度很快,查询一组Bloom Filter数组也不会太影响性能。使用这种手段可以是Bloom Filter的大小可以轻易的扩展。但这样做有个的缺陷,就是错误率会随着数组的增加而上升,因为实际的数组长度并没有增加。
通过上面的两个方法,就可以解决BloomFilter的分配内存的问题。但无论哪种方法都有自己局限性,折叠每次只能减半,不是很精确。动态增加的方法会造成错误率增加。最好还是能预先估计到这个BloomFilter的容量。
多核平台下的JAVA优化
现在多核CPU是主流。利用多核技术,可以有效发挥硬件的能力,提升吞吐量,对于Java程序,可以实现并发垃圾收集。但是Java利用多核技术也带来了一些问题,主要是多线程共享内存引起了。目前内存和CPU之间的带宽是一个主要瓶颈,每个核可以独享一部分高速缓存,可以提高性能。JVM是利用操作系统的”轻量级进程”实现线程,所以线程每操作一次共享内存,都无法在高速缓存中命中,是一次开销较大的系统调用。所以区别于普通的优化,针对多核平台,需要进行一些特殊的优化。
代码优化
线程数要大于等于核数
如果使用多线程,只有运行的线程数比核数大,才有可能榨干CPU资源,否则会有若干核闲置。要注意的是,如果线程数目太多,就会占用过多内存,导致性能不升反降。JVM的垃圾回收也是需要线程的,所以这里的线程数包含JVM自己的线程
尽量减少共享数据写操作
每个线程有自己的工作内存,在这个区域内,系统可以毫无顾忌的优化,如果去读共享内存区域,性能也不会下降。但是一旦线程想写共享内存(使用volatile关键字),就会插入很多内存屏障操作(Memory Barrier或者Memory Fence)指令,保证处理器不乱序执行。相比写本地线程自有的变量,性能下降很多。处理方法是尽量减少共享数据,这样也符合”数据耦合”的设计原则。
使用synchronize关键字
在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。
使用乐观策略
传统的同步并发策略是悲观的。表现语义为:多线程操作一个对象的时候,总觉得会有两个线程在同时操作,所以需要锁起来。乐观策略是,假设平时就一个线程访问,当出现了冲突的时候,再重试。这样更高效一些。Java的AtomicInteger就是使用了这个策略。
使用线程本地变量(ThreadLocal)
使用ThreadLocal可以生成线程本地对象的副本,不会和其他线程共享。当该线程终止的时候,其本地变量可以全部回收。
类中Field的排序
可以将一个类会频繁访问到的几个field放在一起,这样他们就有更多的可能性被一起加入高速缓存。同时最好把他们放在头部。基本变量和引用变量不要交错排放。
批量处理数组
现在处理器可以用一条指令来处理一个数组中的多条记录,例如可以同时向一个byte数组中读或者写store记录。所以要尽量使用System.arraycopy()这样的批量接口,而不是自己操作数组。
JVM优化
启用大内存页
现在一个操作系统默认页是4K。如果你的heap是4GB,就意味着要执行1024*1024次分配操作。所以最好能把页调大。这个配额设计操作系统,单改Jvm是不行的。Linux上的配置有点复杂,不详述。
在Java1.6中UseLargePages是默认开启的,LasrgePageSzieInBytes被设置成了4M。笔者看到一些情况下配置成了128MB,在官方的性能测试中更是配置到256MB。
启用压缩指针
Java的64的性能比32慢,原因是因为其指针由32位扩展到64位,虽然寻址空间从4GB扩大到256 TB,但导致性能的下降,并占用了更多的内存。所以对指针进行压缩。压缩后的指针最多支持32GB内存,并且可以获得32位JVM的性能。
在JDK6 update 23默认开启了,之前的版本可以使用-XX:+UseCompressedOops来启动配置。
性能可以看这个评测,性能的提升是很可观。
启用NUMA
numa是一个CPU的特性。SMP架构下,CPU的核是对称,但是他们共享一条系统总线。所以CPU多了,总线就会成为瓶颈。在NUMA架构下,若干CPU组成一个组,组之间有点对点的通讯,相互独立。启动它可以提高性能。
NUMA需要硬件,操作系统,JVM同时启用,才能启用。Linux可以用numactl来配置numa,JVM通过-XX:+UseNUMA来启用。
激进优化特性
在Java1.6中,激进优化(AggressiveOpts)是默认开启的。激进优化是一般有一些下一个版本才会发布的优化选项。但是有可能造成不稳定。前段时间以讹传讹的JDK7的Bug,就是开启这个选项后测到的。
逃逸分析
让一个对象在一个方法内创建后,如果他传递出去,就可以称为方法逃逸;如果传递到别的线程,成为线程逃逸。如果能知道一个对象没有逃逸,就可以把它分配在栈而不是堆上,节约GC的时间。同时可以将这个对象拆散,直接使用其成员变量,有利于利用高速缓存。如果一个对象没有线程逃逸,就可以取消其中一切同步操作,很大的提高性能。
但是逃逸分析是很有难度的,因为花了cpu去对一个对象去分析,要是他不逃逸,就无法优化,之前的分析血本无归。所以不能使用复杂的算法,同时现在的JVM也没有实现栈上分配。所以开启之后,性能也可能下降。
可以使用-XX:+DoEscapeAnalysis来开启逃逸分析。
高吞吐量GC配置
对于高吞吐量,在年轻态可以使用Parallel Scavenge,年老态可以使用Parallel Old垃圾收集器。
使用-XX:+UseParallelOldGC开启
可以将-XX:ParallelGCThreads根据CPU的个数进行调整。可以是CPU数的1/2或者5/8
低延迟GC配置
对于低延迟的应用,在年轻态可以使用ParNew,年老态可以使用CMS垃圾收集器。
可以使用-XX:+UseConcMarkSweepGC和-XX:+UseParNewGC打开。
可以将-XX:ParallelGCThreads根据CPU的个数进行调整。可以是CPU数的1/2或者5/8
可以调整-XX:MaxTenuringThreshold(晋升年老代年龄)调高,默认是15.这样可以减少年老代GC的压力
可以-XX:TargetSurvivorRatio,调整Survivor的占用比率。默认50%.调高可以提供Survivor区的利用率
可以调整-XX:SurvivorRatio,调整Eden和Survivor的比重。默认是8。这个比重越小,Survivor越大,对象可以在年轻态呆更多时间。
等等
参见:《Java优化白皮书》,《深入理解Java虚拟机》
HBase中文官方文档
HBase – Hadoop Database,是一个构建在Apache Hadoop上的列数据。Hbase有很好的扩展性,被认为是BigTable的一个克隆,可以存储数以亿计的行。
在Hbase的官网,我们看到一篇很好的官方文档。我花了很长的时间,把他汉化了。请点击这里:《HBase中文官方文档》
这篇文档非常的详尽,从Hbase的安装,配置,使用,原理,优化,维护都做了非常全面的介绍。还提供了很多扩展阅读,方便我们知道更多Hbase的工作机制。通过翻译这篇文档,我细细读了这篇文档,受益良多。
翻译是一个吃进去再吐出来的过程,我的Hbase的理解也可能会发生一些偏差,所以如果有地方发生错误,请告诉我,大家一起进步。
最小完美哈希函数简介
什么是保序最小完美哈希函数
我曾经花了很多脑筋来找一个很好很完美的哈希算法,但都没有想到,最近看到了,掩不住一阵激动分享下。最小完美哈希函数是什么,要从定义说起,这个名字很长,一步步解释。
- 哈希函数 任意函数h(x)都可以说哈希函数,一般来说,一个良好的哈希函数可以尽量避免重复。x的集合是参数域,h(x)的集合是值域。
- 完美哈希函数 完美哈希函数,就是完全不会冲突的哈希函数,这要求函数的值域至少比参数域要大
- 最小完美哈希函数 最小完美哈希函数,就是指函数的值域和参数域的大小完全相等,一个也不多
- 保序最小完美哈希函数 保序的意思就是指这个哈希之后顺序是不变的,同时还能满足其他两个条件。
算法实现
该算法的实现方法是这样的。先构造两个普通的哈希函数h1(x)和h2(x),还有一个用数组实现的函数g(x)。使得$$h(x)=g(h1(x))+g(h2(x)) mod n$$,其中n是参数的总个数,H(x)就是最终的有序最小完美哈希函数了。
以上是定义,说不清楚,举个例子就明白了。取一个n为12的数据集。
首先构造这三个函数:
函数h1和h2:
x | h1函数 | h2函数 |
jezebel | 5 | 9 |
jezer | 5 | 7 |
… | … | … |
函数g:
x | g函数 |
5 | 0 |
7 | 1 |
9 | 0 |
… | … |
x | h计算步骤 | h值 |
jezebel | g(5)+g(9) | 0 |
jezer | g(5)+g(7) | 1 |
… | … | … |
这里的h就可以最小完美哈希函数算出的值了,很神奇,不是吗?
大致的流程走了一遍,现在最最关键的是h1(x),h2(x)和g(x)是怎么得来的。
h1(x)和h2(x)比较简单,可以使用一个很简便的方法获得:先定义一个权重数组w[i],这个数据是一系列随机的数。$$h1=(t[1]*w[1]+t[2]*w[2]+…+t[i]*w[i]) mod m$$。其中t[i]指得的字符串x的第i个字符,m值得的函数的值域。只要更换一个权重数组,就可以重新构造一个新函数。有很多方法可以构造这两个函数。
g(x)的获得就比较复杂。可以是凑出来的。就如同上面的例子,因为$$g(5)+g(9) mod 12=0$$,$$g(5)+g(7) mod 12=1$$。所以我们可以凑出$$g(5)=0,g(7)=1,g(9)=0$$这样就可以满足上面的两个条件了。需要一个数组来存储函数g的结果,当然凑也不能瞎凑,是有方法的,下面专门讲凑的步骤。
算法函数生成
首先随意设定一个数,比如是7,设为$$g(7)=1$$,因为我们已知$$g(5)+g(7) mod 12=1$$,所以可以推论出$$g(5)=0$$。因为$$g(5)+g(9) mod 12=0$$,所以可以推出$$g(9)=0$$,以此类推就可以了。但要注意的是千万不能重复设定一个数两次,这样就会形成一个环,永远也推不完。所以遇到已经推算过的数的时候,要检测环的存在。这样下去,就可以猜出全部的值了。
现在需要的就是分析这个凑的过程的运行效率问题。这个就要涉及到h1,h2这两个函数的值域大小,如果越大,越容易凑出一个g函数,但是g函数的参数域就会比较大,存储这个g函数的数据就需要占用更多的空间。相反如果值域越小,在凑的时候就非常容易出现环,需要更长的时间才能凑出这个g函数。
怎么办呢?
我们可以使用3个的h函数来降低形成环的可能,就是这样$$h(x)=g(h1(x))+g(h2(x))+g(h3(x)) mod n$$,这样虽然推理g函数的过程会复杂一些,但是很有效,有实验分析表明,当h函数的值域大约参数域的1.23倍的时候,这个g函数的创建尝试次数是常数。
至此,这个算法介绍完了。这个方法是从《Managing Gigabytes》这本书看到的,这里的讲述更浅显一些。
结语
这个哈希函数是一个静态Hash函数,可以非常有效的缩减索引所需要的空间。《Managing Gigabytes》一书中有一个对比,如果直接使用字符串数组,100万个术语需要28MB的空间,而是要这样的哈希函数,可以缩减到12MB。要知道索引小一点,磁盘就能读得快一点,查询就能快一点。所以这个哈希函数对于提高性能是非常给力的。
但是他是静态的,就意味着事前必须知道需要哈希哪些数据。同时生成的算法比较复杂,需要很长的时间来建立索引。没有办法实时添加更新。给他的应用范围提了个极大的限制。窃以为输入法的词库,数据仓库的查询索引,还有一些不需要更新且对性能有要求的场景,这个算法是适用的。
Java调用外部程序技巧
前些天使用Java调用外部程序的时候,发现线程会堵塞在waitfor()方法。
调用方法如下:
Process process = Runtime.getRuntime().exec(cmd);
process.waitfor();
如果直接在Shell中调用这个程序,程序会很快结束,不会僵死。
为什么会堵塞呢,原因是当调用exec(cmd)后,JVM会启动一个子进程,该进程会与JVM进程建立3个管道连接,标准输入,标准输出和标准错误流。假设该程序不断在向标准输出流和标准错误流写数据,而JVM不读取,数据会暂时缓冲在Linux的缓冲区,缓冲区满后该程序将无法继续写数据,会僵死,所以Java程序就会僵死在waitfor(),永远无法结束。
解决办法就是增加两个线程,一个线程负责读标准输出流,另一个负责读标准错误流,这样子数据就不会积压在缓冲区,程序就能够顺利运行。
查看源代码后,还发现一个潜在的问题。但程序执行到exec的时候,JVM会使用管道,占有3个文件句柄,但程序运行结束后,这三个句柄并不会自动关闭,这样最终会导致java.io.IOException: Too many open files。所以就算外部程序的没有输出,也必须关闭句柄:
Process process=null;
try{
process = Runtime.getRuntime().exec(cmd);
process.waitfor();
}cache{
process.getOutputStream().close();
process.getInputStream().close();
process.getErrorStream().close();
}
我们发觉当调用close()方法后,JVM并不会立即回收句柄,具体的回收时间不确定。另外如果不调用close(),句柄也会被回收,也可能发生“Too many open files”的错误。根据这篇文章,不同的垃圾收集器会选择不同的回收策略。所以最好还是要关闭。
总结
1.如果外部程序有大量输出,需要启动额外的线程来读取标准输出和标准错误流
2.必须关闭三个句柄
另外编写了一个工具类来方便使用,本来两行代码确要写成这么长,有点小折腾了。感兴趣的可以在这里下载: