用top找出cpu消耗过高的java进程

top - 11:49:39 up 1 day,  1:53,  8 users,  load average: 0.24, 0.13, 0.22
Tasks: 210 total,   2 running, 203 sleeping,   0 stopped,   5 zombie
Cpu(s):  7.8%us,  3.7%sy,  0.1%ni, 87.2%id,  1.0%wa,  0.0%hi,  0.2%si,  0.0%st
Mem:   3343444k total,  2968552k used,   374892k free,    84140k buffers
Swap:  1951860k total,   631640k used,  1320220k free,   420808k cached
  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
16423 wangweiw  20   0  677m 205m 8900 S  101  6.3   0:14.16 java
 6783 wangweiw  20   0 1750m 1.1g  33m S    6 35.8 149:45.22 java
 1246 root      20   0  234m  57m  12m S    2  1.8  46:16.49 Xorg

比如我们分16423析这个java进程。

查看cpu消耗过高的线程。

top -H -p 16423

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
16457 wangweiw  20   0  677m 209m 8900 R  100  6.4   2:30.00 java
16423 wangweiw  20   0  677m 209m 8900 S    0  6.4   0:00.00 java
16424 wangweiw  20   0  677m 209m 8900 S    0  6.4   0:01.24 java

然后我们再分析16457这个线程

用jstack分析线程栈

jstack $pid|grep -A N $nid 此处的pid就是16423, nid就是16457,由于jstack输出中的thread id是16进制的,所以要把16457转换成16进制:4049。grep命令中的-A表示显示后面若干行,比如这里显示了grep到的行的后面10行,这个可以根据输出来调整。

jstack 16423|grep -A 10 4049
"qtp16147692-26" prio=10 tid=0x8e7e5c00 nid=0x4049 runnable [0x8e133000]
   java.lang.Thread.State: RUNNABLE
	at org.restlet.engine.http.header.HeaderReader.addValues(HeaderReader.java:254)
	at org.restlet.engine.http.ServerCall.getRequestEntity(ServerCall.java:215)
	at org.restlet.engine.http.HttpRequest.getEntity(HttpRequest.java:488)
	at org.restlet.engine.application.Decoder.beforeHandle(Decoder.java:123)
	at org.restlet.routing.Filter.handle(Filter.java:204)
	at org.restlet.routing.Filter.doHandle(Filter.java:159)
	at org.restlet.engine.application.StatusFilter.doHandle(StatusFilter.java:155)
	at org.restlet.routing.Filter.handle(Filter.java:206)
	at org.restlet.routing.Filter.doHandle(Filter.java:159)

跟踪代码定位问题

此处这个程序是使用了Restlet框架做的一个web服务,addValues这个方法体如下:

  public void addValues(Collection<V> values) {
        try {
            // Skip leading spaces
            skipSpaces();

            do {
                // Read the first value
                V nextValue = readValue();
                if (canAdd(nextValue, values)) {
                    // Add the value to the list
                    values.add(nextValue);
                }

                // Attempt to skip the value separator
                skipValueSeparator();
            } while (peek() != -1);
        } catch (IOException ioe) {
            Context.getCurrentLogger().log(Level.INFO,
                    "Unable to read a header", ioe);
        }
    }

看到这里有个while循环,再分析下skipSpaces,readValue,peek几个函数后就很容易构造一个死循环的例子:

EncodingReader encodingReader = new EncodingReader("修改gzip");
encodingReader.addValues(new ArrayList());

这个bug虽然命中概率很低,但是一旦命中,程序瞬间就崩溃了,这个问题在我们一个服务里遇到,只要用户的某次请求的header出现非法字符,一个线程就被占用并且是100% cpu占用,虽然这样的请求可能一天只有一个,但几天之后服务器cpu load就高的不行,只能重启程序解决。当然这样肯定不是长远的解决方案,更好的方案是在前端nginx上override出问题的header,来避免这样的请求进入到restlet代码层,目前看来进入restlet代码层,我们的程序就无能为力了。比如出问题的header是Content-Encoding,那么nginx的location可以配置如下:

location / {
          proxy_pass http://xxx_upstream;
          proxy_set_header Host $host;
          proxy_set_header Content-Encoding gzip;
          proxy_set_header        X-Real-IP $remote_addr;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_connect_timeout 6;
          proxy_read_timeout 60;
          proxy_send_timeout 60;
        } 当然也可以修改restlet源代码解决,不过这样后续升级restlet的时候会麻烦一些。