Let's Go!

发布新日志

  • Windows性能计数器(转载)

    2011-09-09 11:45:09

     

    Windows性能计数器

    一、性能计数器概述
      性能监视,是Windows NT提供的一种系统功能。Windows NT一直以来总是集成了性能监视工具,它提供有关操作系统当前运行状况的信息,针对各种对象提供了数百个性能计数器。性能对象,就是被监视的对象,典型例子有Processor、Process、Memory、TCP/UDP/IP/ICMP、PhysicalDisk等。计数器通常提供操作系统、应用程序、服务、驱动程序等的性能相关信息,以此来分析系统瓶颈和对系统及应用程序性能进行诊断和调优。性能计数器机制让应用程序和操作系统组件可以向性能监视应用程序,比如性能监视器(Performance Monitor),报告一些与性能有关的统计信息。PerfMon.exe中可以查看性能对象、性能计数器和对象实例,可通过添加计数器来查看相关描述信息。

      实际上,可以通过编写程序来访问所有的Windows性能计数器。Windows中,注册表是访问性能计数器的一种机制。性能信息并不实际存在于注册表中,在注册表编辑器RegEdit.exe中是无法查看的,但可以通过注册表函数来访问,利用注册表键来获得从性能数据提供者那里提供的数据。打开名为HKEY_PERFORMANCE_DATA的特殊键,利用RegQueryValueEx函数查询键下面的值,就可以直接访问注册表性能计数器信息。当然,也可以利用性能数据帮助器(PDH, Performance Data Helper) API (Pdh.dll)来访问性能计数器信息。

    二、相关术语
    性能对象(Performance Object):被监视的性能对象,如Processor、Process、Memory、PhysicalDisk等,相当于类(Class)。
    性能计数器(Performance Counter):描述性能对象性能信息的方式,相当于类属性。
    对象实例(Object Instance):相同性能对象可能有多个,把它们表示为该对象类型的不同实例,相当于类实例。

    三、HKEY_PERFORMANCE_DATA数据组织
      性能数据的头部是一个PERF_DATA_BLOCK结构(如图1所示),它描述系统和性能数据总体信息,可从Global键值处查询得到该结构数据。PERF_DATA_BLOCK之后,定义了系统中的全部性能对象类型(PERF_OBJECT_TYPE),其中每个对象类型头部中描述了下一个性能对象类型的偏移量Offset。
    图1  图1图2图2


      性能对象有两种:一种是单实例对象,另一种是多实例对象。图2和图3分别描述了这两种性能对象的数据组织方式。每个对象数据块包含了一个PERF_OBJECT_TYPE结构,描述对象的性能数据。紧随其后是PERF_COUNTER_DEFINITION结构列表,描述了性能对象的全部计数器定义。对于单实例对象,计数器定义列表后是一个PERF_COUNTER_BLOCK结构,计数器数据紧随其后。每个PERF_COUNTER_DEFINITION结构中定义了计数器数据相对于PERF_COUNTER_BLOCK的偏移量,因此可以非常方便地获得全部计数器的值。对支持多实例性能对象来说,PERF_COUNTER_DEFINITION结构列表之后是一组实例信息数据块,每个表示代表一个对象实例。每个实例信息数据块由一个PERF_INSTANCE_DEFINITION结构体、实例名和一个PERF_COUNTER_BLOCK结构体组成。后面是计数器值数据,与单实例对象相同。

    图3图3

    四、一个简单性能计数器查看器的实现
      基于以上的知识和一些参考文献,本人在Windows Xp Sp3 + VS2003环境下实现了一个Windows性能计数器查看器。界面如图4所示,核心部分代码如下:

    图4

    1、头文件winperf.h

    1. #ifndef _WINPERF  
    2. #define _WINPERF  
    3.   
    4. #include "stdafx.h"  
    5. #include <windows.h>  
    6. #include <tchar.h>          
    7. #include <io.h>  
    8. #include <conio.h>  
    9. #include <string.h>  
    10. #include <stdio.h>  
    11. #include <strsafe.h>  
    12. #include <winperf.h>  
    13. #include <String>  
    14. #include <vector>  
    15. #include <map>  
    16. #include <iostream>  
    17. using namespace std;  
    18.   
    19. #define INITIAL_SIZE        51200  
    20. #define EXTEND_SIZE         25600  
    21. #define PERF_SUBKEY_GLOBAL  _T("Global")  
    22. #define REGSUBKEY_COUNTERS  _T("Counters")  
    23.   
    24. typedef map <DWORDDWORD>::const_iterator CIT;  
    25.   
    26. BOOL ConnectComputerPerformanceRegistry(LPCSTR lpMachineName, HKEY &hKey);  
    27. BOOL GetNameStrings(HKEY hKey, map <DWORDLPSTR> &mPerfObjectIndex);  
    28. BOOL EnumPerfObjects(HKEY hKey, vector <PERF_OBJECT_TYPE> &vPerfObjects);  
    29. BOOL LoadObjectData(HKEY hKey, DWORD dwObjIndex, map <DWORDLPSTR> mPerfCountersIndex, /  
    30.     CListBox *pCListBoxCounters, CListCtrl *pCListCtrlInstances);  
    31.   
    32. #endif  


    2、连接(远程)计算机性能计数器注册表

    1. BOOL ConnectComputerPerformanceRegistry(LPCSTR lpMachineName, HKEY &hKey)  
    2. {  
    3.     DWORD retCode;  
    4.   
    5.     retCode = RegConnectRegistry(lpMachineName, HKEY_PERFORMANCE_DATA, &hKey);  
    6.     return (retCode == ERROR_SUCCESS) ? TRUE : FALSE;  
    7. }  

    3、枚举性能对象

    1. static inline PPERF_OBJECT_TYPE FirstObject(PPERF_DATA_BLOCK PerfData)     
    2. 查看(2335) 评论(0) 收藏 分享 管理

    3. 开发人员行走Unix的随身四艺(转载)

      2011-09-09 11:39:32

      开发人员行走Unix的随身四艺

      作者:江南白衣,原文出处: http://blog.csdn.net/calvinxiu/archive/2007/01/27/1495778.aspx

           Unix系统永远只会越来越多,开发人员就没必要特意学习它们的安装、配置和管理了,就全部交给集成人员吧。
          但开发人员行走于Unix之间,依然有四样东西要熟练。

          一、VI

          虽然Unix上的文本编辑器已经越来越好用,但不在Console前面,网速也不够连XWindows的时候,还是要依赖VI。
          回想VI的时代背景,发现VI对开发人员已经周到得离谱了,热键多到你双手不离键盘就能完成大半编辑工作。
          建议自己制作一张自己认为有用,但又经常忘记的命令的sheet--参见附录A,拿出考试的力气把它背熟。

          二、文本处理

             开发人员在Unix下干得最多的除了Make和除Bug外,大概就是处理日志文件、业务文件的查错和统计了。
             只会more和grep是不够的,开发老手会把awk,sed,grep,sort,uniq,wc,head,tail这些文本处理命令,通过管道玩具式的拆卸拼装,最后完成一件原本以为非编写大段代码不可的工作。周到的参数设定,让人再一次感叹那个简单的年代,这样复杂到极致的设计.......怪不得《Unix 编程艺术》的作者有那么骄傲的自觉。
          
           比如车东的每月访问TOP10 统计脚本:

      awk -F '/ t' '{ print   $ 4 }' 2004_2 . txt| grep chedong . com / tech / | uniq -c| sort  -rn|head - 10  

            以上命令的具体用法--参见附录B:文本处理命令小结,大概说明如下:

      • awk -F '/t' 将2004_2.txt访问纪录文件,用TAB分割,打印第4列
      • grep chedong.com/tech 只列出chedong.com/tech笔记目录下的文档
      • uniq -c 汇总计数
      • sort -rn 按数值排序
      • head -10 TOP 10

               补充:这些命令几乎都支持正则表达式,学正则比较好的书是OReilly的《Mastering.Regular.Expressions.3rd 2006》    

          三、Bash Shell 编程

          上面的纯粹命令管道拼装完成不了所有的事情,有时需要用Shell编程来配合调度。    

          编程是开发人员的天赋本能,不论什么语言,看看参考手册应该就能上手。

          见《Bash新手指南中文版》 ,一份快速的Bash Shell编程指南。
           
           更进一步,可以学习perl。

          四、Make or AutoMake

          用过Java的Ant后,想起Make就觉得很烦,很厌倦。总归还是会的,见《GNU Make 3.8.0 中文手册》    

           不过即使make已经精通到变态,每个人写出来的MakeFile还是千奇百怪,再看看开源项目们个个都是automake+autoconf了,我们自己也长进一点吧。手工编写MakeFile.am,让auotomake变成MakeFile.in,再让用户./configure 生成最终的MakeFile。
          
          autotools既能跨越平台,又是标准的写法,最重要的是,编写MakeFile.am的工作量比MakeFile少多了,只要简单的定义目标文件,先要处理的子目录,需要的源文件,头文件与库文件就可以了。
          
          入门文章
          使用AutoMake轻松生成Makefile 
          IBM DW:例解 autoconf 和 automake 生成 Makefile 文件

          上面两文只作入门了解,实际的操作步骤--参见附录C:我的automake1.9步骤小结。

          完整的免费电子书:
       《GNU Autoconf, Automake and Libtool》

          另外,ACE里还贡献了一个更厉害的MPC(Makefile, Project, and Workspace Creator ),  自动的生成MakeFile.am或者VC的项目文件。

          附录A:我的VI易忘命令手册

          上下左右: 
          ctrl+u/d 上下半屏,ctrl+f/b,上下一屏
          H/G屏幕头/文章末 ,0/$ 行首行末
          
          增删改:
          yy/dd 复制/删除 一行,p/P:将yy/dd的内容paste出来
          I/A 在行首/末添加, o/O 开新行,d0/d$ 删除到行首,行末
          u:undo

          查:
          ? 向前查找, n/N 重复上一次查找

      附录B: 文本处理命令小结

         awk:处理结构化的文本(每行以固定符号分成若干列),提取打印某些字段,如:
          ls -l|awk '{print $1}'  --将ls-l结果的第一列打印出来
          awk -F":" '{print $1"  "$6}' /etc/passwd ,将以:分割的/etc/passwd文件的第1,6列打印出来,中间以空格分开
          详见IBM DW中国的AWK实例(共3篇) 或 Bash新手指南中文版第6章

          grep:过滤,大家用得最多的命令,支持正则表达式。参数有:
          -i忽略大小写,-n显示line number,-c 统计在每个文件的出现次数,-l只显示符合的文件的名字。

          sed:流编辑器,主要用于替换,如:
          sed -e '1,10s/foo/bar/g' myfile2.txt 将1到10行的文本中的foo 替换成bar,s代表替换,g代表全局替换 
          支持正则的替换字符串,可以只替换某个范围内的内容。
          用法不算简单,详见IBM DW中国的Sed实例(共3篇)或 Bash新手指南中文版第5章
          
          sort:排序,参数有:
          -r逆序, -n 数字比较 , -M 日历比较 Feb,Dec, -f 忽略大小写
          同样支持结构化文件,如
          sort -t : -k 1,1 /etc/passwd,以: 分割,只按第1列排序
          sort -t : -k 1,1 -k2.2,3.4 /etc/passwd ,以:分割,先按第1列排序,再按第2列的第二个字符到第3列的第4个字符排序。

          uniq:去除重复行。
          除了正常用法外,还有-c统计重复次数,和-u (唯一)和 -d (重复)两个参数,只显示唯一的和重复的行。

          wc: 统计。
          -l 行,-m 字符,-w 单词

      附录C: 我的automake1.9步骤小结

      1. 先编写MakeFile.am
      2. 运行autoscan,生成configure.scan
      3. 修改configure.scan,同时把文件改名为configure.in :
        去除AC_CONFIG_HEADER([config.h])那行
        加入AM_INIT_AUTOMAKE([1.9 foreign])  (其中1.9是automake的版本号)
        加入AC_PROG_LIBTOOL  (如果用libtool的话)
        检查AC_CONFIG_FILES,如果之前没有先写齐所有MakeFile.am,autoscan就不会帮你加入,需要自己手工补充。
      4. libtoolize (如果用libtool的话)
      5. aclocal
      6. autoconf
      7. automake --add-missing
      8. ./configure
      9. make

        大家需要编写的文件从Makefile转为了Makefile.am 和configure.in, ACE的Examples是很好的参考。

       

       

       

      Client免输密码登录SSH Server的一种方法


       

      我们经常要在自己工作的UNIX/LINUX系统间SSH登录,SCP传输数据,总是要输入密码,很烦是吧?
      SSH支持多种登录验证方式,默认使用的是键盘交互方式(Keyboard-Interactive),也就是手工输入密码的那种。而在实际应用中,有时需要免输密码直接登录SSH SERVER,如MPI作业需要在WNs间使用SCP传输数据(非交互方式)。这就需要使用公钥(Public Key)验证方式,并且设置passphrase为空,以达到免输密码登录的目的。

      假设需要从A主机(CLIENT) 免输密码登录B主机(SSH SERVER),具体配置如下:
      1、A主机

          * cd ~/.ssh
          * 产生公钥文件和私钥文件,类型包括DSA, RSA, RSA1 (如果确定,使用一种就可以了)
            ssh-keygen -t dsa
            ssh-keygen -t rsa
            ssh-keygen -t rsa1
          * cat *.pub > authorized_keys_client
          * scp authorized_keys_client B:~/.ssh/

      2、B主机

          * 将Client的公钥放入Server的信任列表
            cd ~/.ssh
            cat authorized_keys_client >> authorized_keys
          * 更新权限(重要)
            chmod 0600 *

      这样,从主机A ssh 登录主机B就不需要输入密码了。多个用户重复上面的步骤就可以了。 

      (刘爱贵 / Aiguille.LIU, 2008-03-28 )


      转自:


      http://blog.csdn.net/liuben/article/details/2225936

       

       

    4. _Linux文件系统性能优化(转载)

      2011-09-09 11:32:46

      文件系统性能测试

      hdparm -t T 简单测试一下,这个是可以的,我也经常用。

      1、衡量指标

       IOPS:随机小I/O读写能力

       带宽:  顺序大I/O连续读写能力

      2、性能关键点

       顺序/随机读写(sequential/random)

       目录操作:文件创建/删除/查找/更新

       大量小文件读写(Lots of small files)

          大文件读写(large file)

      3、其他指标

       CPU占用率

       IOWAIT

      4、测量基准Benchmarks

       dd

       tar

       IoZone

       Bonnie++

       IoMeter

       FFSB

       Postmark

       RandomIO

       FileBench

      5、一些非标准benchmarks

       文件系统make/mount/umount/remount

       copy/recopy/remove大文件(>=4GB)

       extract/tar linux内核源码树

       copy/recopy/remove linux内核源码树

       list/find linux内核源码树

       编译linux内核 

       create/copy/remove 海量文件目录(>=1,000,000)

      转自:http://blog.csdn.net/liuben/article/details/5777022

       

      Linux文件系统性能优化

      由于各种的I/O负载情形各异,Linux系统中文件系统的缺省配置一般来说都比较中庸,强调普遍适用性。然而在特定应用下,这种配置往往在I/O性能方面不能达到最优。因此,如果应用对I/O性能要求较高,除了采用性能更高的硬件(如磁盘、HBA卡、CPU、MEM等)外,我们还可以通过对文件系统进行性能调优,来获得更高的I/O性能提升。总的来说,主要可以从三个方面来做工作:

      1、Disk相关参数调优

      2、文件系统本身参数调优

      3、文件系统挂载(mount)参数调优

       

      当然,负载情况不同,需要结合理论分析与充分的测试和实验来得到合理的参数。下面以SAS(Serial attached SCSI)磁盘上的EXT3文件系统为例,给出Linux文件系统性能优化的一般方法。请根据自身情况作适合调整,不要生搬硬套。

       

      1、Disk相关参数

      1.1 Cache mode:启用WCE=1(Write Cache Enable), RCD=0(Read Cache Disable)模式

      sdparm -s WCE=1, RCD=0 -S /dev/sdb

       

      1.2 Linux I/O scheduler算法

      经过实验,在重负载情形下,deadline调度方式对squidI/O负载具有更好的性能表现。其他三种为noop(fifo), as, cfq,noop多用于SAN/RAID存储系统,as多用于大文件顺序读写,

       

      cfq适于桌面应用。

      echo deadline > /sys/block/sdb/queue/scheduler

       

      1.3 deadline调度参数

      对于redhat linux建议 read_expire = 1/2 write_expire,对于大量频繁的小文件I/O负载,应当这两者取较小值。更合适的值,需要通过实验测试得到。

      echo 500 > /sys/block/sdb/queue/iosched/read_expire

      echo 1000 > /sys/block/sdb/queue/iosched/write_expire

       

      1.4 readahead 预读扇区数

      预读是提高磁盘性能的有效手段,目前对顺序读比较有效,主要利用数据的局部性特点。比如在我的系统上,通过实验设置通读256块扇区性能较优。

      blockdev --setra 256 /dev/sdb

       

      2、EXT3文件系统参数

      2.1 block size = 4096 (4KB)

      mkfs.ext3 -b指定,大的数据块会浪费一定空间,但会提升I/O性能。EXT3文件系统块大小可以为1KB、2KB、4KB。

       

      2.2 inode size

      这是一个逻辑概念,即一个inode所对应的文件相应占用多大物理空间。mkfs.ext3 -i指定,可用文件系统文件大小平均值来设定,可减少磁盘寻址和元数据操作时间。

       

      2.3 reserved block

      mkfs.ext3 -m指定,缺省为5%,可调小该值以增大部分可用存储空间。

       

      2.4 disable journal

      对数据安全要求不高的应用(如web cache),可以关闭日志功能,以提高I/O性能。

      tune2fs -O^has_journal /dev/sdb

       

      3、mount参数

      3.1 noatime, nodirtime

      访问文件目录,不修改访问文件元信息,对于频繁的小文件负载,可以有效提高性能。

       

      3.2 async

      异步I/O方式,提高写性能。

       

      3.3 data=writeback (if journal)

      日志模式下,启用写回机制,可提高写性能。数据写入顺序不再保护,可能会造成文件系统数据不一致性,重要数据应用慎用。

       

      3.4 barrier=0 (if journal)

      barrier=1,可以保证文件系统在日志数据写入磁盘之后才写commit记录,但影响性能。重要数据应用慎用,有可能造成数据损坏。

       

      4、小结

      以/dev/sdb为例,优化操作方法如下,参数请自行调整。

      sdparm -s WCE=1, RCD=0 -S /dev/sdb

      echo deadline > /sys/block/sdb/queue/scheduler

      echo 500 > /sys/block/sdb/queue/iosched/read_expire

      echo 1000 > /sys/block/sdb/queue/iosched/write_expire

      blockdev --setra 256 /dev/sdb

       

      mkfs.ext3 -b 4096 -i 16384 -m 2 /dev/sdb1

      tune2fs -O^has_journal /dev/sdb1

       

      mount /dev/sdb1 /cache1 -o defaults,noatime,nodirtime,async,data=writeback,barrier=0 (if with journal)

      mount /dev/sdb1 /cache1 -o defaults,noatime,nodirtime,async (if without journal)

      转自:http://blog.csdn.net/liuben/article/details/5482167

    5. PHP的GC垃圾收集机制

      2011-09-02 17:58:32

      今天听了php的gc :

      1. 256字节以下不会立刻回收变量空间
      2. unset:断开符号的引用连接,并且计数-1
      3. NULL:强制将计数清零
      4. xdebug ---->进行调试,打印内存使用情况
      php 5.3:
      1)不会立刻回收,会在根缓冲区满后执行GC
      2)能将内存泄露控制在一个阈值以下(与根缓冲区大小有关)。

       

      另附三篇文章:

      从 PHP 代码分析 PHP 的 GC(垃圾回收) 机制
      http://bbs.chinaunix.net/thread-1610024-1-1.html

      浅谈PHP5中垃圾回收算法(Garbage Collection)的演化
      http://www.cnblogs.com/leoo2sk/archive/2011/02/27/php-gc.html

      PHP的GC垃圾收集机制
      http://www.cnblogs.com/dkblog/archive/2010/06/04/1980694.html

       

      详细内容:

      从 PHP 代码分析 PHP 的 GC(垃圾回收) 机制
      众所周知, PHP 引擎本身是用 C 写的,提到 C 不能不提的就是 GC(垃圾回收).通过 PHP 手册我们了解到, PHP 引擎会自动进行 GC 动作.那么我们不禁要问,到底它是怎么回收的, & 引用操作是不是指针, unset()了一个变量时它是不是真的被回收了呢?这些看似手册有提及的问题,如果仔细分析会发现,远没有那么简单泛泛.也许有人会跳出来说:看 PHP源码不就知道了.是的,等你通读了 PHP 源码后这个问题肯定不在话下了,然本篇要仅从 PHP本身来分析这些看似平常却被忽视的小细节,当然了,其中难免水平所限,有所疏漏,热烈欢迎广大 phper 来共同讨论.

      首先咱先看到例子,最简单不过的执行流程了:
      Example 1: gc.php
      <?php
      error_reporting
      (E_ALL);
      $a = 'I am test.';
      $b = & $a;

      echo
      $b ."\n";

      ?>

      不用说 % php -f gc.php 输出结果非常明了:
      hy0kl% php -f gc.php
      I am test.


      好,下一个:
      Example 2:
      <?php
      error_reporting
      (E_ALL);
      $a = 'I am test.';
      $b = & $a;

      $b = 'I will change?';                                                           

      echo
      $a ."\n";
      echo
      $b ."\n";

      ?>
      执行结果依然很明显:
      hy0kl% php -f gc.php
      I will change?
      I will change?


      君请看:
      Example 3:
      <?php
      error_reporting
      (E_ALL);
      $a = 'I am test.';
      $b = & $a;  

      unset(
      $a);

      echo
      $a ."\n";
      echo
      $b ."\n";
      ?>
      是不是得想一下下呢?
      hy0kl% php -f gc.php
      Notice: Undefined variable: a in /usr/local/www/apache22/data/test/gc.php on line 8
      I am test.

      有点犯迷糊了吗?

      君再看:
      Example 4:
      <?php
      error_reporting
      (E_ALL);
      $a = 'I am test.';
      $b = & $a;

      unset(
      $b);                                                                       

      echo
      $a ."\n";
      echo
      $b ."\n";

      ?>
      其实如果 Example 3 理解了,这个与之异曲同工.
      hy0kl% php -f gc.php
      I am test.
      Notice: Undefined variable: b in /usr/local/www/apache22/data/test/gc.php on line 9


      君且看:
      Example 5:
      <?php
      error_reporting
      (E_ALL);
      $a = 'I am test.';
      $b = & $a;

      $a = null;

      echo
      '$a = '. $a ."\n";
      echo
      '$b = '. $b ."\n";

      ?>
      猛的第一感觉是什么样的?
      hy0kl% php -f gc.php
      $a =
      $b =

      没错,这就是输出结果,对 PHP GC 已有深入理解的 phper 不会觉得有什么奇怪,说实话,当我第一次运行这段代码时很意外,却让我对 PHP GC 有更深刻的理解了.那么下面与之同工的例子自然好理解了.

      Example 6:
      <?php                                                                           
      error_reporting
      (E_ALL);
      $a = 'I am test.';
      $b = & $a;

      $b = null;

      echo
      '$a = '. $a ."\n";
      echo
      '$b = '. $b ."\n";

      ?>

      OK,如果上面的例子的结果对看官来说无任何细节可言,那您可关闭本窗口了,欢迎有空再来!

      下面我们来详细分析 GC 与引用.
      1. 所有例子中,创建了一个变量,这个过程通俗一点讲:是在内存中开辟了一块空间,在里面存放了一个字符串 I am test. . PHP 内部有个符号表,用来记录各块内存引用计数,那么此时会将这块内存的引用计数 加 1,并且用一个名为 $a 的标签(变量)指向这块内存,方便依标签名来操作内存.

      2. 对变量 $a 进行 & 操作,我的理解是找到 $a 所指向的内存,并为 $b 建立同样的一引用指向,并将存放字符串 I am test. 的内存块在符号表中引用计数 加 1.换言之,我们的脚本执行到这一行的时候,存放字符串 I am test. 的那块内存被引用了两次.这里要强调的是, & 操作是建立了引用指向,而不是指针, PHP 没有指针的概念!同时有人提出说类似于 UNIX 的文件软链接.可以在一定程度上这么理解: 存放字符 I am test. 的那块内存是我们的一个真实的文件,而变量 $a$b 是针对真实文件建立的软链接,但它们指向的是同一个真实文件. So, 我们看到,在 Example 2  中给 $b 赋值的同时, $a 的值也跟着变化了.与通过某一软链操作了文件类似.

      3. 在 Example 3 与 4 中,进行了 unset() 操作.根据实际的执行结果,可以看出: unset() 只是断开这个变量对它原先指向的内存的引用,使变量本身成为没有定义过空引用,所在调用时发出了 Notice ,并且使那块内存在符号表中引用计数 减 1,并没有影响到其他指向这块内存的变量.换言之,只有当一块内存在符号表中的引用计数为 0 时, PHP 引擎才会将这块内存回收.
      PHP 手册
      4.0.0                 unset() became an expression. (In PHP 3,         unset() would always return 1).
      这意味着什么?
      看看下面的代码与其结果:
      <?php
      error_reporting
      (E_ALL);
      $a = 'I am test.';
      $b = & $a;

      unset(
      $a);
      unset(
      $a);
      unset(
      $a);

      echo
      '$a = '. $a ."\n";
      echo
      '$b = '. $b ."\n";

      ?>
      hy0kl% php -f gc.php

      Notice: Undefined variable: a in /usr/local/www/apache22/data/test/gc.php on line 10
      $a =
      $b = I am test.
      第一次 unset() 的操作已经断开了指向,所以后继的操作不会对符号表的任何内存的引用记数造成影响了.

      4. 通过 Example 5 & 6 可以明确无误得出: 赋值 null操作是相当猛的,它会直接将变量所指向的内存在符号号中的引用计数置 0,那这块内存自然被引擎回收了,至于何时被再次利用不得而知,有可能马上被用作存储别的信息,也许再也没有使用过.但是无论如何,原来所有指向那块内存变量都将无法再操作被回收的内存了,任何试图调用它的变量都将返回 null.

      <?php
      error_reporting
      (E_ALL);
      $a = 'I am test.';
      $b = & $a;

      $b = null;

      echo
      '$a = '. $a ."\n";
      echo
      '$b = '. $b ."\n";

      if (
      null === $a)
      {                                                                                
      echo
      '$a is null.';     
      } else
      {
      echo
      'The type of $a is unknown.';     
      }

      ?>
      hy0kl% php -f gc.php
      $a =
      $b =
      $a is null.


      综上所述,充分说明了为什么我们在看开源产品源码的时候,常看到一些比较大的临时变量,或使用完不再调用的重用信息都会被集中或显示的赋值为 null 了.它相当于 UNIX 中直接将真实文件干掉了,所有指向它的软链接自然成了空链了.
      之前在讨论到这些细节点时有很多想当然的念头,在实际的执行了测试代码后才发现: 哦,原来如此!
      纸上得来终觉浅,绝知此事须躬行.

      作者: hy0kl
      永久链接: 从 PHP 代码分析 PHP 的 GC(垃圾回收) 机制
      Email/MSN/Gtalk: hy0kle@gmail.com
      Time: 2009.11.07

       

       

      浅谈PHP5中垃圾回收算法(Garbage Collection)的演化

      前言

      PHP是一门托管型语言,在PHP编程中程序员不需要手工处理内存资源的分配与释放(使用C编写PHP或Zend扩展除外),这就意味着PHP本身实现了垃圾回收机制(Garbage Collection)。现在如果去PHP官方网站(php.net)可以看到,目前PHP5的两个分支版本PHP5.2和PHP5.3是分别更新的,这是因为许多项目仍然使用5.2版本的PHP,而5.3版本对5.2并不是完全兼容。PHP5.3在PHP5.2的基础上做了诸多改进,其中垃圾回收算法就属于一个比较大的改变。本文将分别讨论PHP5.2和PHP5.3的垃圾回收机制,并讨论这种演化和改进对于程序员编写PHP的影响以及要注意的问题。

      PHP变量及关联内存对象的内部表示

      垃圾回收说到底是对变量及其所关联内存对象的操作,所以在讨论PHP的垃圾回收机制之前,先简要介绍PHP中变量及其内存对象的内部表示(其C源代码中的表示)。

      PHP官方文档中将PHP中的变量划分为两类:标量类型和复杂类型。标量类型包括布尔型、整型、浮点型和字符串;复杂类型包括数组、对象和资源;还有一个NULL比较特殊,它不划分为任何类型,而是单独成为一类。

      所有这些类型,在PHP内部统一用一个叫做zval的结构表示,在PHP源代码中这个结构名称为“_zval_struct”。zval的具体定义在PHP源代码的“Zend/zend.h”文件中,下面是相关代码的摘录。

      01 typedef union _zvalue_value {
      02     long lval;                  /* long value */
      03     double dval;                /* double value */
      04     struct {
      05         char *val;
      06         int len;
      07     } str;
      08     HashTable *ht;              /* hash table value */
      09     zend_object_value obj;
      10 } zvalue_value;
      11   
      12 struct _zval_struct {
      13     /* Variable information */
      14     zvalue_value value;     /* value */
      15     zend_uint refcount__gc;
      16     zend_uchar type;    /* active type */
      17     zend_uchar is_ref__gc;
      18 };

      其中联合体“_zvalue_value”用于表示PHP中所有变量的值,这里之所以使用union,是因为一个zval在一个时刻只能表示一种类型的变量。可以看到_zvalue_value中只有5个字段,但是PHP中算上NULL有8种数据类型,那么PHP内部是如何用5个字段表示8种类型呢?这算是PHP设计比较巧妙的一个地方,它通过复用字段达到了减少字段的目的。例如,在PHP内部布尔型、整型及资源(只要存储资源的标识符即可)都是通过lval字段存储的;dval用于存储浮点型;str存储字符串;ht存储数组(注意PHP中的数组其实是哈希表);而obj存储对象类型;如果所有字段全部置为0或NULL则表示PHP中的NULL,这样就达到了用5个字段存储8种类型的值。

      而当前zval中的value(value的类型即是_zvalue_value)到底表示那种类型,则由“_zval_struct”中的type确定。_zval_struct即是zval在C语言中的具体实现,每个zval表示一个变量的内存对象。除了value和type,可以看到_zval_struct中还有两个字段refcount__gc和is_ref__gc,从其后缀就可以断定这两个家伙与垃圾回收有关。没错,PHP的垃圾回收全靠这俩字段了。其中refcount__gc表示当前有几个变量引用此zval,而is_ref__gc表示当前zval是否被按引用引用,这话听起来很拗口,这和PHP中zval的“Write-On-Copy”机制有关,由于这个话题不是本文重点,因此这里不再详述,读者只需记住refcount__gc这个字段的作用即可。

      PHP5.2中的垃圾回收算法——Reference Counting

      PHP5.2中使用的内存回收算法是大名鼎鼎的Reference Counting,这个算法中文翻译叫做“引用计数”,其思想非常直观和简洁:为每个内存对象分配一个计数器,当一个内存对象建立时计数器初始化为1(因此此时总是有一个变量引用此对象),以后每有一个新变量引用此内存对象,则计数器加1,而每当减少一个引用此内存对象的变量则计数器减1,当垃圾回收机制运作的时候,将所有计数器为0的内存对象销毁并回收其占用的内存。而PHP中内存对象就是zval,而计数器就是refcount__gc。

      例如下面一段PHP代码演示了PHP5.2计数器的工作原理(计数器值通过xdebug得到):

      1 <?php
      2   
      3 $val1 = 100; //zval(val1).refcount_gc = 1;
      4 $val2 = $val1; //zval(val1).refcount_gc = 2,zval(val2).refcount_gc = 2(因为是Write on copy,当前val2与val1共同引用一个zval)
      5 $val2 = 200; //zval(val1).refcount_gc = 1,zval(val2).refcount_gc = 1(此处val2新建了一个zval)
      6 unset($val1); //zval(val1).refcount_gc = 0($val1引用的zval再也不可用,会被GC回收)
      7   
      8 ?>

      Reference Counting简单直观,实现方便,但却存在一个致命的缺陷,就是容易造成内存泄露。很多朋友可能已经意识到了,如果存在循环引用,那么Reference Counting就可能导致内存泄露。例如下面的代码:

      1 <?php
      2   
      3 $a = array();
      4 $a[] = & $a;
      5 unset($a);
      6   
      7 ?>

      这段代码首先建立了数组a,然后让a的第一个元素按引用指向a,这时a的zval的refcount就变为2,然后我们销毁变量a,此时a最初指向的zval的refcount为1,但是我们再也没有办法对其进行操作,因为其形成了一个循环自引用,如下图所示:

      image

      其中灰色部分表示已经不复存在。由于a之前指向的zval的refcount为1(被其HashTable的第一个元素引用),这个zval就不会被GC销毁,这部分内存就泄露了。

      这里特别要指出的是,PHP是通过符号表(Symbol Table)存储变量符号的,全局有一个符号表,而每个复杂类型如数组或对象有自己的符号表,因此上面代码中,a和a[0]是两个符号,但是a储存在全局符号表中,而a[0]储存在数组本身的符号表中,且这里a和a[0]引用同一个zval(当然符号a后来被销毁了)。希望读者朋友注意分清符号(Symbol)的zval的关系。

      在PHP只用于做动态页面脚本时,这种泄露也许不是很要紧,因为动态页面脚本的生命周期很短,PHP会保证当脚本执行完毕后,释放其所有资源。但是PHP发展到目前已经不仅仅用作动态页面脚本这么简单,如果将PHP用在生命周期较长的场景中,例如自动化测试脚本或deamon进程,那么经过多次循环后积累下来的内存泄露可能就会很严重。这并不是我在耸人听闻,我曾经实习过的一个公司就通过PHP写的deamon进程来与数据存储服务器交互。

      由于Reference Counting的这个缺陷,PHP5.3改进了垃圾回收算法。

      PHP5.3中的垃圾回收算法——Concurrent Cycle Collection in Reference Counted Systems

      PHP5.3的垃圾回收算法仍然以引用计数为基础,但是不再是使用简单计数作为回收准则,而是使用了一种同步回收算法,这个算法由IBM的工程师在论文Concurrent Cycle Collection in Reference Counted Systems中提出。

      这个算法可谓相当复杂,从论文29页的数量我想大家也能看出来,所以我不打算(也没有能力)完整论述此算法,有兴趣的朋友可以阅读上面的提到的论文(强烈推荐,这篇论文非常精彩)。

      我在这里,只能大体描述一下此算法的基本思想。

      首先PHP会分配一个固定大小的“根缓冲区”,这个缓冲区用于存放固定数量的zval,这个数量默认是10,000,如果需要修改则需要修改源代码Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MAX_ENTRIES然后重新编译。

      由上文我们可以知道,一个zval如果有引用,要么被全局符号表中的符号引用,要么被其它表示复杂类型的zval中的符号引用。因此在zval中存在一些可能根(root)。这里我们暂且不讨论PHP是如何发现这些可能根的,这是个很复杂的问题,总之PHP有办法发现这些可能根zval并将它们投入根缓冲区。

      当根缓冲区满额时,PHP就会执行垃圾回收,此回收算法如下:

      1、对每个根缓冲区中的根zval按照深度优先遍历算法遍历所有能遍历到的zval,并将每个zval的refcount减1,同时为了避免对同一zval多次减1(因为可能不同的根能遍历到同一个zval),每次对某个zval减1后就对其标记为“已减”。

      2、再次对每个缓冲区中的根zval深度优先遍历,如果某个zval的refcount不为0,则对其加1,否则保持其为0。

      3、清空根缓冲区中的所有根(注意是把这些zval从缓冲区中清除而不是销毁它们),然后销毁所有refcount为0的zval,并收回其内存。

      如果不能完全理解也没有关系,只需记住PHP5.3的垃圾回收算法有以下几点特性:

      1、并不是每次refcount减少时都进入回收周期,只有根缓冲区满额后在开始垃圾回收。

      2、可以解决循环引用问题。

      3、可以总将内存泄露保持在一个阈值以下。

      PHP5.2与PHP5.3垃圾回收算法的性能比较

      由于我目前条件所限,我就不重新设计试验了,而是直接引用PHP Manual中的实验,关于两者的性能比较请参考PHP Manual中的相关章节:http://www.php.net/manual/en/features.gc.performance-considerations.php

      首先是内存泄露试验,下面直接引用PHP Manual中的实验代码和试验结果图:

      01 <?php
      02 class Foo
      03 {
      04     public $var = '3.1415962654';
      05 }
      06   
      07 $baseMemory = memory_get_usage();
      08   
      09 for ( $i = 0; $i <= 100000; $i++ )
      10 {
      11     $a = new Foo;
      12     $a->self = $a;
      13     if ( $i % 500 === 0 )
      14     {
      15         echo sprintf( '%8d: ', $i ), memory_get_usage() - $baseMemory, "\n";
      16     }
      17 }
      18 ?>
      PHP内存泄露试验

      可以看到在可能引发累积性内存泄露的场景下,PHP5.2发生持续累积性内存泄露,而PHP5.3则总能将内存泄露控制在一个阈值以下(与根缓冲区大小有关)。

      另外是关于性能方面的对比:

      01 <?php
      02 class Foo
      03 {
      04     public $var = '3.1415962654';
      05 }
      06   
      07 for ( $i = 0; $i <= 1000000; $i++ )
      08 {
      09     $a = new Foo;
      10     $a->self = $a;
      11 }
      12   
      13 echo memory_get_peak_usage(), "\n";
      14 ?>
      这个脚本执行1000000次循环,使得延迟时间足够进行对比。

      然后使用CLI方式分别在打开内存回收和关闭内存回收的的情况下运行此脚本:

      1 time php -dzend.enable_gc=0 -dmemory_limit=-1 -n example2.php
      2 # and
      3 time php -dzend.enable_gc=1 -dmemory_limit=-1 -n example2.php
      在我的机器环境下,运行时间分别为6.4s和7.2s,可以看到PHP5.3的垃圾回收机制会慢一些,但是影响并不大。

      与垃圾回收算法相关的PHP配置

      可以通过修改php.ini中的zend.enable_gc来打开或关闭PHP的垃圾回收机制,也可以通过调用gc_enable()或gc_disable()打开或关闭PHP的垃圾回收机制。在PHP5.3中即使关闭了垃圾回收机制,PHP仍然会记录可能根到根缓冲区,只是当根缓冲区满额时,PHP不会自动运行垃圾回收,当然,任何时候您都可以通过手工调用gc_collect_cycles()函数强制执行内存回收。

       

       

      PHP的GC垃圾收集机制

      每一种语言都有自己的自动垃圾回收机制,让程序员不必过分关心程序内存分配,但是在OOP中,有些对象需要显式的销毁;防止程序执行内存溢出。

      一、PHP 垃圾回收机制(Garbage Collector 简称GC)

      在PHP中,没有任何变量指向这个对象时,这个对象就成为垃圾。PHP会将其在内存中销毁;这是PHP 的GC垃圾处理机制,防止内存溢出。

      当一个 PHP线程结束时,当前占用的所有内存空间都会被销毁,当前程序中所有对象同时被销毁。GC进程一般都跟着每起一个SESSION而开始运行的.gc目的是为了在session文件过期以后自动销毁删除这些文件.

      二、__destruct /unset

      __destruct() 析构函数,是在垃圾对象被回收时执行。

      unset 销毁的是指向对象的变量,而不是这个对象。

      三、 Session 与 GC

      由于PHP的工作机制,它并没有一个daemon线程来定期的扫描Session 信息并判断其是否失效,当一个有效的请求发生时,PHP 会根据全局变量 session.gc_probability 和session.gc_divisor的值,来决定是否启用一个GC, 在默认情况下, session.gc_probability=1, session.gc_divisor =100 也就是说有1%的可能性启动GC(也就是说100个请求中只有一个gc会伴随100个中的某个请求而启动).

      GC 的工作就是扫描所有的Session信息,用当前时间减去session最后修改的时间,同session.gc_maxlifetime参数进行比较,如果生存时间超过gc_maxlifetime(默认24分钟) ,就将该session删除。

      但是,如果你Web服务器有多个站点,多个站点时,GC处理session可能会出现意想不到的结果,原因就是:GC在工作时,并不会区分不同站点的session.

      那么这个时候怎么解决呢?
      1. 修改session.save_path,或使用session_save_path() 让每个站点的session保存到一个专用目录,

      2. 提供GC的启动率,自然,GC的启动率提高,系统的性能也会相应减低,不推荐。

      3. 在代码中判断当前session的生存时间,利用session_destroy()删除.
       
    6. 硬盘原理、参数及IOPS

      2011-09-02 14:43:47

       

      硬盘 2
      一、硬盘接口 2
      二、物理结构 2
        1.磁头 4
        2.磁道 4
        3.扇区 4
        4.柱面 4
        5.硬盘扇区、磁道、柱面、磁头: 5
      三、逻辑结构—硬盘寻址模式 5
        1. 3D参数 5
        2. 基本Int 13H 调用 6
        3. 扩展Int 13H 6
        4. 硬盘的寻址模式-C/H/S与LBA的转换关系 6
        5. 为什么引入LBA寻址模式 8
      四、基本参数 9
        4.1 容量 9
        4.2 转速 9
        4.3 平均访问时间 10
        4.4 传输速率 10
        4.5 缓存 10
      五、硬盘扇区大小和文件系统分配单元(IO block)大小的区别是什么? 11
      六、stat命令如何计算文件占用的磁盘blocks大小 12

      IOPS 13
        一、定义 13
        二、磁盘IOPS计算与测量(重点) 13
        三、一则简单的磁盘的iops分析 16
        四、磁盘阵列吞吐量与IOPS两大瓶颈分析 17
         1、吞吐量 17
         2、IOPS 18

      Linux下硬盘信息查看参数设置相关命令 19
        1. fdisk -l 查看硬盘(CHS) 19
        2. 单个硬盘读写速度测试可以用hdparm -t  /dev/sd1 20
        3. df查看硬盘的分区级占用情况: 20
        4. hdparm简介 20
        5. hdparm硬盘参数显示与设置—实例 21
        6. Linux和Solaris中如何查看硬件的资源信息 25

       

       

       

      附件: 硬盘原理、参数及IOPS.rar(1.07 MB)

    7. 用 STAF/STAX + LAMP 实现多任务的自动化测试框架(转载)

      2011-08-30 14:24:03

      转自:
      http://www.ibm.com/developerworks/cn/opensource/os-cn-stax-lamp/index.html

      简介: 本文介绍了一种基于 LAMP+STAF/STAX 的自动化测试框架,以及该框架在 WVS(WebSphere Voice Server) 产品测试中的应用。该框架具有界面友好,操作方便,多任务自动执行等特性。



      前言

      STAF/STAX 是由 IBM 开发的自动化测试运行环境,由于其跨平台和扩展性强的特点,在各种测试工作中被越来越多的使用,但是它也存在流程复杂,操作不便等缺点。而 LAMP 是基于Linux,Apache,MySQL 和 PHP 的开源网络开发平台,PHP 可用 Perl 或 Python 代替。Linux+Apache+MySQL+Perl/PHP/Python 常被放在一起使用,来搭建动态网站或者服务器的开源软件,他们拥有了越来越高的兼容度,共同组成了一个强大的 Web 应用程序平台。LAMP 具有搭建快捷,界面友好等特点。为了提高测试运行效率,提供良好的使用体验,我们开发了基于LAMP+STAF/STAX 的自动化测试框架并应用在 WVS 产品的测试中。该框架中前端是 LAMP 实现的动态网站,后端是 STAF/STAX 服务及脚本。我们还利用 STAF 的参数导入特性实现了多任务的自动执行。本文将对基于 LAMP+STAF/STAX 的自动化测试框架的功能特性,体系结构,以及应用在WVS 产品测试中的拓扑结构,设计实现和配置使用进行介绍和分析。


      功能特性

      该框架不仅利用了 STAF/STAX 的自动化功能,还利用了 LAMP 的强大 Web 应用能力,提供了丰富的自动化测试功能和可扩展特性。总体来说,主要有以下功能特性:

      • 上层应用逻辑和底层自动测试实现松耦合
      • 自动化测试功能可扩展性强,支持多任务执行
      • 支持远程程序调用
      • 方便友好提交测试任务
      • 可实时监控测试任务和 STAX 运行情况
      • 历史测试记录可维护

      体系结构

      该框架符合 MVC 的三层结构,主要的功能模块都在控制层,包括提交和监控测试任务,监控 STAX 运行,支持多任务执行,维护历史测试记录等。在该框架中,表示层和控制层的功能实现是以 PHP 形式存在,采用 MySQL 作为数据容器,Apache Server 作为 Web Server,另外在控制层中关于自动化测试的功能实现是以 xml 形式存在,它是被实现层中的 STAF/STAX 所调用。它的体系结构如下图所示:


      图 1. LAMP+STAF/STAX测试框架结构图
      LAMP+STAF/STAX测试框架结构图


      拓扑结构

      我们把该框架应用在了 WVS 产品的自动化测试中,在这个测试中,我们需要更改 WVS 的配置并对其进行重启,执行 Tester 机器上的脚本,向 Voice Enabler 所在的机器发送 Sip 请求,然后 Voice Enabler 会建立与 WVS 机器的 RTSP 连接以获取其语音识别和语音合成服务,测试结束后再从 WVS 机器拷贝日志进行分析。我们希望这一切都用 STAF/STAX 控制自动完成。因此在所有的机器上都安装了 STAF。另外我们把对测试进行前端控制的 LAMP 软件和代码也配置在了 Tester 机器上,以充分利用其系统资源。对应的,Tester 机器的 STAF 需要安装 STAX 服务来运行本地的 STAX 脚本。该系统的部署图如下所示:


      图 2. LAMP+STAF/STAX 网络拓扑
      LAMP+STAF/STAX 网络拓扑


      设计实现

      关于 LAMP 的部分,这里会给出一些应用示图和代码示例,关于 STAF/STAX 中的一些功能给出代码示例,仅供参考。

      提交测试任务:

      该应用提供了 GUI 方式的任务提交,在提交表单中可以选择平台,版本信息,需要运行的用例类型,任务的名字等,方便快捷。如下图所示


      图 3. 提交测试任务页面
      提交测试任务页面

      维护历史测试记录:

      该应用中的测试记录都被存储在了 MySQL 数据库中,可以进行浏览,查询,删除等操作,方便测试者察看以前的测试信息,辅以参考。如下图所示。


      图 4. 任务列表查看页面
      任务列表查看页面

      多任务支持:

      如前所述,测试任务的执行是由 STAX 模块来完成的。本框架应用中的 STAX 执行模块包括三个主要的 STAX 脚本文件,分别是 queuemgr.xml,testexecute.xml 和 funcdef.xml。其中 funcdef.xml 定义了 testexecute.xml 中要用到的功能函数,如判断操作系统类型和版本、读写文件、启动或停止应用服务器和发送HTTP请求等。testexecute.xml 里定义了测试执行的流程和各种测试变量,它执行时会首先导入记录了当前测试任务的参数的 xml 文件,然后用这些参数初始化各种测试变量并执行相应测试功能,最后在测试任务完成时将任务参数文件转移到测试日志所在目录。queuemgr.xml 定义了一个独立的 STAX 队列管理任务,它在启动后循环检查测试任务参数文件所在的目录,如果目录非空,则选择最早的任务文件名作为运行参数让 STAX 执行 testexecute.xml。此外,它还负责测试计时、错误处理和延时等待等工作。其运行流程如下图。


      图 5. 任务列表处理流程
      任务列表处理流程

      下面给出了 queuemgr.xml 中定义的函数 check-queue 的代码。它被 queuemgr.xml 的 main 函数在初始化各种全局变量后调用。实现自动执行任务队列的 STAX 脚本代码如下。


      清单 1. queuemgr.xml 中的 check-queue 函数

      <function name="check-queue">   
       <sequence>
         <block name="'Queue Manager'">
          <sequence>
           <stafcmd name="'Check Queue'">
            <location>'local'</location>
            <service>'FS'</service>
            <request>'LIST DIRECTORY %s TYPE F SORTBYNAME' % queueDir</request>
           </stafcmd>
           <script>dirList = STAFResult</script>          
           <script>taskCount = len(dirList)</script>
           <script>            
            if (taskCount != 0):               # skip an empty list
             taskParam = dirList[0]               # get first entry             
            else:
             taskParam = "empty"                        
           </script>
           <if expr=" taskParam != 'empty'">
            <if expr=" lastTaskParam != taskParam">
             <sequence>
              <if expr="TestRunInProgress[0] == 0">
               <script>
                TestRunInProgress[0] = 1
                TestRunStartTime[0] = now()
               </script>
              </if>
              <script>TestRunCtr[0] = TestRunCtr[0] + 1</script>
              <script> lastTaskParam = taskParam </script>
              <script>  lastTaskParamJobID = taskParamJobID </script>
              <log>'QueueMgr: Executing taskParam %s' %(taskParam)</log>
              <stafcmd name="'Executing Task: %s' % taskParam">
               <location>'local'</location>
               <service>'STAX'</service>
               <request>'EXECUTE FILE %s CLEARLOGS JOBNAME %s SCRIPT. " taskParam=\'%s\'" WAIT 
      %s' %(TestExecuteFile, taskParam, taskParam,gblTestExecutionTimeoutMS)</request>
              </stafcmd>
              <script> taskParamJobID = STAFResult</script>
              <log>'QueueMgr: taskParam %s  JobID = %s' %( taskParam, taskParamJobID)</log>
              <if expr="RC != STAFRC.Ok">
               <sequence>
               <log>'QueueMgr: Execute Test with Param File %s failed - RC: %s  STAFResult: %s' 
                        %( taskParam,RC,STAFResult)</log>
                <terminate block="'main'"/>
               </sequence>
              </if>
             </sequence>
             <else>
              <sequence>
              <log>'QueueMgr: *** TERMINATING Queue Manager *** Task File %s is the same as the 
                     last one' %( taskParam)</log>
               <log>'QueueMgr: Please check the completion status of Test Record %s Job ID %s 
      and Re-Start the Queue Manager' %( lastTaskParam, lastTaskParamJobID)</log>
               <terminate block="'main'"/> 
              </sequence>
             </else>
            </if>
            <else>
             <sequence>            
              <if expr="TestRunInProgress[0] == 1">
               <sequence>                
                 <script>
                  TestRunInProgress[0] = 0
                  TestRunStopTime[0] = now()
                  testTime = (TestRunStopTime[0] - TestRunStartTime[0])
                  testHrs = int((testTime) / 3600)
                  testMin = int((testTime - (testHrs * 3600)) / 60)
                  testSec = int((testTime - (testHrs * 3600)) - (testMin * 60))
                  TestRunCompletionDateTime = strftime('%a %b %d %Y %H:%M:%S', localtime())
                 </script>
                 <log>'All tests have been run; the testqueue is empty'</log> 
                 <log>'Test Run Completed: %s' % TestRunCompletionDateTime</log>
                 <log>'%d Test Tasks Were Completed In %d Hrs %d Min %d Sec' 
                        % (TestRunCtr[0],testHrs,testMin,testSec)</log>                   
                 <script>TestRunCtr[0] = 0</script>
                 <terminate block="'main'"/> 
               </sequence>
              </if>    
              <block name="'Queue Check Delay'">
               <stafcmd name="'Checking TestQueue Every 5000 Sec' % gblCheckQueueDelayTimeSec">
                <location>'local'</location>
                <service>'DELAY'</service>
                <request>'DELAY 5000'</request>
               </stafcmd>          
              </block>
             </sequence>
            </else>
           </if>
           <block name="'Queue Process Delay'">
            <stafcmd name="'Checking TestQueue 5000 Sec After Last Task'">
            <location>'local'</location>
            <service>'DELAY'</service>
            <request>'DELAY 5000'</request>
           </stafcmd>          
          </block>
         </sequence>
        </block>
       </sequence>
      </function>
      

      代码中第一个延时等待是对空任务队列的查看周期。测试执行人员根据个人习惯有时候希望先启动任务队列执行任务,再生成测试任务参数文件。这时查看队列的循环不能在第一次遇到队列为空时就退出,而是要不断反复查看任务队列。如果在循环之间不加延时,会造成进程对 CPU 开销过大,影响其他进程。第二个延时是前后两个测试任务执行之间的延时。这是为了让刚结束的测试进程有足够时间释放各种资源和执行多个文件操作,如日志文件和测试参数文件的转移。

      每次循环中获得测试任务文件列表时要判断第一个任务参数文件是否在上次循环中已经尝试执行过。若是,则上次任务肯定没有正常结束,否则应该将该任务文件转移到任务结果文件夹中。这时应该中止任务队列处理进程,提示测试执行人员查看 STAX 日志排除错误。

      生成任务参数文件并启动任务队列执行的 PHP 代码如下。该部分代码在测试者提交了图 3 所示的测试任务配置表单后执行。


      清单 2. 生成任务参数文件并启动任务队列的代码

      $isTestStarted = false;
      $testQueueDir=opendir("$testQueuePath");
         
      while($fileName=readdir($testQueueDir)){    
       if ($fileName!="." and $fileName!="..") {
        $isTestStarted = true;  
        break;
       }
      }
      
      //Here should be the code to create a parameter file for this task, using parameters set
      by user from the web UI.
      
      if(!$isTestStarted){
        exec("STAF local STAX EXECUTE FILE /usr/local/staf/xml/queuemanager.xml CLEARLOGS");
      }
      header('Location: / viewTestList.PHP');
      

      在这里我们并没有去检查任务处理进程是否在系统中存在,而是根据任务文件文件夹是否为空来判断。这一判断是基于若队列中有未执行完的任务,则应该有队列处理进程在进行处理的简单假设。当然偶尔也会有测试任务出错,任务处理进程非正常中止的情况发生,使得该假设失准。因此,我们要在 PHP 代码中查询任务处理进程的日志,判断其运行状态,并在页面中向测试执行人员显示出来。如果任务文件夹中尚有文件未执行,而队列处理进程又处于停止状态,就要提示测试人员排查错误并手动重启任务队列处理进程。

      STAX 任务启动后,我们也可以随时让 PHP 代码执行 exec("STAF local STAX TERMINATE JOB $JobID") 来中止任务编号为 JobID 的 STAX 任务。

      监控 STAX 运行情况:

      STAX 一旦出错,意味着任务无法运行,如果能够实时监控 STAX 任务运行状态,可提高可靠性。


      清单 3. 监控 STAX 任务状态

      exec("./viewStaxLog.sh $staxJobID 2>&1", $outPut);
      $isStopped = false;
      foreach($outPut as $outputline){
       $pos = strpos($outputline, "Stop   JobID:");
       if($pos !== false){
        $isStopped = true;
       }
      }
      if($isStopped){
       echo "<fond color=\"blue\">Stopped</font><br>";
      }
      else{
       echo "<fond color=\"red\">Running</font><br>";
      }
      

      上面清单中调用了查看 STAX 日志的 shell 脚本。STAX 日志分为两种,一种是系统对任务运行情况和错误的自动记录,另一种是测试开发人员在 STAX 任务脚本中用 <log></log> 标签主动记录的日志。可以通过 STAF 命令实时查询任何 STAX 任务的日志。在获得日志后可以根据日志中的关键字(如上面代码中的“Stop JobID: ”)判断 STAX 任务的状态。

      STAX 的日志可以用 STAF 命令进行查询,其代码如下。


      清单 4. 查询 STAX 日志

      if [ $1 = “user” ]
      then
      staf local log query machine mytest.cn.ibm.com logname STAX_Job_$2_User
      else
      staf local log query machine mytest.cn.ibm.com logname STAX_Job_$1
      fi
      

      远程程序调用:

      在测试过程中,可以让 STAX 远程执行要运行的程序,这里给出实际应用中经常会调用 shell 脚本的示例,其 STAX 代码如下。


      清单 5. STAX 远程调用 shell 脚本

      <process>
         <location>'local'</location>
         <command  mode="'shell'" >'"./%" result %s %s' 
                                          % (testScriptName,testParam1,testParam2)</command>
         <workdir>'%s' % testScriptDir</workdir>
         <stderr mode="'stdout'"/>
         <returnstdout/>
      </process>
      <if expr="RC != 0">
         <log>'Error: RC=%s, STAXResult=%s, Error running test script' 
               % (RC, STAXResult)</log>
         <else>
            <log>'Running test script. STAXResult=%s' % (STAXResult)</log>
         </else>
      </if>
      

      分析测试结果:

      STAX 使用 Python 处理其脚本中的表达式。为了便于文本分析,使用者可以在 STAX 脚本中嵌入 Python 脚本。用这种方式可以方便地利用 Python 强大的正则表达式处理能力。下面的代码示范如何从日志文件中提取相关文字并进行对比判断。

      <stafcmd>
         <location>'%s' % logServer</location>
         <service>'fileman'</service>   
         <request>'grep file "%s" machine "%s" for "%s" last CODEPAGE ascii' 
                     % (testLogName,logServer,testCaseName)</request>
      </stafcmd>
      
      <!—- Some other code here -->
      
      <script>
         import re
         import string
         result = str(STAFResult[0])
         testCaseMatch = re.search("%s %s" 
            % (testCaseName.upper(),expectedResult.upper()),result.upper())
      </script>
      


      配置使用

      配置这样一个自动化测试框架,需要安装 Apache Server,MySQL,PHP,如何安装该环境,这里就不赘述。我们开发的测试框架由于其功能比较多,也需要在测试中不断的更改配置,有时还要增加测试内容。为了便于其配置,我们将PHP代码中有可能需要修改的变量都放在一个单独的 PHP 文件中,而与测试用例相关的变量用 PHP 数组的形式存放在另一个 PHP 文件中。所有要用到这些配置的 PHP 文件对其进行包含。


      总结

      LAMP 是功能强大的 Web 应用程序平台,STAF/STAX 具有很好的自动测试功能,把二者结合起来就可以形成更加灵活可靠,易于功能扩展的新的自动化测试框架,本文也通过在 WVS 产品测试中使用该框架从而获得了很好的测试效果。


      参考文献

      学习

      获得产品和技术


      作者简介

      于有志,2007 年加入 IBM,目前就职于 IBM 中国软件开发中心,从事 WebSphere Translation Server 和 WebSphere Voice Server 相关的工作。

      刘小杰,2005 年加入 IBM,目前就职于 IBM 中国软件开发中心,从事 WebSphere Voice Server 相关的工作。

    8. 自己动手创建 AIX 平台上的性能监测工具(转载)

      2011-08-30 14:22:00

      http://www.ibm.com/developerworks/cn/aix/library/0810_daijw_monitor/index.html

       

      简介: 对于 AIX 平台上的系统测试人员来说 , 经常需要监测系统和应用程序的 CPU 和内存使用情况。操作系统自带的性能工具由于不具有数据分析和图形化的报表功能 , 使用起来非常不方便。本文介绍了如何利用 AIX 操作系统自带的性能工具以及 JFreeChart 图库去轻松构建一个适于自己项目需要的性能监测工具。

       

      简介

      AIX 操作系统提供了很多性能相关的工具包,比如 perfagent.tools,bos.acct,bos.sysmgt.trace, bos.adt.samples,bos.perf.tools 以及 bos.perf.tune。它们提供了很多可以对系统性能进行监测和调优的工具。比如,监测网络活动的 netpmon,监测内存使用情况的 svmon,监测文件系统性能的 filemon,设置网络属性的 no。

      JFreeChart 是一个开源的 Java lib 库,利用 JFreeChart 可以用来生成各种各样的图表,比如饼图、柱状图、线图、区域图、分布图、混合图、甘特图。

      我们要创建的监测工具如下图 1 所示,可以分成两部分,一部分位于被监测的 AIX 机器 , 用来监测系统性能数据;另外一部分位于另一台机器,用来根据性能数据生成基于 WEB 的性能图表。虽然这两部分也可运行于同一台机器,但是为降低对被监测系统的性能影响,推荐将绘制图表的工作放到另一台机器上执行。


      图 1 性能监测工具示意图

      下面,我就介绍如何利用 AIX 的性能工具包和 JFreeChart 来创建一个简单易用的性能监测工具。


      开始之前

      在开始创建性能监测工具之前,请做下列准备:

      • 准备两台机器,一台是被监测的 AIX 机器 , 另外一台机器用于生成图表 ( 在文中,我们用一台 Linux 机器为例 )。
      • 由于我们在采集内存和 CPU 数据时需要用到 bos.perf.tools 和 bos.acct 工具包中的命令,在使用这些命令之前,请执行下面的命令来确保 bos.perf.tools 和 bos.acct 工具包已经被正确安装在你的 AIX 机器上。

       # lslpp -lI bos.perf.tools bos.acct
      

      如果工具包已经正确安装,会有下面类似的输出:

       Fileset Level State Description 
       ----------------------------------------------------------------------------
       Path: /usr/lib/objrepos
       bos.acct 5.3.0.30 COMMITTED Accounting Services 
       bos.perf.tools 5.3.0.30 COMMITTED Base Performance Tools 
      
       Path: /etc/objrepos
       bos.acct 5.3.0.30 COMMITTED Accounting Services 
      bos.perf.tools 5.3.0.30 COMMITTED Base Performance Tools
      

      • 通过下面的 URL 下载最新的 JFreeChart lib 到 Linux 机器上

      http://www.jfree.org/jfreechart/download.html

      http://sourceforge.net/project/showfiles.php?group_id=15494

      • 由于 JFreeChart 需要 JDK1.3 或者更高版本的支持,请下载并安装 JDK 到 Linux 机器上
      • 在 Linux 机器上搭建一个 WEB 服务器,这样我们就可以把生成的性能数据和图表发布到 WEB 上。
      • 通过下面的 URL 下载最新的 STAF 并且安装到 AIX 和 Linux 机器上。

      http://staf.sourceforge.net/getcurrent.php

      为了能通过 STAF 传送数据文件,这两台机器还必须互相赋予一定的权限。因此在安装 STAF 之后,启动 STAF 之前,我们需要按照下面的方法分别修改 STAF 的配置文件。

      在 Linux 机器 ( 假定 IP 地址为 9.168.0.2) 的 STAF 配置文件 /usr/local/staf/bin/STAF.cfg 里添加如下内容:

      TRUST LEVEL 5 MACHINE tcp://9.168.0.1
      

      在 AIX 机器 ( 假定 IP 地址为 9.168.0.1) 的 STAF 配置文件 /usr/local/staf/bin/STAF.cfg 里添加如下内容:

       TRUST LEVEL 5 MACHINE tcp://9.168.0.2
      


      监测性能数据

      为方便说明起见,我们假定需要监测下列性能数据:

      • 系统的内存使用情况
      • 系统的 CPU 使用情况
      • 某个特定进程的内存使用情况 ( 比如 java)


      对于系统的内存使用情况,我们可以用 svmon 命令的 -G 选项来收集数据。需要注意的是,svmon 输出的内存大小以 pages 为单位,1 page 等于 4kBytes。Svmon –G 的命令输出如下:

      bash-3.00# svmon -G
       size inuse free pin virtual
      memory 2031616 512534 1519082 145239 295968
      pg space 2097152 1214
      
       work pers clnt
      pin 145239 0 0
      in use 295968 0 216566
      

      对于系统的 CPU 使用情况,我们可以用 sar 命令的 -u 选项来收集数据。需要注意的是,-u 选项收集的是 system-wide 的 cpu 数据,如果是多 cpu 系统,命令输出的则是多个 cpu 的使用情况。如果需要某个特定 cpu 的使用情况,则需要用 -P 选项指定 CPU。Sar –u 的命令输出如下,”1”表明只采集一个时间点。

      bash-3.00# sar -u 1
      
      AIX test19 3 5 00034ADAD300 07/23/08
      
      System configuration: lcpu=4 
      
      14:41:54 %usr %sys %wio %idle physc
      14:41:55 0 0 0 100 2.00
      

      对于特定进程的内存使用情况,我们可以用 svmon 命令的 -P 选项来收集数据。-P 选项后面要跟进程 id,所以在运行 svmon –P 之前我们必须先取得要监测的进程 id。svmon –P 的命令输出如下 :

      bash-3.00# svmon -P 1450024
      
      ---------------------------------------------------------------------
      Pid Command Inuse Pin Pgsp Virtual 64-bit Mthrd 16MB
      1450024 java 99308 7384 0 79641 N Y N
      

      知道如何用命令监测我们需要的性能数据后,就可以开始动手编写脚本 aixperfmonitor.sh 来分析命令的输出,把需要的数据写到相应的数据文件当中去以便于后面绘制性能图表。另外,这个脚本也包含按照设定的时间间隔持续监测性能数据的功能。


      清单 1 aixperfmonitor.sh

      #!/bin/sh
      
      function usage {
       echo "Usage aixperfmonitor.sh -t <duration> -i <interval>"
       echo " where:"
       echo " -t: total monitor duration in minutes"
       echo " -i: monitor interval in minutes"
       echo ""
       exit
      }
      
      function checkProcess {
       
       # 如果有多个以 java 命名的进程,还需通过别的关键字精确选取要监测的 java 进程
       ret=`ps -ef|grep java|grep WebSphere`
       if [ $? -ne 0 ]; then
       JAVARun=0
       else
       javapid=`ps -ef|grep java|grep WebSphere|awk '{print $2}'`
       fi
      
      }
      
      function updatePerflog {
      
       #CPU dat file format
       #time %usr %sys %wio %idle
      
       sar -u 1 1|tail -1|awk '{print $1,$2,$3,$4,$5}' >> $CPUDAT
      
       #Mem dat file format
       #time inuse free pin virtual
      
       time=`date +%T`
       svmon -G|sed -e "s/memory/$time/"|sed -n '2p'|awk '{print $1,$3,$4,$5,$6}' >> $MEMDAT
      
       #Process dat file format
       #time inuse pin pgsp virtual
       if [ $JAVARun -eq 1 ]; then
       time=`date +%T`
       svmon -P $javapid | awk '($2 ~/java/){print $2,$3,$4,$5,$6}'|sed -e "s/java/$time/" >>\ 
           $JAVADAT
       fi
      
      }
      
      DATDIR=/perflog
      CPUDAT=cpu.dat
      MEMDAT=mem.dat
      JAVADAT=java.dat
      duration=30
      interval=30
      javapid=0
      running=0
      JAVARun=1
      
      while getopts ":t:i:" opt
      do
       case $opt in
       t) duration=$OPTARG;;
       i) interval=$OPTARG;;
       esac
      done
      
      #check process existence of java
      checkProcess
      
      rm -rf $DATDIR
      mkdir -p $DATDIR
      cd $DATDIR
      touch $CPUDAT
      touch $MEMDAT
      if [ $JAVARun -eq 1 ]; then
      touch $JAVADAT
      fi
      
      # 按照一定的间隔时间,在指定的时间内持续监测系统的性能数据
      while [ $running -lt $duration ]
      do
       updatePerflog
       sleep `expr $interval \* 60`
       running=`expr $running + $interval`
      done
      

      在 aixperfmonitor 脚本中,最重要的部分就是 updatePerflog 这个函数,它对 sar 和 svmon 的命令行输出进行处理以得到我们想要的性能数据。比如,对于系统 CPU 使用情况,我们抓取了 %usr, %sys, %wio, %idle 这四个性能参数;对于系统内存使用情况 , 我们抓取了 inuse, free, pin, virtual 这四个性能参数。另外,checkpProcess 函数也值得我们注意,它检查相关进程是否真的存在,如果进程不存在,则不会在 updatePerflog 函数中抓取相关性能数据。

      有了 aixperfmonitor.sh 脚本之后,我们就可以监测系统性能数据了。比如说,我们希望监测时间为 3 天,监测间隔为 10 分钟,那么我们可以通过下面的脚本完成调用:

      bash-3.00# nohup ./aixperfmonitor.sh -t 4320 -i 10 2>&1 >/tmp/perfmonitor_output &
      


      生成基于 WEB 的性能图表

      有了性能监测数据以后,我们就可以开始利用 JFreeChart 来生成图表并且把它们发布到 WEB 上了。

      首先,让我们利用 JFreeChart 来编写生成图表的 Java 代码。由于下载的 JfreeChart 包中带有丰富的例程,我们不需重头开始,只要对其中的例程加以修改即可。AIXPerfChart.java 包含了生成图表的全部代码。


      清单 2 AIXPerfChart.java( 只显示部分源代码 )

      public class AIXPerfChart {
      
       protected final static Pattern FILE_PATTERN = 
       Pattern.compile("([^\\/]+)\\.dat");
       
       protected final static Pattern DATA_LINE = 
       // 匹配数据文件的格式 , 每个 ([^\\s]+) 匹配数据文件中的一列 , 共 5 列 
       Pattern.compile("\\s*([^\\s]+)\\s+([^\\s]+)\\s+([^\\s]+)\\s+([^\\s]+)\\s+([^\\s]+)\\s*");
      
       // 返回以小时为单位的监测时间 
       private double getTimeInDouble (String ts) {
       String[] hms = ts.split(":");
       double t = (Double.parseDouble(hms[0]) + 
       Double.parseDouble(hms[1]) / 60.0 +
       Double.parseDouble(hms[2]) / 3600.0) ;
       if(duration >= 0.0) {
       if (lastTime > t) 
        duration += t + 24.0 - lastTime;
       else
       duration += t - lastTime; 
       } else {
       duration = 0.0;
       }
       lastTime = t;
       return duration; //by hours 
       }
       
       // 根据数据文件的名字来决定图表的 title, x 轴和 y 轴的标签 
       private void parseFileName() {
       String[] pathParts = dataFile.split("/");
       int len = pathParts.length;
       Matcher m = FILE_PATTERN.matcher(pathParts[len-1]);
       if (m.matches()) {
       chartType = m.group(1);
       }
       if (chartType.equals("cpu")) {
       series.add(new XYSeries("User"));
       series.add(new XYSeries("System"));
       series.add(new XYSeries("WaitIO"));
       series.add(new XYSeries("Idle"));
       title = "SYSTEM CPU";
        yAxisLabel = "Percentage";
       } else if (chartType.equals("mem")) {
       series.add(new XYSeries("Inuse"));
       series.add(new XYSeries("Free"));
       series.add(new XYSeries("Pin"));
       series.add(new XYSeries("Virtual"));
       title = "SYSTEM MEMORY";
        yAxisLabel = "pages";
       } else {
       series.add(new XYSeries("Inuse"));
       series.add(new XYSeries("Pin"));
       series.add(new XYSeries("Pgsp"));
       series.add(new XYSeries("Virtual"));
       title = chartType.toUpperCase();
       yAxisLabel = "pages";
       }
       }
       
       // 创建数据集
       private XYDataset createDataset() {
      
       XYSeries s1 = series.get(0); 
       XYSeries s2 = series.get(1); 
       XYSeries s3 = series.get(2); 
       XYSeries s4 = series.get(3); 
       try {
       if(!dataFile.startsWith("/")) { dataFile = workDir + "/" + dataFile; }
       BufferedReader in = 
       new BufferedReader(new FileReader(dataFile));
          String line;
          while((line = in.readLine()) != null) {
           Matcher m = DATA_LINE.matcher(line);
           if (m.matches()) {
           double x = getTimeInDouble(m.group(1)); 
           double y1 = getYValueInDouble(m.group(2)); 
           double y2 = getYValueInDouble(m.group(3)); 
           double y3 = getYValueInDouble(m.group(4)); 
           double y4 = getYValueInDouble(m.group(5)); 
      
           s1.add(x, y1); 
           s2.add(x, y2); 
           s3.add(x, y3);
           s4.add(x, y4); 
           } else {
           }
          }
      
       in.close();
       } catch (Exception e) {
          System.err.println(e.toString());
       }
      
       XYSeriesCollection dataset = new XYSeriesCollection();
       dataset.addSeries(s1);
       dataset.addSeries(s2);
       dataset.addSeries(s3);
         dataset.addSeries(s4);
       return dataset;
       }
      }
      

      在 AIXPerfChart.java 源文件当中,函数 getTimeInDouble 计算每个监测时间点距开始监测时经过的时间。由于命令行输出的时间点是以二十四小时制计量并且不含日期信息,因此如果当前的监测时间点小于前一个监测时间点,就表明当前时间已经进入新的一天。在计算监测时间的时候特别需要注意到这个因素,不然会得到错误的监测时间。至于具体的计算方法,请参照清单 2 中的代码。

      另外,由于篇幅的关系,在清单 2 中没有列出函数 createChartexportChartCreateChart 通过调用 createDataset 函数生成数据集,然后生成图表 , exportChart 则把图表按照指定的图像文件格式导出为图像文件。在 createChart 中还可设定图表的相关属性,比如大小,背景颜色等等。

      编写好 AIXPerfChart.java 后 , 让我们用下面的命令来进行编译 :

      javac -cp ./jcommon-1.0.5.jar:./jfreechart-1.0.1.jar com/aix/chart/AIXPerfChart.java
      

      编译成功后,我们今后可以通过 aixperfchart.sh 这个脚本来调用绘制图表的功能。


      清单 3 aixperfchart.sh

      #!/bin/sh
      
      # 存放 AIXPerfChart.class 和 JFreeChart lib 的路径
      AIX_CHART=/opt/aixperfmonitor/src
      
      java -cp ${AIX_CHART}:${AIX_CHART}/jfreechart-1.0.1.jar:${AIX_CHART}/jcommon-1.0.5.jar \ 
      com.aix.chart.AIXPerfChart $@
      

      最后,我们来编写脚本 htmlreport.sh,它帮助我们定期把性能数据绘制成图表并发布到 WEB 服务器上。


      清单 4 htmlreport.sh( 只显示部分源代码 )

      ############################################################## 
      # 通过 STAF 从 AIX 机器上获取性能数据,/perflog 为性能数据文件在 AIX 机器上的默认存放目录 #
      ##############################################################
      

      ret=`staf $server process start shell command "staf local fs copy directory /perflog \

      todirectory $dataDir tomachine 9.168.0.2" workdir /tmp wait returnstdout returnstderr`

      ##########################################################   
      # 根据性能数据文件绘制图表 #
      ###########################################################
      
      aixperfchart.sh mem.dat $dataDir $imgFormat
      aixperfchart.sh cpu.dat $dataDir $imgFormat
      charts[0]="mem.$imgFormat"
      charts[1]="cpu.$imgFormat"
      index=2
      
      cd $dataDir
      
      # 如果有多个进程的数据文件 , 逐一绘制相应的图表
      images=`ls *.dat|grep -v mem|grep -v cpu`
      
      for img in $images; do
       aixperfchart.sh $img $dataDir $imgFormat
       charts[$index]="${img%%.*}.$imgFormat"
       let "index=$index+1"
      done
      
      ##########################################################
      # 生成包含性能图表的 html 文件 , 并发布到 WEB 上 #
      ##########################################################
      
      #
      htmlfn="${dataDir}/perfchart.html"
      echo "<html><head></head><body>" > $htmlfn
      if [ -z $server ]; then
       echo "<h2> Perf Charts</h2>" >> $htmlfn
      else
       echo "<h2> $server Perf Charts</h2>" >> $htmlfn
      fi
      echo "<table cellpadding=\"3\" cellspacing=\"0\">" >> $htmlfn
      curIndex=0
      for (( curIndex=0; curIndex < $index; curIndex++ )) ; do
       let "i = $curIndex % 2"
       if [ "$i" -eq 0 ] ; then
       echo "<tr>" >> $htmlfn
       fi
       imageFile=${charts[$curIndex]}
       dataFile="${imageFile%%.*}.dat"
       echo "<td align="center"><img src=\"${charts[$curIndex]}\" width=600 height=320/>\
       <br/><a href= \"${dataFile}\">${dataFile}</a></td>" >> $htmlfn
      
       if [ "$i" -eq 1 ] ; then
       echo "</tr>" >> $htmlfn
       fi
      done
      
      echo "</table></body></html>" >> $htmlfn
      

      在脚本 htmlreport.sh 中有两个地方值得我们注意。一个是调用 STAF 的 FS service 把性能数据文件从 AIX 机器拷贝到 Linux 机器上。为方便起见,建议直接把数据文件拷贝到 web 服务器的发布目录,这样就不需要在发布性能数据及图表时再次拷贝了。另外一个是绘制图表的代码,我们把生成的图表文件名存储在 charts 数组中。考虑到读者可能需要监测多个进程,代码并没有只是简单生成 java 进程的图表,而是逐一为可能存在的进程生成图表并且存储到 charts 数组中。

      有了 htmlreport.sh 脚本之后,我们就可以用下面的命令调用这个脚本让它定期绘制图表,并把图表发布到 WEB 服务器。

      bash-3.00# nohup ./htmlreport.sh -t <duration> -i <interval> 2>&1 \
      >/tmp/htmlreport &
      

      假定性能数据以及图表被发布到 web 服务器的 perflog 目录,那么打开浏览器,输入下面的 URL,我们就能看到监测数据的图表了。

      http://9.168.0.2/perflog/perfchart.html


      图 2 显示性能图表的 web 页面


      结束语

      在本文中,我介绍了如何利用 AIX 系统自带的性能工具包和 JFreeChart 图表库去构建一个简单易用的性能监测工具。通过简单的扩展,这个工具可以完成更多的监测功能,比如说,监测磁盘使用情况,监测 IO 使用情况,等等。


      参考资料

      学习

      获得产品和技术

      • IBM 试用软件:从 developerWorks 可直接下载这些试用软件,您可以利用它们开发您的下一个项目。

      讨论

      关于作者 

      戴建武,IBM 测试工程师,现在在 IBM CDL 语音组从事 Webshpere Voice Server 的系统测试工作。

       

       

    9. 结合使用 Shell 和 STAX 实现 UAT 测试的自动化(转载)

      2011-08-30 14:11:57

      原文地址:http://www.ibm.com/developerworks/cn/opensource/os-cn-stafuat/index.html

      2009 年 2 月 13 日

      文章分析了 UAT 的特性以及在 UAT 中实现测试自动化的重要性,进而提出了一个结合应用 Shell 脚本和 STAX 语言实现的从自动化下载 Build, 安装, 执行测试用例, 生成测试报告的自动化解决方案, 对其中每一个部分进行了具体的分析和实现。

      UAT与测试自动化

      UAT 全称为 User Acceptance Test, 即用户可接受测试,是IBM产品开发周期的重要一环,

      处于产品的构造和功能性测试之间,其目的是验证每天的构造(daily build)的可接受性,即能否在这个 build 上进行后续的功能性测试。由于 UAT 是测试环节的首要步骤,能够尽快给开发人员提供测试结果,所以不管采用传统的软件开发模型,还是使用今天被广泛接受的敏捷开发模式,UAT 在产品开发周期中所扮演的角色均非常重要。

      传统的 UAT 测试基本都是人工完成的,如产品的安装,基本的配置,针对各个模块的功能执行测试用例等等。这种人工的测试方法会有很多问题。首先就是会存在大量的重复性劳动。在目前的软件产品开发过程当中,迭代式开发很常见。开发人员很可能会将已经完成部分功能的软件产品交付测试人员测试,而在测试人员测试的过程中根据测试人员的反馈或者用户需求的改变不断完善产品。这样一来一天可能会有3到5个版本的安装文件产生,而测试人员需要针对每个版本的安装文件执行 UAT 一系列操作,也就是说同样的测试步骤可能要被重复执行3到 5次;其次就是对于大型项目而言,UAT 操作很耗费时间,一次UAT测试的时间可能从 2,3 个小时到 7,8 个小时,每天需要多种平台和多重类型的 build,重复性的操作对测试人员来说是个很大的负担。

      将 UAT 自动化,一方面无人职守的全自动化测试方案会使得 UAT 测试提供 24*7 的服务成为可能,自动检测 build 的状态,下载并进行安装、配置,执行测试用例, 最后生成测试报告,减少了等待时间,加速了产品的测试过程,缩短了开发周期。另一方面,自动化程度的提高解放了测试人员,使得UAT测试人员可以有更多的时间进行一些提高测试覆盖率,准确性方面的工作,更好的保证了测试质量。本文针对 UAT 的特点,提出了结合使用 Shell Script. 和 STAF/STAX 的 UAT 自动化测试解决方案,具有简便、轻量级、开发周期短、灵活性好等特点。结合 shell 脚本对 STAX 任务进行触发并且设计一系列的调度机制使得自动化系统可以对资源进行合理调配,从而使得无人值守的全自动化测试成为可能。另外通过该方案的实施,能够提高软件测试环节中的自动化程度和测试用例的可复用程度,很大程度上提高了测试效率,缩短了测试周期。本文提出并分析了一个一般化的用户可接受性测试包含的各个部分,提出的自动化解决方案适用于所有 web-based 的产品的下载、安装部署、配置、功能测试。作为例子,文中将使用通过此方案在 IBM WebSphere Portal 产品的 UAT 测试当中的应用来对此方案进行分析。





      为什么采用 STAF/STAX

      STAF(Software Testing Automation Framework)是开源的自动化测试框架,封装了不同平台和不同语言间通信的复杂性,提供了消息、互斥、同步、日志等可复用的服务,使用户可以在此基础上构建自动化测试解决方案。STAX(STAF Execution Engine)是运行在 STAF 之上的可解析、执行 XML 格式任务的一种服务。STAF提供了两种服务,一种是内部服务(Internal Service),即它在内部封装了一系列的常用服务供用户使用, 包括时间操作,文件系统操作,变量操作等;另外一种是外部服务(External Service),STAX 本身就是 STAF 的一种外部服务,可以通过 STAX 规定的 XML 格式来编写 STAX 脚本,实现测试人员的测试步骤。

      STAX 与传统的测试脚本,如 Perl,Shell 等,相比较,存在以下优点:

      • STAX 通过 STAF 的支持,能够轻松实现与其他测试机通讯的问题,对用户来说,通讯过程是完全透明的;
      • STAF 本身具有跨平台的特性,可以拓展到不同的平台,而且STAF可以根据测试机的操作系统类型来定义一些在 STAX 脚本当中需要用到的变量,如文件路径分隔符(File Separator)等,这对用户来说十分方便;
      • STAX 脚本具有非常好的兼容性,完全可以在 STAX 脚本当中执行 shell 或者 bat 脚本;
      • STAX 脚本完全是基于 xml 格式的,脚本开发方便;
      • STAX 函数之间可以相互调用,因此能够更加灵活的实现函数的复用;

      下面清单所示的代码片段说明了 STAX 任务的基本结构。


      清单 1. STAXDemo.xml

      <function name="listDir" scope="local">
      <function-prolog>This function lists entries of specified directory on the target
       machine</function-prolog>
      <function-list-args>
      <function-required-arg name="target">Hostname/IP address of the target machine
      </function-required-arg>
      <function-required-arg name="targetDirectory">Target directory </function-required-arg>
      </function-list-args>
      <sequence>
        <script>cmd = 'LIST DIRECTORY %s' % (targetDirectory) </script>
        <!-- Run the staf list directory command using the supplied arguments -->
        <stafcmd>
          <location>target</location>
          <service>'FS'</service>
          <request>cmd</request>
        </stafcmd>
        <return>STAFResult</return>
      </sequence>
      </function>
      
      

      根据上面的代码,我们可以大致看出 STAX 脚本的语法格式:

      首先定义此功能模块的名字,在这里是“listDir”,相当于传统语言中的函数名。然后在<function-list-args>元素里面将脚本需要定义的参数列举出来,相当于传统编程语言中仅能够在函数内部使用的局部变量。如果下面还需要用到一些变量的时候,可以通过<script>元素来定义,如“cmd”变量,这里定义为:LIST DIRECTORY %s' % (targetDirectory),LIST DIRECOTRY 是STAF内部服务的一种,相当于DOS系统的dir命令或者linux系统的ls命令。“%s”是通配符,匹配的是后面括号当中的targetDirectory变量的内容,而targetDirectory是在<function-list-args>元素当中定义的。接下来有一个<stafcmd>元素,这个元素有三个子元素:<location><service><request>。其中location元素指的是这个服务应用的目标机器,service元素表示的是该服务的名称,这里“FS”,即File Service,而request元素指的是具体的服务命令字符串,这里是在前面定义过的变量cmd







      基于 STAF/STAX 的 UAT 自动化测试解决方案


      图 1. UAT 自动化测试解决方案的流程图
      UAT 自动化测试解决方案的流程图

      通常一个UAT的测试流程包括以下几个部分: 检测Build状态,下载Build,检测测试机状态,进行安装,进行配置,执行一些测试用例来验证各个部件的功能,生成测试报告。为了完全覆盖以上的这几个方面,基于STAF/STAX的UAT自动化测试解决方案主要包括以下几个步骤:

      第一步,检测安装文件的状态

      通常,对于一个成熟的软件产品,每天都会有一个或者多个版本的build。这些build一旦生成便会被上传到一台专门的服务器上。当生成过程结束之后,测试人员就可以下载这个版本的build进行测试,而UAT测试是整个测试流程的起点。因此,最节约时间的做法就是当文件刚刚生成结束之后就开始UAT测试,所以需要自动化的方法来不断检测build生成过程是否结束,一旦生成结束,就可以马上开始UAT操作。但是对于一个项目而言,UAT测试人员很难立即得到Build过程完成的信息,这有很多因素:

      • 对于合作开发和测试,Build Team和UAT team不一定在同一地理位置,可能处于不同的时区;
      • 对于大的项目,一次Build的时间可能会很长,从几小时甚至长到十几小时;
      • 对于复杂项目,一个Build成功与否存在着很大的不确定因素,经常会存在构建失败的情况;

      因此, 测试人员人工来检测Build生成状态有很大的难度。Build Team通常提供一些对外的服务来发布Build的信息,比如维护一个HTTP Server或者FTP Server来让远程用户能直接通过这些服务来获得Build的目录结构以便于下载Build。在UAT的自动化方案中,就可以通过Shell脚本访问这些服务来获得Build的信息。比如对于如下的目录结构。


      图 2. Build 目录结构示例
      Build 目录结构示例

      就可以使用如下的shell脚本来获得最新的Build的版本号:


      清单 2. 使用 shell 脚本获取最新 Build 的版本号

      while [ ! -s "./builds.html" ] 
      do
        sleep $sleeptime
        #将所有Build所在的目录存成一个文件待用
        wget $Remote_realese_url -O ./builds.html" >> $LOG_BASEDIR/`date +%Y-%m-%d-%H`.log
      done
      #从保存的文件中提取最新的Build版本信息,这里使用AWK在文件中来获得最新版本信息
      latestversion=`cat builds.html | awk -F"STARTPattern" '{print $2}' | awk -F"EndPattern"
      '{print $1}' | grep $VERSIONPATTERN | sort | tail -1`
      if [ ! -d "$BUILDS_DIR/$ latestversion " ]; then
      #在本地创建当前Build版本的目录
      mkdir $BUILDS_DIR/$ latestversion 
      fi
      
      

      第二步,使用 Shell 下载 Build 并开始触发后续 STAX 任务

      服务器端 build 生成结束之后,接下来是 build 下载的过程,这部分可以和步骤一结合在一起,一旦检测到 build 正确生成,那么马上开始下载。Build 的下载可以使用shell脚本实时访问,根据 Build 服务器提供的不同服务接口使用不同的检测 build 并下载 build 的方式,常用的有 FTP 方式,HTTP 方式或者 HTTPS 方式。在判断Build存在,创建完相关的本地目录之后,对于一个 Build 需要的所有文件,可以使用相应的命令来进行下载。最简单的有 wget,ftp。 也可以写一些相对复杂的shell脚本加强下载的稳定性,可靠性和灵活性。

      检测 Build 状态和下载 Build 都是由 Unix Shell 脚本执行的,而此后的测试机状态检测以及以后的安装都由 STAX 脚本来执行。那么怎样由 Unix shell 来执行 STAX 脚本呢,STAX 的事件机制使得它的任务可以由外部触发,这个触发的方法如下:


      清单 3. 使用 shell 脚本促发自动化测试任务

      #Trigger STAX event
      echo "start"
      while [ "$parameterFileNumber" != "0" ] 
      do
      echo "start a STAX event..." 
      echo "kick off STAX event, file number is $parameterFileNumber"
      let "parameterFileNumber=$parameterFileNumber-1"
      STAF STAXServer.cn.ibm.com SEM POST EVENT "$eventName"
      sleep 600
      echo "a STAX event was triggered successfully!" 
      done
      echo "Kick off UAT finished!" 
      
      

      第三步,检测测试机状态

      对于一个成熟的企业及软件产品来说,测试环境往往比较复杂,而在测试环境的搭建过程当中任何一个小的问题都有可能导致测试的失败,这并不是软件本身的问题,所以在开始执行测试流程之前如何检测测试环境的正确性是十分重要的。针对这一环节,自动化解决方案如图:


      图 3. 检测测试环境流程图
      检测测试环境流程图

      • 首先,分别针对不同的测试环境制定测试任务。这些测试任务存放在一个单独的任务队列里面;
      • 当第二步下载build结束之后,马上检测任务队列里面是否有等待执行的任务,如果有的话,马上执行当前测试任务,这里利用STAF的跨平台性,可以将同一个测试任务针对不同的平台分成多个任务,并发执行;
      • 执行测试任务的第一步,就是检测目标测试机的环境及状态,通过 STAX 脚本在目标测试机上收集测试相关的环境信息,如操作系统的类型,版本,服务器,数据库的型号及版本等等;
      • 接下来是对收集到的环境信息加以判断,如果环境符合当前的测试要求,则继续执行下一步;否则生成错误日志,发送测试报告给测试人员,中断测试流程;
      • 测试环境的检测通过之后,则开始执行真正的测试任务;

      第四步,安装软件

      在 UAT 测试流程当中,当 build 下载过程结束,环境检测通过,接下来的就是软件产品的测试,也是整个测试流程一个真正意义上的“起点”,如果在接下来的测试过程中出现问题,很有可能就是产品本身的问题。

      首先,就是软件的安装,一般软件的安装往往分为静默安装和图形界面安装,针对这两种不同的安装方式,自动化测试解决方案如下:

      1. 静默安装:
        1. 静默安装的第一步是准备一个安装过程中的响应文件(response file),这个文件包括了整个安装过程当中所有需要用到的参数,比如路径信息,安装组件的选择等等。在本方案中,这个响应文件是根据用户事先配置的参数通过 STAX 脚本自动生成的,而且用户可以在提交或者修订测试任务的时候灵活的对这个响应文件进行更改;
        2. 接下来,STAX 脚本会自动生成一个命令文件,根据操作系统的不同会分别生成 bat 文件或者 .sh 文件,命令文件的内容为执行安装过程需要的命令;
        3. 然后,执行刚刚生成的命令文件,并将安装过程中一些重要的信息记录在日志文件当中,这些日志文件是以后生成测试报告的重要依据;
        4. 在安装的过程中,STAX 任务会不间断的监测测试机的状态和软件安装的状态,一旦安装过程出现不可逆的异常或者错误,则马上中断测试流程,发送测试报告给测试人员;
      1. 图形界面安装:
        1. 因为STAF/STAX本身并不对任何图形界面的测试提供支持,所以,在执行图形界面安装的过程中首先需要采用其他的工具进行安装脚本的录制或者编写,可以采用的工具有IBM Rational Functional Tester, Auto It等。可以针对不同的环境分别录制多个安装脚本,然后分别存放,这样的好处就是在测试过程中可以根据测试目标机的环境灵活调用安装文件,而且如果环境没有大的改动,完全可以实现安装脚本的重复使用;
        2. 在自动化测试过程当中,首先由STAX脚本根据目标机的环境自动拷贝相应的安装脚本到测试机上,然后执行该安装脚本;
        3. 同静默安装一样,在安装流程开始之后,STAX任务会不间断的进行监控并及时处理出现的问题;

      安装软件的STAX脚本片段如下:


      清单 4. 安装WebSphere Portal的STAX脚本片断

      <stax>
      <function name="InstallWebSpherePortal">
      <function-prolog>InstallWebSpherePortal</function-prolog>
      <function-list-args>
      </function-list-args>
      <sequence>
      <!-- Step 1. Mount build server -->
      <call function="'mountBuildServer'">target, mountServer, 
      remoteMountPoint, localMountPoint</call>
      <!-- Step 2. Copy image and create dir-->
      <call function="'copyImage'">target,destinyDir,destinyFPDir,destinyFile, 
      localMountPoint,buildPuiDir,buildPtfDir,buildPuiNum,buildPtfNum, 
      buildZipFileName</call>
      <if expr="STAXResult == 0">
      <call function="'writeLog'">[target, 'Success copy image files .']</call>
      </if>
      <!-- Step 3. Stop all servers-->
      <call function="'stopAllServers'">target,portalProfileRoot,
      WPSUsername,WPSPasswd, WASUsername,WASPasswd</call>
      <if expr="STAXResult == 0">
      <call function="'writeLog'">[target, 'Stop all servers success !']</call>
      </else>
      </if>
      <!-- Step 4. Start to install-->
      <sequence>
      <call function="'StartInstall'">target,portalServerDir,
      setUpCmdLineDir,destinyDir, destinyFPDir,WPSPasswd,
      WASPasswd,portalConfigDir</call>
      <if expr="STAXResult == 0">
      <call function="'writeLog'">[target, 'Fixpack install finish success !']</call>
      </if>
      </sequence>
      <return>0</return>
      </sequence>
      </function>
      </stax>
      
      

      第五步,对安装好的软件进行配置

      一般来说,在软件安装结束之后需要进行一些基本的配置操作,如软件调优,配置数据库数据源, 配置安全性等。基本上在 IBM 产品中这些操作都有相同的特性,即首先修改配置文件,然后执行配置命令, 中间会涉及到一些停止或者启动服务器的操作。针对不同的软件及操作系统,这些操作都可以结合事先写好的 Shell 或者 Bat 脚本,然后通过 STAX 脚本来触发这些脚本。与安装软件的过程类似,可以通过 STAX 任务实时监控整个过程的状态并及时汇报给测试人员。这一步骤成功结束之后,自动进入下一个测试环节。对于很多产品来说,两个基本的配置工作分别是进行数据库的配置和安全性的配置,例如在 WebSphere Portal 产品中,数据库迁移配置和 LDAP 的安全配置是不可缺少的两个配置步骤,实现这种配置的 STAX 示例脚本如下:


      清单 5. Enable Security STAX 脚本片段

      <stax>
      <function name="enableSecurity" scope="local">
      <function-prolog>'Enable security for the portal on the target machine'</function-prolog>
      <function-list-args>
      <!-- Define arguments-->
      </function-list-args>
      <sequence>
      <!-- Step 1. Stop Portal. -->
      <call function="'retrieveWasPortalStatus'">[target, portalProfileRoot, wasUserId, 
       wasPassword]</call>
      <if expr="STAXResult == 0">
      <sequence>
      <call function="'writeLog'">[target, 'Portal is already started, begin to stop it...', 
      'user1']</call>
      <call function="'stopWasPortal'">[target, portalProfileRoot, wasUserId, 
      wasPassword]</call>
      <if expr="STAXResult == 0">
      <call function="'writeLog'">[target, 'Stop Portal successfully.', 'user1']</call>
      </if>
      </sequence>
      </if>
      <!-- Step 2. Run task -->
      <call function="'runConfigEngineTask'">[target, portalProfileRoot, 
      'wp-modify-ldap-security']</call>
      <if expr="STAXResult == 0">
      <call function="'writeLog'">[target, 'Task wp-modify-ldap-security succeeded.', 
      'info']</call>
      </if>
      <!-- Step 3. Start portal -->
      <call function="'startWasPortal'">[target, portalProfileRoot]</call>
      <if expr="STAXResult == 0">
      <sequence>
      <call function="'writeLog'">[target, 'Task EnableSecurity succeeded!', 'pass']</call>
      <return>0</return>
      </sequence>
      </if>
      </sequence>
      </function>
      </stax>
      
      


      清单 6. DBTransfer STAX 脚本片段
      <stax>
      <function name="DBTransfer" scope="local">
      <function-prolog>'Transfer DB'</function-prolog>
      <function-list-args>
      <!-- Define arguments-->
      </function-list-args>
      <sequence>
      <!-- Step 1. Modify property files -->
      <call function="'MergeProperyFilesTool'">
       [target, targetDirectory, targetFileName1, sourceFileName1, MergePropertyScript, 
       domains1, append] 
      </call>
      <call function="'MergeProperyFilesTool'">
      [target, targetDirectory, targetFileName2, sourceFileName2, MergePropertyScript, 
      domains2, append] 
      </call>
      <!-- Step 2. Run task -->
      <call function="'runConfigEngineTask'">[target, portalProfileRoot, 
      'database-transfer']</call>
      <if expr="STAXResult == 0">
      <call function="'writeLog'">[target, 'Task database-transfer succeed.', 'user1']</call>
      </if>
      <!-- Step 3. Start portal -->
      <call function="'startWasPortal'">[target, portalProfileRoot]</call>
      <if expr="STAXResult == 0">
      <sequence>
      <call function="'writeLog'">[target, 'Task DBTranfer succeeded!', 'user1']</call>
      <return>0</return>
      </sequence>
      </if>
      </sequence>
      </function>
      </stax>
      
      

      第六步,执行RFT测试用例

      在本文的开头已经对 UAT 做了简要的介绍,UAT 是保证一个新的版本的软件或者软件补丁产生之后,其基本的各个功能模块能够正常运行的测试环节。所以,在软件安装和配置工作结束之后,执行覆盖软件基本功能的测试用例,是很有必要的。

      对于基于 Web 的产品,通常用RFT针对不同软件的功能实现写好测试脚本就已经能够单独运行。为了实现 UAT 的全部过程自动化,必须将这一部分同 STAX 脚本进行连接,用 STAX 触发这些测试脚本并实时监控脚本的执行状态。通过 STAX 来触发 RFT 的 web 自动化脚本的任务示例如下:


      清单 7. 触发 RFT 自动化测试的 STAX 脚本片段

      <stax>
      <function name="runExampleRFTTest" scope="local">
      <function-prolog>Runs a RFT testcase using LA</function-prolog>
      <function-list-args>
      <!-- Define arguments-->
      </function-list-args>
      <sequence>
      <!-- Run the RFT test on the RFT server -->
      <call function="'cafRunRFTTest'">
      {
      'rftServer' : target, 
      'properties' : mySuite, 
      'rftProjectArchive' : 'LWPServerGUI.zip', 
      'rftScript' : rftScript, 
      'rftScriptArgs' : rftArgs, 
      'maxExecutionTime': maxTime, 
      'browserName': browserType, 
      'browserPath': browserLocation, 
      'browserCommand': browserCmd, 
      'rftExecutionMode' : 'standalone'
      }
      </call>
      <script>(rftRunTestRC,rftRunTestCallResult) = STAXResult</script>
      <return>rftRunTestRC</return>
      </sequence>
      </function>
      </stax>
      
      

      第七步,生成测试报告和日志

      在所有的测试步骤结束之后,或者如果中途有环节出现测试错误,可以通过 STAF/STAX 生成一份测试报告并发送给测试人员,测试报告当中包括详细的测试结果,失败原因等等。试想一下,每天早上来上班的时候,一边喝咖啡一边打开邮箱,发现原来需要花费一天时间的工作已经在昨天夜里执行完毕并且有一份完整的测试报告发送到邮箱,难道不是一件很惬意的事情吗?

      作为测试报告的来源,日志是自动化测试任务的重要组成部分,一方面测试任务的开发人员可以利用日志来调试脚本,另一方面,日志作为测试报告的一部分使得测试任务的使用者了解测试任务执行结果以及执行过程信息。自动化脚本开发人员通过在脚本中调用 STAF/STAX 提供的 LOG Service 来生成纯文本格式的任务日志,较为方便。但其缺点在于不直观,由于日志本身的层次结构淹没在海量的文本中,测试人员很难快速地找到自己想要了解的信息。针对这一缺点,我们把测试任务的结构自顶向下分为以下三个层次:

      • 测试用例(Testcases):测试任务的顶级层次,描述此次测试任务的主要目标。一个测试任务由一个或者多个测试用例组成,同一个测试任务的多个测试用例可以串行、也可以并行执行。测试用例在任务的执行过程中,有“开始”、“成功”、“失败”等不同的状态;
      • 步骤(Steps):描述了测试用例执行过程中的关键步骤,一个测试用例由多个步骤组成。步骤往往是顺序执行的,也有“开始”、“成功”、“失败”等不同的状态,步骤的执行结果决定了测试用例的结果;
      • 细节(Details):组成测试任务的基本单位,由程序员在测试任务脚本中调用STAF/STAX的LOG服务自行记录,根据STAF对的LOG定义,一条日志信息可以有“info”、“debug”、“warning”、“error”等不同的类型;

      这样,在任务执行的过程当中会有一份更加友好的日志生成,包括各个步骤的执行情况,测试用例的通过情况等,日志的概述结果就可以当作测试报告发送给测试者。生成的测试报告格式如下图,这是一个基于HTML格式的报告。


      图 4. HTML 格式的任务报告
      HTML 格式的任务报告

      下面所示的STAX代码片断提供了记录日志的函数writeLog,它接受日志内容、日志状态、日志类型等参数,向该任务的日志目录下的日志文件中追加一条日志。


      清单 8. 自定义STAX函数writeLog

      <function name="writeLog" scope="local">
      <function-list-args>
      <function-required-arg name="content"></function-required-arg>
      <function-optional-arg name="level" default="'info'"></function-optional-arg>
      <function-optional-arg name="type" default="'detail'"></function-optional-arg>
      <function-optional-arg name="state" default="'start'"></function-optional-arg>
      </function-list-args>
      <sequence>
      <script>
      displayLogName = 'Automation Runtime Text Output Log'
      #the actual name of the text log file. 
      logFileName = '%s.txt' % STAXJobID
      #if content contains multiply lines, separate it
      from string import *
      lines = split(content, '\n') 
      </script>
      <iterate in="lines" var="line">
        <sequence>
          <if expr="type == 'testcase'">
           <sequence>
              <script>line = '*TESTCASE %s:%s' % (state, line)</script>
           </sequence>
          <elseif expr="type == 'step'">
              <script>line = '*STEP %s:%s' % (state, line)</script>
          </elseif>
          </if>
          <call function="'cafAppendJobOutputLog'">[target, 'local', line
         displayLogName, logFileName]</call>
        </sequence>
      </iterate>
      </sequence>
      </function>
      
      

      值得注意的是,为了进一步解析生成 HTML 日志,我们把日志的类型和状态信息通过固定的标签写入了日志之中:

      *TESTCASE  STATE TestcaseName
      *STEP STATE StepName
      

      对于一个确定的测试用例或者步骤,上述的标签总是成对出现的,这样才能保证能够正确的解析到它们的边界。LogHelper为一个 Python 类,它封装了从文本日志中解析层次信息后生成 html 格式日志的细节,下图为LogHelper类图。


      图 5. LogHelper 类图
      LogHelper 类图

      下面的代码片断为LogHelper类的核心方法parserLog方法。


      清单 9. 使用 Python 构建的 LogHelper 类的 parseLog 方法

      def parseLog(self, logTxtFile): 
      """
      Parse the txt log file, information of testcases and steps will be assigned to
      the data field of this class.Translate the common txt lines to html and assign
      it to the htmlContent field. 
      """
          try: 
              self._txtLogFileName = logTxtFile
              fp = open(self._txtLogFileName, 'r') 
          except IOError, e: 
              return (1, 'Open file %s failed' % logTxtFile) 
          #use stacks to contain the parsed entries
          testcases = []
          steps = []
          for line in fp.readlines():
              #remove the timestamp
              logInfo = line.strip().split('\t', 1)[-1] 
              if logInfo.startswith('*TESTCASE') or logInfo.startswith('*STEP'): 
              #it is a tag
                  type,name = logInfo.split(':', 1) 
                  type,state = type.split(' ', 1) 
                  if type == '*TESTCASE': 
                      if state == 'start': 
                          testcases.append(name) 
                          self.addTestcase(name) 
                      else: 
                          testcases.pop()
                          try: 
                              self.closeTestcase(name, state) 
                          except EntryNotFoundException, e: 
                              return (1, e.msg) 
                  else: 
                      if state == 'start': 
                          steps.append(name) 
                              try: 
                                  self.addTestcaseStep(name, testcases[-1]) 
                              except EntryNotFoundException, e: 
                                  return (1, e.msg) 
                      else: 
                          steps.pop()
                          try: 
                              self.closeTestcaseStep(name, testcases[-1], state) 
                          except EntryNotFoundException, e: 
                              return (1, e.msg) 
          fp.close()
          return (0, '')
      

      代码下载 (code downloads)

      单击下载logHelper.py代码

      通过以上的步骤描述看出,测试人员所有的工作仅仅是制定测试任务,查看测试结果,而且测试任务仅仅需要制定一次就可以通过加入任务队列的方式来重复执行,可见,这种基于STAF/STAX的UAT自动化测试解决方案极大程度上减少了测试人员的工作量,节约了测试人员宝贵的时间。






      总结

      以上阐述了基于STAF/STAX并结合使用Shell Script的UAT自动化解决方案,并且结合本团队负责测试的逐个步骤进行了分析。根据UAT测试人员给予的反馈信息,这样的一套全自动测试平台可以节约测试人员约80%的测试时间,极大提高了工作效率。同样的方法结合使用shell脚本和STAX脚本可以实现很多日常工作中的自动化工作,在这方面,我们正在进行更多的尝试。



       

      参考资料

      • Software Testing Automation Framework (STAF) 是一个开源的、多平台、多语言框架,详细内容请参考http://staf.sf.net。

      • 更多有关 Software Testing Automation Framework (STAF) 内容,请参考软件测试自动化专题。

      • 访问 developerWorks Open source 专区,获得丰富的 how-to 信息、工具和项目更新,帮助您用开放源码技术进行开发,并与 IBM 产品结合使用。



       

      作者简介

      乌晓峰,IBM 中国开发中心软件工程师,曾从事WebSphere Portal 的测试工作和 UAT 的自动化开发,现在负责 WPLC 产品的软件安全测试。


      肖慧斌,IBM 中国软件开发中心实习生,负责 WebSphere Portal UAT 的 automation 开发。


      李夏安, IBM 中国软件开发中心实习生,负责 WebSphere Portal UAT 的 automation 开发。




      原文链接: http://www.ibm.com/developerworks/cn/opensource/os-cn-stafuat/index.html

    10. MySQL数据库服务器优化

      2011-08-30 11:57:34

      MySQL数据库服务器优化

      转自:http://www.iteye.com/topic/154700

      我最近在网上查了一些有关优化MySql的资料,并对照MySql手册对一些调优设置进行了细结,但是由于时间比较勿忙,没来得及对这些设置进行实验/测试,你可以把这些方法应用到实际中,得出具体结论。调优方法大致如下:

      MySql服务器的后台管理程序,要想使用客户端程序,该程序必须运行,因为客户端通过连接服务器来访问数据库。下面让我们以服务器的系统变量和状态变量为根据,优化我们的MySql数据库服务。


      在这之前,我们需要掌握以下方法:


      查看MySql状态及变量的方法:


      Mysql> show status ——显示状态信息(扩展show status like 'XXX')
      Mysql> show variables ——显示系统变量(扩展show variables like 'XXX')
      Mysql> show innodb status ——显示InnoDB存储引擎的状态
      Shell> mysqladmin variables -u username -p password——显示系统变量
      Shell> mysqladmin extended-status -u username -p password——显示状态信息


      查看状态变量及帮助:


      Shell> mysqld --verbose --help [|more #逐行显示]


      首先,让我们看看有关请求连接的变量:


      为了能适应更多数据库应用用户,MySql提供了连接(客户端)变量,以对不同性质的用户群体提供不同的解决方案,笔者就max_connections,back_log 做了一些细结,如下:


      max_connections 是指MySql的最大连接数,如果服务器的并发连接请求量比较大,建议调高此值,以增加并行连接数量,当然这建立在机器能支撑的情况下,因为如果连接数越多,介于MySql会为每个连接提供连接缓冲区,就会开销越多的内存,所以要适当调整该值,不能盲目提高设值。可以过'conn%'通配符查看当前状态的连接数量,以定夺该值的大小。


      back_log 是要求MySQL能有的连接数量。当主要MySQL线程在一个很短时间内得到非常多的连接请求,这就起作用,然后主线程花些时间(尽管很短)检查连接并且启动一个新线程。back_log值指出在MySQL暂时停止回答新请求之前的短时间内多少个请求可以被存在堆栈中。如果期望在一个短时间内有很多连接,你需要增加它。也就是说,如果MySql的连接数据达到max_connections时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即back_log,如果等待连接的数量超过back_log,将不被授予连接资源。另外,这值(back_log)限于您的操作系统对到来的TCP/IP连接的侦听队列的大小。你的操作系统在这个队列大小上有它自己的限制(可以检查你的OS文档找出这个变量的最大值),试图设定back_log高于你的操作系统的限制将是无效的。


      优化了MySql的连接后属性后,我们需要看看缓冲区变量:


      使用MySql数据库存储大量数据(或使用复杂查询)时,我们应该考虑MySql的内存配置。如果配置MySQL服务器使用太少的内存会导致性能不是最优的;如果配置了太多的内存则会导致崩溃,无法执行查询或者导致交换操作严重变慢。在现在的32位平台下,仍有可能把所有的地址空间都用完,因此需要审视。


      计算内存使用的秘诀公式就能相对地解决这一部分问题。不过,如今这个公式已经很复杂了,更重要的是,通过它计算得到的值只是“理论可能”并不是真正消耗的值。事实上,有8GB内存的常规服务器经常能运行到最大的理论值(100GB甚至更高)。此外,你轻易不会使用到“超额因素”(它实际上依赖于应用以及配置)。一些应用可能需要理论内存的10%而有些仅需1%。
      那么,我们可以做什么呢?


      来看看那些在启动时就需要分配并且总是存在的全局缓冲吧!


      全局缓冲:
      key_buffer_size, innodb_buffer_pool_size, innodb_additional_mem_pool_size,innodb_log_buffer_size, query_cache_size


      注:如果你大量地使用MyISAM表,那么你也可以增加操作系统的缓存空间使得MySQL也能用得着。把这些也都加到操作系统和应用程序所需的内存值之中,可能需要增加32MB甚至更多的内存给MySQL服务器代码以及各种不同的小静态缓冲。这些就是你需要考虑的在MySQL服务器启动时所需的内存。其他剩下的内存用于连接。


      key_buffer_size 决定索引处理的速度,尤其是索引读的速度。一般我们设为16M,通过检查状态值Key_read_requests和Key_reads,可以知道key_buffer_size设置是否合理。比例key_reads / key_read_requests应该尽可能的低,至少是1:100,1:1000更好(上述状态值可以使用'key_read%'获得用来显示状态数据)。key_buffer_size只对MyISAM表起作用。即使你不使用MyISAM表,但是内部的临时磁盘表是MyISAM表,也要使用该值。可以使用检查状态值'created_tmp_disk_tables'得知详情。


      innodb_buffer_pool_size 对于InnoDB表来说,作用就相当于key_buffer_size对于MyISAM表的作用一样。InnoDB使用该参数指定大小的内存来缓冲数据和索引。对于单独的MySQL数据库服务器,最大可以把该值设置成物理内存的80%。


      innodb_additional_mem_pool_size 指定InnoDB用来存储数据字典和其他内部数据结构的内存池大小。缺省值是1M。通常不用太大,只要够用就行,应该与表结构的复杂度有关系。如果不够用,MySQL会在错误日志中写入一条警告信息。


      innodb_log_buffer_size 指定InnoDB用来存储日志数据的缓存大小,如果您的表操作中包含大量并发事务(或大规模事务),并且在事务提交前要求记录日志文件,请尽量调高此项值,以提高日志效率。


      query_cache_size 是MySql的查询缓冲大小。(从4.0.1开始,MySQL提供了查询缓冲机制)使用查询缓冲,MySQL将SELECT语句和查询结果存放在缓冲区中,今后对于同样的SELECT语句(区分大小写),将直接从缓冲区中读取结果。根据MySQL用户手册,使用查询缓冲最多可以达到238%的效率。通过检查状态值’Qcache_%’,可以知道query_cache_size设置是否合理:如果Qcache_lowmem_prunes的值非常大,则表明经常出现缓冲不够的情况,如果Qcache_hits的值也非常大,则表明查询缓冲使用非常频繁,此时需要增加缓冲大小;如果Qcache_hits的值不大,则表明你的查询重复率很低,这种情况下使用查询缓冲反而会影响效率,那么可以考虑不用查询缓冲。此外,在SELECT语句中加入SQL_NO_CACHE可以明确表示不使用查询缓冲。


      除了全局缓冲,MySql还会为每个连接发放连接缓冲。


      连接缓冲:
      每个连接到MySQL服务器的线程都需要有自己的缓冲。大概需要立刻分配256K,甚至在线程空闲时,它们使用默认的线程堆栈,网络缓存等。事务开始之后,则需要增加更多的空间。运行较小的查询可能仅给指定的线程增加少量的内存消耗,然而如果对数据表做复杂的操作例如扫描、排序或者需要临时表,则需分配大约read_buffer_size,sort_buffer_size,read_rnd_buffer_size,tmp_table_size 大小的内存空间。不过它们只是在需要的时候才分配,并且在那些操作做完之后就释放了。有的是立刻分配成单独的组块。tmp_table_size 可能高达MySQL所能分配给这个操作的最大内存空间了。注意,这里需要考虑的不只有一点 —— 可能会分配多个同一种类型的缓存,例如用来处理子查询。一些特殊的查询的内存使用量可能更大——如果在MyISAM表上做成批的插入时需要分配 bulk_insert_buffer_size 大小的内存;执行 ALTER TABLE, OPTIMIZE TABLE, REPAIR TABLE 命令时需要分配 myisam_sort_buffer_size 大小的内存。


      read_buffer_size 是MySql读入缓冲区大小。对表进行顺序扫描的请求将分配一个读入缓冲区,MySql会为它分配一段内存缓冲区。read_buffer_size变量控制这一缓冲区的大小。如果对表的顺序扫描请求非常频繁,并且你认为频繁扫描进行得太慢,可以通过增加该变量值以及内存缓冲区大小提高其性能。


      sort_buffer_size 是MySql执行排序使用的缓冲大小。如果想要增加ORDER BY的速度,首先看是否可以让MySQL使用索引而不是额外的排序阶段。如果不能,可以尝试增加sort_buffer_size变量的大小。


      read_rnd_buffer_size 是MySql的随机读缓冲区大小。当按任意顺序读取行时(例如,按照排序顺序),将分配一个随机读缓存区。进行排序查询时,MySql会首先扫描一遍该缓冲,以避免磁盘搜索,提高查询速度,如果需要排序大量数据,可适当调高该值。但MySql会为每个客户连接发放该缓冲空间,所以应尽量适当设置该值,以避免内存开销过大。


      tmp_table_size是MySql的heap (堆积)表缓冲大小。所有联合在一个DML指令内完成,并且大多数联合甚至可以不用临时表即可以完成。大多数临时表是基于内存的(HEAP)表。具有大的记录长度的临时表 (所有列的长度的和)或包含BLOB列的表存储在硬盘上。如果某个内部heap(堆积)表大小超过tmp_table_size,MySQL可以根据需要自动将内存中的heap表改为基于硬盘的MyISAM表。还可以通过设置tmp_table_size选项来增加临时表的大小。也就是说,如果调高该值,MySql同时将增加heap表的大小,可达到提高联接查询速度的效果。


      当我们设置好了缓冲区大小之后,再来看看:


      table_cache 所有线程打开的表的数目,增大该值可以增加mysqld需要的文件描述符的数量。每当MySQL访问一个表时,如果在表缓冲区中还有空间,该表就被打开并放入其中,这样可以更快地访问表内容。通过检查峰值时间的状态值’Open_tables’和’Opened_tables’,可以决定是否需要增加table_cache的值。如果你发现open_tables等于table_cache,并且opened_tables在不断增长,那么你就需要增加table_cache的值了(上述状态值可以使用’Open%tables’获得)。注意,不能盲目地把table_cache设置成很大的值。如果设置得太高,可能会造成文件描述符不足,从而造成性能不稳定或者连接失败。


      做了以上方面的调优设置之后,MySql应该基本能满足您需求(当然是建立在调优设置适当的情况下),我们还应该了解并注意:


      只有简单查询OLTP(联机事务处理)应用的内存消耗经常是使用默认缓冲的每个线程小于1MB,除非需要使用复杂的查询否则无需增加每个线程的缓冲大小。使用1MB的缓冲来对10行记录进行排序和用16MB的缓冲基本是一样快的(实际上16MB可能会更慢,不过这是其他方面的事了)。


      找出MySQL服务器内存消耗的峰值。这很容易就能计算出操作系统所需的内存、文件缓存以及其他应用。在32位环境下,还需要考虑到32位的限制,限制 “mysqld” 的值大约为2.5G(实际上还要考虑到很多其他因素)。现在运行 “ps aux” 命令来查看 “VSZ” 的值(MySQL 进程分配的虚拟内存)。监视着内存变化的值,就能知道是需要增加或减少当前的内存值了。

       

      最后来看看调优设置方法:

       

      安装好MySql后,配制文件应该在 ./share/mysql ("./"即MySql安装目录) 目录中,配制文件有几个,有my-huge.cnf my-medium.cnf my-large.cnf my-small.cnf。win环境下即存在于MySql安装目录中的.ini文件。不同的流量的网站和不同配制的服务器环境,当然需要有不同的配制文件了。

      一般的情况下,my-medium.cnf这个配制文件就能满足我们的大多需要;一般我们会把配置文件拷贝到 /etc/my.cnf ,win环境下则拷备到 my.ini 下即可,只需要修改这个配置文件就可以了。

    11. linux 性能监控工具dstat

      2011-08-29 11:31:24

      下载:dstat官方网站:
      http://dag.wieers.com/home-made/dstat/

      解压:
      # tar jxvf dstat-0.7.2.tar.bz2 && cd dstat-0.7.2

      执行
      #dstat -tclpymsgdn

      执行并输出到文件
      #dstat -tclpymsgdn --output status01.csv

      查询占用系统资源最高的东东:
      dstat --top-cpu --top-mem --top-io --top-latency  --top-int

      ./dstat -tclpymsgdn --nocolor


      数据图表范例:

       

      参数及使用说明参考:
      dstat-0.7.2\docs\dstat.1.html
      dstat-0.7.2\docs\dstat-paper.html

      系统调优参考:
      http://people.redhat.com/alikins/system_tuning.html

       

      命令工具.rar(345 KB)

       

      ===========================================================

      SEE ALSO

      Performance tools

      ifstat(1), iftop(8), iostat(1), mpstat(1), netstat(1), nfsstat(1), nstat, vmstat(1), xosview(1)

      Debugging tools

      htop(1), lslk(1), lsof(8), top(1)

      Process tracing

      ltrace(1), pmap(1), ps(1), pstack(1), strace(1)

      Binary debugging

      ldd(1), file(1), nm(1), objdump(1), readelf(1)

      Memory usage tools

      free(1), memusage, memusagestat, slabtop(1)

      Accounting tools

      dump-acct, dump-utmp, sa(8)

      Hardware debugging tools

      dmidecode, ifinfo(1), lsdev(1), lshal(1), lshw(1), lsmod(8), lspci(8), lsusb(8), smartctl(8), x86info(1)

      Application debugging

      mailstats(8), qshape(1)
      xdpyinfo(1), xrestop(1)

      Other useful info

      collectl(1), proc(5), procinfo(8)
    12. loadrunner sprintf格式化输出百分号

      2011-08-16 19:19:02

      sprintf————Writes formatted output to a string.

      sprintf(mingText,"00%s%s##%##",uniqeNum,sysTime) ;

      之前是这样写的,最后做MD5摘要好,数据总是不一致
      先排除了算法本身的问题

      然后比对加密前的明文,发现少了一个不起眼的百分号%
      于是问百度

      就知道了
      因为C语言中如果要打印百分号,需要这样写 printf("%%");

      遂,改为sprintf(mingText,"00%s%s##%%##",uniqeNum,sysTime) ;

      结果正确,ok.

    13. 淘宝测试blog推荐_02

      2011-07-29 18:21:03

       

      1. 性能测试瓶颈分析

      在性能测试过程中,瓶颈犹如功能测试的bug,瓶颈的分析犹如bug的定位。性能测试工程师好比医生,看到病象,定位病因。性能瓶颈的定位更像庖丁解牛,层层解剖,最后定位问题之所在。下面分享一个内存泄漏的瓶颈分析。

      病象:TPS波动非常大;狂打超时日志;偶尔有500错误。

      看到这个现象,其实说明不了什么问题,就象人咳嗽,不一定是感冒,可能是上火,嗓子发炎。但是看到这个现象至少说明系统是有性能问题存在,我们就要进一步进行分析,看看问题到底在哪?用jconsole监控内存,发现内存使用如图1

      图1:内存使用情况图

      从图1中,我们可以很清晰的看到内存使用不正常,FGC非常频繁,差不多5分钟进行一次,而且内存回收不彻底,每次回收在1G左右徘徊。到这里我们已经可以定位是内存问题,导致了我们看到的TPS波动大,FGC频繁,超时严重等等一系列现象。

      那么是谁吃了我的内存???

      用简单的jstat命令查看系统GC情况,看到情况如图2所示

      图2

      在图2的绿色框标注,我们可以很清晰的看到进行一次FGC,内存只回收12%左右,回收很不彻底,而且FGC的时间持续5秒。内存回收不彻底,肯定是有些方法霸占了内存不释放,导致系统频繁FGC来进行回收。

      那么谁是真正的凶手呢??

      用jmap 命令对内存使用进行分析,发现情况如图3所示

      图3

      通过FGC前后的内存使用进行比对,发现这三个方法快速占用内存从最少到最多,而且回收不掉,始终霸占着前几位。再通过其他工具分析,看看这三个是不是真正的凶手。

      用MAT分析工具进行分析,图4所示

      图4

      这三个方法各占了内存使用的14%,那么问题就很清晰明朗了。这三个方法就是真正的凶手,调优就从这三个方法入手。

      性能瓶颈的分析,犹如庖丁解牛,层层剖析。最终定位问题之所在。

      VN:F [1.9.7_1111]

       

      转载务必注明出处Taobao QA Team,原文地址:http://qa.taobao.com/?p=8641

       

      2. JAVA-OPTS引发的思考

       

      我们在性能测试过程中,经常要修改应用的JAVA-OPTS参数。修改这些参数,不单单是修改这些数字,本着知其所以然的态度,我们要知道这些参数背后的意义。

      常见的JAVA-OPTS的配置项。

      JAVA_OPTS=”-server -Xms1536m -Xmx1536m -XX:NewSize=320m -XX:MaxNewSize=320m -XX:PermSize=96m -XX:MaxPermSize=256m -Xmn500m -XX:MaxTenuringThreshold=5″

      -Xms设置堆内存池的最小值

      -Xmx:设置堆内存池的最大值

      -XX:NewSize设置新对象生产堆内存

      -XX:MaxNewSize设置最大新对象生产堆内存

      -XX:PermSize设置Perm区的大小

      XX:MaxPermSize设置Perm区的最大值

      -Xmn设置Young区的大小

      -XX:MaxTenuringThreshold设置垃圾最大年龄。

      更加详细的说明,请参考图1

      图1

      看到这里,或许你会迷糊什么是堆内存,什么Perm区?这样就引出了第二个思考:JAVA内存模型。

      JAVA内存模型如图2所示。

      图2

      JAVA的内存模型可以分为3个代:Young、Tenured、Perm。有的版本又叫:New、Old、Perm。中文叫:年轻代、终生代、永久代。或许中文还有其他的叫法,但是表示的意思是一样的。

      Young和Tenured共同组成了堆内存(heap)。

      Young(年轻代)还可以分为Eden区和两个Survivor区(from和to,这两个Survivor区大小严格一至)。新的对象实例总是首先放在Eden区,Survivor区作为Eden区和 Tenured(终生代)的缓冲,可以向 Tenure(终生代)转移活动的对象实例。如图3所示。

      图3

      在明白了JAVA内存模型后,就引出了第三个思考:Jconsole在性能测试中的使用。Jconsole是我们在性能测试中常用的工具,打开Jconsole后,可以看到很多监控图表。当你明白JAVA内存模型后,也就很容易明白Jconsole的监控图表了。如图4所示。

      图4

      在Jconsole图中,我们可以很明显看到内存可以分为堆内存和非堆在堆内存。在堆的上面有三个柱子,分别代别的是:Eden、Survivor、Old。Old区就是Tenured区。

      在了解上述知识后,我们在使用Jconsole来监控应用的内存时,会更加清晰。

      VN:F [1.9.7_1111]

       

      转载务必注明出处Taobao QA Team,原文地址:http://qa.taobao.com/?p=6621

       

       

      3. 谈一谈JVM内存JAVA_OPTS参数

      最近几个月,做的性能测试项目中,发现了一些内存方面的问题,其中有涉及到对JBOSS里的JAVA_OPTS配置,例如一下所示;
      JAVA_OPTS=”-server -Xms1536m -Xmx1536m -XX:NewSize=320m -XX:MaxNewSize=320m -XX:PermSize=96m -XX:MaxPermSize=256m -Xmn500m -XX:MaxTenuringThreshold=5″
      JAVA_OPTS并不是已成不变的,不同的应用、软硬件环境下,要想充分发挥应用的性能,这些参数里边的设置可是非常有技巧和具有经验积累的。
      经过查找资料,先看下JAVA_OPTS参数表示的意义。

      -server:一定要作为第一个参数,在多个CPU时性能佳
      -Xms:初始Heap大小,使用的最小内存,cpu性能高时此值应设的大一些
      -Xmx:java heap最大值,使用的最大内存
      上面两个值是分配JVM的最小和最大内存,取决于硬件物理内存的大小,建议均设为物理内存的一半。-XX:PermSize:设定内存的永久保存区域
      -XX:MaxPermSize:设定最大内存的永久保存区域
      -XX:MaxNewSize:
      -Xss 15120 这使得JBoss每增加一个线程(thread)就会立即消耗15M内存,而最佳值应该是128K,默认值好像是512k.
      +XX:AggressiveHeap 会使得 Xms没有意义。这个参数让jvm忽略Xmx参数,疯狂地吃完一个G物理内存,再吃尽一个G的swap。
      -Xss:每个线程的Stack大小
      -verbose:gc 现实垃圾收集信息
      -Xloggc:gc.log 指定垃圾收集日志文件
      -Xmn:young generation的heap大小,一般设置为Xmx的3、4分之一
      -XX:+UseParNewGC :缩短minor收集的时间
      -XX:+UseConcMarkSweepGC :缩短major收集的时间
      提示:此选项在Heap Size 比较大而且Major收集时间较长的情况下使用更合适。

      稳定的开发架构环境下,建议出一份有实践、经验论证的JAVA_OPTS配置,能够非常切合实际的服务于当前开发、测试的软件流程。

      VN:F [1.9.7_1111]
       

      谈一谈JVM内存JAVA_OPTS参数, 9.0 out of 10 based on 5 ratings 转载务必注明出处Taobao QA Team,原文地址:http://qa.taobao.com/?p=3395

       

      4. JAVA内存泄露

       

      最近在写性能宝典,正好内存泄露是其中非常重要的主题,记录下来,欢迎大家参与讨论,给出建议,谢谢!

      内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。
      JAVA中的内存溢出的导致原因很多,最主要的可能有以下几种:
      A. 由于JVM堆内存设置过小,可以通过-Xms -Xmm设置。
      B. JVM堆内存是足够的,但只是没有连续的内存空间导致,比如申请连续内存空间的数组:String[] array = new String[10000]。
      C.由于导入较多的依赖jar包以及项目本身引用的class太多。
      D. 测试过程中生成太多的对象。
      E. 缓存池载入太多的等待队列。
      F. 还有可能是不断的内存泄露导致最后内存不足溢出

      内存泄漏的慨念

        1.c/c++是程序员自己治理内存,Java内存是由GC自动回收的。

        我虽然不是很熟悉C++,不过这个应该没有犯常识性错误吧。

        2.什么是内存泄露?

        内存泄露是指系统中存在无法回收的内存,有时候会造成内存不足或系统崩溃。

        在C/C++中分配了内存不释放的情况就是内存泄露。

        3.Java存在内存泄露

        我们必须先承认这个,才可以接着讨论。虽然Java存在内存泄露,但是基本上不用很关心它,非凡是那些对代码本身就不讲究的就更不要去关心这个了。

        Java中的内存泄露当然是指:存在无用但是垃圾回收器无法回收的对象。而且即使有内存泄露问题存在,也不一定会表现出来。

        4.Java中参数都是传值的。

        对于基本类型,大家基本上没有异议,但是对于引用类型我们也不能有异议。

        Java内存泄露情况

        JVM回收算法是很复杂的,我也不知道他们怎么实现的,但是我只知道他们要实现的就是:对于没有被引用的对象是可以回收的。所以你要造成内存泄露就要做到:

        持有对无用对象的引用!

        不要以为这个很轻易做到,既然无用,你怎么还会持有它的引用? 既然你还持有它,它怎么会是无用的呢?
      以下以堆栈更经典这个经典的例子来剖析。
      public class Stack {
       private Object[] elements=new Object[10];
       private int size = 0;

       public void push(Object e){
        ensureCapacity();
        elements[size++] = e;
       }

       public Object pop(){
        if( size == 0)
         throw new EmptyStackException();
         return elements[--size];
       }

      private void ensureCapacity(){
       if(elements.length == size){
        Object[] ldElements = elements;
        elements = new Object[2 * elements.length+1];
        System.arraycopy(oldElements,0, elements, 0, size);
       }
      }
      }
        上面的原理应该很简单,假如堆栈加了10个元素,然后全部弹出来,虽然堆栈是空的,没有我们要的东西,但是这是个对象是无法回收的,这个才符合了内存泄露的两个条件:无用,无法回收。

        但是就是存在这样的东西也不一定会导致什么样的后果,假如这个堆栈用的比较少,也就浪费了几个K内存而已,反正我们的内存都上G了,哪里会有什么影响,再说这个东西很快就会被回收的,有什么关系。下面看两个例子。

        例子1

      public class Bad{
       public static Stack s=Stack();
        static{
         s.push(new Object());
         s.pop(); //这里有一个对象发生内存泄露
         s.push(new Object()); //上面的对象可以被回收了,等于是自愈了
        }
      }
        因为是static,就一直存在到程序退出,但是我们也可以看到它有自愈功能,就是说假如你的Stack最多有100个对象,那么最多也就只有100个对象无法被回收其实这个应该很轻易理解,Stack内部持有100个引用,最坏的情况就是他们都是无用的,因为我们一旦放新的进取,以前的引用自然消失!

        例子2

      public class NotTooBad{
       public void doSomething(){
        Stack s=new Stack();
        s.push(new Object());
        //other code
        s.pop();//这里同样导致对象无法回收,内存泄露.
       }//退出方法,s自动无效,s可以被回收,Stack内部的引用自然没了,所以
       //这里也可以自愈,而且可以说这个方法不存在内存泄露问题,不过是晚一点
       //交给GC而已,因为它是封闭的,对外不开放,可以说上面的代码99.9999%的
       //情况是不会造成任何影响的,当然你写这样的代码不会有什么坏的影响,但是
       //绝对可以说是垃圾代码!没有矛盾吧,我在里面加一个空的for循环也不会有
       //什么太大的影响吧,你会这么做吗?
      }

      VN:F [1.9.7_1111]

      转载务必注明出处Taobao QA Team,原文地址:http://qa.taobao.com/?p=3625

       

      5. jmap -dump:live为啥会触发Full GC

      昨天组里的新人小朋友问是不是每次执行jmap -dump:live都会触发一次Full GC,因为当时他在做性能测试时某应用已经好几个小时没有一次FGC了,结果他执行了下dump就增加了次FGC。

      我当时模糊回答应该会,以前看过哪篇文章好像提过^-^,不过本着严谨不误导新人小朋友的原则,还是找时间抽空验证实践了把:

      测试环境:linux , sun jdk 1.6.07 , 32位

      测试结果: jmap -dump:live 以及 jmap -histo:live都会触发Full GC,即使加上JVM参数-XX:+DisableExplicitGC也不影响结果

      那么为什么呢? 其实大概猜也能知道,live选项的,如果FGC后,看到的活的对象比没有FGC的自然更精确。

      我们来通过源码验证学习一下:

      入口自然是 $j2se/src/share/classes/sun/tools/JMap.java

      关键点是

      VirtualMachine vm = attach(pid);
      InputStream in = ((HotSpotVirtualMachine)vm).
      heapHisto(live ? LIVE_OBJECTS_OPTION : ALL_OBJECTS_OPTION);

      以及

      InputStream in = ((HotSpotVirtualMachine)vm).
      dumpHeap((Object)filename,
      (live ? LIVE_OBJECTS_OPTION : ALL_OBJECTS_OPTION));

      于是找到这里

      HotSpotVirtualMachine.java:

      public InputStream dumpHeap(Object … args) throws IOException {
      return executeCommand(“dumpheap”, args);

      }

      public InputStream heapHisto(Object … args) throws IOException {
      return executeCommand(“inspectheap”, args);
      }

      接着看到

      LinuxVirtualMachine.java:

      先是创建UNIX socket,然后连到target VM,把dumpheap或inspectheap命令通过socket发过去。

      那么具体的inspectheap在sun hotspot核心代码里是如何处理的呢?

      看这里 $hotspot/src/share/vm/services/attachListener.cpp

      heap_inspection函数有如下关键代码:

      VM_GC_HeapInspection heapop(out, live_objects_only /* request full gc */, true /* need_prologue */);

      同样dumpheap在hotspot里也是这个文件里处理的:

      jint dump_heap(AttachOperation* op, outputStream* out) {

      // Request a full GC before heap dump if live_objects_only = true
      // This helps reduces the amount of unreachable objects in the dump
      // and makes it easier to browse.
      HeapDumper dumper(live_objects_only /* request GC */);
      int res = dumper.dump(op->arg(0));

      这下我们就可以明白了,与实验结果也对上号了

      参考资料: http://forums.java.net/jive/message.jspa?messageID=115907

      VN:F [1.9.7_1111]

       

      转载务必注明出处Taobao QA Team,原文地址:http://qa.taobao.com/?p=10010

       

       

    14. 淘宝测试blog推荐_01

      2011-07-29 16:25:43

      1.  我眼中的性能测试工程师

      许久以前就答应悟石要分享一下我眼中的性能测试工程师,结果托来托去快托过年了,囧…

      想想来杭州有半年了,也对目前主站的性能评测团队工作模式有些许了解了,再加上以前在上家雇主也做过几年自认还算很有技术含量的性能测试工作,我想我还算有点资格说的吧:)

      性能测试说的装B点儿,其实没啥,就是和Response Time(或者说latency)、throughput(也可以说capacity)以及scalability打交道。弄懂了这三个要素,应该就算是一个合格的性能测试工程师了。

      当然,我不会装B,只是一介武夫,所以我接下来只想从偏技术层面聊聊我心目中真正的主站性能测试工程师是啥样的:

      1. 大局观。性能测试工程师一定要有系统化的思维,要站在整个系统测试的角度看问题。一个优秀的性能工程师必须要有相当的知识广度。否则在测试期间,你必须依赖外界援助(比如DBA,Dev或OPS)来协助,效率不高,更关键的是可能会被误导,漏掉很多性能BUG。我常常看到组里的童鞋们在压测时一看到TPS降了,就死盯着应用,就着急的去分析线程或做CPU Profiling。找不到原因后有时问到我时,我习惯的第一句总是 你看过DB么?确认DB端正常么?看过压测客户端么?确认压测端正常么? 我个人意见:不要老凭经验,一有重复症状就思维定式;一定要坚持先从全局看问题,隔离到是应用层面、DB层面抑或是压测客户端层面后再进一步深入定位问题。
      2. 技能深度。在性能测试工具方面有自己独特的理解;同时也应该在操作系统、数据库、应用程序等方向的配置管理与调优方向上非常的熟悉。
      3. 敏感。这个一方面是天赋,一方面是经验积累吧, 很多隐蔽的性能问题确实是需要丰富的经验才能发现,极容易漏掉:)
      4. 兴趣。  其实这条才是最重要的^-^

      如果说具体些通俗些,我眼里主站真正的性能工程师是这样的:

      1. 熟悉Java(包括JVM内在机理)/c/c++。理由很简单,主站大部分的外围应用和中间件都是JAVA写的,底层核心系统是c/c++写的。
      2. 精通linux管理和shell编程。理由更简单,我一直觉得,shell熟练与否非常大程度决定了一个工程师的工作效率。
      3. 对数据库管理和性能优化有自己的实践和心得(数据库永远是个性能要点)
      4. 精通某一个性能测试工具。不止是使用,更包括原理,如何改造扩展。
      5. 熟悉linux kernel的实现(比如内存管理、文件系统、系统调用… )。 这条感触在最近两个月特别深,可能是受到褚霸、子团等大侠们的影响吧,如果不熟悉kernel,确实很难在底层系统的性能测试上有所真正建树。其实这块也算是整个质量保证部的技术短板吧,现在淘宝的linux内核组都是自测+他人review的形式,如果。。。^-^
      6. 了解常见硬件,特别是存储相关。这块主要是受国外Percona公司的Peter和Vadim影响,他们能成为世界公认的mysql性能专家,他们熟悉mysql源码当然很重要,但也与他们那非常渊博的底层硬件知识是分不开的。

      当然以上都是我个人意见,从我自己的角度出发看的问题。其实性能测试还有很多领域,比如前端性能测试这块,我是小白,就不发表任何相关意见了^-^    但说到底,做性能这块关键一是经验积累二是掌握相关底层技术

      至今还记得百淘65期让我最为难忘的细节,达人青云在分享他的牛P经历时总结到的:

      • 结合优势,做别人做不了的
      • 发现问题,做别人没做过的
      • 主动出击,做别人不爱做的

      希望自己能一直铭记这三句话,有天能成为一个真正的性能工程师

      VN:F [1.9.7_1111]

      转载务必注明出处Taobao QA Team,原文地址:http://qa.taobao.com/?p=8308

       

      2. 项目性能测试体验感想二

      最近有幸和云帅参与了新江湖的性能测试, 这个项目中,由于测试时间紧,性能点多,我们从开发提交测试后就进行性能测试。提早介入测试导致我们后来遇到很多问题。我主要的工作是协助云帅,申请性能测试服务器,验证搭建好的性能测试环境和功能,准备性能测试数据,后期我参与了好友最近更新的相册这个性能点的执行,下面说下具体是怎么做上面的事情的:

      (1)申请性能测试服务器:
      先找总的开发负责人给出本阶段性能点所需要信息,包括 性能点,服务名,Hsf版本号,数据源,data-pool设置,jdk版本号,apache版本号,jboss版本号,jvm设置,代码库路径,压测页面链接,依赖系统和该性能点对应的开发负责人。收集完这些信息后我们可以向悟石他们申请机器啦。申请时除了上面最后三点,其他内容都需要提供。这样做是为了之后让scm参照上面的信息部署环境。

      (2)验证搭建的环境/功能:
      1、验证jdk、apache、jboss的版本:可以通过拷贝文件valid-env,执行check.sh来快速验证jdk、apache、jboss的版本。或者通过如下的方法来依次验证。
      a、jdk版本查看 先通过jbosscle文件查找到使用的JAVA_HOME地址,然后根据目录查找 /opt/taobao/java/bin/java -version 或者 /opt/taobao/java/bin/java1 -version;
      b、apache版本查看 /opt/taobao/install/httpd/bin/httpd -version;
      c、jboss版本查看 jboss启动日志jboss_stdout.log中有,只要看前面几行就能查找到。
      2、证数据库配置:数据库的配置,一般存放在应用下的conf目录下,orcle-***-ds.xml/mysql-***-ds.xml文件里。检查它是否连接了正确的数据库schema,连接数是否正确。
      3、查看apache的访问日志是否屏蔽掉。查看conf目录下,httpd.conf文件里——CustomLog这个配置项。
      4、验证功能:需要确保所要测试的性能点的功能及相关功能正确。执行几个主流程查看或者跑一下接口是否通。

      (3)准备测试数据:
      1.向DBA讨教了如何快速准备大量的性能测试数据。两种方法。写存储或者设置autoincrement。我这次主要用的是后者。将表的主键设置autoincrement,这样可以通过insert into 表(字段1,字段2…) select value1,value2… from 表 执行一次可以成倍增长当前的数据。这种方法简单快捷,如果只是为了纯粹增加表的数据流这个方法还是比较好的。当然除了数据库的方法还可以通过接口去插数据,在lr中执行下也是不错的选择。
      2.性能测试环境下什么数据也没有,所以通过导入功能测试环境的数据来充当,mysql工具中有一个很好的功能:可以将功能测试数据库库的表结构和数据复制到性能测试数据库,如果性能测试数据库中已经存在该表,就drop掉该表,执行速度很快,大大方便我们准备数据。推荐大家用SQLyog Enterprise这个工具,真的不错。方法如下:打开mysql的数据库,连接到功能测试库上,点击File——New Conections,连接到性能测试库上,选择你要操作的表,右击选择第二项——Copy Table To Diffierent Host/Database,在界面中左边是源数据库,也就是我们的功能测试数据库,选择你要复制的表,支持多选,右边选择目标数据库,也就是我们的性能测试数据库及对应的用户,如果表已经存在,勾选选项:Drop table if exists in target ,具体要拷贝表结构和数据或者只是表结构都可以自己选择,最后点击copy,数据库表结构和数据很快可以复制完。

      (4)录制脚本:
      淘江湖二期很多页面是采用了异步方式调用,所以录制完一个页面后,通过抽取每个请求作为一个脚本。接口测试这块采用http的方式去模拟测试,这样方式最大的好处是脚本准备比较方便。

      (5)测试执行:
      1、一个页面通过加载该页面所有的异步请求,逐步增加各个脚本的并发用户数,调整并发用户数,查看是否满足预期的tps。
      2、测试过程中时刻关注lr的运行情况,曲线有没有波动很大,几个日志信息,debug日志,超时日志和velocity日志,是否有大量日志出现。监控java虚拟内存是否正常释放,曲线是否平稳,而不是往上的趋势。一般如果有问题的话,刚开始运行脚本就会出现频繁报错,这时需要马上停下来查看问题原因。排查原因有很多,由于我们介入较早,有一些是因为数据库表结构没有建索引,或者是应用的版本没更新导致(初期bug比较多,版本经常更新),所以经过这次测试,深刻体会到性能测试的前提需要在sql审核通过,索引加好,功能比较稳定的前提下进行,也就是功能测试第二阶段开始,此时功能相对稳定,表结构也不会怎么变化,会少走很多弯路。
      3.由于这次页面性能测试依赖的应用比较多,最多有用到13台应用服务器,当测试某个性能点的时候,需要查看与该应用交互的应用的情况,可以用netstat -nal命令。查出来后,监控这些相关的服务器,cpu,load,日志情况等。
      4.用lr监控cpu这些数据时发现有一台服务器监控到的cpu有问题,空闲状态cpu就已经达到60%了,具体原因也不清楚,所以换了方式采用我们这边的一个监控cpu和load的shell脚本来做。

      (6)关于性能测试计划:这次测试有11个性能点,6个接口和5个页面,计划中根据测试优先级高低分三个阶段进行,分阶段搭建测试环境和准备数据和脚本,先接口后页面的顺序执行。

      (7)关于性能测试日报:日报中主要记录目前所处的测试阶段,当天的测试工作,测试结果和结果分析,BugList,问题和风险,以及明天的计划。

      最后非常非常感谢云帅,像老师一样,非常细心和耐心,教我很多很多,让我受益很多很多,更让我体会到了一个优秀的性能测工程师认真严谨的工作态度。

      VN:F [1.9.7_1111]

      转载务必注明出处Taobao QA Team,原文地址:http://qa.taobao.com/?p=3676

       

      3. TPS真的重要吗

      TPS 每秒成功事务数,做过性能测试的人都知道这个词,那么TPS真的重要么,除了它我们就不需要关注其它的指标了吗?
      带着疑问,我们一起看一下这次的性能测试我们的过程,我们先是通过线上的访问统计整理出高峰时段的访问量,然后经过算出每台机器的访问量,把这个访问量作为目标TPS,测试过程就是验证系统能否达到这个目标。
      然而,线下测试达到目标后,系统上线却出现了性能问题,而 TPS并没有线下测试的高,认真分析后,发现问题在于,我们测试的场景还是与线上有很大的差异,线上是很多Client请求造成的压力,而线下测试Client数量去只有一台,虽然并发请求的量可能相同,可对系统造成的压力去有很大的不同,为什么压力不同,线上的情况就是一个有力的证明,探究原因,应有不少,比如线上测试时,单台client自身压力比较大,对服务器压力有影响,1台Client并发40个请求与10台Client并发4个请求对服务器进行施压的效果一定是不同的。
      总结起来,我感觉性能测试并不是那么简单,简单的只关注TPS这一个指标,应做到的是尽可能符合现实的场景,综合考虑各个关键指标。

      VN:F [1.9.7_1111]

      转载务必注明出处Taobao QA Team,原文地址:http://qa.taobao.com/?p=3784

       

      4. 性能测试总结

      加入底层平台测试大概有一年了,接触了比较多的项目,做了各种测试,其中觉得最难做得就是性能测试。曾经迷茫过、曾经犯过错、曾经失败返工过,尽管底层平台系统的测试产品业务很如此的单纯。深知性能测试的博大,把自己的一点小小经验整理下,让自己和别人以后少走些弯路。
      要做好性能测试首先要明确性能测试的目标。性能测试主要目标是发现系统的瓶颈,找出系统的性能基线,对系统进行调优,确认所测系统是否满足需求方的性能要求。有了准确的目标,性能测试才能测试准确。
      产品性能测试通常粗的分,我会将性能测试分为两类:可靠性测试(压力测试、负载测试等)和性能测试。这两种测试有很多的不同点:
      1)可靠性测试往往模拟的用户的使用情况,强调的为时间的延续性,要求产品没有不可接受的失败。
      2)性能测试往往需要和硬件条件联系在一起,寻找性能的最好发挥以及最优的方案
      通常性能测试需要在产品设计时就要进行简单性能测试,以对产品进行性能初期评估和调优,早于可靠性测试。同时在系统稳定时,常常还需要做详细的性能测试,以给应用方以数据参考。
      那如何做性能测试那?
      1) 性能测试应该早期就积极介入。介入代码审查和分析性能目标,多提出疑问,早期发现潜在的性能问题
      2) 性能测试考虑全局,他是一个系统的测试。需要在产品的每个部件都做了一个测试,并全部成功后才开始系统测试执行。需要考虑多种因素:环境的、硬件的、软件的等等。
      3) 测试前一定要检查确认配置。参数一定要配置对,否则测试无效。最好对于每个测试都有一个checklist,每次检查前都一一检查。这一步骤一定不可以省略,并需要被开发review。
      4) 数据预热和数据准备很关键。一开始系统并不是一个干净的环境。我们需要在性能测试开始前预存一定的数据,并且让其有个增量。而且也要考虑到哪些方面数据多少(要更具实际情况)。
      5) 准备测试脚本和工具要考虑实际情况。比如人的思考时间,场景的设计,不同操作的比例,数据的随机性等都要仔细设计,最和可以开发以及应用方进行讨论和确认。
      6) 测试执行前一定要确保服务器独占,执行中如果是5-7天的测试,最好拿出一天先跑一个一天压力不高的测试,潜在一天发现可发现的问题。
      7) 多种测试结果的分析发现问题。
      a) Log日志的分析
      b) 系统状态的分析
      c) 数据一致性的分析
      性能测试需要长时间的经验积累,我知道的只是些皮毛,后面还会继续探索,喜欢性能测试并将会在实践中不断积累和成长

      VN:F [1.9.7_1111]

      转载务必注明出处Taobao QA Team,原文地址:http://qa.taobao.com/?p=9339

       

       

       

      5. 服务器性能测试典型工具介绍 性能测试工具

       

      服务器性能测试典型工具介绍 性能测试工具

      众所周知,服务器是整个网络系统和计算平台的核心,许多重要的数据都保存在服务器上,很多网络服务都在服务器上运行,因此服务器性能的好坏决定了整个应用系统的性能。

         现在市面上不同品牌、不同种类的服务器有很多种,用户在选购时,怎样从纷繁的型号中选择出所需要的,适合于自己应用的服务器产品,仅仅从配置上判别是不 够的,最好能够通过实际测试来筛选。而各种的评测软件有很多种,你应该选择哪个软件测试?下面就介绍一些较典型的测试工具:

        (一)服务器整机系统性能测试工具

        一台服务器系统的性能可以按照处理器、内存、存储、网络几部分来划分,而针对不同的应用,可能会对某些部分的性能要求高一些。

        Iometer(www.iometer.org):存储子系统读写性能测试

         Iometer是Windows系统下对存储子系统的读写性能进行测试的软件。可以显示磁盘系统的最大IO能力、磁盘系统的最大吞吐量、CPU使用率、错误信息等。用户可以通过设置不同的测试的参数,有存取类型(如sequential ,random)、读写块大小(如64K、256K),队列深度等,来模拟实际应用的读写环境进行测试。

        Iometer操作简单,可以录制测试脚本,可以准确有效的反映存储系统的读写性能,为各大服务器和存储厂商所广泛采用。

        Sisoft Sandra(www.sisoftware.co.uk):WINDOWS下基准评测

        SiSoft发行的Sandra系列测试软件是Windows系统下的基准评测软件。此软件有超过三十种以上的测试项目,能够查看系统所有配件的信息,而且能够对部分配件(如CPU、内存、硬盘等)进行打分(benchmark),并且可以与其它型号硬件的得分进行对比。另外,该软件还有系统稳定性综合测试、性能调整向导等附加功能。

        Sisoft Sandra软件在最近发布的Intel bensley平台上测试的内存带宽性能并不理想,不知道采用该软件测试的FBD内存性能是否还有参考价值,或许软件应该针对FBD内存带宽的测试项目做一个升级。

        Iozone(www.iozone.org):linux下I/O性能测试

        现在有很多的服务器系统都是采用linux操作系统,在linux平台下测试I/O性能可以采用iozone。

        iozone是一个文件系统的benchmark工具,可以测试不同的操作系统中文件系统的读写性能。可以测试Read, write, re-read, re-write, read backwards, read strided, fread, fwrite, random read, pread ,mmap, aio_read, aio_write 等等不同的模式下的硬盘的性能。测试所有这些方面,生成excel文件,另外, iozone还附带了用gnuplot画图的脚本。

        该软件用在大规模机群系统上测试NFS的性能,更加具有说服力。

        Netperf(www.netperf.org):网络性能测试

         Netperf可以测试服务器网络性能,主要针对基于TCP或UDP的传输。Netperf根据应用的不同,可以进行不同模式的网络性能测试,即批量数 据传输(bulk data transfer)模式和请求/应答(request/reponse)模式。Netperf测试结果所反映的是一个系统能够以多快的速度向另外一个系统发送数据,以及另外一个系统能够以多块的速度接收数据。

        Netperf工具以client/server方式工作。server 端是netserver,用来侦听来自client端的连接,client端是 netperf,用来向server发起网络测试。在client与server之间,首先建立一个控制连接,传递有关测试配置的信息,以及测试的结果;在控制连接建立并传递了测试配置信息以后,client与server之间会再建立一个测试连接,用来来回传递着特殊的流量模式,以测试网络的性能。

        对于服务器系统来说,网络性能显得尤其重要,有些服务器上为了节省成本,采用了桌面级的网络芯片,性能怎样,用这个软件一测便知了。

        以上介绍的这几款测试工具都是可以免费从网上下载的非商业软件,但是其测试结果和认可程度均是为大多数使用者所认同的。你可以根据自己的应用需求选择不同的软件进行测试。

      (二)针对应用的测试工具

         随着web应用的增多,服务器应用解决方案中以Web为核心的应用也越来越多,很多公司各种应用的架构都以web应用为主。一般的web测试和以往的应 用程序的测试的侧重点不完全相同,在基本功能已经通过测试后,就要进行重要的系统性能测试了。系统的性能是一个很大的概念,覆盖面非常广泛,对一个软件系 统而言包括执行效率、资源占用率、稳定性、安全性、兼容性、可靠性等等,以下重点从负载压力方面来介绍服务器系统性能的测试。系统的负载和压力需要采用负载测试工具进行,虚拟一定数量的用户来测试系统的表现,看是否满足预期的设计指标要求。负载测试的目标是测试当负载逐渐增加时,系统组成部分的相应输出项,例如通过量、响应时间、CPU负载、内存使用等如何决定系统的性能,例如稳定性和响应等。

        负载测试一般使用工具完成,有LoadRunner,Webload,QALoad等,主要的内容都是编写出测试脚本,脚本中一般包括用户常用的功能,然后运行,得出报告。

        使用压力测试工具对web服务器进行压力测试。测试可以帮助找到一些大型的问题,如死机、崩损、内存泄漏等,因为有些存在内存泄漏问题的程序,在运行一两次时可能不会出现问题,但是如果运行了成千上万次,内存泄漏得越来越多,就会导致系统崩滑。

        Loadrunner:预测系统行为和性能的负载测试工具

        目前,业界中有不少能够做性能和压力测试的工具,Mercury(美科利)Interactive公司的LoadRunner是其中的佼佼者,也已经成为了行业的规范,目前最新的版本8.1。

        LoadRunner 是一种预测系统行为和性能的负载测试工具,通过模拟上千万用户实施并发负载及实时性能监测的方式来确认和查找问题,LoadRunner 能够对整个企业架构进行测试,LoadRunner 适用于各种体系架构,能支持广范的协议和技术(如Web、Ftp、Database等),能预测系统行为并优化系统性能。它通过模拟实际用户的操作行为和实行实时性能监测,来帮助您更快的查找和发现问题。Loadrunner是一个强大有力的压力测试工具,它的脚本可以录制生成,自动关联。测试场景面向指标,实现了多方监控。而且测试结果采用图表显示,可以自由拆分组合。

      文章来源于领测软件测试网 http://www.ltesting.net/

      VN:F [1.9.7_1111]

      转载务必注明出处Taobao QA Team,原文地址:http://qa.taobao.com/?p=4091

    15. loadrunner 使用自定义c函数

      2011-07-29 16:17:56

      loadrunner 使用自定义c函数:

      1. 在 C:\Program Files\Mercury\LoadRunner\include  放入  test.h

      2. 脚本中 globals.h 增加  #include "test.h"

      或者:修改 C:\Program Files\Mercury\LoadRunner\include\globals.h 也可(未验证)

      3. 在脚本中可以直接使用 test.h 中的c函数。

      其他:

      1. 将函数的定义直接写在脚本中,然后调用也可以
      2. 使用DLL方法,然后加载动态库也可以

    16. java jvm内存管理/gc策略/参数设置(二)

      2011-07-29 13:02:34

       

      1.GC的类型以及对应日志的解释

      http://www.iteye.com/topic/603347

      在大型的java运用中,要进行程序调优,指定一个合适的垃圾回收机制是必不可少的,那如何可确定某gc是否使得程序最优化呢?我们可以查看jvm打印出的gc日志来分析,并做出进一步优化,而目前并没有一篇文章明确的指明java中各种gc算法打印出日志的格式,以及如何阅读。所以接下来本文将试着边介绍各种垃圾回收机制边解释该回收机制下log的格式。
      1,垃圾收集算法
       1.1 引用计数法(Reference Counting Collector)
         
      系统记录对象被应用的次数,当应用次数为0时,就可以将该对象所占内存回收。该算法可以不用暂停运用,但缺点是无法解决重复运用的问题。所以java并没有提供此类垃圾回收器。
        1.2  tracing算法(Tracing Collector)
             tracing算法的垃圾收集器从根集开始扫描,识别出哪些对象可达,哪 些对象不可达,并用某种方式标记可达对象。
        1.2.1 
      复制( Copying )算法
          
      复制算法将堆等分成2个区域,一个区域含有现在的数据对象(ToSpace),而另一个区域包含废弃的数据(FromSpace)。复制算法将存活的对象从FromSpace复制到ToSpace,然后切换Fromspace和ToSpace的指针,以前的FromSpace变为现在的ToSpace区域。

        1.2.2 标记-整理( Mark-Compact )算法
             
        1.2.3 标记-清除(Mark-Sweep)

      Using the -XX flags for our collectors for jdk6,

      • UseSerialGC is "Serial" + "Serial Old"
      • UseParNewGC is "ParNew" + "Serial Old"
      • UseConcMarkSweepGC is "ParNew" + "CMS" + "Serial Old". "CMS" is used most of the time to collect the tenured generation. "Serial Old" is used when a concurrent mode failure occurs.
      • UseParallelGC is "Parallel Scavenge" + "Serial Old"
      • UseParallelOldGC is "Parallel Scavenge" + "Parallel Old" 

       

      SerailGC

      1,Serial Young GC 
      0.246: [GC 0.246: [DefNew: 1403K->105K(1984K), 0.0109275 secs] 1403K->1277K(6080K), 0.0110143 secs] 
      2, Serial Olg Gc

      1.133: [GC 1.133: [DefNew: 960K->64K(960K), 0.0012208 secs]1.135: [Tenured: 7334K->7142K(7424K), 0.0213756 secs] 7884K->7142K(8384K), [Perm : 364K->364K(12288K)], 0.0226997 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]

       

      Parrallel GC serailGC的适应muti Core的加强版,就是在minorGC时候采用并行的进行收集,而fullGC并没有改变

      Parralllel Compacting GC 在parrallelGC的基础上将fullgc也变为并发的了
      With the parallel compacting collector, the old and permanent generations are collected in a stop-theworld,mostly parallel fashion with sliding compaction. The collector utilizes three phases. First, each generation is logically divided into fixed-sized regions. In the marking phase, the initial set of live objects directly reachable from the application code is divided among garbage collection threads, and then all live objects are marked in parallel. As an object is identified as live, the data for the region it is in is updated with information about the size and location of the object.备注 待翻译



       

      Concurrent Mark-Sweep (CMS) Collector   有一种需求是应用的相应时间比应用的吞吐重要,为了满足这种需求,jvm提供了该场景下的垃圾收集器CMS,使用该垃圾收集器的时候minorGC和 ParralelGC当中采用的一样,只是在老生代更换了不同的算法。

      CMS将老生代的回收分为4个阶段 其中只有2个阶段是要stop-the-world的,而其余阶段是不需要的,因此降低了系统暂停时间,缺点是在其余的2个阶段会更应用抢jvm资源。

       
        

      从上图可以看出,CMS的运行过程。

      A collection cycle for the CMS collector starts with a short pause, called the initial mark, that
      identifies the initial set of live objects directly reachable from the application code. Then,
      during the concurrent marking phase, the collector marks all live objects that are transitively
      reachable from this set. Because the application is running and updating reference fields while the marking phase is taking place, not all live objects are guaranteed to be marked at the end of the concurrent marking phase. To handle this, the application stops again for a second pause, called remark, which finalizes marking by revisiting any objects that were modified during the concurrent marking phase. Because the remark pause is more substantial than the initial mark, multiple threads are run in parallel to increase its efficiency. 备注 待翻译


      Concurrent Mark-Sweep GC log format:



      Full GC 被调用的出现情况
       

      promotion failed( mark-sweep-compact stop-the-world ) ParNew (promotion failed):  当老生代空闲空间存在碎片,导致没有足够大的连续空间开存放新生代对象的升级时,机会触发promotion failed。 此时调用一个Mark-Compact 垃圾收集器是很有必要的。(默认采用 Serial Old GC)

      106.641: [GC 106.641: [ParNew (promotion failed): 14784K->14784K(14784K), 0.0370328 secs]106.678: [CMS106.715: [CMS-concurrent-mark: 0.065/0.103 secs] [Times: user=0.17 sys=0.00, real=0.11 secs]
       (concurrent mode failure): 41568K->27787K(49152K), 0.2128504 secs] 52402K->27787K(63936K), [CMS Perm : 2086K->2086K(12288K)], 0.2499776 secs] [Times: user=0.28 sys=0.00, real=0.25 secs]

      full promotion guarantee failure ( concurrent mode failure ): 当垃圾回收算法预测在下一次Conc-Mark-Sweep算法调用之前,老生代的空余空间将会被系统占用光。为了解决这一问题,垃圾回收算法进入conccurent mode failure状态,调用一个 stop-the-world(serail Old GC)来清理系统Heap。


      eg:为了触发这种情况 我们先分配64m内存给jvm,然后新生代和老年代的占用比例为7,即老年代用了7*8=58 触发concurrent mode failure的情况:

       

       

      public class FullPromotionGuaranteeFailure
      {
          public static void main(String[] args)
          {
          List<byte[]> bytesList = new ArrayList<byte[]>();
          for (int i = 0; i < 7 * 8 * 1024; i++)
          {
              bytesList.add(new byte[1024]);
          }
          
          //bytesList = null; 没有必要的 gc会知道函数里面变量是否还会被引用
          byte[] bytes = new byte[16 * 1024 * 1024];
          String.valueOf(bytes[0]);
          }
      }


      运行时JVM参数:
      -Xmx64m -Xms64m -XX:NewRatio=7 -XX:MaxTenuringThreshold=0 -verbose:gc  -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplicationStoppedTime -XX:+UseConcMarkSweepGC
      -Xloggc:full-promotion-guarantee-failure.log
      full-promotion-guarantee-failure.log 内容


      0.195: [GC 0.195: [ParNew: 2986K->2986K(8128K), 0.0000083 secs]0.195: [CMS0.212: [CMS-concurrent-preclean: 0.011/0.031 secs] [Times: user=0.03 sys=0.02, real=0.03 secs]
       (concurrent mode failure): 56046K->138K(57344K), 0.0271519 secs] 59032K->138K(65472K), [CMS Perm : 2079K->2078K(12288K)], 0.0273119 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 

       

       

      调用Serial Old GC 是费时并且暂停了整个应用,这显然不是我们想看到的。为了 避免办法出现( concurrent mode failure )这种情况,可以参考bluedavy的该篇blog GC 策略的调优


      Stop-the-world GC happens when a JNI critical section is released . Here again the young generation collection failed due to "full promotion guarantee failure" and then the Full GC was invoked.

      283.736: [Full GC 283.736: [ParNew: 261599K->261599K(261952K), 0.0000615 secs]
      826554K->826554K(1048384K), 0.0003259 secs]
      GC locker: Trying a full collection because scavenge failed





      参考链接:
      1. Java SE 6 HotSpot[tm] Virtual Machine Garbage Collection Tuning
      2. Turbo-charging Java HotSpot Virtual Machine, v1.4.x to Improve the Performance and Scalability of Application Servers
      3. Understanding Concurrent Mark Sweep Garbage Collector Logs
      4. What the Heck's a Concurrent Mode?
      5. https://java.sun.com/j2se/reference/...whitepaper.pdf  
      6. http://blogs.sun.com/jonthecollector...the_sum_of_the  
      7. http://blogs.sun.com/jonthecollector...t_the_heck_s_a

       

       

      2. CMS gc实践总结

      http://www.iteye.com/topic/473874

      首先感谢阿宝同学的帮助,我才对这个gc算法的调整有了一定的认识,而不是停留在过去仅仅了解的阶段。在读过sun的文档和跟阿宝讨论之后,做个小小的总结,如果有谬误,敬请指正。
          CMS,全称Concurrent Low Pause Collector,是jdk1.4后期版本开始引入的新gc算法,在jdk5和jdk6中得到了进一步改进,它的主要适合场景是对响应时间的重要性需求大于对吞吐量的要求,能够承受垃圾回收线程和应用线程共享处理器资源,并且应用中存在比较多的长生命周期的对象的应用。CMS是用于对tenured generation的回收,也就是年老代的回收,目标是尽量减少应用的暂停时间,减少full gc发生的几率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代。在我们的应用中,因为有缓存的存在,并且对于响应时间也有比较高的要求,因此希望能尝试使用CMS来替代默认的server型JVM使用的并行收集器,以便获得更短的垃圾回收的暂停时间,提高程序的响应性。
          CMS并非没有暂停,而是用两次短暂停来替代串行标记整理算法的长暂停,它的收集周期是这样:
          初始标记(CMS-initial-mark) -> 并发标记(CMS-concurrent-mark) -> 重新标记(CMS-remark) -> 并发清除(CMS-concurrent-sweep) ->并发重设状态等待下次CMS的触发(CMS-concurrent-reset)
          其中的1,3两个步骤需要暂停所有的应用程序线程的。第一次暂停从root对象开始标记存活的对象,这个阶段称为初始标记;第二次暂停是在并发标记之后,暂停所有应用程序线程,重新标记并发标记阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致)。第一次暂停会比较短,第二次暂停通常会比较长,并且 remark这个阶段可以并行标记。

          而并发标记、并发清除、并发重设阶段的所谓并发,是指一个或者多个垃圾回收线程和应用程序线程并发地运行,垃圾回收线程不会暂停应用程序的执行,如果你有多于一个处理器,那么并发收集线程将与应用线程在不同的处理器上运行,显然,这样的开销就是会降低应用的吞吐量。Remark阶段的并行,是指暂停了所有应用程序后,启动一定数目的垃圾回收进程进行并行标记,此时的应用线程是暂停的。

          CMS的young generation的回收采用的仍然是并行复制收集器,这个跟Paralle gc算法是一致的。

          下面是参数介绍和遇到的问题总结,

      1、启用CMS:-XX:+UseConcMarkSweepGC。 咳咳,这里犯过一个低级错误,竟然将+号写成了-号

       

      2。CMS默认启动的回收线程数目是  (ParallelGCThreads + 3)/4) ,如果你需要明确设定,可以通过-XX:ParallelCMSThreads=20来设定,其中ParallelGCThreads是年轻代的并行收集线程数


      3、CMS是不会整理堆碎片的,因此为了防止堆碎片引起full gc,通过会开启CMS阶段进行合并碎片选项:-XX:+UseCMSCompactAtFullCollection,开启这个选项一定程度上会影响性能,阿宝的blog里说也许可以通过配置适当的CMSFullGCsBeforeCompaction来调整性能,未实践。

      4.为了减少第二次暂停的时间,开启并行remark: -XX:+CMSParallelRemarkEnabled。如果remark还是过长的话,可以开启-XX:+CMSScavengeBeforeRemark选项,强制remark之前开始一次minor gc,减少remark的暂停时间,但是在remark之后也将立即开始又一次minor gc。

      5.为了避免Perm区满引起的full gc,建议开启CMS回收Perm区选项:

      +CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled


      6.默认CMS是在tenured generation沾满68%的时候开始进行CMS收集,如果你的年老代增长不是那么快,并且希望降低CMS次数的话,可以适当调高此值:
      -XX:CMSInitiatingOccupancyFraction=80

      这里修改成80%沾满的时候才开始CMS回收。

      7.年轻代的并行收集线程数默认是(cpu <= 8) ? cpu : 3 + ((cpu * 5) / 8),如果你希望降低这个线程数,可以通过-XX:ParallelGCThreads= N 来调整。

      8.进入重点,在初步设置了一些参数后,例如:

       

      Java代码 复制代码 收藏代码
      1. -server -Xms1536m -Xmx1536m -XX:NewSize=256m -XX:MaxNewSize=256m -XX:PermSize=64m   
      2. -XX:MaxPermSize=64m -XX:-UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection   
      3. -XX:CMSInitiatingOccupancyFraction=80 -XX:+CMSParallelRemarkEnabled   
      4. -XX:SoftRefLRUPolicyMSPerMB=0  

       

      需要在生产环境或者压测环境中测量这些参数下系统的表现,这时候需要打开GC日志查看具体的信息,因此加上参数:

      -verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/home/test/logs/gc.log

      在运行相当长一段时间内查看CMS的表现情况,CMS的日志输出类似这样:

       

      Java代码 复制代码 收藏代码
      1. 4391.322: [GC [1 CMS-initial-mark: 655374K(1310720K)] 662197K(1546688K), 0.0303050 secs] [Times: user=0.02 sys=0.02, real=0.03 secs]   
      2. 4391.352: [CMS-concurrent-mark-start]   
      3. 4391.779: [CMS-concurrent-mark: 0.427/0.427 secs] [Times: user=1.24 sys=0.31, real=0.42 secs]   
      4. 4391.779: [CMS-concurrent-preclean-start]   
      5. 4391.821: [CMS-concurrent-preclean: 0.040/0.042 secs] [Times: user=0.13 sys=0.03, real=0.05 secs]   
      6. 4391.821: [CMS-concurrent-abortable-preclean-start]   
      7. 4392.511: [CMS-concurrent-abortable-preclean: 0.349/0.690 secs] [Times: user=2.02 sys=0.51, real=0.69 secs]   
      8. 4392.516: [GC[YG occupancy: 111001 K (235968 K)]4392.516: [Rescan (parallel) , 0.0309960 secs]4392.547: [weak refs processing, 0.0417710 secs] [1 CMS-remark: 655734K(1310720K)] 766736K(1546688K), 0.0932010 secs] [Times: user=0.17 sys=0.00, real=0.09 secs]   
      9. 4392.609: [CMS-concurrent-sweep-start]   
      10. 4394.310: [CMS-concurrent-sweep: 1.595/1.701 secs] [Times: user=4.78 sys=1.05, real=1.70 secs]   
      11. 4394.310: [CMS-concurrent-reset-start]   
      12. 4394.364: [CMS-concurrent-reset: 0.054/0.054 secs] [Times: user=0.14 sys=0.06, real=0.06 secs]  


      其中可以看到CMS-initial-mark阶段暂停了0.0303050秒,而CMS-remark阶段暂停了0.0932010秒,因此两次暂停的总共时间是0.123506秒,也就是123毫秒左右。两次短暂停的时间之和在200以下可以称为正常现象。

      但是你很可能遇到两种fail引起full gc:Prommotion failed和Concurrent mode failed。

      Prommotion failed的日志输出大概是这样:

       

      Java代码 复制代码 收藏代码
      1. [ParNew (promotion failed): 320138K->320138K(353920K), 0.2365970 secs]42576.951: [CMS: 1139969K->1120688K(   
      2. 166784K), 9.2214860 secs] 1458785K->1120688K(2520704K), 9.4584090 secs]  


      这个问题的产生是由于救助空间不够,从而向年老代转移对象,年老代没有足够的空间来容纳这些对象,导致一次full gc的产生。解决这个问题的办法有两种完全相反的倾向:增大救助空间、增大年老代或者去掉救助空间。增大救助空间就是调整-XX:SurvivorRatio参数,这个参数是Eden区和Survivor区的大小比值,默认是32,也就是说Eden区是 Survivor区的32倍大小,要注意Survivo是有两个区的,因此Surivivor其实占整个young genertation的1/34。调小这个参数将增大survivor区,让对象尽量在survitor区呆长一点,减少进入年老代的对象。去掉救助空间的想法是让大部分不能马上回收的数据尽快进入年老代,加快年老代的回收频率,减少年老代暴涨的可能性,这个是通过将-XX:SurvivorRatio 设置成比较大的值(比如65536)来做到。在我们的应用中,将young generation设置成256M,这个值相对来说比较大了,而救助空间设置成默认大小(1/34),从压测情况来看,没有出现prommotion failed的现象,年轻代比较大,从GC日志来看,minor gc的时间也在5-20毫秒内,还可以接受,因此暂不调整。

      Concurrent mode failed的产生是由于CMS回收年老代的速度太慢,导致年老代在CMS完成前就被沾满,引起full gc,避免这个现象的产生就是调小-XX:CMSInitiatingOccupancyFraction参数的值,让CMS更早更频繁的触发,降低年老代被沾满的可能。我们的应用暂时负载比较低,在生产环境上年老代的增长非常缓慢,因此暂时设置此参数为80。在压测环境下,这个参数的表现还可以,没有出现过Concurrent mode failed。


      参考资料:
      JDK5.0垃圾收集优化之--Don't Pause》 by 江南白衣
      《记一次Java GC调整经历》1,2 by Arbow
      Java SE 6 HotSpot[tm] Virtual Machine Garbage Collection Tuning
      Tuning Garbage Collection with the 5.0 JavaTM Virtual Machine

       

       

       

      3.  通过GC输出分析内存泄露问题
      http://www.iteye.com/topic/256701

      SIP5.0以后服务的请求量爆发性增长,因此也暴露了原来没有暴露出来的问题。由于过去一般一个新版本发布周期在一个月左右,因此如果是小的内存泄露,在一个月之内重新发布以后也就看不出任何问题。

      因此这阵子除了优化Memcache客户端和SIP框架逻辑以外其他依赖部分以外,对于内存泄露的压力测试也开始实实在在的做起来。经过这次问题的定位和解决以后,大致觉得对于一个大用户量应用要放心的话,那么需要做这么几步。

      1.       在GC输出的环境下,大压力下做多天的测试。(可以在 JAVA_OPTS增加-verbose:gc -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError)

      2.       检查GC输出日志来判断是否有内存泄露。(这部分后面有详细的实例说明)

      3.       如果出现内存泄露问题,则使用jprofiler等工具来排查内存泄露点(之所以不一开始使用,因为jprofiler等工具对于压力测试有影响,使得大压力无法上去,也使问题不那么容易暴露)

      4.       解决问题,并在重复2步骤。

      这里对SIP在jdk1.5和jdk1.6下做压力测试的GC 日志来做一个实际的分析对比,通过对比来大致描述一下如何根据输出情况能够了解应用是否存在内存泄露问题。(这里的内存泄露问题就是在以前blog写过的jdk的concurrent包内LinkedBlockingQueue的poll方法存在比较严重的内存泄露,调用频率越高,内存泄露的越厉害)

      两次压力测试都差不多都是两天,测试方案如下:

      开始50个并发,每个并发每次请求完毕后休息0.1秒,10分钟后增长50个并发,按此规律增长到500并发。

      旧版本SIP是在JDK1.5环境下完成的压力测试,

      新版本SIP的JDK版本是1.6,

      压力机和以前一样,是10.2.226.40,DELL1950,8CPU,8G内存。

      压力机模拟发出对一个需要签名的API不断的调用请求。

      看看两个Log的具体内容(内容很多截取部分做分析)

      先说一下日志输出的结构:(1.6和1.5略微有一些不同,只是1.6对于时间统计更加细致)

      [GC [<collector>: <starting occupancy1> -> <ending occupancy1>, <pause time1> secs] <starting occupancy3> -> <ending occupancy3>, <pause time3> secs]

      <collector>GC收集器的名称

      <starting occupancy1> 新生代在GC前占用的内存

      <ending occupancy1> 新生代在GC后占用的内存

      <pause time1> 新生代局部收集时jvm暂停处理的时间

      <starting occupancy3> JVM Heap 在GC前占用的内存

      <ending occupancy3> JVM Heap 在GC后占用的内存

      <pause time3> GC过程中jvm暂停处理的总时间

      Jdk1.5 log:

      启动时GC输出:

      [GC [DefNew: 209792K->4417K(235968K), 0.0201630 secs] 246722K->41347K(498112K), 0.0204050 secs]

      [GC [DefNew: 214209K->4381K(235968K), 0.0139200 secs] 251139K->41312K(498112K), 0.0141190 secs]

      一句输出:

      新生代回收前209792K,回收后4417K,回收数量205375K,Heap总量回收前246722K回收后41347K,回收总量205375K。这就表示100%的收回,没有任何新生代的对象被提升到中生代或者永久区(名字说的不一定准确,只是表达意思)。

      第二句输出:

      按照分析也就只是有1K内容被提升到中生代。

      运行一段时间后:

      [GC [DefNew: 210686K->979K(235968K), 0.0257140 secs] 278070K->68379K(498244K), 0.0261820 secs]

      [GC [DefNew: 210771K->1129K(235968K), 0.0275160 secs] 278171K->68544K(498244K), 0.0280050 secs]

      第一句输出:

               新生代回收前210686K,回收后979K,回收数量209707K,Heap总量回收前278070K回收后68379K,回收总量209691K。这就表示有16k没有被回收。

      第二句输出:

               新生代回收前210771K,回收后1129K,回收数量209642K,Heap总量回收前278171K回收后68544K,回收总量209627K。这就表示有15k没有被回收。

      比较一下启动时与现在的新生代占用内存情况和Heap使用情况发现Heap的使用增长很明显,新生代没有增长,而Heap使用总量增长了27M,这就表明可能存在内存泄露,虽然每一次泄露的字节数很少,但是频率很高,大部分泄露的对象都被升级到了中生代或者持久代。

      又一段时间后:

      [GC [DefNew: 211554K->1913K(235968K), 0.0461130 secs] 350102K->140481K(648160K), 0.0469790 secs]

      [GC [DefNew: 211707K->2327K(235968K), 0.0546170 secs] 350275K->140921K(648160K), 0.0555070 secs]

      第一句输出:

               新生代回收前211554K,回收后1913K,回收数量209641K,Heap总量回收前350102K回收后140481K,回收总量209621K。这就表示有20k没有被回收。



               分析到这里就可以看出每一次泄露的内存只有10几K,但是在大压力长时间的测试下,内存泄露还是很明显的,此时Heap已经增长到了140M,较启动时已经增长了100M。同时GC占用的时间越来越长。

      后续的现象:

               后续观察日志会发现,Full GC的频率越来越高,收集所花费时间也是越来越长。(Full GC定期会执行,同时局部回收不能满足分配需求的情况下也会执行)。



      [Full GC [Tenured: 786431K->786431K(786432K), 3.4882390 secs] 1022399K->1022399K(1022400K), [Perm : 36711K->36711K(98304K)], 3.4887920 secs]

      java.lang.OutOfMemoryError: Java heap space

      Dumping heap to java_pid7720.hprof ...



               出现这个语句表示内存真的被消耗完了。

      Jdk1.6 log:



      启动时GC的输出:

      [GC [PSYoungGen: 221697K->31960K(229376K)] 225788K->36051K(491520K), 0.0521830 secs] [Times: user=0.26 sys=0.05, real=0.05 secs]

      [GC [PSYoungGen: 228568K->32752K(229376K)] 232659K->37036K(491520K), 0.0408620 secs] [Times: user=0.21 sys=0.02, real=0.04 secs]



      第一句输出:

               新生代回收前221697K,回收后31960K,回收数量189737K,Heap总量回收前225788K回收后36051K,回收总量189737K。100%被回收。



      运行一段时间后输出:

      [GC [PSYoungGen: 258944K->2536K(259328K)] 853863K->598135K(997888K), 0.0471620 secs] [Times: user=0.15 sys=0.00, real=0.05 secs]

      [GC [PSYoungGen: 259048K->2624K(259328K)] 854647K->598907K(997888K), 0.0462980 secs] [Times: user=0.16 sys=0.02, real=0.04 secs]



      第一句输出:

               新生代回收前258944K,回收后2536K,回收数量256408K,Heap总量回收前853863K回收后598135K,回收总量255728K。680K没有被回收,但这并不意味着就会产生内存泄露。同时可以看出GC回收时间并没有增加。



      在运行一段时间后输出:

      [GC [PSYoungGen: 258904K->2488K(259264K)] 969663K->713923K(1045696K), 0.0485140 secs] [Times: user=0.16 sys=0.01, real=0.04 secs]

      [GC [PSYoungGen: 258872K->2448K(259328K)] 970307K->714563K(1045760K), 0.0473770 secs] [Times: user=0.16 sys=0.01, real=0.05 secs]



      第一句输出:

               新生代回收前258904K,回收后2488K,回收数量256416K,Heap总量回收前969663K回收后713923K,回收总量255740K。676K没有被回收,同时总的Heap也有所增加。

               此时看起来好像和1.5的状况一样。但是查看了一下Full GC的执行还是400-500次GC执行一次,因此继续观察。



      运行一天多以后输出:

      [GC [PSYoungGen: 257016K->3304K(257984K)] 1019358K->766310K(1044416K), 0.0567120 secs] [Times: user=0.18 sys=0.01, real=0.06 secs]

      [GC [PSYoungGen: 257128K->2920K(258112K)] 1020134K->766622K(1044544K), 0.0549570 secs] [Times: user=0.19 sys=0.00, real=0.05 secs]



      可以发现Heap增长趋缓。



      运行两天以后输出:

      [GC [PSYoungGen: 256936K->3584K(257792K)] 859561K->606969K(1044224K), 0.0565910 secs] [Times: user=0.18 sys=0.01, real=0.06 secs]

      [GC [PSYoungGen: 256960K->3368K(257728K)] 860345K->607445K(1044160K), 0.0553780 secs] [Times: user=0.18 sys=0.01, real=0.06 secs]



      发现Heap反而减少了,此时可以对内存泄露问题作初步排除了。(其实在jdk1.6环境下用jprofiler来观察,对于concurrent那个内存泄露点的跟踪发现,内存的确还是会不断增长的,不过在一段时间后还是有回收,因此也就可以部分解释前面出现的情况)



      总结:

               对于GC输出的观察需要分两个维度来看。一个是纵向比较,也就是一次回收对于内存变化的观察。一个是横向比较,对于长时间内存分配占用情况的比较,这部分比较需要较长时间的观察,不能仅仅凭短时间的几个抽样比较,因为对于抽样来说,Full GC前后的区别,运行时长的区别,资源瞬时占用的区别都会影响判断。同时要结合Full GC发生的时间周期,每一次GC收集所耗费的时间作为辅助判断标准。

               顺便说一下,Heap的 YoungGen,OldGen,PermGen的设置也是需要注意的,并不是越大越好,越大执行收集的时间越久,但是可能执行Full GC的频率会比较低,因此需要权衡。这些仔细的去了解一下GC的基础设计思想会更有帮助,不过一般用默认的也不错。还有就是可以配置一些特殊的GC,并行,同步等等,充分利用多CPU的资源。

               对于GC的优化可以通过现在很多图形工具来做,也可以类似于我这样采用最原始的分析方式,好处就是任何时间任何地点只要知道原理就可以分析无需借助外部工具。原始的总是最好的^_^。

       

       

    17. java jvm内存管理/gc策略/参数设置

      2011-07-29 11:49:20

      1. JVM内存管理:深入垃圾收集器与内存分配策略

      http://www.iteye.com/topic/802638

      Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。

      概述:

        说起垃圾收集(Garbage Collection,下文简称GC),大部分人都把这项技术当做Java语言的伴生产物。事实上GC的历史远远比Java来得久远,在1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。当Lisp还在胚胎时期,人们就在思考GC需要完成的3件事情:哪些内存需要回收?什么时候回收?怎么样回收?

        经过半个世纪的发展,目前的内存分配策略与垃圾回收技术已经相当成熟,一切看起来都进入“自动化”的时代,那为什么我们还要去了解GC和内存分配?答案很简单:当需要排查各种内存溢出、泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术有必要的监控、调节手段。

        把时间从1960年拨回现在,回到我们熟悉的Java语言。本文第一章中介绍了Java内存运行时区域的各个部分,其中程序计数器、VM栈、本地方法栈三个区域随线程而生,随线程而灭;栈中的帧随着方法进入、退出而有条不紊的进行着出栈入栈操作;每一个帧中分配多少内存基本上是在Class文件生成时就已知的(可能会由JIT动态晚期编译进行一些优化,但大体上可以认为是编译期可知的),因此这几个区域的内存分配和回收具备很高的确定性,因此在这几个区域不需要过多考虑回收的问题。而Java堆和方法区(包括运行时常量池)则不一样,我们必须等到程序实际运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,我们本文后续讨论中的“内存”分配与回收仅仅指这一部分内存。

      对象已死?

        在堆里面存放着Java世界中几乎所有的对象,在回收前首先要确定这些对象之中哪些还在存活,哪些已经“死去”了,即不可能再被任何途径使用的对象。

      引用计数算法(Reference Counting)

        最初的想法,也是很多教科书判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,当有一个地方引用它,计数器加1,当引用失效,计数器减1,任何时刻计数器为0的对象就是不可能再被使用的。

        客观的说,引用计数算法实现简单,判定效率很高,在大部分情况下它都是一个不错的算法,但引用计数算法无法解决对象循环引用的问题。举个简单的例子:对象A和B分别有字段b、a,令A.b=B和B.a=A,除此之外这2个对象再无任何引用,那实际上这2个对象已经不可能再被访问,但是引用计数算法却无法回收他们。

      根搜索算法(GC Roots Tracing)

        在实际生产的语言中(Java、C#、甚至包括前面提到的Lisp),都是使用根搜索算法判定对象是否存活。算法基本思路就是通过一系列的称为“GC Roots”的点作为起始进行向下搜索,当一个对象到GC Roots没有任何引用链(Reference Chain)相连,则证明此对象是不可用的。在Java语言中,GC Roots包括:

        1.在VM栈(帧中的本地变量)中的引用
        2.方法区中的静态引用
        3.JNI(即一般说的Native方法)中的引用

      生存还是死亡?

        判定一个对象死亡,至少经历两次标记过程:如果对象在进行根搜索后,发现没有与GC Roots相连接的引用链,那它将会被第一次标记,并在稍后执行他的finalize()方法(如果它有的话)。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这点是必须的,否则一个对象在finalize()方法执行缓慢,甚至有死循环什么的将会很容易导致整个系统崩溃。finalize()方法是对象最后一次逃脱死亡命运的机会,稍后GC将进行第二次规模稍小的标记,如果在finalize()中对象成功拯救自己(只要重新建立到GC Roots的连接即可,譬如把自己赋值到某个引用上),那在第二次标记时它将被移除出“即将回收”的集合,如果对象这时候还没有逃脱,那基本上它就真的离死不远了。

        需要特别说明的是,这里对finalize()方法的描述可能带点悲情的艺术加工,并不代表笔者鼓励大家去使用这个方法来拯救对象。相反,笔者建议大家尽量避免使用它,这个不是C/C++里面的析构函数,它运行代价高昂,不确定性大,无法保证各个对象的调用顺序。需要关闭外部资源之类的事情,基本上它能做的使用try-finally可以做的更好。

      关于方法区

        方法区即后文提到的永久代,很多人认为永久代是没有GC的,《Java虚拟机规范》中确实说过可以不要求虚拟机在这区实现GC,而且这区GC的“性价比”一般比较低:在堆中,尤其是在新生代,常规应用进行一次GC可以一般可以回收70%~95%的空间,而永久代的GC效率远小于此。虽然VM Spec不要求,但当前生产中的商业JVM都有实现永久代的GC,主要回收两部分内容:废弃常量与无用类。这两点回收思想与Java堆中的对象回收很类似,都是搜索是否存在引用,常量的相对很简单,与对象类似的判定即可。而类的回收则比较苛刻,需要满足下面3个条件:

        1.该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
        2.加载该类的ClassLoader已经被GC。
        3.该类对应的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法。

        是否对类进行回收可使用-XX:+ClassUnloading参数进行控制,还可以使用-verbose:class或者-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载、卸载信息。

        在大量使用反射、动态代理、CGLib等bytecode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要JVM具备类卸载的支持以保证永久代不会溢出。

      垃圾收集算法

        在这节里不打算大量讨论算法实现,只是简单的介绍一下基本思想以及发展过程。最基础的搜集算法是“标记-清除算法”(Mark-Sweep),如它的名字一样,算法分层“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,然后回收所有需要回收的对象,整个过程其实前一节讲对象标记判定的时候已经基本介绍完了。说它是最基础的收集算法原因是后续的收集算法都是基于这种思路并优化其缺点得到的。它的主要缺点有两个,一是效率问题,标记和清理两个过程效率都不高,二是空间问题,标记清理之后会产生大量不连续的内存碎片,空间碎片太多可能会导致后续使用中无法找到足够的连续内存而提前触发另一次的垃圾搜集动作。

        为了解决效率问题,一种称为“复制”(Copying)的搜集算法出现,它将可用内存划分为两块,每次只使用其中的一块,当半区内存用完了,仅将还存活的对象复制到另外一块上面,然后就把原来整块内存空间一次过清理掉。这样使得每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存就可以了,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,未免太高了一点。

        现在的商业虚拟机中都是用了这一种收集算法来回收新生代,IBM有专门研究表明新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的eden空间和2块较少的survivor空间,每次使用eden和其中一块survivor,当回收时将eden和survivor还存活的对象一次过拷贝到另外一块survivor空间上,然后清理掉eden和用过的survivor。Sun Hotspot虚拟机默认eden和survivor的大小比例是8:1,也就是每次只有10%的内存是“浪费”的。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有10%以内的对象存活,当survivor空间不够用时,需要依赖其他内存(譬如老年代)进行分配担保(Handle Promotion)。

        复制收集算法在对象存活率高的时候,效率有所下降。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保用于应付半区内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。因此人们提出另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然一样,但后续步骤不是进行直接清理,而是令所有存活的对象一端移动,然后直接清理掉这端边界以外的内存。

        当前商业虚拟机的垃圾收集都是采用“分代收集”(Generational Collecting)算法,这种算法并没有什么新的思想出现,只是根据对象不同的存活周期将内存划分为几块。一般是把Java堆分作新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,譬如新生代每次GC都有大批对象死去,只有少量存活,那就选用复制算法只需要付出少量存活对象的复制成本就可以完成收集。

      垃圾收集器

        垃圾收集器就是收集算法的具体实现,不同的虚拟机会提供不同的垃圾收集器。并且提供参数供用户根据自己的应用特点和要求组合各个年代所使用的收集器。本文讨论的收集器基于Sun Hotspot虚拟机1.6版。

      图1.Sun JVM1.6的垃圾收集器


        图1展示了1.6中提供的6种作用于不同年代的收集器,两个收集器之间存在连线的话就说明它们可以搭配使用。在介绍着些收集器之前,我们先明确一个观点:没有最好的收集器,也没有万能的收集器,只有最合适的收集器。

      1.Serial收集器
        单线程收集器,收集时会暂停所有工作线程(我们将这件事情称之为Stop The World,下称STW),使用复制收集算法,虚拟机运行在Client模式时的默认新生代收集器。

      2.ParNew收集器
        ParNew收集器就是Serial的多线程版本,除了使用多条收集线程外,其余行为包括算法、STW、对象分配规则、回收策略等都与Serial收集器一摸一样。对应的这种收集器是虚拟机运行在Server模式的默认新生代收集器,在单CPU的环境中,ParNew收集器并不会比Serial收集器有更好的效果。

      3.Parallel Scavenge收集器
        Parallel Scavenge收集器(下称PS收集器)也是一个多线程收集器,也是使用复制算法,但它的对象分配规则与回收策略都与ParNew收集器有所不同,它是以吞吐量最大化(即GC时间占总运行时间最小)为目标的收集器实现,它允许较长时间的STW换取总吞吐量最大化。

      4.Serial Old收集器
        Serial Old是单线程收集器,使用标记-整理算法,是老年代的收集器,上面三种都是使用在新生代收集器。

      5.Parallel Old收集器
        老年代版本吞吐量优先收集器,使用多线程和标记-整理算法,JVM 1.6提供,在此之前,新生代使用了PS收集器的话,老年代除Serial Old外别无选择,因为PS无法与CMS收集器配合工作。

      6.CMS(Concurrent Mark Sweep)收集器
        CMS是一种以最短停顿时间为目标的收集器,使用CMS并不能达到GC效率最高(总体GC时间最小),但它能尽可能降低GC时服务的停顿时间,这一点对于实时或者高交互性应用(譬如证券交易)来说至关重要,这类应用对于长时间STW一般是不可容忍的。CMS收集器使用的是标记-清除算法,也就是说它在运行期间会产生空间碎片,所以虚拟机提供了参数开启CMS收集结束后再进行一次内存压缩。
      内存分配与回收策略

        了解GC其中很重要一点就是了解JVM的内存分配策略:即对象在哪里分配和对象什么时候回收。

        关于对象在哪里分配,往大方向讲,主要就在堆上分配,但也可能经过JIT进行逃逸分析后进行标量替换拆散为原子类型在栈上分配,也可能分配在DirectMemory中(详见本文第一章)。往细节处讲,对象主要分配在新生代eden上,也可能会直接老年代中,分配的细节决定于当前使用的垃圾收集器类型与VM相关参数设置。我们可以通过下面代码来验证一下Serial收集器(ParNew收集器的规则与之完全一致)的内存分配和回收的策略。读者看完Serial收集器的分析后,不妨自己根据JVM参数文档写一些程序去实践一下其它几种收集器的分配策略。

      清单1:内存分配测试代码

       
      1. public class YoungGenGC {   
      2.   
      3.     private static final int _1MB = 1024 * 1024;   
      4.   
      5.     public static void main(String[] args) {   
      6.         // testAllocation();   
      7.         testHandlePromotion();   
      8.         // testPretenureSizeThreshold();   
      9.         // testTenuringThreshold();   
      10.         // testTenuringThreshold2();   
      11.     }   
      12.   
      13.     /**  
      14.      * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8  
      15.      */  
      16.     @SuppressWarnings("unused")   
      17.     public static void testAllocation() {   
      18.         byte[] allocation1, allocation2, allocation3, allocation4;   
      19.         allocation1 = new byte[2 * _1MB];   
      20.         allocation2 = new byte[2 * _1MB];   
      21.         allocation3 = new byte[2 * _1MB];   
      22.         allocation4 = new byte[4 * _1MB];  // 出现一次Minor GC   
      23.     }   
      24.   
      25.     /**  
      26.      * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8  
      27.      * -XX:PretenureSizeThreshold=3145728  
      28.      */  
      29.     @SuppressWarnings("unused")   
      30.     public static void testPretenureSizeThreshold() {   
      31.         byte[] allocation;   
      32.         allocation = new byte[4 * _1MB];  //直接分配在老年代中   
      33.     }   
      34.   
      35.     /**  
      36.      * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1  
      37.      * -XX:+PrintTenuringDistribution  
      38.      */  
      39.     @SuppressWarnings("unused")   
      40.     public static void testTenuringThreshold() {   
      41.         byte[] allocation1, allocation2, allocation3;   
      42.         allocation1 = new byte[_1MB / 4];  // 什么时候进入老年代决定于XX:MaxTenuringThreshold设置   
      43.         allocation2 = new byte[4 * _1MB];   
      44.         allocation3 = new byte[4 * _1MB];   
      45.         allocation3 = null;   
      46.         allocation3 = new byte[4 * _1MB];   
      47.     }   
      48.   
      49.     /**  
      50.      * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15  
      51.      * -XX:+PrintTenuringDistribution  
      52.      */  
      53.     @SuppressWarnings("unused")   
      54.     public static void testTenuringThreshold2() {   
      55.         byte[] allocation1, allocation2, allocation3, allocation4;   
      56.         allocation1 = new byte[_1MB / 4];   // allocation1+allocation2大于survivo空间一半   
      57.         allocation2 = new byte[_1MB / 4];     
      58.         allocation3 = new byte[4 * _1MB];   
      59.         allocation4 = new byte[4 * _1MB];   
      60.         allocation4 = null;   
      61.         allocation4 = new byte[4 * _1MB];   
      62.     }   
      63.   
      64.     /**  
      65.      * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:-HandlePromotionFailure  
      66.      */  
      67.     @SuppressWarnings("unused")   
      68.     public static void testHandlePromotion() {   
      69.         byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7;   
      70.         allocation1 = new byte[2 * _1MB];   
      71.         allocation2 = new byte[2 * _1MB];   
      72.         allocation3 = new byte[2 * _1MB];   
      73.         allocation1 = null;   
      74.         allocation4 = new byte[2 * _1MB];   
      75.         allocation5 = new byte[2 * _1MB];   
      76.         allocation6 = new byte[2 * _1MB];   
      77.         allocation4 = null;   
      78.         allocation5 = null;   
      79.         allocation6 = null;   
      80.         allocation7 = new byte[2 * _1MB];   
      81.     }   
      82. }  



      规则一:通常情况下,对象在eden中分配。当eden无法分配时,触发一次Minor GC。

        执行testAllocation()方法后输出了GC日志以及内存分配状况。-Xms20M -Xmx20M -Xmn10M这3个参数确定了Java堆大小为20M,不可扩展,其中10M分配给新生代,剩下的10M即为老年代。-XX:SurvivorRatio=8决定了新生代中eden与survivor的空间比例是1:8,从输出的结果也清晰的看到“eden space 8192K、from space 1024K、to space 1024K”的信息,新生代总可用空间为9216K(eden+1个survivor)。

        我们也注意到在执行testAllocation()时出现了一次Minor GC,GC的结果是新生代6651K变为148K,而总占用内存则几乎没有减少(因为几乎没有可回收的对象)。这次GC是发生的原因是为allocation4分配内存的时候,eden已经被占用了6M,剩余空间已不足分配allocation4所需的4M内存,因此发生Minor GC。GC期间虚拟机发现已有的3个2M大小的对象全部无法放入survivor空间(survivor空间只有1M大小),所以直接转移到老年代去。GC后4M的allocation4对象分配在eden中。

      清单2:testAllocation()方法输出结果

      [GC [DefNew: 6651K->148K(9216K), 0.0070106 secs] 6651K->6292K(19456K), 0.0070426 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
      Heap
      def new generation   total 9216K, used 4326K [0x029d0000, 0x033d0000, 0x033d0000)
        eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)
        from space 1024K,  14% used [0x032d0000, 0x032f5370, 0x033d0000)
        to   space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)
      tenured generation   total 10240K, used 6144K [0x033d0000, 0x03dd0000, 0x03dd0000)
         the space 10240K,  60% used [0x033d0000, 0x039d0030, 0x039d0200, 0x03dd0000)
      compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
         the space 12288K,  17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)
      No shared spaces configured.

      规则二:配置了PretenureSizeThreshold的情况下,对象大于设置值将直接在老年代分配。

        执行testPretenureSizeThreshold()方法后,我们看到eden空间几乎没有被使用,而老年代的10M控件被使用了40%,也就是4M的allocation对象直接就分配在老年代中,则是因为PretenureSizeThreshold被设置为3M,因此超过3M的对象都会直接从老年代分配。

      清单3:

      Heap
      def new generation   total 9216K, used 671K [0x029d0000, 0x033d0000, 0x033d0000)
        eden space 8192K,   8% used [0x029d0000, 0x02a77e98, 0x031d0000)
        from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)
        to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)
      tenured generation   total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)
         the space 10240K,  40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)
      compacting perm gen  total 12288K, used 2107K [0x03dd0000, 0x049d0000, 0x07dd0000)
         the space 12288K,  17% used [0x03dd0000, 0x03fdefd0, 0x03fdf000, 0x049d0000)
      No shared spaces configured.

      规则三:在eden经过GC后存活,并且survivor能容纳的对象,将移动到survivor空间内,如果对象在survivor中继续熬过若干次回收(默认为15次)将会被移动到老年代中。回收次数由MaxTenuringThreshold设置。

        分别以-XX:MaxTenuringThreshold=1和-XX:MaxTenuringThreshold=15两种设置来执行testTenuringThreshold(),方法中allocation1对象需要256K内存,survivor空间可以容纳。当MaxTenuringThreshold=1时,allocation1对象在第二次GC发生时进入老年代,新生代已使用的内存GC后非常干净的变成0KB。而MaxTenuringThreshold=15时,第二次GC发生后,allocation1对象则还留在新生代survivor空间,这时候新生代仍然有404KB被占用。

      清单4:
      MaxTenuringThreshold=1

      [GC [DefNew
      Desired survivor size 524288 bytes, new threshold 1 (max 1)
      - age   1:     414664 bytes,     414664 total
      : 4859K->404K(9216K), 0.0065012 secs] 4859K->4500K(19456K), 0.0065283 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
      [GC [DefNew
      Desired survivor size 524288 bytes, new threshold 1 (max 1)
      : 4500K->0K(9216K), 0.0009253 secs] 8596K->4500K(19456K), 0.0009458 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
      Heap
      def new generation   total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)
        eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)
        from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)
        to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)
      tenured generation   total 10240K, used 4500K [0x033d0000, 0x03dd0000, 0x03dd0000)
         the space 10240K,  43% used [0x033d0000, 0x03835348, 0x03835400, 0x03dd0000)
      compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
         the space 12288K,  17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)
      No shared spaces configured.

      MaxTenuringThreshold=15
      [GC [DefNew
      Desired survivor size 524288 bytes, new threshold 15 (max 15)
      - age   1:     414664 bytes,     414664 total
      : 4859K->404K(9216K), 0.0049637 secs] 4859K->4500K(19456K), 0.0049932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
      [GC [DefNew
      Desired survivor size 524288 bytes, new threshold 15 (max 15)
      - age   2:     414520 bytes,     414520 total
      : 4500K->404K(9216K), 0.0008091 secs] 8596K->4500K(19456K), 0.0008305 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
      Heap
      def new generation   total 9216K, used 4582K [0x029d0000, 0x033d0000, 0x033d0000)
        eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)
        from space 1024K,  39% used [0x031d0000, 0x03235338, 0x032d0000)
        to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)
      tenured generation   total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)
         the space 10240K,  40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)
      compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
         the space 12288K,  17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)
      No shared spaces configured.

      规则四:如果在survivor空间中相同年龄所有对象大小的累计值大于survivor空间的一半,大于或等于个年龄的对象就可以直接进入老年代,无需达到MaxTenuringThreshold中要求的年龄。

        执行testTenuringThreshold2()方法,并将设置-XX:MaxTenuringThreshold=15,发现运行结果中survivor占用仍然为0%,而老年代比预期增加了6%,也就是说allocation1、allocation2对象都直接进入了老年代,而没有等待到15岁的临界年龄。因为这2个对象加起来已经到达了512K,并且它们是同年的,满足同年对象达到survivor空间的一半规则。我们只要注释掉其中一个对象new操作,就会发现另外一个就不会晋升到老年代中去了。

      清单5:
      [GC [DefNew
      Desired survivor size 524288 bytes, new threshold 1 (max 15)
      - age   1:     676824 bytes,     676824 total
      : 5115K->660K(9216K), 0.0050136 secs] 5115K->4756K(19456K), 0.0050443 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]
      [GC [DefNew
      Desired survivor size 524288 bytes, new threshold 15 (max 15)
      : 4756K->0K(9216K), 0.0010571 secs] 8852K->4756K(19456K), 0.0011009 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
      Heap
      def new generation   total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)
        eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)
        from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)
        to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)
      tenured generation   total 10240K, used 4756K [0x033d0000, 0x03dd0000, 0x03dd0000)
         the space 10240K,  46% used [0x033d0000, 0x038753e8, 0x03875400, 0x03dd0000)
      compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
         the space 12288K,  17% used [0x03dd0000, 0x03fe09a0, 0x03fe0a00, 0x049d0000)
      No shared spaces configured.

      规则五:在Minor GC触发时,会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间,如果大于,改为直接进行一次Full GC,如果小于则查看HandlePromotionFailure设置看看是否允许担保失败,如果允许,那仍然进行Minor GC,如果不允许,则也要改为进行一次Full GC。

        前面提到过,新生代才有复制收集算法,但为了内存利用率,只使用其中一个survivor空间来作为轮换备份,因此当出现大量对象在GC后仍然存活的情况(最极端就是GC后所有对象都存活),就需要老年代进行分配担保,把survivor无法容纳的对象直接放入老年代。与生活中贷款担保类似,老年代要进行这样的担保,前提就是老年代本身还有容纳这些对象的剩余空间,一共有多少对象在GC之前是无法明确知道的,所以取之前每一次GC晋升到老年代对象容量的平均值与老年代的剩余空间进行比较决定是否进行Full GC来让老年代腾出更多空间。

        取平均值进行比较其实仍然是一种动态概率的手段,也就是说如果某次Minor GC存活后的对象突增,大大高于平均值的话,依然会导致担保失败,这样就只好在失败后重新进行一次Full GC。虽然担保失败时做的绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure打开,避免Full GC过于频繁。

      清单6:
      HandlePromotionFailure = false

      [GC [DefNew: 6651K->148K(9216K), 0.0078936 secs] 6651K->4244K(19456K), 0.0079192 secs] [Times: user=0.00 sys=0.02, real=0.02 secs]
      [GC [DefNew: 6378K->6378K(9216K), 0.0000206 secs][Tenured: 4096K->4244K(10240K), 0.0042901 secs] 10474K->4244K(19456K), [Perm : 2104K->2104K(12288K)], 0.0043613 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]


      总结

        本章介绍了垃圾收集的算法、6款主要的垃圾收集器,以及通过代码实例具体介绍了新生代串行收集器对内存分配及回收的影响。

        GC在很多时候都是系统并发度的决定性因素,虚拟机之所以提供多种不同的收集器,提供大量的调节参数,是因为只有根据实际应用需求、实现方式选择最优的收集方式才能获取最好的性能。没有固定收集器、参数组合,也没有最优的调优方法,虚拟机也没有什么必然的行为。笔者看过一些文章,撇开具体场景去谈论老年代达到92%会触发Full GC(92%应当来自CMS收集器触发的默认临界点)、98%时间在进行垃圾收集系统会抛出OOM异常(98%应该来自parallel收集器收集时间比率的默认临界点)其实意义并不太大。因此学习GC如果要到实践调优阶段,必须了解每个具体收集器的行为、优势劣势、调节参数。

       

      2. 一次Java垃圾收集调优实战

      http://www.iteye.com/topic/212967

      1 资料

      • JDK5.0垃圾收集优化之--Don't Pause(花钱的年华)
      • 编写对GC友好,又不泄漏的代码(花钱的年华)
      • JVM调优总结
      • JDK 6所有选项及默认值

      2 GC日志打印

        GC调优是个很实验很伽利略的活儿,GC日志是先决的数据参考和最终验证:

      -XX:+PrintGCDetails -XX:+PrintGCTimeStamps(GC发生的时间) 
      -XX:+PrintGCApplicationStoppedTime(GC消耗了多少时间)
      -XX:+PrintGCApplicationConcurrentTime(GC之间运行了多少时间)

      3 收集器选择

      CMS收集器:暂停时间优先

         配置参数:-XX:+UseConcMarkSweepGC
         已默认无需配置的参数:-XX:+UseParNewGC(Parallel收集新生代) -XX:+CMSPermGenSweepingEnabled(CMS收集持久代) -XX:UseCMSCompactAtFullCollection(full gc时压缩年老代)

         初始效果:1g堆内存的新生代约60m,minor gc约5-20毫秒,full gc约130毫秒。

      Parallel收集器:吞吐量优先

          配置参数: -XX:+UseParallelGC -XX:+UseParallelOldGC(Parallel收集年老代,从JDK6.0开始支持)

          已默认无需配置的参数: -XX:+UseAdaptiveSizePolicy(动态调整新生代大小)

          初始效果:1g堆内存的新生代约90-110m(动态调整),minor gc约5-20毫秒,full gc有无UseParallelOldGC 参数分别为1.3/1.1秒,差别不大。

          另外-XX:MaxGCPauseMillis=100 设置minor gc的期望最大时间,JVM会以此来调整新生代的大小,但在此测试环境中对象死的太快,此参数作用不大。

      4 调优实战

            Parallel收集高达1秒的暂停时间基本不可忍受,所以选择CMS收集器。

            在被压测的Mule 2.0应用里,每秒都有大约400M的海量短命对象产生:

      1. 因为默认60M的新生代太小了,频繁发生minor gc,大约0.2秒就进行一次。
      2. 因为CMS收集器中MaxTenuringThreshold(生代对象撑过过多少次minor gc才进入年老代的设置)默认0,存活的临时对象不经过Survivor区直接进入年老代,不久就占满年老代发生full gc。

           对这两个参数的调优,既要改善上面两种情况,又要避免新生代过大,复制次数过多造成minor gc的暂停时间过长。

      1. 使用-Xmn调到1/3 总内存。观察后设置-Xmn500M,新生代实际约460m。(用-XX:NewRatio设置无效,只能用 -Xmn)。
      2. 添加-XX:+PrintTenuringDistribution 参数观察各个Age的对象总大小,观察后设置-XX:MaxTenuringThreshold=5。

            优化后,大约1.1秒才发生一次minor gc,且速度依然保持在15-20ms之间。同时年老代的增长速度大大减缓,很久才发生一次full gc,

            参数定稿:

       -server -Xms1024m -Xmx1024m -Xmn500m -XX:+UseConcMarkSweepGC   -XX:MaxTenuringThreshold=5  
      -XX:+ExplicitGCInvokesConcurrent

            最后服务处理速度从1180 tps 上升到1380 tps,调整两个参数提升17%的性能还是笔很划算的买卖。

       

           另外,JDK6 Update 7自带了一个VisualVM工具,内里就是之前也有用过的Netbean Profiler,类似JConsole一样使用,可以看到线程状态,内存中对象以及方法的CPU时间等调优重要参考依据。免费捆绑啊,Sun 这样搞法,其他做Profiler的公司要关门了。

      3. JVM gc参数设置与分析

      来自:http://hi.baidu.com/i1see1you/blog/item/7ba0d250c30131481038c20c.html

      一.概述

      java的最大好处是自动垃圾回收,这样就无需我们手动的释放对象空间了,但是也产生了相应的负效果,gc是需要时间和资源的,不好的gc会严重影响系统的系能,因此良好的gc是JVM的高性能的保证。JVM堆分为新生代,旧生代和年老代,新生代可用的gc方式有:串行gc(Serial Copying),并行回收gc(Parellel Scavenge),并行gc(ParNew),旧生代和年老代可用的gc方式有串行gc(Serial MSC),并行gc(Parallel MSC),并发gc(CMS)。

      二.回收方式的选择

      jvm有client和server两种模式,这两种模式的gc默认方式是不同的:

      clien模式下,新生代选择的是串行gc,旧生代选择的是串行gc

      server模式下,新生代选择的是并行回收gc,旧生代选择的是并行gc

      一般来说我们系统应用选择有两种方式:吞吐量优先和暂停时间优先,对于吞吐量优先的采用server默认的并行gc方式,对于暂停时间优先的选用并发gc(CMS)方式。

      三.CMS gc

      CMS,全称Concurrent Low Pause Collector,是jdk1.4后期版本开始引入的新gc算法,在jdk5和jdk6中得到了进一步改进,它的主要适合场景是对响应时间的重要性需求大于对吞吐量的要求,能够承受垃圾回收线程和应用线程共享处理器资源,并且应用中存在比较多的长生命周期的对象的应用。CMS是用于对tenured generation的回收,也就是年老代的回收,目标是尽量减少应用的暂停时间,减少full gc发生的几率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代。在我们的应用中,因为有缓存的存在,并且对于响应时间也有比较高的要求,因此希望能尝试使用CMS来替代默认的server型JVM使用的并行收集器,以便获得更短的垃圾回收的暂停时间,提高程序的响应性。
          CMS并非没有暂停,而是用两次短暂停来替代串行标记整理算法的长暂停,它的收集周期是这样:
          
      初始标记(CMS-initial-mark) -> 并发标记(CMS-concurrent-mark) -> 重新标记(CMS-remark) -> 并发清除(CMS-concurrent-sweep) ->并发重设状态等待下次CMS的触发(CMS-concurrent-reset)

          其中的1,3两个步骤需要暂停所有的应用程序线程的。第一次暂停从root对象开始标记存活的对象,这个阶段称为初始标记;第二次暂停是在并发标记之后,暂停所有应用程序线程,重新标记并发标记阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致)。第一次暂停会比较短,第二次暂停通常会比较长,并且 remark这个阶段可以并行标记。

          而并发标记、并发清除、并发重设阶段的所谓并发,是指一个或者多个垃圾回收线程和应用程序线程并发地运行,垃圾回收线程不会暂停应用程序的执行,如果你有多于一个处理器,那么并发收集线程将与应用线程在不同的处理器上运行,显然,这样的开销就是会降低应用的吞吐量。Remark阶段的并行,是指暂停了所有应用程序后,启动一定数目的垃圾回收进程进行并行标记,此时的应用线程是暂停的。

      四.full  gc

      full gc是对新生代,旧生代,以及持久代的统一回收,由于是对整个空间的回收,因此比较慢,系统中应当尽量减少full gc的次数。

      如下几种情况下会发生full gc:

      《旧生代空间不足

      《持久代空间不足

      《CMS GC时出现了promotion failed和concurrent mode failure

      《统计得到新生代minor gc时晋升到旧生代的平均大小小于旧生代剩余空间

      《直接调用System.gc,可以DisableExplicitGC来禁止

      《存在rmi调用时,默认会每分钟执行一次System.gc,可以通过-Dsun.rmi.dgc.server.gcInterval=3600000来设置大点的间隔。

      五.示例

      下面对如下的参数进行分析:

      JAVA_OPTS="-server -Xms2000m -Xmx2000m -Xmn800m -XX:PermSize=64m -XX:MaxPermSize=256m -XX:SurvivorRatio=4

      -verbose:gc -Xloggc:$CATALINA_HOME/logs/gc.log -Djava.awt.headless=true -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000

      -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15"

      -Xms2000m -Xmx2000m -Xmn800m -XX:PermSize=64m -XX:MaxPermSize=256m

      Xms,即为jvm启动时得JVM初始堆大小,Xmx为jvm的最大堆大小,xmn为新生代的大小,permsize为永久代的初始大小,MaxPermSize为永久代的最大空间。

      -XX:SurvivorRatio=4

      SurvivorRatio为新生代空间中的Eden区和救助空间Survivor区的大小比值,默认是32,也就是说Eden区是 Survivor区的32倍大小,要注意Survivo是有两个区的,因此Surivivor其实占整个young genertation的1/34。调小这个参数将增大survivor区,让对象尽量在survitor区呆长一点,减少进入年老代的对象。去掉救助空间的想法是让大部分不能马上回收的数据尽快进入年老代,加快年老代的回收频率,减少年老代暴涨的可能性,这个是通过将-XX:SurvivorRatio 设置成比较大的值(比如65536)来做到。

       -verbose:gc -Xloggc:$CATALINA_HOME/logs/gc.log

      将虚拟机每次垃圾回收的信息写到日志文件中,文件名由file指定,文件格式是平文件,内容和-verbose:gc输出内容相同。

      -Djava.awt.headless=true

      Headless模式是系统的一种配置模式。在该模式下,系统缺少了显示设备、键盘或鼠标。

      -XX:+PrintGCTimeStamps -XX:+PrintGCDetails

      设置gc日志的格式

      -Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000 

      指定rmi调用时gc的时间间隔

      -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15

      采用并发gc方式,经过15次minor gc 后进入年老代六.一些常见问题1.为了避免Perm区满引起的full gc,建议开启CMS回收Perm区选项:
      +CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled

      2.默认CMS是在tenured generation沾满68%的时候开始进行CMS收集,如果你的年老代增长不是那么快,并且希望降低CMS次数的话,可以适当调高此值:
      -XX:CMSInitiatingOccupancyFraction=80

      3.遇到两种fail引起full gc:Prommotion failed和Concurrent mode failed时:
      Prommot

    18. java gc,java内存分析(性能测试总结之内存泄露和内存溢出)

      2011-07-28 19:10:51

       

      1. Java

      http://wenku.baidu.com/view/cb7becb765ce050876321307.html

       

      2.  性能测试总结之内存泄露和内存溢出

      http://www.uml.org.cn/Test/200912106.asp

      刚刚做完了一个项目的性能测试,“有幸”也遇到了内存泄露的案例,所以在此和大家分享一下。

      主要从以下几部分来说明,关于内存和内存泄露、溢出的概念,区分内存泄露和内存溢出;内存的区域划分,了解GC回收机制;重点关注如何去监控和发现内存问题;此外分析出问题还要如何解决内存问题。

      下面就开始本篇的内容:

      第一部分 概念

      众所周知,java中的内存java虚拟机自己去管理的,他不想C++需要自己去释放。笼统地去讲,java的内存分配分为两个部分,一个是数据堆,一个是栈。程序在运行的时候一般分配数据堆,把局部的临时的变量都放进去,生命周期和进程有关系。但是如果程序员声明了static的变量,就直接在栈中运行的,进程销毁了,不一定会销毁static变量。

      另外为了保证java内存不会溢出,java中有垃圾回收机制。 System.gc()即垃圾收集机制是指jvm用于释放那些不再使用的对象所占用的内存。java语言并不要求jvm有gc,也没有规定gc如何工作。垃圾收集的目的在于清除不再使用的对象。gc通过确定对象是否被活动对象引用来确定是否收集该对象。

      而其中,内存溢出就是你要求分配的java虚拟机内存超出了系统能给你的,系统不能满足需求,于是产生溢出。

      内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问,该块已分配出来的内存也无法再使用,随着服务器内存的不断消耗,而无法使用的内存越来越多,系统也不能再次将它分配给需要的程序,产生泄露。一直下去,程序也逐渐无内存使用,就会溢出。

      第二部分 原理

      JAVA垃圾回收及对内存区划分

      在Java虚拟机规范中,提及了如下几种类型的内存空间:

      ◇ 栈内存(Stack):每个线程私有的。

      ◇ 堆内存(Heap):所有线程公用的。

      ◇ 方法区(Method Area):有点像以前常说的“进程代码段”,这里面存放了每个加载类的反射信息、类函数的代码、编译时常量等信息。

      ◇ 原生方法栈(Native Method Stack):主要用于JNI中的原生代码,平时很少涉及。

      而Java的使用的是堆内存,java堆是一个运行时数据区,类的实例(对象)从中分配空间。Java虚拟机(JVM)的堆中储存着正在运行的应用程序所建立的所有对象,“垃圾回收”也是主要是和堆内存(Heap)有关。

      垃圾回收的概念就是JAVA虚拟机(JVM)回收那些不再被引用的对象内存的过程。一般我们认为正在被引用的对象状态为“alive”,而没有被应用或者取不到引用属性的对象状态为“dead”。垃圾回收是一个释放处于”dead”状态的对象的内存的过程。而垃圾回收的规则和算法被动态的作用于应用运行当中,自动回收。

      JVM的垃圾回收器采用的是一种分代(generational )回收策略,用较高的频率对年轻的对象(young generation)进行扫描和回收,这种叫做minor collection,而对老对象(old generation)的检查回收频率要低很多,称为major collection。这样就不需要每次GC都将内存中所有对象都检查一遍,这种策略有利于实时观察和回收。

      (Sun JVM 1.3 有两种最基本的内存收集方式:一种称为copying或scavenge,将所有仍然生存的对象搬到另外一块内存后,整块内存就可回收。这种方法有效率,但需要有一定的空闲内存,拷贝也有开销。这种方法用于minor collection。另外一种称为mark-compact,将活着的对象标记出来,然后搬迁到一起连成大块的内存,其他内存就可以回收了。这种方法不需要占用额外的空间,但速度相对慢一些。这种方法用于major collection. )

      一些对象被创建出来只是拥有短暂的生命周期,比如 iterators 和本地变量。

      另外一些对象被创建是拥有很长的生命周期,比如 高持久化对象等。

      垃圾回收器的分代策略是把内存区划分为几个代,然后为每个代分配一到多个内存区块。当其中一个代用完了分配给他的内存后,JVM会在分配的内存区内执行一个局部的GC(也可以叫minor collection)操作,为了回收处于“dead”状态的对象所占用的内存。局部GC通常要不Full GC要快很多。

      JVM定义了两个代,年轻代(yong generation)(有时称为“nursery”托儿所)和老年代(old generation)。年轻代包括 “Eden space(伊甸园)”和两个“survivor spaces”。虚拟内存初始化的时候会把所有对象都分配到 Eden space,并且大部分对象也会在该区域被释放。 当进行  minor GC的时候,VM会把剩下的没有释放的对象从Eden space移动到其中一个survivor spaces当中。此外,VM也会把那些长期存活在survivor spaces 里的对象移动到 老生代的“tenured” space中。当 tenured generation 被填满后,就会产生Full GC,Full GC会相对比较慢因为回收的内容包括了所有的 live状态的对象。pemanet generation这个代包括了所有java虚拟机自身使用的相对比较稳定的数据对象,比如类和对象方法等。

      关于代的划分,可以从下图中获得一个概况:

      JVM

      如果垃圾回收器影响了系统的性能,或者成为系统的瓶颈,你可以通过自定义各个代的大小来优化它的性能。使用JConsole,可以方便的查看到当前应用所配置的垃圾回收器的各个参数。想要获得更详细的参数,可以参考以下调优介绍:

      Tuning Garbage collection with the 5.0 HotSpot VM

      http://java.sun.com/docs/hotspot/gc/index.html

      最后,总结一下各区内存:

      Eden Space (heap): 内存最初从这个线程池分配给大部分对象。

      Survivor Space (heap):用于保存在eden space内存池中经过垃圾回收后没有被回收的对象。

      Tenured Generation (heap):用于保持已经在 survivor space内存池中存在了一段时间的对象。

      Permanent Generation (non-heap): 保存虚拟机自己的静态(refective)数据,例如类(class)和方法(method)对象。Java虚拟机共享这些类数据。这个区域被分割为只读的和只写的,

      Code Cache (non-heap):HotSpot Java虚拟机包括一个用于编译和保存本地代码(native code)的内存,叫做“代码缓存区”(code cache)

      第三部分 监控(工具发现问题)

      谈到内存监控工具,JConsole是必须要介绍的,它是一个用JAVA写的GUI程序,用来监控VM,并可监控远程的VM,易用且功能强大。具体可监控JAVA内存、JAVA CPU使用率、线程执行情况、加载类概况等,Jconsole需要在JVM参数中配置端口才能使用。

      由于是GUI程序,界面可视化,这里就不做详细介绍,

      具体帮助支持文档请参阅性能测试JConsole使用方法总结:

      http://www.taobao.ali.com/chanpin/km/test/DocLib/性能测试辅助工具-JConsole的使用方法.aspx

      或者参考SUN官网的技术文档:

      http://Java.sun.com/j2se/1.5.0/docs/guide/management/jconsole.html

      http://Java.sun.com/javase/6/docs/technotes/tools/share/jconsole.html

      在实际测试某一个项目时,内存出现泄露现象。起初在性能测试的1个小时中,并不明显,而在稳定性测试的时候才发现,应用的HSF调用在经过几个小时运行后,就出现性能明显下降的情况。在服务日志中报大量HSF超时,但所调用系统没有任何超时日志,并且压力应用的load都很低。经过查看日志后,认为应用可能存在内存泄漏。通过jconsole 以及 jmap 工具进行分析发现,确实存在内存泄漏问题,其中PS Old Gen最终达到占用 100%的占用。如图所示:

      内存泄露

      从上图可以看到,虽然每次Full GC,JVM内存会有部分回收,但回收并不彻底,不可回收的内存对象会越来越多,这样便会出现以上的一个趋势。在Full GC无法回收的对象越来越多时,最终已使用内存达到系统分配的内存最大值,系统最后无内存可分配,最终down机。

      第四部分 分析

      经过开发和架构师对应用的分析,查看此时内存队列,看哪个对象占用数据最多,再利用jmap命令,对线程数据分析,如下所示:

      num     #instances         #bytes  class name

      ———————————————-

      1:       9248056        665860032  com.taobao.matrix.mc.domain.**

      2:       9248031        295936992  com.taobao.matrix.**

      3:       9248068        147969088  java.util.**

      4:       1542111        37010664   java.util.Date

      前三个instances不断增加,指代的是同一个代码逻辑,异步分发的问题,堵塞消息,回收多次都无法回收成功。导致内存溢出。

      此外,对应用的性能单独做了压测,他的性能只能支撑到一半左右,故发送消息的TPS,应用肯定无法处理过来,导致消息堆积,而JAVA垃圾回收期认为这些都是有用的对象,导致内存堆积,直至系统崩溃。

      调优方法

      由于具体调优方法涉及到应用的配置信息,故在此暂不列出,可以参考性能测试小组发布的《性能测试调优宝典》

      第四部分 总结

      内存溢出主要是由于代码编写时对某些方法、类应用不合理,或者没有预估到临时对象会占用很大内存量,或者把过多的数据放入JVM缓存,或者性能压力大导致消息堆积而占用内存,以至于在性能测试时,生成庞大数量的临时对象,GC时没有做出有效回收甚至根本就不能回收,造成内存空间不足,内存溢出。

      如果编码之前,对内存使用量进行预估,对放在内存中的数据进行评估,保证有用的信息尽快释放,无用的信息能够被GC回收,这样在一定程度上是可以避免内存溢出问题的。

       

       

      3. java gc监视方法与工具

      http://wjason.iteye.com/blog/356193

      项目需要,将gc的监视方法,和gc log的分析工具整理一下,

      问题一  让jvm输出log

      打印jvm的gc log, 需要如下配置jvm启动参数:

      方法1:
      -Xloggc:D:/gc.log

      方法2
      -Xloggc:D:/gc.log -XX:+PrintGCTimeStamps -XX:-PrintGCDetails -XX:+UseConcMarkSweepGC
       

      这些jvm启动参数的意义参考这里:the Java application launcher

      问题二  分析jvm的log

      我现在用的是HPjmeter, 和HPjtune,收费的东西公司用不了, 没调查, 但目前这两个基本满足了我的需求.

      网上有这么一篇blog: 用HPjmeter分析jvm的gc日志和heap dump . 里面有各种工具的下载地址.

      整理如下:

      HPjmeter 下载:

      ftp://ftp.hp.com/pub/gsy/HPjmeter_3.0.01.jar
      ftp://ftp.hp.com/pub/gsy/hpjmeter_console_3.0.01.00_windows_setup.exe
      ftp://ftp.hp.com/pub/gsy/hpjmeter_console_3.0.01.00_linux_setup.sh

      HPjtune 下载:ftp://ftp.hp.com/pub/gsy (支持目录方式浏览。)

      这篇blog中还介绍了其他的一些gc分析工具,同时也给出了相应的下载地址, 分别是:

      SAP Memory Analyzer(Eclipse插件),  gcviewer,  scimark2 benchmark

       

       

      4.用HPjtune分析GC日志(一个实际案例的诊断过程)

      http://www.hashei.me/2009/07/use-hpjtune-to-analysis-gc-log.html

       

       

    19. LoadRunner_GUID,关联长度问题,中文乱码,用Eclipse编写脚本,执行Linux命令,Memcached

      2011-07-28 18:26:14

      以下均转自:http://hi.baidu.com/higkoo/blog/category/Loadrunner/index/0

       

      5. LoadRunner实现:模拟生成UUID/GUID

      GUID: 即Globally Unique Identifier(全球唯一标识符) 也称作 UUID(Universally Unique IDentifier) 。 GUID是一个通过特定算法产生的二进制长度为128位的数字标识符,用于指示产品的唯一性。GUID 主要用于在拥有多个节点、多台计算机的网络或系统中,分配必须具有唯一性的标识符。

      LUID就是指locally unique identifier,GUID/UUID大家是比较熟悉的,和GUID/UUID的要求保证全局唯一不同,LUID只要保证局部唯一,就是指在系统的每一次运行期间保证是唯一的就可以了。

        在 Windows 平台上,GUID 广泛应用于微软的产品中,用于标识如如注册表项、类及接口标识、数据库、系统目录等对象。

          灵感来自:LoadRunner生成唯一数 + LoadRunner实现:计算字符串Md5 。

         原理非常简单,先生成唯一字符串,然后算Md5。以下代码就是在Md5的基础上稍稍加工一下:

      void Md5toLUID(char* inStr,char *outStr)
      {
          int i;
          strncpy(outStr,inStr,8);
          strcat(outStr,"-");
          for (i=9;i<13;i++)
              outStr[i]=inStr[i-1];
          strcat(outStr,"-");
          for (i=14;i<18;i++)
              outStr[i]=inStr[i-2];
          strcat(outStr,"-");
          for (i=19;i<23;i++)
              outStr[i]=inStr[i-3];
          strcat(outStr,"-");
          for (i=24;i<37;i++)
              outStr[i]=inStr[i-4];
          strcat(outStr,"\0");
      }
      调用方法:

      void main()
      {
          char uStr[33],lStr[37];
          int i;
         
          for (i=0;i<10;i++) {
              lr_save_int(i,"iValue");
              GetUniqueString(lr_eval_string("{iValue}"),uStr);
              GetMd5FromString(uStr,uStr);
              Md5toLUID(uStr,lStr);
              lr_output_message(lStr);
          }
      }
      输出:

      main.c(18): b7f163a8-f89c-59e3-6705-a3823a358c0d
      main.c(18): 20fcb7ab-0879-9572-fb5b-5c9848b37930
      main.c(18): 869b718d-126c-eaeb-b099-b1ec15d3c9db
      main.c(18): fd12c050-0975-3641-1de9-3685431d4a01
      main.c(18): 604bbc51-e787-1955-d721-ee5032640629
      main.c(18): 4fffdc48-0c44-66c9-34d7-697e473d20da
      main.c(18): a5d0d30c-5053-03e8-6e1a-1f112ef49007
      main.c(18): 4babb152-de2f-1136-d4a6-8aa78a90f2c7
      main.c(18): 833f6f33-da3d-efeb-7ec8-95f5491bf1a1
      main.c(18): 89148aad-8040-e70c-b406-69d56f570293

         测试的时候这个LUID,就可以当成GUID/UUID使用了。除非同时使用了多个Contorller而且脚本组名也相同,而且即使是如此,取到重复值的机率也非常小!

      函数就是在拼接“-”到指定位置,平常用得更多的是字符串替换:C语言实现:替换字符串中指定字符。

       

      6. LoadRunner/C语言 实现:生成GUID

      在LoadRunner里使用GUID有以下三种方法:

      1、使用LoadRunner自带的函数:已实现。

      2、使用C++封装DLL供LoadRunner调用。

      3、直接调用Windows系统API,方式和第2种类似。


      下面是使用第3种方式的实现示例:

      vuser_init(){
      lr_load_dll("ole32.dll");
      }


      char* lr_guid_gen(char* paramName){
      typedef struct _GUID    {
      unsigned long Data1;
      unsigned short Data2;
      unsigned short Data3;
      unsigned char Data4[8];
      } GUID;

      GUID m_guid;
      char buf[50];
      char pNameStr[50];   

      CoCreateGuid(&m_guid);
      // 定义输出格式
      //sprintf (buf, "{%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}", // 大写
      // sprintf (buf, "{%08lx-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}",// 小写
      sprintf (buf, "%08lX%04X%04X%02X%02X%02X%02X%02X%02X%02X%02X",// 小写
      m_guid.Data1, m_guid.Data2, m_guid.Data3,
      m_guid.Data4[0], m_guid.Data4[1], m_guid.Data4[2], m_guid.Data4[3],
      m_guid.Data4[4], m_guid.Data4[5], m_guid.Data4[6], m_guid.Data4[7]);   

      lr_save_string(buf, paramName);
      sprintf(pNameStr,"{%s}",paramName);

      return lr_eval_string(pNameStr);
      }


      Action(){
      lr_output_message(lr_guid_gen("GUID"));
      }

      vuser_end(){
      return 0;
      }

      相对自己编写DLL而言,调用系统API更安全更稳定。

      而且C++要求测试人员有一定的编码能力,编写的代码要稳定且安全。

      每次修改GUID的输出方式,需要重新修改并编译DLL或编写多个导出。

      直接调用系统API,安全稳定。且输出格式随手掌控。

       

      7. LoadRunner 函数 web_reg_save_param 的长度问题

      发现web_reg_save_param在查找字符串时有问题,找不到指定字符串。

          使用 web_reg_save_param("Wholebody","LB=", "RB=","Notfound=warning","Search=Body", LAST); 可以把整个服务器返回的Body内容都拿出来。

          譬如内容如下:

      {"content":"0zIUKjQevu6ygxCSsggU+g30XZL1rEAGOWsjRPQ9CHz1Np28wP4HZ26scc\/sHX9Yq\/hEhB+Au4OwRnIdTUFCP1sDvGb+MY8e2C6aP2ZsBxgpMdiPSkZZ30N9OE+fjTdugiXfAdQMgjzkuluTqbpI6Dhbcyv2k\/ymM9f+zTBOS2wfKOSbVMEnVrBla31XCkvFLJXb60YUrLlVuhUioTcIKXe3800iYQ6ipL3FF9sg6QBkFZ7vri3\/y06le7LMxMS9BrSJs03SLtqHlA2frbaTnFuaLmeDzIvWWDeie4o9r6QWrYtO3MfQ37aK9xn1hh\/A"}

          但是如果你只想要content值的时候,使用 web_reg_save_param("Content","LB=content\":\"", "RB=\"}","Notfound=warning","Search=Body", LAST); 结果是找不到!

          如果把右匹配改为"RB=cc",就可以取到值。起初以为转义字符串导致web_reg_save_param出错从而没有取到值。

          后来作了静态页面专门试验发现和转义字符没有关系,怀疑和长度有关系。但整个Body都取出来了,Body会更长呀。而且如果RB设为空,也能取到值。又让人不去怀疑长度问题。

          长话短说,就是长度问题!添加 web_set_max_html_param_len("1024"); 即可

          为什么会这样呢?分析一下,如果RB留空的话,LoadRunner应该直接给变量赋值为一个指针,所以结束就可以到末尾。如果RB不为空,那么需要经过赋值。而默认长度是256,超长就导致内容错误,所以添加长度设定后就能正常取到值了。

      8. LoadRunner无法显示XML的问题

      最近测试一个升级程序遇到以下问题:
          要求测试一个XML文件的下载性能。然后发现以下情况:
      直接访问URL,无法显示网页:
      脚本的目录下,还有“t6_a.xml”;快照不可用,导致无法查看源文件,也不能进行其它操作。

      请教了小荷老师:
      1、不是所有服务器返回的内容都能看到快照的。
      2、 如果非要看快照,你可以试试 url-based () web/http property。
      3、 这个快照不可用,不会导致你无法看源文件。
      4、 如果你现在确实再无法切换回源文件,这可能因为有语法错误,你在切换的界面的时候,录制脚本的内容已经被改变了。
      所以你可以先不着急看快照,先看源文件,检查你的脚本,有没有格式很奇怪的,或者没有结束的,或者xml的语法被当作你的给处理的情况。
           顿时云开雾散,使用URL模式录制问题解决!相比较HTML模式和URL模式的录制日志、脚本、回放日志,都完全相同的。
          小荷老师的四点说得很到位:并不是所有返回内容都是可以在LR里用快照查看的;第2点我没有想到会解决问题;是否有录制到源文件,和快照不能查看不是必然的关系;第4点我倒是有遇到过,是XML树,不过可以正常显示,但源码里有个标记和HTML里不一样。
      问题解决了,在此特别感谢小荷老师! 


      9.  LoadRunner中文出现乱码的解决办法


      录制选项,高级,支持字符集,UTF-8。
      一般情况下不需要开启此选项,否则在Submit里会出现有乱码。
      如果未开启此功能还出现乱码,可尝试开启此选项。

      如果开启此选项还是有乱码,且乱码处的内容你需要进行参数化,可使用lr_convert_string_encoding函数将字符转化为你需要的模式。


      函数原型:

      int lr_convert_string_encoding ( const char *sourceString, const char *fromEncoding, const char *toEncoding, const char *paramName);

      sourceString The string to convert
      fromEncoding The encoding of the sourceString
      toEncoding The encoding to convert of the string saved in parameter paramName
      paramName The name of the parameter in which the destination string will be saved

       

      Constant Value
      LR_ENC_SYSTEM_LOCALE NULL
      LR_ENC_UTF8 "utf-8"
      LR_ENC_UNICODE "ucs-2"


      //中文乱码互相转换:
          lr_convert_string_encoding("乱码内容",LR_ENC_UTF8,LR_ENC_SYSTEM_LOCALE,"mt") ;

          lr_output_message(" 2 ---\n%s",lr_eval_string("{mt}")) ;

              lr_convert_string_encoding("未通过审核应用",LR_ENC_SYSTEM_LOCALE,LR_ENC_UTF8,"dt") ;

          lr_output_message(" 3 ---\n%s",lr_eval_string("{dt}")) ;


      浏览器也有转码功能:菜单“查看”-->编码-->Unicode(UTF-8)

      为什么要启用UTF-8,某些请求中会出现:


      web_custom_request("CALL-H001I",
              "EncType=text/xml; charset=UTF-8",
      ……);

      这时候,此函数你就派得上用场了!

      lr_convert_string_encoding函数的使用,直接查看帮助即可。

      说明:
      1. 在树视图里的源码(server Response)的乱码是没法解决的;
      2. 在树视图的页面显示可以是正常,源码视图也可以显示正常;
      3. 虽然在server Response显示乱码,但查找中文字符串还是正常的。

       

      10. 使用Eclipse编写LoadRunner测试脚本

      真是后知后觉,今天才开始尝试用Eclipse编写LoadRunner脚本。

          过去多数都用LoadRunner自带的IDE编写,也有用过Visual Stdio插件。复杂一点的使用DLL或JAR扩展。虽然调试起来比较麻烦,但把常用函数和方法包装起来,一劳永逸还算值得。

          最近项目时间紧,希望能快速开发测试脚本。

          过去遇到的xml、json都不算复杂,用LoadRunner自带函数和C函数一起处理一下就好了。最痛苦的一次是使用LoadRunner实现 Web/Http协议 + Windows Sockets协议 + Mysql + Memcached 脚本的编写,懂LoadRunner的同学应该能明白。虽然内置C编译器,能使用C++扩展,但用法并不完全一样,陷阱多多。使用Java Vuser也没有不像Java开发那样自如。

          今天,我已无法忍受用C语句的char解析服务端返回的大堆Json串了! 忍无可忍,不可再忍

          解析Json串还是用Java最方便,若能使用Eclipse编写LoadRunner脚本是件多么美好的事情呀,就像LoadRunner的VS插件一样。


      方法很简单:

          1、使用Eclipse新建一个Java工程

          2、将“%LoadRunner_Home%\classes\lrapi”目录导入到工程中

          3、将工程导出为Jar包,譬如:命名为lrapi.jar

          4、再新建Java工程时,将lrapi.jar引入扩展库中

          5、"import lrapi.lr;"即可使用LoadRunner函数了

          6、 “import lrapi.web;”则可使用LoadRunner的WEB函数

       

       

       

       

      11. 在LoadRunner向远程Linux/Unix执行命令行并收集性能数据

      前面介绍过在LoadRunner的Java协议实现“使用SSH连接Linux”,当然连接之后的故事由你主导。

          今天要讲的,是一个非Java版本。是对“在LoadRunner中执行命令行程序之:popen()取代system()”的一个升华。

           下面的脚本,是在LoadRunner里连接Linux/Unix远程服务器,收集其磁盘IO的负载到测试结果中。

      涉及到三个知识点:

          1、LoadRunner自带“PuTTY Link”的使用,路径为“%LR_PATH%\bin\plink.exe”;

          2、Linux/Unix的磁盘监控指令,读者也可以扩展为其它任何实用指令;

          3、LoadRunner自带函数lr_user_data_point的使用,保存自定义数据到测试结果。

      脚本贴出如下:

      #define BUFFER_SIZE 20480 // 初始给它 20 KB
      extern char* strtok(char *token, const char *delimiter); // 显示申明
      Action(){
          long fp; // 数据流
          int count; // 用于保存流长度
          char buffer[BUFFER_SIZE]; // 给数据流分配内存空间
          char * row_token; // 记录每一行的地址
          char field_name[100]; // 第一列的名称
          int field_value; // 保存系列的值
          char lrudp_name[100]; // 保存LR自定义指标值
          int rc; // 保存返回值

          lr_start_transaction("DiskIO");// Linux采样方式: plink -ssh -l username -pw password hostname command
          lr_save_string("higkoo", "UserName");
          lr_save_string("123456", "Password");
          lr_save_string("192.168.10.31", "Server");
          lr_save_string("iostat -xc | awk 'NR >2 {print $1, $10}'", "Command"); // 使用iostat拿到磁盘IO的状态信息
          lr_save_string(lr_eval_string("\"%LR_PATH%\\bin\\plink\" -ssh -l {UserName} -pw {Password} {Server} \"{Command}\" 2>&1 "), "Result"); // 使用plink连接远程Linux服务器并拿到执行结果
          fp = popen(lr_eval_string("{Result}"), "r");
          if (fp == NULL) {
              lr_error_message("执行命令失败");
              return -1;
          }

          count = fread(buffer, sizeof(char), BUFFER_SIZE, fp); // 读取结果
          if (feof(fp) == 0) {
              lr_error_message("返回结果太大,请给数据流分配更大内存空间,谢谢!");
              return -1;
          }
          if (ferror(fp)) {
              lr_error_message ("监控指令返回错误");
              return -1;
          }
          if (count == 0) {
              lr_error_message("监控指令返回结果为熔");
              return -1;
          }
          buffer[count] = NULL;

          row_token = (char*) strtok(buffer, "\n"); // 按换行符分割
          if (row_token == NULL) {
              lr_error_message ("未发现有效数据");
              return -1;
          }
          while (row_token != NULL) { // 开始读取数据
              rc = sscanf(row_token, "%s %d", field_name, &field_value); //分割名称与值
              if (rc != 2) {
                  lr_error_message("Incorrect number of items read from the row.");
                  return -1;
              }

              sprintf(lrudp_name, "disk_busy_%s", field_name);// 自定义数据的名称
              lr_user_data_point(lrudp_name, field_value);// 保存到LR自定义数据

              row_token = (char*) strtok(NULL, "\n");
          }

          pclose(fp);

          lr_end_transaction("DiskIO", LR_AUTO);
      }
      功能实现了,后面的故事你来讲~~ 

      在LoadRunner中执行命令行程序之:popen()取代system()
      http://hi.baidu.com/higkoo/blog/item/3f6e6a467e02a0076b63e5b5.html
      在LoadRunner向远程Linux/Unix执行命令行并收集性能数据
      http://hi.baidu.com/higkoo/blog/item/5ab2ea82f36b559ef703a67a.html

       

       

      12.  LoadRunner使用Libmemcached与Memcached通讯

      在一包含业务的大数据量测试过程中,需要使用快速、大容量的数据库。

          可考虑的有关系型数据库或键值缓存数据库,建立集群。 LoadRunner操作Mysql实例:C语言篇已完成。

          今日来尝试在LoadRunner里操作Memcached,使用Libmemcached。

          首先下载源码:?http://svn.coderepos.org/share/lang/c/libmemcached-win32

            然后按照说明进行编译,源码里还有测试代码和示例代码。编译方法有三种:

              一、使用?mingw32进行编译,打开? Visual Studio Command Prompt:


      ?    ?    cd libmemcached-latest\libmemcached
      ?    ?    mingw32-make -f Makefile.w32

      ?    ?    cd ..\cilents
      ?    ?    mingw32-make -f Makefile.w32

      ?    ?    cd ..\..\example
      ?    ?    mingw32-make -f Makefile.w32

          二、使用Visual Studio 2005或更高版本执行编译:
      ?    ?    libmemcached-latest\visualc\libmemcached.sln
          三、使用nmake进行编译,?打开? Visual Studio Command Prompt:
      ?    ?    cd libmemcached-latest\libmemcached
      ?    ?    nmake -f Makefile.msc

      ??    ?    cd ..\cilents
      ?    ?    nmake -f Makefile.msc

      ?    ?    cd ..\..\example
      ??    ?    nmake -f Makefile.msc

          编译过程将生成memcached.dll,供LoadRunner扩展调用。如下图:
      ?

       

       

      源码:


      #define SERVER_NAME "192.168.223.106"
      #define SERVER_PORT 11211
      Action(){ //先加载libmemcached.dll
          int memc;
          int rc;
          int value_length=0;
          intf lags=0;
          int result;
          long int num;
          char* key="name";
          char* value="higkoo";
          char* discription="Performance";

          memc=memcached_create(NULL);
          rc=memcached_server_add(memc,SERVER_NAME,SERVER_PORT);
          lr_output_message("server add: %s\n",memcached_strerror(memc,rc));

           rc=memcached_set(memc,key,strlen(key),value,strlen(value),0,0);
           lr_output_message("set '%s' to '%s': %s\n",key,value,memcached_strerror(memc,rc));

          result=memcached_get(memc,key,strlen(key),&value_length,&flags,&rc);
          lr_output_message("get '%s': %s\n",key,memcached_strerror(memc,rc));
          lr_output_message("%s = %s\n",key,result);

          rc=memcached_behavior_set(memc,0,1);
          lr_output_message("behavior. set to non-block: %s\n",memcached_strerror(memc,rc));

          result=memcached_get(memc,key,strlen(key),&value_length,&flags,&rc);
          lr_output_message("get '%s': %s\n",key,memcached_strerror(memc,rc));
          lr_output_message("%s = %s\n",key,value);

          rc=memcached_set(memc,key,strlen(key),discription,strlen(discription),0,0);
          lr_output_message("set '%s' to '%s': %s\n",key,discription,memcached_strerror(memc,rc));

          rc=memcached_increment(memc,key,strlen(key),1,&num);
          lr_output_message("incr '%s': %s\n",key,memcached_strerror(memc,rc));
          rc=memcached_increment(memc,key,strlen(key),1,&num);
          lr_output_message("incr '%s': %s\n",key,memcached_strerror(memc,rc));
          rc=memcached_increment(memc,key,strlen(key),1,&num);
          lr_output_message("incr '%s': %s\n",key,memcached_strerror(memc,rc));
          rc=memcached_decrement(memc,key,strlen(key),1,&num);
          lr_output_message("decr '%s': %s\n",key,memcached_strerror(memc,rc));

          result=memcached_get(memc,key,strlen(key),&value_length,&flags,&rc);
          lr_output_message("get '%s': %s\n",key,memcached_strerror(memc,rc));
          lr_output_message("test = %s\n",value);

          rc=memcached_delete(memc,key,strlen(key),0);
          lr_output_message("delete '%s': %s\n",key,memcached_strerror(memc,rc));

          result=memcached_get(memc,key,strlen(key),&value_length,&flags,&rc);
          lr_output_message("get(was deleted) '%s': %s\n",key,memcached_strerror(memc,rc));

          memcached_free(memc);
      }


      ...

    20. LoadRunner_c语言扩展_Base64,Md5,替换字符串中指定字符,生成唯一数(上)

      2011-07-28 18:02:32

      以下均转自:http://hi.baidu.com/higkoo/home

      http://hi.baidu.com/higkoo/blog/category/Loadrunner/index/0

       

      1. LoadRunner/C语言 实现:Base64加解码

      之前分享过一个LoadRunner执行Base64编码的函数,由于当时没有提供解码。如今又在网上重新收集了一套编解码的函数。

          在LoadRunner脚本里包含头文件即可使用,示例如下:

      #include "base64.h"
       
      Action(){
       int res;
       // ENCODE
       lr_save_string("testddd_001@dddd.cn:ttttt","plain");
       b64_encode_string( lr_eval_string("{plain}"), "b64str" );
       lr_output_message("Encoded: %s", lr_eval_string("{b64str}") );
       // DECODE
       b64_decode_string( lr_eval_string("{b64str}"), "plain2" );
       lr_output_message("Decoded: %s", lr_eval_string("{plain2}") );
       // Verify decoded matches original plain text
       res = strcmp( lr_eval_string("{plain}"), lr_eval_string("{plain2}") );
       if (res==0) lr_output_message("Decoded matches original plain text");

         return 0 ;
      }
       

      文件base64.h源码如下:

      //为LoadRunner提供Base64的编码和解码函数,
      //有直接编码和解决的函数和输出LoadRunner参数的函数。
       
       
      /*
      Base 64 Encode and Decode functions for LoadRunner
      ==================================================
      This include file provides functions to Encode and Decode
      LoadRunner variables. It's based on source codes found on the
      internet and has been modified to work in LoadRunner.
      Created by Kim Sandell / Celarius - www.celarius.com
      */
      // Encoding lookup table
      char base64encode_lut[] = {
      'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q',
      'R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f','g','h',
      'i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y',
      'z','0','1','2','3','4','5','6','7','8','9','+','/','='};
      // Decode lookup table
      char base64decode_lut[] = {
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0,62, 0, 0, 0,63,52,53,54,55,56,57,58,59,60,61, 0, 0,
      0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,
      15,16,17,18,19,20,21,22,23,24,25, 0, 0, 0, 0, 0, 0,26,27,28,
      29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,
      49,50,51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };
      void base64encode(char *src, char *dest, int len)
      // Encodes a buffer to base64
      {
        int i=0, slen=strlen(src);
        for(i=0;i<slen && i<len;i+=3,src+=3)
        { // Enc next 4 characters
        *(dest++)=base64encode_lut[(*src&0xFC)>>0x2];
        *(dest++)=base64encode_lut[(*src&0x3)<<0x4|(*(src+1)&0xF0)>>0x4];
        *(dest++)=((i+1)<slen)?base64encode_lut[(*(src+1)&0xF)<<0x2|(*(src+2)&0xC0)>>0x6]:'=';
        *(dest++)=((i+2)<slen)?base64encode_lut[*(src+2)&0x3F]:'=';
        }
        *dest='\0'; // Append terminator
      }
       
      void base64decode(char *src, char *dest, int len)
      // Encodes a buffer to base64
      {
        int i=0, slen=strlen(src);
        for(i=0;i<slen&&i<len;i+=4,src+=4)
        { // Store next 4 chars in vars for faster access
        char c1=base64decode_lut[*src], c2=base64decode_lut[*(src+1)], c3=base64decode_lut[*(src+2)], c4=base64decode_lut[*(src+3)];
        // Decode to 3 chars
        *(dest++)=(c1&0x3F)<<0x2|(c2&0x30)>>0x4;
        *(dest++)=(c3!=64)?((c2&0xF)<<0x4|(c3&0x3C)>>0x2):'\0';
        *(dest++)=(c4!=64)?((c3&0x3)<<0x6|c4&0x3F):'\0';
        }
        *dest='\0'; // Append terminator
      }
      int b64_encode_string( char *source, char *lrvar )
      // ----------------------------------------------------------------------------
      // Encodes a string to base64 format -----  Method 1
      //
      // Parameters:
      //        source    Pointer to source string to encode
      //        lrvar     LR variable where base64 encoded string is stored
      //
      // Example:
      //
      //        b64_encode_string( "Encode Me!", "b64" )
      // ----------------------------------------------------------------------------
      {
        int dest_size;
        int res;
        char *dest;
        // Allocate dest buffer
        dest_size = 1 + ((strlen(source)+2)/3*4);
        dest = (char *)malloc(dest_size);
        memset(dest,0,dest_size);
        // Encode & Save
        base64encode(source, dest, dest_size);
        lr_save_string( dest, lrvar );
        // Free dest buffer
        res = strlen(dest);
        free(dest);
        // Return length of dest string
        return res;
      }
       
      int b64_decode_string( char *source, char *lrvar )
      // ----------------------------------------------------------------------------
      // Decodes a base64 string to plaintext  -----  Method 2
      //
      // Parameters:
      //        source    Pointer to source base64 encoded string
      //        lrvar     LR variable where decoded string is stored
      //
      // Example:
      //
      //        b64_decode_string( lr_eval_string("{b64}"), "Plain" )
      // ----------------------------------------------------------------------------
      {
        int dest_size;
        int res;
        char *dest;
        // Allocate dest buffer
        dest_size = strlen(source);
        dest = (char *)malloc(dest_size);
        memset(dest,0,dest_size);
        // Encode & Save
        base64decode(source, dest, dest_size);
        lr_save_string( dest, lrvar );
        // Free dest buffer
        res = strlen(dest);
        free(dest);
        // Return length of dest string
        return res;
      }
       
       

         

       

      2.LoadRunner实现:计算字符串Md5

      尝试在LR里实现字符串的MD5计算。

          在LR里添加头文件md5.h,在globals.h里添加引用#include "md5.h";md5.h代码如下:

      #ifndef MD5_H
      #define MD5_H
      #ifdef __alpha
      typedef unsigned int uint32;
      #else
      typedef unsigned long uint32;
      #endif
      struct MD5Context {
              uint32 buf[4];
              uint32 bits[2];
              unsigned char in[64];
      };
      extern void MD5Init();
      extern void MD5Update();
      extern void MD5Final();
      extern void MD5Transform();
      typedef struct MD5Context MD5_CTX;
      #endif
      #ifdef sgi
      #define HIGHFIRST
      #endif
      #ifdef sun
      #define HIGHFIRST
      #endif
      #ifndef HIGHFIRST
      #define byteReverse(buf, len)    /* Nothing */
      #else
      void byteReverse(buf, longs)unsigned char *buf; unsigned longs;
      {
          uint32 t;
          do {
          t = (uint32) ((unsigned) buf[3] << 8 | buf[2]) << 16 |((unsigned) buf[1] << 8 | buf[0]);

          *(uint32 *) buf = t;
          buf += 4;
          } while (--longs);
      }
      #endif
      void MD5Init(ctx)struct MD5Context *ctx;
      {
          ctx->buf[0] = 0x67452301;
          ctx->buf[1] = 0xefcdab89;
          ctx->buf[2] = 0x98badcfe;
          ctx->buf[3] = 0x10325476;
          ctx->bits[0] = 0;
          ctx->bits[1] = 0;
      }
      void MD5Update(ctx, buf, len) struct MD5Context *ctx; unsigned char *buf; unsigned len;
      {
          uint32 t;
          t = ctx->bits[0];
          if ((ctx->bits[0] = t + ((uint32) len << 3)) < t)
          ctx->bits[1]++;
          ctx->bits[1] += len >> 29;
          t = (t >> 3) & 0x3f;
          if (t) {
          unsigned char *p = (unsigned char *) ctx->in + t;
          t = 64 - t;
          if (len < t) {
              memcpy(p, buf, len);
              return;
          }
          memcpy(p, buf, t);
          byteReverse(ctx->in, 16);
          MD5Transform(ctx->buf, (uint32 *) ctx->in);
          buf += t;
          len -= t;
          }
          while (len >= 64) {
          memcpy(ctx->in, buf, 64);
          byteReverse(ctx->in, 16);
          MD5Transform(ctx->buf, (uint32 *) ctx->in);
          buf += 64;
          len -= 64;
          }
          memcpy(ctx->in, buf, len);
      }
      void MD5Final(digest, ctx)
          unsigned char digest[16]; struct MD5Context *ctx;
      {
          unsigned count;
          unsigned char *p;
          count = (ctx->bits[0] >> 3) & 0x3F;
          p = ctx->in + count;
          *p++ = 0x80;
          count = 64 - 1 - count;
          if (count < 8) {
          memset(p, 0, count);
          byteReverse(ctx->in, 16);
          MD5Transform(ctx->buf, (uint32 *) ctx->in);
          memset(ctx->in, 0, 56);
          } else {
          memset(p, 0, count - 8);
          }
          byteReverse(ctx->in, 14);
          ((uint32 *) ctx->in)[14] = ctx->bits[0];
          ((uint32 *) ctx->in)[15] = ctx->bits[1];
          MD5Transform(ctx->buf, (uint32 *) ctx->in);
          byteReverse((unsigned char *) ctx->buf, 4);
          memcpy(digest, ctx->buf, 16);
          memset(ctx, 0, sizeof(ctx));
      }
      #define F1(x, y, z) (z ^ (x & (y ^ z)))
      #define F2(x, y, z) F1(z, x, y)
      #define F3(x, y, z) (x ^ y ^ z)
      #define F4(x, y, z) (y ^ (x | ~z))
      #define MD5STEP(f, w, x, y, z, data, s) ( w += f(x, y, z) + data, w = w<<s | w>>(32-s), w += x )
      void MD5Transform(buf, in)
          uint32 buf[4]; uint32 in[16];
      {
          register uint32 a, b, c, d;
          a = buf[0];
          b = buf[1];
          c = buf[2];
          d = buf[3];
          MD5STEP(F1, a, b, c, d, in[0] + 0xd76aa478, 7);
          MD5STEP(F1, d, a, b, c, in[1] + 0xe8c7b756, 12);
          MD5STEP(F1, c, d, a, b, in[2] + 0x242070db, 17);
          MD5STEP(F1, b, c, d, a, in[3] + 0xc1bdceee, 22);
          MD5STEP(F1, a, b, c, d, in[4] + 0xf57c0faf, 7);
          MD5STEP(F1, d, a, b, c, in[5] + 0x4787c62a, 12);
          MD5STEP(F1, c, d, a, b, in[6] + 0xa8304613, 17);
          MD5STEP(F1, b, c, d, a, in[7] + 0xfd469501, 22);
          MD5STEP(F1, a, b, c, d, in[8] + 0x698098d8, 7);
          MD5STEP(F1, d, a, b, c, in[9] + 0x8b44f7af, 12);
          MD5STEP(F1, c, d, a, b, in[10] + 0xffff5bb1, 17);
          MD5STEP(F1, b, c, d, a, in[11] + 0x895cd7be, 22);
          MD5STEP(F1, a, b, c, d, in[12] + 0x6b901122, 7);
          MD5STEP(F1, d, a, b, c, in[13] + 0xfd987193, 12);
          MD5STEP(F1, c, d, a, b, in[14] + 0xa679438e, 17);
          MD5STEP(F1, b, c, d, a, in[15] + 0x49b40821, 22);
          MD5STEP(F2, a, b, c, d, in[1] + 0xf61e2562, 5);
          MD5STEP(F2, d, a, b, c, in[6] + 0xc040b340, 9);
          MD5STEP(F2, c, d, a, b, in[11] + 0x265e5a51, 14);
          MD5STEP(F2, b, c, d, a, in[0] + 0xe9b6c7aa, 20);
          MD5STEP(F2, a, b, c, d, in[5] + 0xd62f105d, 5);
          MD5STEP(F2, d, a, b, c, in[10] + 0x02441453, 9);
          MD5STEP(F2, c, d, a, b, in[15] + 0xd8a1e681, 14);
          MD5STEP(F2, b, c, d, a, in[4] + 0xe7d3fbc8, 20);
          MD5STEP(F2, a, b, c, d, in[9] + 0x21e1cde6, 5);
          MD5STEP(F2, d, a, b, c, in[14] + 0xc33707d6, 9);
          MD5STEP(F2, c, d, a, b, in[3] + 0xf4d50d87, 14);
          MD5STEP(F2, b, c, d, a, in[8] + 0x455a14ed, 20);
          MD5STEP(F2, a, b, c, d, in[13] + 0xa9e3e905, 5);
          MD5STEP(F2, d, a, b, c, in[2] + 0xfcefa3f8, 9);
          MD5STEP(F2, c, d, a, b, in[7] + 0x676f02d9, 14);
          MD5STEP(F2, b, c, d, a, in[12] + 0x8d2a4c8a, 20);
          MD5STEP(F3, a, b, c, d, in[5] + 0xfffa3942, 4);
          MD5STEP(F3, d, a, b, c, in[8] + 0x8771f681, 11);
          MD5STEP(F3, c, d, a, b, in[11] + 0x6d9d6122, 16);
          MD5STEP(F3, b, c, d, a, in[14] + 0xfde5380c, 23);
          MD5STEP(F3, a, b, c, d, in[1] + 0xa4beea44, 4);
          MD5STEP(F3, d, a, b, c, in[4] + 0x4bdecfa9, 11);
          MD5STEP(F3, c, d, a, b, in[7] + 0xf6bb4b60, 16);
          MD5STEP(F3, b, c, d, a, in[10] + 0xbebfbc70, 23);
          MD5STEP(F3, a, b, c, d, in[13] + 0x289b7ec6, 4);
          MD5STEP(F3, d, a, b, c, in[0] + 0xeaa127fa, 11);
          MD5STEP(F3, c, d, a, b, in[3] + 0xd4ef3085, 16);
          MD5STEP(F3, b, c, d, a, in[6] + 0x04881d05, 23);
          MD5STEP(F3, a, b, c, d, in[9] + 0xd9d4d039, 4);
          MD5STEP(F3, d, a, b, c, in[12] + 0xe6db99e5, 11);
          MD5STEP(F3, c, d, a, b, in[15] + 0x1fa27cf8, 16);
          MD5STEP(F3, b, c, d, a, in[2] + 0xc4ac5665, 23);
          MD5STEP(F4, a, b, c, d, in[0] + 0xf4292244, 6);
          MD5STEP(F4, d, a, b, c, in[7] + 0x432aff97, 10);
          MD5STEP(F4, c, d, a, b, in[14] + 0xab9423a7, 15);
          MD5STEP(F4, b, c, d, a, in[5] + 0xfc93a039, 21);
          MD5STEP(F4, a, b, c, d, in[12] + 0x655b59c3, 6);
          MD5STEP(F4, d, a, b, c, in[3] + 0x8f0ccc92, 10);
          MD5STEP(F4, c, d, a, b, in[10] + 0xffeff47d, 15);
          MD5STEP(F4, b, c, d, a, in[1] + 0x85845dd1, 21);
          MD5STEP(F4, a, b, c, d, in[8] + 0x6fa87e4f, 6);
          MD5STEP(F4, d, a, b, c, in[15] + 0xfe2ce6e0, 10);
          MD5STEP(F4, c, d, a, b, in[6] + 0xa3014314, 15);
          MD5STEP(F4, b, c, d, a, in[13] + 0x4e0811a1, 21);
          MD5STEP(F4, a, b, c, d, in[4] + 0xf7537e82, 6);
          MD5STEP(F4, d, a, b, c, in[11] + 0xbd3af235, 10);
          MD5STEP(F4, c, d, a, b, in[2] + 0x2ad7d2bb, 15);
          MD5STEP(F4, b, c, d, a, in[9] + 0xeb86d391, 21);
          buf[0] += a;
          buf[1] += b;
          buf[2] += c;
          buf[3] += d;
      }
      void GetMd5FromString(const char* s,char* resStr)
      {
           struct MD5Context md5c;
           unsigned char ss[16];
           char subStr[3];
           int i;
           MD5Init( &md5c );
           MD5Update( &md5c, s, strlen(s) );
           MD5Final( ss, &md5c );
           strcpy(resStr,"");
           for( i=0; i<16; i++ )
           {
               sprintf(subStr, "%02x", ss[i] );
               itoa(ss[i],subStr,16);
               if (strlen(subStr)==1) {
                   strcat(resStr,"0");
               }
               strcat(resStr,subStr);
           }
           strcat(resStr,"\0");
      }


      源码都是网上共享的,仅CMd5()函数稍作了改动,要验证可以上http://www.cmd5.com/。在LR里调用就非常简单:void main()
      {
          char Md5[33];
          GetMd5FromString("a",Md5);
          lr_output_message(Md5);
      }

      输出结果:
      Running Vuser...
      Starting iteration 1.
      Starting action main.
      main.c(5): 0cc175b9c0f1b6a831c399e269772661
      Ending action main.

       

       

      3. C语言实现:替换字符串中指定字符

      不用多介绍了,可以在LoadRunner里直接使用,挺好的!

      int ReplaceStr(char* sSrc, char* sMatchStr, char* sReplaceStr)
      {
              int StringLen;
              char caNewString[64];
              char* FindPos;
              FindPos =(char *)strstr(sSrc, sMatchStr);
              if( (!FindPos) || (!sMatchStr) )
                      return -1;

              while( FindPos )
              {
                      memset(caNewString, 0, sizeof(caNewString));
                      StringLen = FindPos - sSrc;
                      strncpy(caNewString, sSrc, StringLen);
                      strcat(caNewString, sReplaceStr);
                      strcat(caNewString, FindPos + strlen(sMatchStr));
                      strcpy(sSrc, caNewString);

                      FindPos =(char *)strstr(sSrc, sMatchStr);
              }
              free(FindPos);
              return 0;
      }

      譬如:ReplaceStr("abcd-efgh-ijklm-nopq","-","");//把字符串中的“-”删除掉!

       

      4. LoadRunner生成唯一数

      void Main()
      {
          int i;
          char uStr[64];
          srand( (unsigned)time( NULL ) );// 最好放在vuser_init里

          for (i=0;i<10;i++) {
              GetUniqueString(i,uStr);
              lr_output_message(uStr);
          }
      }

      void GetUniqueString(int inValue,char *outStr)
      {
          int id, scid;
          char *vuser_group;   
         
          lr_whoami(&id, &vuser_group, &scid);
          web_save_timestamp_param("tStamp", LAST);
          sprintf(outStr,"%s%05d%010d%04d",lr_eval_string("{tStamp}"),id,rand(),inValue);
          free(vuser_group);
      }


          建议把随机种子(srand( (unsigned)time( NULL ) );)放在脚本初始化函数里,只需要初始化一次。若放在子函数里,每次调用都初始化一下的话,产生的随机数可能是一样的。是不安全的代码!
          这个唯一数,有四关:毫秒级的时间+虚拟用户ID+随机数+传入的参数;基本上在同一个Controller里不会出现重复了!再稍微处理一下就可以得到想法的东西了,譬如:LoadRunner实现:计算字符串Md5 加密成md5串,再改装一下就成GUID了!

          这里有一个安全问题值得说明,随机种子最好只初始化一次,随机数的算法是和时间有一定关系的。若把随机种子放在子函数里,你会发现生成出来的随机数都是一样的。
          不要惊讶,为什么说是个安全问题,说严重一点,随机数是一种算法,有可能被别人劫获并计算出下一个随机值,故不安全!

       

      base64.rar(1.28 KB)

      md5.rar(1.91 KB)

       

    21. linux wa%过高,iostat查看io状况

      2011-07-28 17:14:14

       

      命令总结:

      1. top/vmstat 发现 wa%过高,vmstat b >1;

       

       

      参考文章:

      1. 关于Linux系统指令 top 之 %wa 占用高,用`iostat`探个究竟

      最近测试一项目,性能非常不理想。老版本逻辑和功能都简单时,性能是相当的好!接口点击率是万级的。谁知修改后上不了百。

          架设Jboss服务器,业务逻辑用Java处理,核心模块使用C++处理,使用JNI衔接。

          本应用对CPU和硬盘第三非常敏感,因为有压缩解压和大量数据交互。起初作压力测试时,发现服务器各资源使用都有剩余,而点击率曲线波动却非常大,简单看似乎是应用程序有问题。

          使用top查看Cpu各核的使用情况,发现一个非常诡异的现象:

               1. 经常只有部分核是满载的,另外一部分基本空闲;

               2. 在CPU满载时,%wa 的波动比较大,有时会占到较大比例。

          所以,监控整个CPU时会发现CPU使用率不高,实际上任务总是分派到某个核上且导致对应核满载。无法有效使用CPU,其它资源自然也难以有效调度。

          废话不多说,%waCPU等待磁盘写入完成的时间。莫非是磁盘忙,怎样证明是磁盘在忙?

         首先看下%wa的解释:Percentage of time that the CPU or CPUs were idle during which the system had an outstanding disk I/O request.

          起初用`lsof | less`查看文件的读写情况,发现/tmp目录下有大量文件读写。经查证,是Jboss处理上传文件会默认写入到/tmp文件夹,然后再执行了一次拷贝到程序读取的目录。修改Jboss配置直接写入到程序读写目录,性能没有本质上的改变。

          关于CPU使用波动大,我们也在程序内部加了很多计时器,发现某些模块在处理并发时会有响应时间很长的情况,这点证实了为什么点击率波动很大。

          但此模块进行单进程串行测试时,每秒完成事务数是相当可观的。一个进程每秒完成的事务数都比当前测试点击率要高很多!使用多进程来测试此模块时,发现“进程数=核数”时效果最佳。于是在Java层控制同时进入此模块的数量,毕竟Java是调用JNI来使用此模块,使用全局锁来控制并发,最终结果没有想象的那么理想,但明显可以看出:通过控制并发数,能有效提高CPU的使用率,点击率也上升了一些。

          另外一个问题就是,CPU会出现一会满载,一会空闲的情况,导致点击率曲线仍然波动大的问题。商讨后决定在C++代码中加入“释放CPU控制权”的逻辑,这样就在代码层来作了一个负载均衡。点击率波动的问题得到了好转,但点击率仍然不理想,预期瓶颈是网络而实际变成了CPU

          优化了压缩解决的处理后,性能没有明显提升。这时我才想起%wa,我还没有进一步证明是磁盘的闲忙程度。使用了一些监控工具,诸如:vmstatsardstatsysstat 没有发现对磁盘作非常详细的监控。最后试了下iostat,搞定!

          iostat的编译非常简单,就一个c文件,MakeFile里作者写了一句话“Cann't be simpler”。直接make install就在目录下生成了iostat的可执行文件,看一下帮助,执行 `iostat -cdDx 10` 。其中有一列“%b”描述了磁盘的闲忙程序,简单直接。另外还有详细的磁盘IO读写数据,帮助里也解释得非常清楚。

         再进行一次压力测试,拿着这份数据,已经绝对性的说明问题了。此时那些大牛把代码改了一下,性能立马就上去了,千兆网络直接成为系统瓶颈。并于Java的控制问题,改用Apache直接编译程序模块调用,完成变为可控,问题瞬间解决!

      附上iostat的源码:

       

      转自: http://hi.baidu.com/higkoo/blog/item/d4f7f822e7871cf8d6cae25e.html

       

      2.iostat来对linux硬盘IO性能进行了解

       

      以前一直不太会用这个参数。现在认真研究了一下iostat,因为刚好有台重要的服务器压力高,所以放上来分析一下.下面这台就是IO有压力过大的服务器
        # iostat -x 1 10
        Linux 2.6.18-92.el5xen 02/03/2009
        avg-cpu: %user %nice %system %iowait %steal %idle
        1.10 0.00 4.82 39.54 0.07 54.46
        Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s avgrq-sz avgqu-sz await svctm %util
        sda 0.00 3.50 0.40 2.50 5.60 48.00 18.48 0.00 0.97 0.97 0.28
        sdb 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
        sdc 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
        sdd 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
        sde 0.00 0.10 0.30 0.20 2.40 2.40 9.60 0.00 1.60 1.60 0.08
        sdf 17.40 0.50 102.00 0.20 12095.20 5.60 118.40 0.70 6.81 2.09 21.36
        sdg 232.40 1.90 379.70 0.50 76451.20 19.20 201.13 4.94 13.78 2.45 93.16
        rrqm/s: 每秒进行 merge 的读操作数目。即 delta(rmerge)/s
        wrqm/s: 每秒进行 merge 的写操作数目。即 delta(wmerge)/s
        r/s: 每秒完成的读 I/O 设备次数。即 delta(rio)/s
        w/s: 每秒完成的写 I/O 设备次数。即 delta(wio)/s
        rsec/s: 每秒读扇区数。即 delta(rsect)/s
        wsec/s: 每秒写扇区数。即 delta(wsect)/s
        rkB/s: 每秒读K字节数。是 rsect/s 的一半,因为每扇区大小为512字节。(需要计算)
        wkB/s: 每秒写K字节数。是 wsect/s 的一半。(需要计算)
        avgrq-sz: 平均每次设备I/O操作的数据大小 (扇区)。delta(rsect+wsect)/delta(rio+wio)
        avgqu-sz: 平均I/O队列长度。即 delta(aveq)/s/1000 (因为aveq的单位为毫秒)。
        await: 平均每次设备I/O操作的等待时间 (毫秒)。即 delta(ruse+wuse)/delta(rio+wio)
        svctm: 平均每次设备I/O操作的服务时间 (毫秒)。即 delta(use)/delta(rio+wio)
        %util: 一秒中有百分之多少的时间用于 I/O 操作,或者说一秒中有多少时间 I/O 队列是非空的。即 delta(use)/s/1000 (因为use的单位为毫秒)
        如果 %util 接近 100%,说明产生的I/O请求太多,I/O系统已经满负荷,该磁盘
        可能存在瓶颈。
        idle小于70% IO压力就较大了,一般读取速度有较多的wait.
        同时可以结合vmstat 查看查看b参数(等待资源的进程数)和wa参数(IO等待所占用的CPU时间的百分比,高过30%时IO压力高)
        另外还可以参考
        svctm 一般要小于 await (因为同时等待的请求的等待时间被重复计算了),svctm 的大小一般和磁盘性能有关,CPU/内存的负荷也会对其有影响,请求过多也会间接导致 svctm 的增加。await 的大小一般取决于服务时间(svctm) 以及 I/O 队列的长度和 I/O 请求的发出模式。如果 svctm 比较接近 await,说明 I/O 几乎没有等待时间;如果 await 远大于 svctm,说明 I/O 队列太长,应用得到的响应时间变慢,如果响应时间超过了用户可以容许的范围,这时可以考虑更换更快的磁盘,调整内核 elevator 算法,优化应用,或者升级 CPU。
        队列长度(avgqu-sz)也可作为衡量系统 I/O 负荷的指标,但由于 avgqu-sz 是按照单位时间的平均值,所以不能反映瞬间的 I/O 洪水。
        别人一个不错的例子.(I/O 系统 vs. 超市排队)
        举一个例子,我们在超市排队 checkout 时,怎么决定该去哪个交款台呢? 首当是看排的队人数,5个人总比20人要快吧? 除了数人头,我们也常常看看前面人购买的东西多少,如果前面有个采购了一星期食品的大妈,那么可以考虑换个队排了。还有就是收银员的速度了,如果碰上了连钱都点不清楚的新手,那就有的等了。另外,时机也很重要,可能 5 分钟前还人满为患的收款台,现在已是人去楼空,这时候交款可是很爽啊,当然,前提是那过去的 5 分钟里所做的事情比排队要有意义 (不过我还没发现什么事情比排队还无聊的)。
        I/O 系统也和超市排队有很多类似之处:
        r/s+w/s 类似于交款人的总数
        平均队列长度(avgqu-sz)类似于单位时间里平均排队人的个数
        平均服务时间(svctm)类似于收银员的收款速度
        平均等待时间(await)类似于平均每人的等待时间
        平均I/O数据(avgrq-sz)类似于平均每人所买的东西多少
        I/O 操作率 (%util)类似于收款台前有人排队的时间比例。
        我们可以根据这些数据分析出 I/O 请求的模式,以及 I/O 的速度和响应时间。
        下面是别人写的这个参数输出的分析
        # iostat -x 1
        avg-cpu: %user %nice %sys %idle
        16.24 0.00 4.31 79.44
        Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s rkB/s wkB/s avgrq-sz avgqu-sz await svctm %util
        /dev/cciss/c0d0
        0.00 44.90 1.02 27.55 8.16 579.59 4.08 289.80 20.57 22.35 78.21 5.00 14.29
        /dev/cciss/c0d0p1
        0.00 44.90 1.02 27.55 8.16 579.59 4.08 289.80 20.57 22.35 78.21 5.00 14.29
        /dev/cciss/c0d0p2
        0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
        上面的 iostat 输出表明秒有 28.57 次设备 I/O 操作: 总IO(io)/s = r/s(读) +w/s(写) = 1.02+27.55 = 28.57 (次/秒) 其中写操作占了主体 (w:r = 27:1)。
        平均每次设备 I/O 操作只需要 5ms 就可以完成,但每个 I/O 请求却需要等上 78ms,为什么? 因为发出的 I/O 请求太多 (每秒钟约 29 个),假设这些请求是同时发出的,那么平均等待时间可以这样计算:
        平均等待时间 = 单个 I/O 服务时间 * ( 1 + 2 + … + 请求总数-1) / 请求总数
        应用到上面的例子: 平均等待时间 = 5ms * (1+2+…+28)/29 = 70ms,和 iostat 给出的78ms 的平均等待时间很接近。这反过来表明 I/O 是同时发起的。
        每秒发出的 I/O 请求很多 (约 29 个),平均队列却不长 (只有 2 个 左右),这表明这 29 个请求的到来并不均匀,大部分时间 I/O 是空闲的。
        一秒中有 14.29% 的时间 I/O 队列中是有请求的,也就是说,85.71% 的时间里 I/O 系统无事可做,所有 29 个 I/O 请求都在142毫秒之内处理掉了。
        delta(ruse+wuse)/delta(io) = await = 78.21 => delta(ruse+wuse)/s =78.21 * delta(io)/s = 78.21*28.57 = 2232.8,表明每秒内的I/O请求总共需要等待2232.8ms。所以平均队列长度应为 2232.8ms/1000ms = 2.23,而 iostat 给出的平均队列长度 (avgqu-sz) 却为 22.35,为什么?! 因为 iostat 中有 bug,avgqu-sz 值应为 2.23,而不是 22.35。

       

       

      3. linux lsof详解

       

      lsof简介
      lsof(list open files)是一个列出当前系统打开文件的工具。在linux环境下,任何事物都以文件的形式存在,通过文件不仅仅可以访问常规数据,还可以访问网络连接和硬件。所以如传输控制协议 (TCP) 和用户数据报协议 (UDP) 套接字等,系统在后台都为该应用程序分配了一个文件描述符,无论这个文件的本质如何,该文件描述符为应用程序与基础操作系统之间的交互提供了通用接口。因为应用程序打开文件的描述符列表提供了大量关于这个应用程序本身的信息,因此通过lsof工具能够查看这个列表对系统监测以及排错将是很有帮助的。
      lsof使用
       
      lsof输出信息含义
      在终端下输入lsof即可显示系统打开的文件,因为 lsof 需要访问核心内存和各种文件,所以必须以 root 用户的身份运行它才能够充分地发挥其功能。
      COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME init 1 root cwd DIR 3,3 1024 2 / init 1 root rtd DIR 3,3 1024 2 / init 1 root txt REG 3,3 38432 1763452 /sbin/init init 1 root mem REG 3,3 106114 1091620 /lib/libdl-2.6.so init 1 root mem REG 3,3 7560696 1091614 /lib/libc-2.6.so init 1 root mem REG 3,3 79460 1091669 /lib/libselinux.so.1 init 1 root mem REG 3,3 223280 1091668 /lib/libsepol.so.1 init 1 root mem REG 3,3 564136 1091607 /lib/ld-2.6.so init 1 root 10u FIFO 0,15 1309 /dev/initctl
      每行显示一个打开的文件,若不指定条件默认将显示所有进程打开的所有文件。lsof输出各列信息的意义如下:
      COMMAND:进程的名称 PID:进程标识符 USER:进程所有者 FD:文件描述符,应用程序通过文件描述符识别该文件。如cwd、txt等 TYPE:文件类型,如DIR、REG等 DEVICE:指定磁盘的名称 SIZE:文件的大小 NODE:索引节点(文件在磁盘上的标识) NAME:打开文件的确切名称
      其中FD 列中的文件描述符cwd 值表示应用程序的当前工作目录,这是该应用程序启动的目录,除非它本身对这个目录进行更改。
      txt 类型的文件是程序代码,如应用程序二进制文件本身或共享库,如上列表中显示的 /sbin/init 程序。其次数值表示应用
      程序的文件描述符,这是打开该文件时返回的一个整数。如上的最后一行文件/dev/initctl,其文件描述符为 10。u 表示该
      文件被打开并处于读取/写入模式,而不是只读 ® 或只写 (w) 模式。同时还有大写 的W 表示该应用程序具有对整个文件的写
      锁。该文件描述符用于确保每次只能打开一个应用程序实例。初始打开每个应用程序时,都具有三个文件描述符,从 0 到 2,
      分别表示标准输入、输出和错误流。所以大多数应用程序所打开的文件的 FD 都是从 3 开始。
      与 FD 列相比,Type 列则比较直观。文件和目录分别称为 REG 和 DIR。而CHR 和 BLK,分别表示字符和块设备;
      或者 UNIX、FIFO 和 IPv4,分别表示 UNIX 域套接字、先进先出 (FIFO) 队列和网际协议 (IP) 套接字。
      lsof常用参数
      lsof 常见的用法是查找应用程序打开的文件的名称和数目。可用于查找出某个特定应用程序将日志数据记录到何处,或者正在跟踪某个问题。
      例如,linux限制了进程能够打开文件的数目。通常这个数值很大,所以不会产生问题,并且在需要时,应用程序可以请求更大的值(直到某
      个上限)。如果你怀疑应用程序耗尽了文件描述符,那么可以使用 lsof 统计打开的文件数目,以进行验证。lsof语法格式是:
      lsof [options] filename
      常用的参数列表:
      lsof filename 显示打开指定文件的所有进程 lsof -a 表示两个参数都必须满足时才显示结果 lsof -c string 显示COMMAND列中包含指定字符的进程所有打开的文件 lsof -u username 显示所属user进程打开的文件 lsof -g gid 显示归属gid的进程情况 lsof +d /DIR/ 显示目录下被进程打开的文件 lsof +D /DIR/ 同上,但是会搜索目录下的所有目录,时间相对较长 lsof -d FD 显示指定文件描述符的进程 lsof -n 不将IP转换为hostname,缺省是不加上-n参数 lsof -i 用以显示符合条件的进程情况 lsof -i[46] [protocol][@hostname|hostaddr][:service|port] 46 --> IPv4 or IPv6 protocol --> TCP or UDP hostname --> Internet host name hostaddr --> IPv4地址 service --> /etc/service中的 service name (可以不只一个) port --> 端口号 (可以不只一个)
      例如: 查看22端口现在运行的情况
      # lsof -i :22 COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME sshd 1409 root 3u IPv6 5678 TCP *:ssh (LISTEN)
      查看所属root用户进程所打开的文件类型为txt的文件:
      # lsof -a -u root -d txt COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME init 1 root txt REG 3,3 38432 1763452 /sbin/init mingetty 1632 root txt REG 3,3 14366 1763337 /sbin/mingetty mingetty 1633 root txt REG 3,3 14366 1763337 /sbin/mingetty mingetty 1634 root txt REG 3,3 14366 1763337 /sbin/mingetty mingetty 1635 root txt REG 3,3 14366 1763337 /sbin/mingetty mingetty 1636 root txt REG 3,3 14366 1763337 /sbin/mingetty mingetty 1637 root txt REG 3,3 14366 1763337 /sbin/mingetty kdm 1638 root txt REG 3,3 132548 1428194 /usr/bin/kdm X 1670 root txt REG 3,3 1716396 1428336 /usr/bin/Xorg kdm 1671 root txt REG 3,3 132548 1428194 /usr/bin/kdm startkde 2427 root txt REG 3,3 645408 1544195 /bin/bash ... ...
      lsof使用实例
       
      一、查找谁在使用文件系统
      在卸载文件系统时,如果该文件系统中有任何打开的文件,操作通常将会失败。那么通过lsof可以找出那些进程在使用当前要卸载的文件系统,如下:
      # lsof /GTES11/ COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME bash 4208 root cwd DIR 3,1 4096 2 /GTES11/ vim 4230 root cwd DIR 3,1 4096 2 /GTES11/
      在这个示例中,用户root正在其/GTES11目录中进行一些操作。一个 bash是实例正在运行,并且它当前的目录为/GTES11,另一个则显示的是vim正在编辑/GTES11下的文件。要成功地卸载/GTES11,应该在通知用户以确保情况正常之后,中止这些进程。 这个示例说明了应用程序的当前工作目录非常重要,因为它仍保持着文件资源,并且可以防止文件系统被卸载。这就是为什么大部分守护进程(后台进程)将它们的目录更改为根目录、或服务特定的目录(如 sendmail 示例中的 /var/spool/mqueue)的原因,以避免该守护进程阻止卸载不相关的文件系统。
      二、恢复删除的文件
      当Linux计算机受到入侵时,常见的情况是日志文件被删除,以掩盖攻击者的踪迹。管理错误也可能导致意外删除重要的文件,比如在清理旧日志时,意外地删除了数据库的活动事务日志。有时可以通过lsof来恢复这些文件。
      当进程打开了某个文件时,只要该进程保持打开该文件,即使将其删除,它依然存在于磁盘中。这意味着,进程并不知道文件已经被删除,它仍然可以向打开该文件时提供给它的文件描述符进行读取和写入。除了该进程之外,这个文件是不可见的,因为已经删除了其相应的目录索引节点。
      在/proc 目录下,其中包含了反映内核和进程树的各种文件。/proc目录挂载的是在内存中所映射的一块区域,所以这些文件和目录并不存在于磁盘中,因此当我们对这些文件进行读取和写入时,实际上是在从内存中获取相关信息。大多数与 lsof 相关的信息都存储于以进程的 PID 命名的目录中,即 /proc/1234 中包含的是 PID 为 1234 的进程的信息。每个进程目录中存在着各种文件,它们可以使得应用程序简单地了解进程的内存空间、文件描述符列表、指向磁盘上的文件的符号链接和其他系统信息。lsof 程序使用该信息和其他关于内核内部状态的信息来产生其输出。所以lsof 可以显示进程的文件描述符和相关的文件名等信息。也就是我们通过访问进程的文件描述符可以找到该文件的相关信息。
       
      当系统中的某个文件被意外地删除了,只要这个时候系统中还有进程正在访问该文件,那么我们就可以通过lsof从/proc目录下恢复该文件的内容。 假如由于误操作将/var/log/messages文件删除掉了,那么这时要将/var/log/messages文件恢复的方法如下:
      首先使用lsof来查看当前是否有进程打开/var/logmessages文件,如下:
      # lsof |grep /var/log/messages syslogd 1283 root 2w REG 3,3 5381017 1773647 /var/log/messages (deleted)
      从上面的信息可以看到 PID 1283(syslogd)打开文件的文件描述符为 2。同时还可以看到/var/log/messages已经标记被删除了。因此我们可以在 /proc/1283/fd/2 (fd下的每个以数字命名的文件表示进程对应的文件描述符)中查看相应的信息,如下:
      # head -n 10 /proc/1283/fd/2 Aug 4 13:50:15 holmes86 syslogd 1.4.1: restart. Aug 4 13:50:15 holmes86 kernel: klogd 1.4.1, log source = /proc/kmsg started. Aug 4 13:50:15 holmes86 kernel: Linux version 2.6.22.1-8 (root@everestbuilder.linux-ren.org) (gcc version 4.2.0) #1 SMP Wed Jul 18 11:18:32 EDT 2007 Aug 4 13:50:15 holmes86 kernel: BIOS-provided physical RAM map: Aug 4 13:50:15 holmes86 kernel: BIOS-e820: 0000000000000000 - 000000000009f000 (usable) Aug 4 13:50:15 holmes86 kernel: BIOS-e820: 000000000009f000 - 00000000000a0000 (reserved) Aug 4 13:50:15 holmes86 kernel: BIOS-e820: 0000000000100000 - 000000001f7d3800 (usable) Aug 4 13:50:15 holmes86 kernel: BIOS-e820: 000000001f7d3800 - 0000000020000000 (reserved) Aug 4 13:50:15 holmes86 kernel: BIOS-e820: 00000000e0000000 - 00000000f0007000 (reserved) Aug 4 13:50:15 holmes86 kernel: BIOS-e820: 00000000f0008000 - 00000000f000c000 (reserved)
      从上面的信息可以看出,查看 /proc/8663/fd/15 就可以得到所要恢复的数据。如果可以通过文件描述符查看相应的数据,那么就可以使用 I/O 重定向将其复制到文件中,如:
      cat /proc/1283/fd/2 > /var/log/messages
      对于许多应用程序,尤其是日志文件和数据库,这种恢复删除文件的方法非常有用。

       

       

      4. Linux iostat监测IO状态[转]

      Linux系统出现了性能问题,一般我们可以通过top、iostat、free、vmstat等命令 来查看初步定位问题。其中iostat可以给我们提供丰富的IO状态数据。

      1. 基本使用

      $iostat -d -k 1 10

      参数 -d 表示,显示设备(磁盘)使用状态;-k某些使用block为单位的列强制使用Kilobytes为单位;1 10表示,数据显示每隔1秒刷新一次,共显示10次。

      $iostat -d -k 1 10
      Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
      sda 39.29 21.14 1.44 441339807 29990031
      sda1 0.00 0.00 0.00 1623 523
      sda2 1.32 1.43 4.54 29834273 94827104
      sda3 6.30 0.85 24.95 17816289 520725244
      sda5 0.85 0.46 3.40 9543503 70970116
      sda6 0.00 0.00 0.00 550 236
      sda7 0.00 0.00 0.00 406 0
      sda8 0.00 0.00 0.00 406 0
      sda9 0.00 0.00 0.00 406 0
      sda10 60.68 18.35 71.43 383002263 1490928140

      Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
      sda 327.55 5159.18 102.04 5056 100
      sda1 0.00 0.00 0.00 0 0

      tps:该设备每秒的传输次数(Indicate the number of transfers per second that were issued to the device.)。“一次传输”意思是“一次I/O请求”。多个逻辑请求可能会被合并为“一次I/O请求”。“一次传输”请求的大小是未知的。

      kB_read/s:每秒从设备(drive expressed)读取的数据量;kB_wrtn/s:每秒向设备(drive expressed)写入的数据量;kB_read:读取的总数据量;kB_wrtn:写入 的总数量数据量;这些单位都为Kilobytes。

      上面的例子中,我们可以看到磁盘sda以及它的各个分区的统计数据,当时统计的磁盘总TPS是39.29,下面是各个分区的TPS。(因为是瞬间 值,所以总TPS并不严格等于各个分区TPS的总和)

      2. -x 参数

      使用-x参数我们可以获得更多统计信息。

      iostat -d -x -k 1 10
      Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s rkB/s wkB/s avgrq-sz avgqu-sz await svctm %util
      sda 1.56 28.31 7.80 31.49 42.51 2.92 21.26 1.46 1.16 0.03 0.79 2.62 10.28
      Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s rkB/s wkB/s avgrq-sz avgqu-sz await svctm %util
      sda 2.00 20.00 381.00 7.00 12320.00 216.00 6160.00 108.00 32.31 1.75 4.50 2.17 84.20

      rrqm/s:每秒这个设备相关的读取请求有多少被Merge了(当系统调用需要读取数据的 时候,VFS将请求发到各个FS,如果FS发现不同的读取请求读取的是相同Block的数据,FS会将这个请求合并Merge);wrqm/s:每秒这个 设备相关的写入请求有多少被Merge了。

      rsec/s:每秒读取的扇区数;wsec/: 每秒写入的扇区数。r/s:The number of read requests that were issued to the device per second;w/s:The number of write requests that were issued to the device per second;

      await:每一个IO请求的处理的平均时间(单位是微秒)。这里可以理解为IO的响应时 间,一般地系统IO响应时间应该低于5ms,如果大于10ms就比较大了。

      %util:在统计时间内所有处理IO时间,除以总共统计时间。例如,如果统计间隔1秒,该 设备有0.8秒在处理IO,而0.2秒闲置,那么该设备的%util = 0.8/1 = 80%,所以该参数暗示了设备的繁忙程度。一般地,如果该参数是100%表示设备已经接近满负荷运行了(当然如果是多磁盘,即使%util是100%,因 为磁盘的并发能力,所以磁盘使用未必就到了瓶颈)。

      3. -c 参数

      iostat还可以用来获取cpu部分状态值:

      iostat -c 1 10
      avg-cpu: %user %nice %sys %iowait %idle
      1.98 0.00 0.35 11.45 86.22
      avg-cpu: %user %nice %sys %iowait %idle
      1.62 0.00 0.25 34.46 63.67

      4. 常见用法

      $iostat -d -k 1 10 #查看TPS和吞吐量信息
      iostat -d -x -k 1 10 #查看设备使用率(%util)、响应时间(await)
      iostat -c 1 10 #查看cpu状态

      5. 实例分析

      $$iostat -d -k 1 |grep sda10
      Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
      sda10 60.72 18.95 71.53 395637647 1493241908
      sda10 299.02 4266.67 129.41 4352 132
      sda10 483.84 4589.90 4117.17 4544 4076
      sda10 218.00 3360.00 100.00 3360 100
      sda10 546.00 8784.00 124.00 8784 124
      sda10 827.00 13232.00 136.00 13232 136

      上面看到,磁盘每秒传输次数平均约400;每秒磁盘读取约5MB,写入约1MB。

      iostat -d -x -k 1
      Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s rkB/s wkB/s avgrq-sz avgqu-sz await svctm %util
      sda 1.56 28.31 7.84 31.50 43.65 3.16 21.82 1.58 1.19 0.03 0.80 2.61 10.29
      sda 1.98 24.75 419.80 6.93 13465.35 253.47 6732.67 126.73 32.15 2.00 4.70 2.00 85.25
      sda 3.06 41.84 444.90 54.08 14204.08 2048.98 7102.04 1024.49 32.57 2.10 4.21 1.85 92.24

      可以看到磁盘的平均响应时间<5ms,磁盘使用率>80。磁盘响应正常,但是已经很繁忙了。

      参考文献:

      1. Linux man iostat
      2. How Linux iostat computes its results
      3. Linux iostat
      http://blog.csdn.net/AE86_FC/archive/2010/02/03/5284112.aspx

      最近要对分布式集群做一些性能测试,其中一个很重要的项就是测试hadoop分布式集群在支持多磁盘轮转 写入的时候在各种磁盘配置的情况下的读写性能,如 在RAID0,RAID5和JBOD情况下的磁盘性能,所以linux 下的iostat命令就在产生report的脚本中非常有用,特此记录下iostat命令的一些使用笔 记
      [命令:] iostat [-c|-d] [-k] [-t] [间隔描述] [检测次数]
      参 数:
      -c : 仅显示cpu的状态
      -d : 仅显示存储设备的状态,不可以和-c一起使用
      -k : 默认显示的是读入读出的block信息,用-k可以改成KB大小来显示
      -t  : 显示日期
      -p device | ALL : device为某个设备或者某个分区,如果使用ALL,就表示要显示所有分区和设备的信息

      显示示例:
      avg-cpu:  %user   %nice    %sys %iowait   %idle
      4.55    0.00    0.63    0.26   94.56

      Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
      cciss/c0d0       30.11        68.20        67.13 1232784060 1213452142
      cciss/c0d0p1      0.00         0.00         0.00       2531          2
      cciss/c0d0p2     83.78        68.18        67.11 1232572011 1213204536
      dm-0              1.06         0.60         4.07   10873201   73555720
      dm-1             82.50        67.42        62.23 1218704309 1124966656
      dm-2              0.21         0.18         0.83    3199605   14929540
      dm-3              0.00         0.00         0.00        372        224

      以上显示分为上下两个部 分,上半部分显示CPU的信息,下面的数 据 显示存储设备的相关数据,它的数据意义如下:
      tps:平均每秒钟的传送次数,与数据传输“次数”相关,非容 量
      kB_read/s:启动到现在的平均读取单位
      kB_wrtn/s:启动到现在的平均写入单位
      kB_read:启动到现在总共 读出来的文件 单位
      kB_wrtn: 启动到现在总共写入的文件单位

      如果想要对iostat检查多此,每次之间的间隔一定数量的秒数,这样就可以查看每几秒钟之内的io统计数 据,这对性能的测试才具有实际意义:
      $> iostat -d 2 3
      表示没量秒钟检查一次,一共检查三次
      avg-cpu:  %user   %nice    %sys %iowait   %idle
      4.55    0.00    0.63    0.26   94.56

      Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
      cciss/c0d0       30.11        68.20        67.13 1232900288 1213456210
      cciss/c0d0p1      0.00         0.00         0.00       2531          2
      cciss/c0d0p2     83.78        68.19        67.11 1232688239 1213208604
      dm-0              1.06         0.60         4.07   10873201   73558008
      dm-1             82.50        67.42        62.23 1218820537 1124967604
      dm-2              0.21         0.18         0.83    3199605   14930372
      dm-3              0.00         0.00         0.00        372        224

      avg-cpu:  %user   %nice    %sys %iowait   %idle
      0.00    0.00    0.63    0.00   99.37

      Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
      cciss/c0d0        1.02         0.00        63.27          0        124
      cciss/c0d0p1      0.00         0.00         0.00          0          0
      cciss/c0d0p2     15.82         0.00        63.27          0        124
      dm-0             15.82         0.00        63.27          0        124
      dm-1              0.00         0.00         0.00          0          0
      dm-2              0.00         0.00         0.00          0          0
      dm-3              0.00         0.00         0.00          0          0

      avg-cpu:  %user   %nice    %sys %iowait   %idle
      0.00    0.00    0.32    0.00   99.68

      Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
      cciss/c0d0        3.06         0.00        26.53          0         52
      cciss/c0d0p1      0.00         0.00         0.00          0          0
      cciss/c0d0p2      6.63         0.00        26.53          0         52
      dm-0              0.00         0.00         0.00          0          0
      dm-1              6.63         0.00        26.53          0         52
      dm-2              0.00         0.00         0.00          0          0
      dm-3              0.00         0.00         0.00          0          0

      其中每一次的统计都是上 一次的统计时间到这次的统计时间之间的统计数据

       

      5. 如何查看进程 IO 读写情况?

      Linux Kernel 2.6.20 以上的内核支持进程 IO 统计,可以用类似 iotop 这样的工具来监测每个进程对 IO 操作的情况,就像用 top 来实时查看进程内存、CPU 等占用情况那样。但是对于 2.6.20 以下的 Linux 内核版本就没那么幸运了,根据 Stack Overflow 的这篇回帖 给出的方法,VPSee 写了一个简单的 Python 脚本用来在 linux kernel < 2.6.20 下打印进程 IO 状况。

      Kernel < 2.6.20

      这个脚本的想法很简单,把 dmesg 的结果重定向到一个文件后再解析出来,每隔1秒钟打印一次进程 IO 读写的统计信息,执行这个脚本需要 root:

      #!/usr/bin/python
      # Monitoring per-process disk I/O activity
      # written by http://www.vpsee.com 
      
      import sys, os, time, signal, re
      
      class DiskIO:
          def __init__(self, pname=None, pid=None, reads=0, writes=0):
              self.pname = pname
              self.pid = pid
              self.reads = 0
              self.writes = 0
      
      def main():
          argc = len(sys.argv)
          if argc != 1:
              print "usage: ./iotop"
              sys.exit(0)
      
          if os.getuid() != 0:
              print "must be run as root"
              sys.exit(0)
      
          signal.signal(signal.SIGINT, signal_handler)
          os.system('echo 1 > /proc/sys/vm/block_dump')
          print "TASK              PID       READ      WRITE"
          while True:
              os.system('dmesg -c > /tmp/diskio.log')
              l = []
              f = open('/tmp/diskio.log', 'r')
              line = f.readline()
              while line:
                  m = re.match(\
                      '^(\S+)\((\d+)\): (READ|WRITE) block (\d+) on (\S+)', line)
                  if m != None:
                      if not l:
                          l.append(DiskIO(m.group(1), m.group(2)))
                          line = f.readline()
                          continue
                      found = False
                      for item in l:
                          if item.pid == m.group(2):
                              found = True
                              if m.group(3) == "READ":
                                  item.reads = item.reads + 1
                              elif m.group(3) == "WRITE":
                                  item.writes = item.writes + 1
                      if not found:
                          l.append(DiskIO(m.group(1), m.group(2)))
                  line = f.readline()
              time.sleep(1)
              for item in l:
                  print "%-10s %10s %10d %10d" % \
                      (item.pname, item.pid, item.reads, item.writes)
      
      def signal_handler(signal, frame):
          os.system('echo 0 > /proc/sys/vm/block_dump')
          sys.exit(0)
      
      if __name__=="__main__":
          main()
      

      Kernel >= 2.6.20

      如果想用 iotop 来实时查看进程 IO 活动状况的话,需要下载和升级新内核(2.6.20 或以上版本)。编译新内核时需要打开 TASK_DELAY_ACCT 和 TASK_IO_ACCOUNTING 选项。解压内核后进入配置界面:

      # tar jxvf linux-2.6.30.5.tar.bz2
      # mv linux-2.6.30.5 /usr/src/
      # cd /usr/src/linux-2.6.30.5
      
      # make menuconfig
      

      选择 Kernel hacking –> Collect scheduler debugging info 和 Collect scheduler statistics,保存内核后编译内核:

      # make; make modules; make modules_install; make install
      

      修改 grub,确认能正确启动新内核:

      # vi /boot/grub/menu.lst
      

      出了新内核外,iotop 还需要 Python 2.5 或以上才能运行,所以如果当前 Python 是 2.4 的话需要下载和安装最新的 Python 包。这里使用源代码编译安装:

      # tar jxvf Python-2.6.2.tar.bz2
      # cd Python-2.6.2
      # ./configure
      # make; make install
      

      别忘了下载 setuptools:

      # mv setuptools-0.6c9-py2.6.egg.sh setuptools-0.6c9-py2.6.egg
      # sh setuptools-0.6c9-py2.6.egg
      

      更多信息

      如果想知道更多关于 block_dump 的信息,可以看看这篇 监测 Linux 进程的实时 IO 情况。使用 block_dump 的时候,最好能关掉 klogd 进程

       

       

Open Toolbar