dev2dev.bea.com.cn
首页 资源中心 dev2dev学堂 在线技术论坛 专家Blog User Group CodeShare

YODA

袁玉强
  袁玉强,Dev2Dev ID:JadeYuan 熟悉WebLogic Server,WebLogic Integration及Tuxedo产品。


WebLogic Server 性能优化 (1) 执行线程篇

2007-12-21 16:00:15 | 评论 (1) | 被访问(1111)次

一直以来都想整理一篇WebLogic Server 8.1 优化的文档,但是一直懒得写。这次在Dev2Dev申请了BLOG,咱也不能让这块宝地荒废不是?下面都是自己的经验之谈,如有不对之处,还请各位高手多多指教。

说实话,执行线程的设置没有什么准确的计算公式,一切都得依赖测试和实际的运行情况。设置少了,会有等待队列;设置多了,性能也未必高。但是,对于调优,我还是建议先不足再增加,即先少后多。通常情况下,如果一开始就把各种资源设置的很高,那么很可能问题就被掩盖了。如果设置的少,当出现故障时会有比较明显的表征。

虽然没有计算公式,但是根据经验值,一颗CPU可以配置25-30之间的执行线程,当然,也有人说可以按照50线程/CPU计算。这个经验值的前提是,主机上没有其他的大型的占用资源较多的应用在运行。也就是说,一台主机上WebLogic Server实例,无论是一个,还是多个,他们的执行线程的总和大概是25*CPU数量到30*CPU数量之间。我经历过一个应用的压力测试,4CPU的HP主机,同样的测试脚本,在不同的执行线程配置下进行测试和比较。结果是配置50和120的时候,TPS在300左右,当配置70的时候,能够达到400左右。有可能70也不是最好的,但是至少说明执行线程的配置不是越多越好。

说到这里,想起来另外一个比较常见的问题,就是设置的执行线程的数量还算合适,没有过高也没有过低,但是就是在压力测试的时候,发现TPS值很低,CPU的利用率也上不去。我遇到的有两种情况:

1. 操作系统的补丁问题

在HP-UX上执行大型Java应用,需要打上很多的补丁,在HP的官方站点可以查到

http://www.hp.com/products1/unix/java/patches/index.html

其中有一个补丁包就是和线程调度相关的,如果不打上这些补丁,那么对于Java的线程调度存在问题,导致CPU利用率也不高,但是执行效率较低,甚至可能导致进程Crash。所以在安装软件之前去官方网站查看所需要的补丁以及经过认证的相关产品是一个好习惯。

2. 同步代码

如果代码中存在同步方法,并且是调用率比较高的,那么可能会出现CPU利用率低并且TPS也低的情况。通常Java应用自身的同步代码并不是很多,但是所用到的一些扩展的包,就有可能用到了同步代码。最典型的一个例子是Log4j. 给出一个示例的Thread Dump

"ExecuteThread: '152' for queue: 'weblogic.kernel.Default'" daemon prio=1 tid=0x08d9fef8 nid=0x1f28 waiting for monitor entry [5a1fd000..5a1fe24c]
    at org.apache.log4j.Category.callAppenders(Category.java:185)
    - waiting to lock <0x838664b0> (a org.apache.log4j.spi.RootCategory)
    at org.apache.log4j.Category.forcedLog(Category.java:372)
    at org.apache.log4j.Category.info(Category.java:674)

    ...

 

152号执行线程在等待<0x838664b0> (a org.apache.log4j.spi.RootCategory) 对象被其他线程释放同步锁。在完整的Thread dump中可以看出,有大量的线程都在等待同一个对象。这个对象被158号线程使用:

"ExecuteThread: '158' for queue: 'weblogic.kernel.Default'" daemon prio=1 tid=0x08da4d28 nid=0x1f28 waiting for monitor entry [59efd000..59efe24c]
    at java.lang.StackTraceElement.toString(StackTraceElement.java:130)
    at java.lang.String.valueOf(String.java:2131)
    at java.lang.StringBuffer.append(StringBuffer.java:370)
    - locked <0xa32e5370> (a java.lang.StringBuffer)
    at java.lang.Throwable.printStackTrace(Throwable.java:512)
    - locked <0xa326af28> (a org.apache.log4j.spi.VectorWriter)
    at org.apache.log4j.spi.ThrowableInformation.getThrowableStrRep(ThrowableInformation.java:50)
    at org.apache.log4j.spi.LoggingEvent.getThrowableStrRep(LoggingEvent.java:333)
    at org.apache.log4j.WriterAppender.subAppend(WriterAppender.java:295)
    at org.apache.log4j.WriterAppender.append(WriterAppender.java:150)
    at org.apache.log4j.AppenderSkeleton.doAppend(AppenderSkeleton.java:221)
    - locked <0x83866430> (a org.apache.log4j.ConsoleAppender)
    at org.apache.log4j.helpers.AppenderAttachableImpl.appendLoopOnAppenders(AppenderAttachableImpl.java:57)
    at org.apache.log4j.Category.callAppenders(Category.java:187)
    - locked <0x838664b0> (a org.apache.log4j.spi.RootCategory)
    at org.apache.log4j.Category.forcedLog(Category.java:372)

 

由于Log4j的同步代码导致出现上述的问题。所以,如果不是为了跟踪问题,尽量将Log4j的日志级别设置的高一些。减少同步代码的调用,能够极大的利用硬件资源。

 

如果一个应用中有部分页面执行时间较长,并且使用频率较低,可以为这些JSP/Servlet或者EJB配置独立的执行线程(当然了,尽量优化应用才是治本之道),将执行效率低下的放到一个执行线程队列,将效率高的放到一个执行线程队列,这样就避免因为一个执行效率低的页面影响整个应用了。

下面是简单的步骤说明:

1. 配置整个Web应用到单独的执行队列

编辑web应用的weblogic.xml,加入<wl-dispatch-policy>节点: <wl-dispatch-policy>yourexecutequeue</wl-dispatch-policy>

2. 配置JSP/Servlet到单独的执行队列

如果是JSP,编辑web应用的web.xml,加入一个<servlet>节点

<servlet>
       <servlet-name>myServlet</servlet-name>
       <jsp-file>/context/package/file.jsp</jsp-file>
</servlet>

然后在weblogic.xml中对这些Servlet进行配置

<servlet-descriptor>
        <servlet-name>the servlet name or the servlet for the jsp file</servlet-name>
        <dispatch-policy>your customized queue name</dispatch-policy>
</servlet-descriptor>

3. 配置EJB到单独的执行队列

编辑weblogic-ejb-jar.xml,例如:

<weblogic-enterprise-bean>
  <ejb-name>Hello</ejb-name>
  <jndi-name>lab.ejb.Hello</jndi-name>
  <dispatch-policy>your customized queue name</dispatch-policy>
</weblogic-enterprise-bean>

 

详细可以参考:

Developing Web Applications for WebLogic Server http://edocs.bea.com/wls/docs81/webapp/index.html

Programming WebLogic Enterprise JavaBeans http://edocs.bea.com/wls/docs81/ejb/index.html

 

基本上能想到的就这些了,欢迎大侠们补充


验证码的背后

2007-12-21 12:05:38 | 评论 (0) | 被访问(261)次

验证码的背后(基于JDK 1.4.2)
现在很多站点为了防止口令的暴力破解,或者BBS上为了防止使用程序发垃圾帖子,都加上了验证码的功能,这个大家可能也都有体验,就是随机出几个数字,然后在内存中画出来,再加上一些扰乱线,有的甚至还加入了变形,然后输出到页面。这样的图片人眼基本上可以识别,但是如果使用OCR之类的程序识别率就会比较低。
如何用JSP实现这个功能?在网络上可以搜索出很多,下面有一个估计是最常见的,也是大家用得最多的:
package test;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletException;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
import java.io.IOException;
/**
* @author <a href="mailto:jade_yuan@hotmail.com">Jade Yuan</a>
* @version 2006-3-9 18:34:36
*/
public class ImageServlet extends HttpServlet {
    private static final String CONTENT_TYPE = "image/jpeg";
//  private static final String [] FontNames={"SansSerif","Serif","MonoSpaced","Dialog","DialogInput"};
    //Initialize global variables
    public void init() throws ServletException {
    }
    //Process the HTTP Get request
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws
            ServletException, IOException {
        response.setContentType(CONTENT_TYPE);
        response.setHeader("Pragma", "no-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);
        // 在内存中创建图象
        int width = 60, height = 20;
        BufferedImage image = new BufferedImage(width, height,
                BufferedImage.TYPE_INT_RGB);
        // 获取图形上下文
        Graphics g = image.createGraphics();
        //生成随机类
        Random random = new Random();
        // 设定背景色
        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height);
        //设定字体
        g.setFont(new Font("DialogInput", Font.ITALIC, 18));
        //    g.setFont(new Font(FontNames[random.nextInt(6)%5], Font.ITALIC, 18));
        //画边框
        //g.setColor(new Color());
        //g.drawRect(0,0,width-1,height-1);
        // 随机产生255条干扰线,使图象中的认证码不易被其它程序探测到
        g.setColor(getRandColor(160, 200));
        for (int i = 0; i < 255; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }
// 取随机产生的认证码(4位数字)
//String rand = request.getParameter("rand");
//rand = rand.substring(0,rand.indexOf("."));
        String sRand = "";
        for (int i = 0; i < 4; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            // 将认证码显示到图象中
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110),
                    20 + random.nextInt(110))); //调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成
            g.drawString(rand, 13 * i + 6, 16);
        }
        //System.out.println("validate code=" + sRand);
        // 将认证码存入SESSION
        HttpSession session = request.getSession();
        session.setAttribute("validecode", sRand);
        // 图象生效
        g.dispose();
        // 输出图象到页面
        ServletOutputStream outputstream = response.getOutputStream();
        ImageIO.write(image, "JPEG", outputstream);
    }
    //Clean up resources
    public void destroy() {
    }
    Color getRandColor(int fc, int bc) { //给定范围获得随机颜色
        Random random = new Random();
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }
    public static void main(String[] args) {
    }
}
一般来说,验证码通常出现在登录的首页,一旦我们顺利登录系统,就开始使用相应功能,不再出现验证码的页面。这个时候你可能感觉不出来什么。但是如果有人频繁的刷新这个出现验证码的页面呢?事实是:你的系统几乎难以再对外提供服务了。我就遇到了这样的系统,一开始非常难以定位原因,当时也不知道有人频繁刷新这个页面。直到在Thread dump中看到了System.gc()的字样!是由com.sun.imageio.plugins.jpeg.JPEGImageWriter.setOutput()方法调用的。于是,在启动脚本中加上了-verbose:gc,观察输出,结果可想而知,每次调用这个servlet,就会做一次System.gc(),就是Full GC了,如果频繁的请求这个页面,那么系统肯定陷入了无法正常响应的情况。
于是乎,打开JDK 1.4.2 的源码,com.sun.imageio.plugins.jpeg.JPEGImageWriter.setOutput()是这样写的:
public void setOutput(Object output) {
        super.setOutput(output); // validates output
        ios = (ImageOutputStream) output; // so this will always work
        // Set the native destination
        setDest(structPointer, ios);
        srcRas = null;
        raster = null;
        currentImage = 0;
        System.gc();
    }
为什么这个Sun 的JDK自带的类会这么写呢?可能是编写者担心JPEG图片尺寸大的时候占用太多的内存吧,所以及时的显式调用System.gc()。
问题找到了,那么就需要找到解决方案。一开始我打算自己读出Image的byte[],再写出到输出流中,但是找了半天也没有找到好的办法。
没有办法了,转换思路吧,先研读一下PNGImageWriter的代码,发现没有System.gc()的调用,那就用PNG的格式输出一下试试。
修改了
private static final String CONTENT_TYPE = "image/jpeg";

ImageIO.write(image, "JPEG", outputstream);
两行代码中JPEG为PNG,测试一下:
成功!

其实还有一点,我补充一下,Sun 的 Hotspot有一个参数XX:+DisableExplicitGC可以禁用显式的GC,也就是System.gc()的调用,但是不建议使用,尤其是对于运行在中间件上的应用,如果使用了这个参数,说不定可能引发其他的问题。

 

另外,有热心网友的评论,我也一并整理过来了,非常感谢他们。(大家不要和我要稿费啊~~)

dkmilk:

我也补充一点:
很多系统在生成验证码后,把字符写入session,然后进行比较。
但在比较结束后一般就根本没有注意去清空session.
这样就造成一种现象:只要不去刷新生成验证码的URL,SESSION中的数字永远是一样的。这样就从根本上失去了验证码的作用。我测试过几个较大型的站点,都有这个问题。
所以说程序员也应该多多少少懂些常见的破解技术。

stevegy:

呵呵,楼主还是应该说明一下JDK的版本的,最新的1.5.0_09已经没有这样的代码了。


2007年12月

          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 26 27 28 29 30
31            
RSS订阅

袁玉强's Blog搜索