很多时候,我们需要连到远程应用服务器上去观察java进程的运行情况。由于邪恶的防火墙限制我们很难直接连到应用服务器。所以,也有了本文… 常见的网络拓扑结构如下:

如上图所示,办公电脑通过互联网连接跳板机,在跳板机上,我们可以访问应用服务器,查看应用服务器日志或做其他操作。现在我们需要在办公电脑上监控应用服务器1上的java进程。

ssh tunneling

由于防火墙的限制,我们不能直接访问应用服务器。我们可以通过ssh tunneling来实现从办公电脑访问到应用服务器服务端口。

  • 跳板机:内网ip:192.168.0.1 外网ip:14.17.32.211 ssh端口:22

  • 应用服务器1:内网ip:192.168.0.2 应用端口:11113

通过ssh tunneling,我们利用ssh client建立ssh tunneling映射如下:

127.0.0.1:11113->14.17.32.211:22->192.168.0.2:11113

本地应用客户端通过访问本地11113端口,ssh client会把请求转发到应用服务器192.168.0.2:11113

JMX

一个典型的jmx url:

service:jmx:rmi://localhost:5000/jndi/rmi://localhost:6000/jmxrmi

这个JMX URL可以分为如下几个部分:

  • service:jmx: 这个是JMX URL的标准前缀,所有的JMX URL都必须以该字符串开头。

  • rmi: 这个是connector server的传输协议,在这个url中是使用rmi来进行传输的。JSR 160规定了所有connector server都必须至少实现rmi传输,是否还支持其他的传输协议依赖于具体的实现。比如MX4J就支持soap、soap+ssl、hessian、burlap等等传输协议。

  • localhost:5000: 这个是connector server的IP和端口,该部分是一个可选项,可以被省略掉。因为我们可以通过后面的服务注册端口,拿到jmx服务运行的端口信息。

  • /jndi/rmi://localhost:6000/jmxrmi: 这个是connector server的路径,具体含义取决于前面的传输协议。比如该URL中这串字符串就代表着该connector server的stub是使用jndi api绑定在rmi://localhost:6000/jmxrmi这个地址。可以理解为,localhost:6000提供了服务的注册查询端口,具体的jmx服务实现在localhost:5000

java进程一般通过如下的配置启动jmx:

-Dcom.sun.management.jmxremote -Djava.rmi.server.hostname=192.168.0.2 -Dcom.sun.management.jmxremote.port=11113 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false

通过上面的配置可以看出,只配置了服务注册查询端口11113,而实际的jmx服务运行端口是在运行时通过11113获取到的。

如何实现

上面提到了用ssh tunneling来实现端口转发,跳过防火墙的限制,也讲到了jmx的服务暴露方式。同时引出了我们遇到的问题,我们为了监控远程服务器上的java进程,我们能通过本地的11113端口访问到远程服务器上的JMX服务注册查询端口11113,但是JMX服务运行端口,我们不知道(因为是在运行时随机指定的),这样貌似走进了死胡同。

幸运的是我们自己来初始化JMXConnectorServer时,我们可以指定具体的jmx服务端口,并且还可以指定JMX服务端口和JMX注册查询端口为同一个端口。比如我们可以设置JMX url为:

service:jmx:rmi://localhost:11113/jndi/rmi://localhost:11113/jmxrmi

解决方案如下:

  1. 通过Java Agent实现在java业务代码运行之前,启动jmx server,并且设置jxm服务注册查询端口和服务端口为同一端口,JMX URL为:

     service:jmx:rmi://127.0.0.1:11113/jndi/rmi://127.0.0.1:11113/jmxrmi
    
  2. 通过ssh tunneling实现端口转发,我们的JMX client只需要访问本地的端口就能跳过防火墙的限制

注意:这里ip地址写为127.0.0.1是有原因的,看看我们的请求流程:

  • JMX client访问本地的127.0.0.1:11113
  • 注册查询请求被ssh tunneling转发到应用服务192.168.0.2:11113
  • 应用服务器上的java进程JMX注册查询服务会告诉JMX client,JMX服务在127.0.0.1:11113
  • 然后JMX client再访问127.0.0.1:11113
  • 服务请求又被ssh tunneling转发到应用服务192.168.0.2:11113,这次建立了JMX服务请求连接

操作步骤

  1. 下载jmx agent后执行mvn package,在target目录会生成jmxagent-0.0.1.jar,上传此jar包到服务器
  2. 配置java服务进程启动参数

     -javaagent:/root/jmxagent-0.0.1.jar -Djmx.rmi.agent.hostname=127.0.0.1 -Djmx.rmi.agent.port=11113
    

    上面设置jmx服务ip为127.0.0.1,服务端口为11113,使用javaagent jar包路径为/root/jmxagent-0.0.1.jar

  3. 启动java服务

    在控制台中,可以看到Start the RMI connector server的字样,说明服务正常启动了。

  4. 建立ssh tunneling

    在xshell中配置ssh tunneling很简单,只需要两个步骤:

    配置连接,我们这里需要连接到跳板机的ssh服务,如下图:

    配置tunneling,配置稳定端口11113,应用服务器192.168.0.2:11113

  5. 使用jmx client监控远程服务

    在jvisualvm中添加JMX连接,如下图:

  6. enjoy!

PS:附带一个maven启用此解决方案的脚本

export MAVEN_OPTS="-server -Xms8192m -Xmx8192m -XX:PermSize=128m -XX:MaxPermSize=256m -XX:+PrintGCTimeStamps -XX:+PrintGCDetails  -XX:SurvivorRatio=4 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:MaxTenuringThreshold=5 -XX:+CMSClassUnloadingEnabled -verbosegc  -Xloggc:/var/log/xxx/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/xxx/oom.hprof  -Djava.awt.headless=true  -javaagent:/root/jmxagent-0.0.1.jar -Djmx.rmi.agent.hostname=127.0.0.1 -Djmx.rmi.agent.port=11113"
mvn exec:java -Dexec.mainClass="com.xxx.Bootstrap" &

JDK7在Throwable对象中增加了一个新的构造器

protected Throwable(String message, Throwable cause,
                    boolean enableSuppression,
                    boolean writableStackTrace) 第三个参数表示是否启用suppressedExceptions(try代码快中抛出异常,在finally中又抛出异常,导致try中的异常丢失) 。第四个参数表示是否填充异常栈,如果为false,异常在初始化的时候不会调用本地方法fillInStackTrace。 <!--more--> 在业务开发中,我们会使用很多异常来表示不同的业务限制,比如用户余额不足、用户权限不够、参数不合法,这样的异常是不需要填充栈信息的,所以我们可以使用如下的代码来提高异常生成性能:

package com.yjf.common.exception;	
public class AppExcetpion extends RuntimeException {
	private static final long serialVersionUID = 1L;		
	public AppExcetpion() {
		this(null, null);
	}
	public AppExcetpion(String message, Throwable cause) {
		super(message, cause, true, false);
	}
	
	public AppExcetpion(String message) {
		this(message, null);
	}
	
	public AppExcetpion(Throwable cause) {
		this(null, cause);
	}

}

使用此异常会比以前我们使用的异常性能提高10多倍。

2013-05-09 14:58:39 [main] ERROR com.yjf.common.exception.JAVA7ExceptionTest.testJava7:23 - 运行:2000000/2010000 耗时:457ms
2013-05-09 14:58:45 [main] ERROR com.yjf.common.exception.JAVA7ExceptionTest.testOri:40 - 运行:2000000/2010000 耗时:6442ms

ps:没用使用jdk7或者某些场景下不能使用新特性的,可以考虑重写fillInStackTrace方法:

public class OrderCheckException extends IllegalArgumentException {

	private static final long serialVersionUID = 1L;

	private Map<String, String> errorMap = new HashMap<>();

	private String msg;

	public OrderCheckException() {
		super();
	}

	public OrderCheckException(Throwable cause) {
		super(cause);
	}

	public Map<String, String> getErrorMap() {
		return errorMap;
	}

	/**
	 * 增加参数错误信息
	 * 
	 * @param parameter 校验失败参数
	 * @param msg 参数信息
	 */
	public void addError(String parameter, String msg) {
		this.errorMap.put(parameter, msg);
		this.msg = null;
	}

	@Override
	public String getMessage() {
		if (msg == null) {
			if (errorMap.isEmpty()) {
				msg = "";
			} else {
				StringBuilder sb = new StringBuilder();
				for (Map.Entry entry : errorMap.entrySet()) {
					sb.append(entry.getKey()).append(SplitConstants.SEPARATOR_CHAR_COLON)
					.append(entry.getValue()).append(SplitConstants.SEPARATOR_CHAR_COMMA);
				}
				msg = sb.toString();
			}
		}
		return msg;
	}

	@Override
	public synchronized Throwable fillInStackTrace() {
		return this;
	}
  	}

上面这个OrderCheckException,它继承了IllegalArgumentException,但是此异常没有重写java.lang.RuntimeException#RuntimeException(java.lang.String, java.lang.Throwable, boolean, boolean),只有通过重写fillInStackTrace来达到效果.

Thinking Clearly about Performance笔记

原文链接:Thinking Clearly about Performance

RESPONSE TIME VERSUS THROUGHPUT

响应时间和吞吐量没有太多关系.你要了解两个值需要测试两个值.下面两个例子说明为什么两者之间没有太多关系.

  1. 应用吞吐量为1000笔/s,用户的平均响应时间是多少?

    如果应用下面是1000个服务提供者在提供服务,每一笔的响应时间最大可以为1s.所以,只能得出的结论是平均响应时间为0-1s

  2. 客户对某应用的需求为在单cpu的服务器上吞吐量为100笔/s.现在你写的应用每次执行耗时1ms,你的程序满足客户需求吗?

    如果请求串行发过来,每次执行一个,一个执行完在执行下一个,这种情况应该还是可以满足需求的.但是如果这100个请求在1s内随机的发送过来,CPU调度器(比如线程上下文切换)和串行资源(比如CAS导致的重试)可能让你不能满足客户需求.

PERCENTILE SPECIFICATIONS

平均并不能精确的定义响应时间.假如你能容忍的响应时间是1s,对于不同的应用,他们的平均响应时间都是1s.但是应用A90%的请求响应时间都小于1s和应用B60%的请求响应时间都小于1s,这两个应用提供的服务性能是不一样的.我们一般可以如下的形式来定义响应时间:the “Track Shipment” task must complete in less than .5 second in at least 99.9 percent of executions.

PROBLEM DIAGNOSIS

明确用户的需求,用户不会精确的定义他对性能的需求.大多数时候,他只是说”系统太慢了,我们没办法使用”,可以引导用户提出他的需求Response time of X is more than 20 seconds in many cases. We’ll be happy when response time is one second or less in at least 95 percent of executions.

THE SEQUENCE DIAGRAM

The sequence diagram is a good tool for conceptualizing flow of control and the corresponding flow of time. To think clearly about response time, however, you need something else.

THE PROFILE

A profile shows where your code has spent your time and—sometimes even more importantly—where it has not. There is tremendous value in not having to guess about these things.

With a profile, you can begin to formulate the answer to the question, “How long should this task run?” which, by now, you know is an important question in the first step of any good problem diagnosis.

AMDAHL’S LAW

Performance improvement is proportional to how much a program uses the thing you improved. If the thing you’re trying to improve contributes only 5 percent to your task’s total response time, then the maximum impact you’ll be able to make is 5 percent of your total response time. This means that the closer to the top of a profile that you work (assuming that the profile is sorted in descending response-time order), the bigger the benefit potential for your overall response time.

MINIMIZING RISK

when everyone is happy except for you, make sure your local stuff is in order before you go messing around with the global stuff that affects everyone else, too.

LOAD

One measure of load is utilization, which is resource usage divided by resource capacity for a specified time interval.

There are two reasons that systems get slower as load increases: queuing delay and coherency delay.

  • QUEUING DELAY

    Response time (R), in the perfect scalability M/M/m model, consists of two components: service time (S) and queuing delay (Q), or R = S + Q.

当谈到性能时,你期望一个系统满足下面两个目标:

  • 最佳的响应时间(不用等太久就能获得结果)
  • 最佳吞吐量(能服务更多的人)

但是这两个目标是互相矛盾的,优化第一个目标,需要你较少系统的负载.优化第二个目标,又需要你提高系统使用率,增加负载.你不能同时满足这两个目标,只能权衡取舍.

COHERENCY DELAY

Your system doesn’t have theoretically perfect scalability. Coherency delay is the factor that you can use to model the imperfection. It is the duration that a task spends communicating and coordinating access to a shared resource.

The utilization value at which this optimal balance occurs is called the knee. This is the point at which throughput is maximized with minimal negative impact to response times.