Java程序员也应该知道的系统知识系列之磁盘

除了CPU、内存这两个最重要的也是看起来和应用性能最为相关的硬件外,磁盘也是一个非常重要的部件,尤其是IO压力比较大的存储类的系统,磁盘是一个慢速设备,所以如果使用不当,会导致应用性能受到很大的影响。

首先我们需要知道运行的机器上的磁盘的状况,可以通过执行cat /proc/scsi/scsi来查看,例如:
Attached devices:
Host: scsi0 Channel: 01 Id: 00 Lun: 00
Vendor: SEAGATE Model: ST3300655SS
Type: Direct-Access
上面这个信息表示机器上插了一块SEAGATE的硬盘,型号为ST3300655SS,剩下就可以去google下看看这个硬盘的具体信息了。

一般来说服务器为了提升数据的安全性和读写磁盘的速度,都会做RAID(关于raid是什么具体请大家google),RAID的话又分为软Raid和硬Raid,软Raid的话可以通过cat /proc/mdstat来查看,如果是做硬Raid的,那么在cat /proc/scsi/scsi的时候看到的可能就是类似下面的信息:
Attached devices:
Host: scsi0 Channel: 00 Id: 00 Lun: 00
Vendor: HP Model: P410
Type: RAID
这种表示的是机器带了Raid卡,并且做过了Raid。
Raid卡的具体信息,以及具体做了什么方式的Raid,则需要通过硬件Raid厂商提供的工具来具体查看,例如上面的HP Raid卡,可以用
hpacucli ctrl slot=1 show config detail
上面slot到底是多少,可以通过
hpacucli ctrl all show来拿到。

在了解了这些硬件信息的情况下,磁盘要真正被应用使用,需要做分区,还有个重要的是要做文件系统的选择,linux上目前我们要选择的主要是ext3、ext4,ext4现在已经比较成熟,所以可以优先考虑,具体文件系统的不同请大家自行google吧,分区/文件系统的信息可以通过cat /proc/mounts来查看。

磁盘空间的状况可通过df -h来查看,如果要分析每个目录的空间状况,可以用du -sh .或du -sh *看目录里面所有文件或目录的磁盘占用状况,在磁盘空间这块可能会碰到一个现象,就是df -h看到使用了很多,但du -sh *的时候看到相应的目录下加起来也没用掉那么多,这种有可能是直接在程序里把这个文件删除了,但统计磁盘空间占用的时候还统计了,这种可以重启应用来解决,也可以通过lsof | grep -i deleted来找下是什么文件,以及是什么进程中的操作。

Java程序在写文件的时候,默认情况下并不是直接写到磁盘上,而是先写到os的cache里,这也是为什么很多时候去测试写文件会发现速度快的无法理解,关于os cache怎么去控制请参见之前写的一篇文章,而对于有Raid卡的则还会有所不同,如果raid卡是带cache的,那么即使是os在把cache的内容刷入磁盘时,通常默认情况下也是先放入raid卡的cache,之后才写入磁盘,可以看到这些多重的措施都是为了提升程序在读写文件时的性能,raid卡的cache策略需要通过raid卡的工具来管理,例如
Current Cache Policy: WriteBack, ReadAheadNone, Direct, Write Cache OK if Bad BBU
raid卡为了保障放入cache的数据不会丢失,会采用电池或电容的方式来保障,对于只是记录日志的一些应用场景,就可以设置为即使电池没电了,也写入cache,这种情况下的风险就是如果机器挂了,那么在raid卡cache里的数据也就丢失了。

对于一些用来做存储类型的系统,数据的丢失是不可接受的,像这类的场景,就需要强制写入磁盘,这种在Java里需要显示的去调用FileDescriptor.sync或FileChannel.force,在这种情况下,os的cache以及raid卡的cache都将失去作用。

插播一个容易疑惑的点:在不同的机器上Java去获取目录上的文件时,有可能会发现返回的文件的顺序是不一样的,这个是因为目录里文件的返回顺序在linux上是取决于inode的顺序,而这个不太好控制,所以有可能会出现这样的状况,这个在Java应用中如果有类版本冲突的话很容易导致一个现象是不同机器的行为不一致。

由于磁盘是属于慢速设备,所以磁盘的利用率如果要利用满通常不是难事,磁盘的利用率可以通过iostat -x来查看,需要特别注意下这个里面的cpu iowait以及%util的值,这两个值只要稍微高一些对应用的影响就会非常的明显,要查磁盘利用率到底是读写什么文件造成的,需要用iotop、blktrace这些来查,具体可以看看我之前写的一篇cpu iowait高排查的case的文章。

对于非存储类的系统而言,通常来说要追求的目标都是文件的读写这块不要成为瓶颈,一旦真的这块成为了瓶颈(如果看到应用的线程大部分时候都卡在文件的读写上),那么通常可以这么去优化:
1. raid和raid卡;
如果没有且资金没问题的话可以考虑。
2. cache
读尽可能采用cache来提升,而不是去操作磁盘,这几乎是大多数场景的必杀技,君不见各家互联网公司都是超级的依赖cache吗…
3. 同步写变异步写
对于不是很重要的信息,例如一些可丢失的日志,可以将其写从同步写入的方式改为异步写,这个现在的log4j等都是支持的,另外要注意控制日志级别以及需要输出的日志是否有必要;
4. 随机写转顺序写
去了解过一些存储系统的同学可能都会发现一个现象,这类系统很多都是先写日志,然后真正的数据则是放到内存,等内存积累到一定的量才真正写入磁盘,这个就是典型的随机写转顺序写的一种办法,大家都知道的是磁盘之所以慢的一个主要原因是寻道时间,所以随机写转为顺序写后写的速度其实是提升了不少的。

对于存储类的系统而言,则很有可能磁盘会成为整个系统的瓶颈点,也就是这个时候cpu/内存可能就不是主角了,因为存储类的系统没法靠os和raid卡cache来提升性能了,这种如果到了磁盘的瓶颈的话,基本就只能靠纯粹的硬件升级了,例如ssd或更高端的fusionio。

=============================
题图来源于:http://news.mydrivers.com/img/20090811/04182623.jpg
欢迎关注微信公众号:hellojavacases

关于此微信号:
分享Java问题排查的Case、Java业界的动态和新技术、Java的一些小知识点Test,以及和大家一起讨论一些Java问题或场景,这里只有Java细节的分享,没有大道理、大架构和大框架。

公众号上发布的消息都存放在http://hellojava.info上。

cpu iowait高排查的case

在之前的常见的Java问题排查方法一文中,没有写cpu iowait时的排查方法,主要的原因是自己之前也没碰到过什么cpu iowait高的case,很不幸的是在最近一周连续碰到了两起cpu iowait的case,通过这两起case让自己学习到了很多系统层面的知识,也许这些知识对于熟悉系统的人来说没什么,不过对于写Java的同学我觉得还是值得分享下(由于Java基本不用于存储类型的场景,所以通常来说碰到iowait高的机会会比其他几类问题更低很多)。

当出现iowait高时,最重要的是要先找出到底哪个进程在消耗io,以最快的速度解决问题,但linux默认的一些工具例如像top、iostat等都只能看到io的消耗状况,但对应不到是哪个进程在消耗,比较好用的用来定位的工具是iotop,不过有些环境要装上可能不太容易,装上了后直接执行iotop,就可以看到到底是哪个进程消耗了比较多的io,对应解决问题而言,通常在找到进程后杀掉基本就算解决了(还有一种方法是通过打开syslog以及blk_dump来看一段时间内消耗io的进程,但在我的两个case里试过效果不太理想)。

但通常而言,上面的方法只能算勉强解决了问题,但还是没有定位到程序里哪个地方有问题,甚至有可能重启仍然iowait很高,于是需要借助其他工具来进一步排查,所幸系统层面是有这样的工具,主要可通过blktrace/debugfs来定位到到底是读或写哪个(或哪些)文件造成了iowait高(这个方法主要学习自阿里集团内核组的伯瑜的一篇blog)。

在装上了blktrace后,先mount -t debugfs none /sys/kernel/debug下,然后可通过iostat查看到底是哪个设备在消耗io,例如假设看到是sda在消耗,那么即可执行blktrace /dev/sda,在执行时将会自动在执行的目录下生成一些sda.blktrace.*的文件,当觉得采集的差不多后,即可ctrl+c停掉。

之后执行blkparse sda.blktrace.* > result.log,再生成了result.log后执行grep ‘A’ result.log | head -n 5看看在采集的这段过程中,消耗io比较多的地方在哪,例如在我碰到的case中执行后看到的为:
8,0 11 1 0.000071219 248366 A R 218990140 + 16 <- (8,9) 148994872 8,0 11 1 0.000071219 248366 A R 218990140 + 16 <- (8,9) 148994872 8,0 11 1 0.000071219 248366 A R 218990140 + 16 <- (8,9) 148994872 8,0 11 1 0.000071219 248366 A R 218990140 + 16 <- (8,9) 148994872 8,0 11 1 0.000071219 248366 A R 218990140 + 16 <- (8,9) 148994872 这里A后面的R到底的意思是读(如果是写则为WS),和之前iostat看到的是一样的,io主要是大量的读造成的。 通过上面的信息8,0和(8,9)可以看到主要的消耗是在sda9(这个通过iostat也可以看到),(8,9)后的148994872代表读的扇区号。 再通过debugfs -R 'stats' /dev/sda9 | grep 'Block size'可以找到sda9的block size,在我碰到的case中block size是4096,也是通常ext2/ext3文件系统默认的。 每个扇区的大小为512,148994872/(4096/512) = 18624359即可找到文件系统上对应的block号,之后通过debugfs -R 'icheck 18624359' /dev/sda9可找到对应的inode,在我碰到的case中,执行后的结果为: Block Inode number 18624359 18284971 而debugfs还提供了通过inode number找到具体的文件的功能,于是就好办了,继续执行debugfs -R 'ncheck 18284971',执行后看到类似如下的信息: Inode Pathname 18284971 [相应的文件名] 在找到了文件名后就好办了,可结合之前的iotop或直接lsof找出对应的进程id,然后就可以看看从代码上层面怎么避免对此文件的大量读。 除了上面的这种case外,还有些情况的iowait其实是比较简单的,例如读写了巨大的文件(通常在大量出现异常时可能会出现)... 在解决上周碰到的两个cpu iowait高的case中,其中一个是如上面的业务代码造成,但另一个则是和raid卡配置相关,因为从iostat来看,当时写的量也不是很大,但iowait却比较高,请系统的人帮忙看了后,告诉我是因为raid卡写策略配置的问题,我之前对raid卡的这些配置完全不懂。 通过服务器上会带有raid卡,而现在的raid卡基本是带有cache的,为了保障cache里的数据的安全性,通常raid卡会带有电池或电容,相对而言电容的故障率比较低,raid卡会提供写策略的配置,写策略通常是Write Back和Write Through两种,Wirte Back是指写到cache后即认为写成功,Write Through是指写到磁盘上才算成功,通常Raid卡的写策略会分为正常时,以及电池或电容出问题时两种来配置,而在碰到的case中是因为配置了当电池/电容出问题时采用Write Through,当时机器的Raid卡的电池出故障了,所以导致策略切换为了Write Through,能够支撑的iops自然是大幅度下降了,而在我们的场景中,本地数据丢掉是无所谓的,所以Raid卡的策略可以配置为即使电池出故障了也仍然采用Write Back。 通常各种Raid卡都会提供工具来配置写策略,例如HP卡的hpacucli,可通过cat /proc/scsi/scsi查看硬盘和Raid卡的信息(可以先用cat /proc/mdstat来查看raid信息),有助于确认raid卡的cache/cache容量/电池以及硬盘本身能支撑的iops等。 因为建议在碰到iowait高的场景时,可以先看看raid卡的写策略,如果没问题的话再通过iotop、blktrace、debugfs、lsof等来定位到具体的根源。 可见即使是对于Java的同学,在排查问题时对于各种系统工具、硬件层面的一些知识还是要有些了解,不一定要很深,但至少要知道,对于工具要会用。 ------------微信互动------------ 1. 在设置LANG即可改变Java的默认编码的原因分析的那篇文章发出后,有同事给我反馈其中的这段话:“顺带说一句,其实即使在启动时带上-Dfile.encoding也不会改变Charset.defaultCharset(这个默认编码还是以LC_CTYPE为准),加上这个参数只会改变系统属性中的file.encoding值而已。” 和他测试时的表现不一致,后来我确认才发现这段话是我过于武断的判断了,在这里要纠正为“,-Dfile.encoding是可以生效的,只是在指定的file.encoding没有对应的Charset实现时,会默认为UTF-8,这里要稍微注意的一点是,file.encoding的指定方法和LANG有些不同,LANG通常的语言大集.小集,例如zh_CN.gbk,而file.encoding就直接是语言的小集,例如gbk。“。 问: String.intern那篇文章里关于s1 == s2的那个case,为什么要保证s1.intern是在String s2赋值的那行之前,因为如果String s2赋值是在s1.intern之后,那么即使是在jdk 7中s1 == s2也会是false。 答: 这里的原因是在JDK 7中,当执行String.intern时,如String Pool中没有相同内容的,则会将此内容作为key,而value即为此String对象,在对String进行赋值时,会先在String pool中找是否会相同内容的,有的话则直接就指向改对象,没有就新创建一个对象,因此在那篇文章的case里,如果在String s2赋值之前执行s1.intern,那么String pool里就已经有了”GoodMorning“对应的String对象实例了,也就是s1,因此之后在执行String s2 = "GoodMorning"时,就已经是直接将s1赋给了s2,但调转这行代码后就不一样了。 问:你说之前那个cpu us诡异的case的根本原因是BeanUtils里频繁调用Class.getMethod造成的,那不是很容易出现这个现象? 答:其实出现这个现象不会太容易,原因是如果传入的methodName基本是固定的,那么其实执行String.intern真正放进String pool的并不会多,而在我之前碰到的case里为什么有问题,原因是类似struts之类的结构,通常是会把一个form里的所有域直接填充到对应的对象里去,而如果form里有一些隐藏域,是和每次请求有关的话,那么BeanUtils在执行class.getMethod时传入的methodName可能就是经常变化的,如果这个页面访问量又比较大的话,那就很容易导致String pool性能疯狂下降,建议最好是在用BeanUtils填充对象时,先检查下是否有必要。 ============================= 欢迎关注微信公众号:hellojavacases 关于此微信号: 用于分享Java的一些小知识点,Java问题排查的Cases,Java业界的动态,以及和大家一起互动讨论一些Java的场景和问题。 公众号上发布的消息都存放在http://hellojava.info上。