现代java开发指南 第二部分
第二部分:部署、监控 & 管理,性能分析和基准测试
===================
欢迎来到现代 Java 开发指南第二部分。在第一部分中,我们已经展示了有关 Java 新的语言特性,库和工具。这些新的工具使 Java 变成了相当轻量级的开发环境,这个开发环境拥有新的构建工具、更容易使用的文档、富有表现力的代码还有用户级线程的并发。而在这部分中,我们将比代码层次更高一层,讨论 Java 的运维———— Java 的部署、监控&管理,性能分析和基准测试。尽管这里的例子都会用 Java 来做示意,但是我们讨论的内容与所有的 JVM 语言都相关,而不仅仅是 Java 语言。
在开始之前,我想简短地回答一下第一部分读者的问题,并且澄清一下说的不清楚的地方。第一部分中最受争议的地方出现在构建工具这一节。在那一节中,我写到现代的 Java 开发者使用 Gradle
。有些读者对此提出异议,并且举出了例子来证明 Maven 同样也是一个很好的工具。我个人喜欢 Gradle 漂亮 DSL 和能使用指令式代码来编写非通用的构建操作,同时我也能够理解喜欢完全声明式的 Maven 的偏好,即使这样做需要大量的插件。因此,我承认:现代的 Java 开发者可能更喜欢 Maven 而不是 Gradle 。我还想说,虽然使用 Gradle 不用了解 Groovy ,甚至人们希望在不是那么标准的事情中也不用了解 Groovy 。但是我不会这样,我从 Gradle 的在线例子中已经学习了很多有用的 Groovy 的语句。
有些读者指出我在第一部分的代码示例中使用 Junit 和 Guava ,意味着我有意推广它们。好吧,我确实有这样的想法。Guava 是一个非常有用的库,而 JUnit 是一个很好的单元测试框架。虽然 TestNG 也很好,但是 JUnit 非常常见,很少有人会选择别的就算有优势的测试框架。
同样,就示例代码中测试使用 Hamcrest ,一个读者指出 AssertJ,可能是一个比 Hamcrest 更好的选择。
需要理解到本系列指南并不打算覆盖到 Java 的方方面面,能认识到这一点很重要。所以当然会有很多很好的库因为没有在文章中出现,我们没有去探索它们。我写这份指南的本意就是给大家示意一下现代 Java 开发可能是什么样的。
有些读者表达了他们更喜欢短的 Javadoc 注释,这种注释不必像 Javadoc 标准形式那样需要把所有的字段都写上。如下面的例子:
/**
* This method returns the result.
* @return the result
*/
int getResult();
更喜欢这样:
/**
* Returns the result
*/
int getResult();
我完全同意。我在例子中简单示范了混合 Markdown 和标准的 Javadoc 标签的使用。这只是用来展示如何使用,并不是意图把这种使用方式当成指导方针。
最后,关于 Android 我有一些话要说。 Android 系统通过一系列变换之后,能够执行用 java (还有可能是别的 JVM 语言)写的代码,但是 Android 不是 JVM,并且事实上 Android 无论在正式场合和实际使用中也不完全是 Java (造成这个问题的原因是两个跨国公司,这里指谷歌和甲骨文,没有就 Java 的使用达成一个许可协议)。正因为 Android 不完全是 Java ,所以在第一部分中讨论的内容对 Android 可能有用或者也可能没有用,而且因为 Android 没有包括 JVM ,所以在这部分讨论的内容很少能应用到 Android 上面。
好了,现在让我们回到正文。
现代 Java 的打包和部署
对于不熟悉 Java 生态体系的人来说,Java(或者任何 JVM 语言)源文件,被编绎成 .class
文件(本质上是 Java 二进制文件),每一个类一个文件。打包这些 class 文件的基本机制就把这些文件打包在一起(这项工作通常由构建工具或者IDE来完成)放到JAR(Java存档)文件,JAR 文件叫 Java 二进制包。 JAR 文件仅仅是 Zip 压缩文件,它包括 class 文件,还有一个附加的清单文件用来描述内容,清单中还可以包括其它的关于分发的信息(如在被签名的 JARs 中,清单可以包括数字签名)。如果你打包一个应用(与此相反是打包一个库)到 JAR 中,清单文件应该指出应用的主类(也就是 main 函数所在类),在这种情况下,应用通过命令java -jar app.jar
启动,我们称这个 JAR 文件为可执行的 JAR 。
Java 库被打包成 JAR 文件,然后部署到 Maven 仓库中(这个仓库能被所有的 JVM 构建工具使用,不仅仅是 Maven )。 Maven 仓库管理这些库二进制文件的版本和依赖(当你发一个请求想从Maven仓库中加载一个库,此外你请求了该库所有的依赖)。开源 Java 库经常托管在这个中央仓库中,或者其它类似的公开仓库中。并且组织机构通过 Artifactory 或者 Nexus 等工具,管理他们私有 Maven 仓库。你甚至能在 GitHub 上建立自己的 Maven 仓库。但是 Maven 仓库在构建过程中应该能正常使用,并且 Maven 仓库通常托管库形式 JAR 而不是可执行的 JAR 。
Java 网站应用传统上应该在应用服务器(或者 servlet 容器)中执行。这些容器能运行多个网站应用,能按需加载或卸载应用。 Java 网站应用以 WAR 的形式部署在 servlet 容器中。WAR 也是 JAR 文件,它的内容以某种标准形式排好,并且包括额外的配置信息。但是,正如我们将在第三部分看到一样,就现代 Java 开发而言, Java 应用服务器已死。
Java 桌面应用经常被打包成与平台相关的二进制文件,还包括一个平台相关的 JVM。 JDK 工具包中有一个打包工具来做这个事情(这里是讲的是如何在 NetBeans 中使用它)。第三方工具 Packer 也提供了类似的功能。对于游戏和桌面应用来说,这种打包机非常好。但是对于服务器软件来说,这种打包机制就不是我想要的。此外,因为要打包一个 JVM 的拷贝,这种机制不能以补丁形式安全和平滑地升级应用。
对服务器端代码,我们想要的是一种简单、轻量、能自动的打包和部署的工具。这个工具最好能利用可执行 JAR 的简单和平台无关性。但是可执行 JAR 有几个不足的地方。每一个库通常打包到各自的 JAR 文件中,然后和所有的依赖一起打包成单个 JAR 文件,这一过程可能造成冲突,特别是已打包的资源库(没有 class
文件的库)一起打包时。还有,一个原生库在打包时不能直接放到 JAR 中。打包中可能最重要的是, JVM 配置信息(如 heap
的大小)对用户来说是遗漏的,这个工作必须在命令行下才能做。像 Maven’s Shade plugin 和 Gradle’s Shadow plugin 等工具,解决了资源冲突的问题,而 One-Jar 支持原生的库,但是这些工具都可能对应用产生影响,而且也没有解决 JVM 参数配置的问题。 Gradle 能把应用打包成一个 ZIP 文件,并且产生一个与系统相关的启脚本去配置 JVM ,但是这种方法要求安装应用。我们可以做的比这样更轻量级。同样,我们有强大的、普遍存在的资源像 Maven 仓库任我们使用,如果不充分利用它们是件令人可耻的事。
这一系列博客打算讲讲用现代 Java 工作是多么简单和有趣(不需牺牲任何性能),但是当我去寻找一种有趣、简单和轻量级的方法去打包、分发和部署服务器端的 Java 应用时,我两手空空。所以 Capsule 诞生了(如果你知道有其它更好的选择,请告诉我)。
Capsule 使用平台独立的可执行 JAR 包,但是没有依赖,并且(可选的)能整合强大和便捷的 Maven 仓库。一个 capsule 是一个 JAR 文件,它包括全部或者部分的 Capsule 项目 class,和一个包括部署配置的清单文件。当启动时(java -jar app.jar
), capsule 会依次执行以下的动作:解压缩 JAR 文件到一个缓存目录中,下载依赖,寻找一个合适的 JVM 进行安装,然后配置和运行应用在一个新的JVM进程中。
现在让我们把 Capsule 拿出来溜一溜。我们把第一部的 JModern
项目做为开始的项目。这是我们的 build.gradle
文件:
apply plugin: 'java'
apply plugin: 'application'
sourceCompatibility = '1.8'
mainClassName = 'jmodern.Main'
repositories {
mavenCentral()
}
configurations {
quasar
}
dependencies {
compile "co.paralleluniverse:quasar-core:0.5.0:jdk8"
compile "co.paralleluniverse:quasar-actors:0.5.0"
quasar "co.paralleluniverse:quasar-core:0.5.0:jdk8"
testCompile 'junit:junit:4.11'
}
run {
jvmArgs "-javaagent:${configurations.quasar.iterator().next()}"
}
这里是我们的 jmodern.Main
类:
package jmodern;
import co.paralleluniverse.fibers.Fiber;
import co.paralleluniverse.strands.Strand;
import co.paralleluniverse.strands.channels.Channel;
import co.paralleluniverse.strands.channels.Channels;
public class Main {
public static void main(String[] args) throws Exception {
final Channel<Integer> ch = Channels.newChannel(0);
new Fiber<Void>(() -> {
for (int i = 0; i < 10; i++) {
Strand.sleep(100);
ch.send(i);
}
ch.close();
}).start();
new Fiber<Void>(() -> {
Integer x;
while((x = ch.receive()) != null)
System.out.println("--> " + x);
}).start().join(); // join waits for this fiber to finish
}
}
为了测试一下我们的程序工作是正常的,我们运行一下gradle run
。
现在,我们来把这个应用打包成一个 capsule 。在构建文件中,我们将增加 capsule
配置。然后,我们增加依赖包:
capsule "co.paralleluniverse:capsule:0.3.1"
当前 Capsule 有两种方法来创建 capsule (虽然你也可以混合使用)。第一种方法是创建应用时把所有的依赖都加入到 capsule 中;第二种方法是第一次启动 capsule 时让它去下载依赖。我来试一下第一种—— "full" 模式。我们添加下面的任务到构建文件中:
task capsule(type: Jar, dependsOn: jar) {
archiveName = "jmodern-capsule.jar"
from jar // embed our application jar
from { configurations.runtime } // embed dependencies
from(configurations.capsule.collect { zipTree(it) }) { include 'Capsule.class' } // we just need the single Capsule class
manifest {
attributes(
'Main-Class' : 'Capsule',
'Application-Class' : mainClassName,
'Min-Java-Version' : '1.8.0',
'JVM-Args' : run.jvmArgs.join(' '), // copy JVM args from the run task
'System-Properties' : run.systemProperties.collect { k,v -> "$k=$v" }.join(' '), // copy system properties
'Java-Agents' : configurations.quasar.iterator().next().getName()
)
}
}
好了,现在我们输入gradle capsule
构建 capsule ,然后运行:
java -jar build/libs/jmodern-capsule.jar
如果你想准确的知道 Capsule 现在在做什么,可以把-jar
换成-Dcapsule.log=verbose
,但是因为它是一个包括依赖的 capsule ,第一次运行时, Capsule 会解压 JAR 文件到一个缓存目录下
(这个目录是在当前用户的根文件夹中下.capsule/apps/jmodern.Main
),然后启动一个新通过 capsule 清单文件配置好的 JVM 。如果你已经安装好了 Java7 ,你可以使用 Java7 启动 capsule (通过设置 JAVA_HOME 环境变量)。虽然 capsule 能在 java7 下启动,但是因为 capsule 指定了最小的 Java 版本是 Java8 (或者是 1.8,同样的意思), capsule 会寻找 Java8 并且用它来跑我们的应用。
现在讲讲第二方法。我们将创建一个有外部依赖的 capsule 。为了使创建工作简单点,我们先在构建文件中增加一个函数(你不需要理解他;做成 Gradle 的插件会更好,欢迎贡献。但是现在我们手动创建这个 capsule ):
// converts Gradle dependencies to Capsule dependencies
def getDependencies(config) {
return config.getAllDependencies().collect {
def res = it.group + ':' + it.name + ':' + it.version +
(!it.artifacts.isEmpty() ? ':' + it.artifacts.iterator().next().classifier : '')
if(!it.excludeRules.isEmpty()) {
res += "(" + it.excludeRules.collect { it.group + ':' + it.module }.join(',') + ")"
}
return res
}
}
然后,我们改变构建文件中capsule
任务,让它能读:
task capsule(type: Jar, dependsOn: classes) {
archiveName = "jmodern-capsule.jar"
from sourceSets.main.output // this way we don't need to extract
from { configurations.capsule.collect { zipTree(it) } }
manifest {
attributes(
'Main-Class' : 'Capsule',
'Application-Class' : mainClassName,
'Extract-Capsule' : 'false', // no need to extract the capsule
'Min-Java-Version' : '1.8.0',
'JVM-Args' : run.jvmArgs.join(' '),
'System-Properties' : run.systemProperties.collect { k,v -> "$k=$v" }.join(' '),
'Java-Agents' : getDependencies(configurations.quasar).iterator().next(),
'Dependencies': getDependencies(configurations.runtime).join(' ')
)
}
}
运行gradle capsule
,再次运行:
java -jar build/libs/jmodern-capsule.jar
首次运行, capsule 将会下载我们项目的所有依赖到一个缓存目录下。其他的 capsule 共享这个目录。 相反你不需要把依赖列在 JAR 清单文件中,取而代之,你可以把项目依赖列在 pom
文件中(如果你使用 Maven 做为构建工具,这将特别有用),然后放在 capsule 的根目录。详细信息可以查看 Capsule 文档。
最后,因为这篇文章的内容对于任何 JVM 语言都是有用的,所以这里有一个小例子用来示意把一个 Node.js 的应用打包成一个 capsule 。这个小应用使用了 Avatar ,该项目能够在 JVM 上运行 javascript 应用
,就像 Nodejs 一样。代码如下:
var http = require('http');
var server = http.createServer(function (request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.end("Hello World
");
});
server.listen(8000);
console.log("Server running at http://127.0.0.1:8000/");
应用还有两个 Gradle 构建文件。一个用来创建full
模式的 capsule ,另外一个用来创建external
模式的 capsule 。这个例子示范了打包原生库依赖。创建该 capsule ,运行:
gradle -b build1.gradle capsule
就得到一个包括所有依赖的 capsule 。或者运行下面的命令:
gradle -b build2.gradle capsule
就得到一个不包括依赖的 capsule (里面包括 Gradle wrapper,所以你不需要安装 Gradle ,简单的输入./gradlew
就能构建应用)。
运行它,输入下面的命令:
java -jar build/libs/hello-nodejs.jar
Jigsaw,原计划在包括在 Java9 中。该项目的意图是解决 Java 部署和一些其它的问题,例如:一个被精减的JVM发行版,减少启动时间(这里有一个有趣演讲关于 Jigsaw )。同时,对于现代 Java 开发打包和布署,Capsule 是一个非常合适的工具。Capsule 是无状态和不用安装的。
日志
在我们进入 Java 先进的监控特性之前,让我们把日志搞定。据我所知,Java 有大量的日志库,它们都是建立在 JDK 标准库之上。如果你需要日志,用不着想太多,直接使用 slf4j 做为日志 API 。它变成了事实上日志 API 的标准,而且已绑定几乎所有的日志引擎。一但你使用 SLF4J,你可以推迟选择日志引擎时机(你甚至能在部署的时候决定使用哪个日志引擎)。 SLF4J 在运行时选择日志引擎,这个日志引擎可以是任何一个只要做为依赖添加的库。大部分库现在都使用SLF4J,如果开发中有一个库没有使用SLF4J,它会让你把这个库的日志导回SLF4J,然后你就可以再选择你的日志引擎。谈谈选择日志引擎事,如果你想选择一个简单的,那就 JDK 的java.util.logging。如果你想选择一个重型的、高性能的日志引擎,就选择 Log4j2 (除了你感觉真的有必要尝试一下其它的日志引擎)。
现在我们来添加日志到我们的应用中。在依赖部分,我们增加:
compile "org.slf4j:slf4j-api:1.7.7" // the SLF4J API
runtime "org.slf4j:slf4j-jdk14:1.7.7" // SLF4J binding for java.util.logging
如果运行gradle dependencies
命令,我们可以看到当前的应用有哪些依赖。就当前来说,我们依赖了 Log4j ,这不是我们想要的。因此好得在build.gradle
的配置部分增加一行代码:
all*.exclude group: "org.apache.logging.log4j", module: "*"
好了,我们来给我们的应用添加一些日志:
package jmodern;
import co.paralleluniverse.fibers.Fiber;
import co.paralleluniverse.strands.Strand;
import co.paralleluniverse.strands.channels.Channel;
import co.paralleluniverse.strands.channels.Channels;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
static final Logger log = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) throws Exception {
final Channel<Integer> ch = Channels.newChannel(0);
new Fiber<Void>(() -> {
for (int i = 0; i < 100000; i++) {
Strand.sleep(100);
log.info("Sending {}", i); // log something
ch.send(i);
if (i % 10 == 0)
log.warn("Sent {} messages", i + 1); // log something
}
ch.close();
}).start();
new Fiber<Void>(() -> {
Integer x;
while ((x = ch.receive()) != null)
System.out.println("--> " + x);
}).start().join(); // join waits for this fiber to finish
}
}
然后运行应用(gradle run
),你会看见日志打印到标准输出(这个默认设置;我们不打算深入配置日志引擎,你想做的话,可以参考想关文档)。info
和warn
级的日志都默认输出。日志的输出等级可以在配置文件中设置(现在我们不打算改了),或者一会可以看到,我们在运行时进行修改设置,
用jcmd和jstat进行监控和管理
JDK 中已经包括了几个用于监控和管理的工具,而这里我们只会简短介绍其中的一对工具:jcmd 和 jstat 。
为了演示它们,我们要使我们的应用程序别那么快的终止。所以我们把for
循环次数从10
改成1000000
,然后在终端下运行应用gradle run
。在另外一个终端中,我们运行jcmd
。如果你的JDK安装正确并且jcmd
在你的目录中,你会看到下面的信息:
22177 jmodern.Main
21029 org.gradle.launcher.daemon.bootstrap.GradleDaemon 1.11 /Users/pron/.gradle/daemon 10800000 86d63e7b-9a18-43e8-840c-649e25c329fc -XX:MaxPermSize=256m -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m -Dfile.encoding=UTF-8
22182 sun.tools.jcmd.JCmd
上面信息列出了所有正在JVM上运行的程序。再远行下面的命令:
jcmd jmodern.Main help
你会看到打印出了特定 JVM 程序的 jcmd 支持的命令列表。我们来试一下:
jcmd jmodern.Main Thread.print
打印出了 JVM 中所有线程的当前堆栈信息。试一下这个:
jcmd jmodern.Main PerfCounter.print
这将打印出一长串各种 JVM 性能计数器(你问问谷歌这些参数的意思)。你可以试一下其他的命令(如GC.class_histogram
)。
jstat
对于 JVM 来说就像 Linux 中的 top
,只有它能查看关于 GC 和 JIT 的活动信息。假设我们应用的 pid
是95098(可以用 jcmd
和 jps
找到这个值)。现在我们运行:
jstat -gc 95098 1000
它将会每 1000 毫秒打印 GC 的信息。看起来像这样:
S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT
80384.0 10752.0 0.0 10494.9 139776.0 16974.0 148480.0 125105.4 ? ? 65 1.227 8 3.238 4.465
80384.0 10752.0 0.0 10494.9 139776.0 16985.1 148480.0 125105.4 ? ? 65 1.227 8 3.238 4.465
80384.0 10752.0 0.0 10494.9 139776.0 16985.1 148480.0 125105.4 ? ? 65 1.227 8 3.238 4.465
80384.0 10752.0 0.0 10494.9 139776.0 16985.1 148480.0 125105.4 ? ? 65 1.227 8 3.238 4.465
这些数字表示各种 GC 区域当前的容量。想知道每一个的意思,查看 jsata 文档。
使用JMX进行监控和管理
JVM 最大的一个优点就是它能在运行时监控和管理时,暴露每一个操作的详细信息。JMX(Java Management Extensions),是 JVM 运行时管理和监控的标准。 JMX 详细说明了 MBeans ,该对象用来暴露有关 JVM 、 JDK 库和 JVM 应用的监控和管理操作方法。 JMX 还定义了连接 JVM 实例的标准方法,包括本地连接和远程连接的方式。还有定义了如何与 MBeans 交互。实际上, jcmd 就是使用 JMX 获得相关的信息的。在本文后面,我们也写一个自己的 MBeans ,但是还是首先来看看内置的 MBeans 如何使用。
当我们的应用运行在一个终端,运行 jvisualvm
命令(该工具是 JDK 的一部分)在另一个终端。这会启动 VisualVM 。在我们开始使用之前,还需要装一些插件。打开 Tools->Plugins
菜单,选择可以可以使用的插件。当前的演示,我们只需要VisualVM-MBeans
,但是你可能除了 VisualVM-Glassfish 和 BTrace Workbench ,其他的插件都装上。现在在左边面板选择 jmodern.Main
,然后选择监控页。你会看到如下信息:
该监控页把 JMX-MBeans 暴露的使用信息用图表的型式表达出来。我们也可以通过 Mbeans 选项卡选择一些 MBeans (有些需要安装完成插件后才能使用),我们能查看和交互已注册的 MBeans 。例如有个常用的堆图,就在 java.lang/Memory
中(双击属性值展开它):
现在我们选择 java.util.logging/Logging
MBean 。在右边面板中,属性 LoggerNames
会列出所有已注册的 logger ,包括我们添加到 jmodern.Main
(双击属性值展开它):
MBeans 使我们不仅能够探测到监测值,还可以改变这些值,然后调用各种管理操作。选择 Operations
选项卡(在右面板中,位于属性选项卡的右边)。我们现在在运行时通过 JMX-MBean 改变日志等级。在 setLoggerLevel
属性中,第一个地方填上 jmodern.Main
,第二个地方填上 WARNING
,载图如下:
现在,点击 setLoggerLevel
按钮, info
级的日志信息不再会打印出来。如果调整成 SEVERE
,就没有信息打印。 VisualVM 对 MBean 都会生成简单的 GUI,不用费力的去写界面。
我们也可以在远程使用 VisualVM 访问我们的应用,只用增加一些系统的设置。在构建文件中的run
部分中增加如下代码:
systemProperty "com.sun.management.jmxremote", ""
systemProperty "com.sun.management.jmxremote.port", "9999"
systemProperty "com.sun.management.jmxremote.authenticate", "false"
systemProperty "com.sun.management.jmxremote.ssl", "false"
(在生产环境中,你应该打开安全选项)
正如我们所看到的,除了 MBean 探测, VisualVM 也可以使用 JMX 提供的数据创建自定义监控视图:监控线程状态和当前所有线程的堆栈情况,查看 GC 和通用内存使用情况,执行堆转储和核心转储操作,分析转储堆和核心堆,还有更多的其它功能。因此,在现代 Java 开发中, VisualVM 是最重要的工具之一。这是 VisualVM 跟踪插件提供的监控信息截图:
现代 Java 开发人员有时可能会喜欢一个 CLI 而不是漂亮的 GUI 。 jmxterm 提供了一个 CLI 形式的 JMX-MBeans 。不幸的是,它还不支持 Java7 和 Java8 ,但开发人员表示将很快来到(如果没有,我们将发布一个补丁,我们已经有一个分支在做这部分工作了)。
不过,有一件事是肯定的。现代 Java 开发人员喜欢 REST-API (如果没有其他的原因,因为它们无处不在,并且很容易构建 web-GUI )。虽然 JMX 标准支持一些不同的本地和远程连接器,但是标准中没有包括 HTTP 连接器(应该会在 Java9 中)。现在,有一个很好的项目 Jolokia,填补这个空白。它能让我们使用 RESTful 的方式访问 MBeans 。让我们来试一试。将以下代码合并到build.gradle
文件中:
configurations {
jolokia
}
dependencies {
runtime "org.jolokia:jolokia-core:1.2.1"
jolokia "org.jolokia:jolokia-jvm:1.2.1:agent"
}
run {
jvmArgs "-javaagent:${configurations.jolokia.iterator().next()}=port=7777,host=localhost"
}
(我发现 Gradle 总是要求对于每一个依赖重新设置 Java agent,这个问题一直困扰我。)
改变构建文件 capsule
任务的 Java-Agents
属性,可以让 Jolokia 在 capsule 中可用。代码如下:
'Java-Agents' : getDependencies(configurations.quasar).iterator().next() +
+ " ${getDependencies(configurations.jolokia).iterator().next()}=port=7777,host=localhost",
通过 gradle run
或者 gradle capsule; java -jar build/libs/jmodern-capsule.jar
运行应用,然后打开浏览器输入 http://localhost:7777/jolokia/version
。如果 Jolokia 正常工作,会返回一个JSON。现在我们要查看一下应用的堆使用情况,可以这样做:
curl http://localhost:7777/jolokia/read/java.lang:type=Memory/HeapMemoryUsage
设置日志等级,你可以这样做:
curl http://localhost:7777/jolokia/exec/java.util.logging:type=Logging/setLoggerLevel(java.lang.String,java.lang.String)/jmodern.Main/WARNING
Jolokia 提供了 Http API ,这就就使用 GET 和 POST 方法进行操作。同时还提供安全访问的方法。需要更多的信息,请查看文档。
有了 JolokiaHttpAPI 就能通过Web进行管理。这里有一个例子,它使用Cubism为 GUI 进行 JMX MBeans进行管理。还有如 hawtio , JBoss 创建的项目,它使用 JolokiaHttpAPI 构建了一个全功能的网页版的管理应用。与 VisualVM 静态分析功能不同的是, hawatio 意图是为生产环境提供一个持续监控和管理的工具。
写一个自定义的MBeans
写一个 Mbeans 并注册很容易:
package jmodern;
import co.paralleluniverse.fibers.Fiber;
import co.paralleluniverse.strands.Strand;
import co.paralleluniverse.strands.channels.*;
import java.lang.management.ManagementFactory;
import java.util.concurrent.atomic.AtomicInteger;
import javax.management.MXBean;
import javax.management.ObjectName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
static final Logger log = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) throws Exception {
final AtomicInteger counter = new AtomicInteger();
final Channel<Object> ch = Channels.newChannel(0);
// create and register MBean
ManagementFactory.getPlatformMBeanServer().registerMBean(new JModernInfo() {
@Override
public void send(String message) {
try {
ch.send(message);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public int getNumMessagesReceived() {
return counter.get();
}
}, new ObjectName("jmodern:type=Info"));
new Fiber<Void>(() -> {
for (int i = 0; i < 100000; i++) {
Strand.sleep(100);
log.info("Sending {}", i); // log something
ch.send(i);
if (i % 10 == 0)
log.warn("Sent {} messages", i + 1); // log something
}
ch.close();
}).start();
new Fiber<Void>(() -> {
Object x;
while ((x = ch.receive()) != null) {
counter.incrementAndGet();
System.out.println("--> " + x);
}
}).start().join(); // join waits for this fiber to finish
}
@MXBean
public interface JModernInfo {
void send(String message);
int getNumMessagesReceived();
}
}
我们添加了一个 JMX-MBean ,让我们监视第二个 fiber
收到消息的数量,也暴露了一个发送操作,能将一条消息进入 channel
。当我们运行应用程序时,我们可以在 VisualVM 中看到监控的属性:
双击,绘图:
在 Operations
选项卡中,使用我们定义在MBean的操作,来发个消息:
使用Metrics进行健康和性能监控
Metrics 一个简洁的监控 JVM 应用性能和健康的现代库,由 Coda Hale 在 Yammer 时创建的。 Metrics 库中包含一些通用的指标集和发布类,如直方图,计时器,统计议表盘等。现在我们来看看如何使用。
首先,我们不需要使用 Jolokia ,把它从构建文件中移除掉,然后添加下面的代码:
compile "com.codahale.metrics:metrics-core:3.0.2"
Metrics 通过 JMX-MBeans 发布指标,你可以将这些指标值写入 CSV 文件,或者做成 RESTful 接口,还可以发布到 Graphite 和 Ganglia
中。在这里只是简单发布到 JMX (第三部分中讨论到 Dropwizard 时,会使用 HTTP )。这是我们修改后的 Main.class
:
package jmodern;
import co.paralleluniverse.fibers.Fiber;
import co.paralleluniverse.strands.Strand;
import co.paralleluniverse.strands.channels.*;
import com.codahale.metrics.*;
import static com.codahale.metrics.MetricRegistry.name;
import java.util.concurrent.ThreadLocalRandom;
import static java.util.concurrent.TimeUnit.*;
public class Main {
public static void main(String[] args) throws Exception {
final MetricRegistry metrics = new MetricRegistry();
JmxReporter.forRegistry(metrics).build().start(); // starts reporting via JMX
final Channel<Object> ch = Channels.newChannel(0);
new Fiber<Void>(() -> {
Meter meter = metrics.meter(name(Main.class, "messages" , "send", "rate"));
for (int i = 0; i < 100000; i++) {
Strand.sleep(ThreadLocalRandom.current().nextInt(50, 500)); // random sleep
meter.mark(); // measures event rate
ch.send(i);
}
ch.close();
}).start();
new Fiber<Void>(() -> {
Counter counter = metrics.counter(name(Main.class, "messages", "received"));
Timer timer = metrics.timer(name(Main.class, "messages", "duration"));
Object x;
long lastReceived = System.nanoTime();
while ((x = ch.receive()) != null) {
final long now = System.nanoTime();
timer.update(now - lastReceived, NANOSECONDS); // creates duration histogram
lastReceived = now;
counter.inc(); // counts
System.out.println("--> " + x);
}
}).start().join(); // join waits for this fiber to finish
}
}
在例子中,使用了 Metrics 记数器。现在运行应用,启动 VisualVM :
性能分析
性能分析是一个应用是否满足我们对性能要求的关键方法。只有经过性能分析我们才能知道哪一部分代码影响了整体执行速度,然后集中精力只改进这一部分代码。一直以来,Java 都有很好的性能分析工具,它们有的在 IDE 中,有的是一个单独的工具。而最近 Java 的性能分析工具变得更精确和轻量级,这要得益于 HotSpot 把 JRcokit
JVM 中的代码合并自己的代码中。在这部分讨论的工具不是开源的,在这里讨论它们是因为这些工具已经包括在标准的 OracleJDK 中,你可以在开发环境中自由使用(但是在生产环境中你需要一个商业许可)。
开始一个测试程序,修改后的代码:
package jmodern;
import co.paralleluniverse.fibers.Fiber;
import co.paralleluniverse.strands.Strand;
import co.paralleluniverse.strands.channels.*;
import com.codahale.metrics.*;
import static com.codahale.metrics.MetricRegistry.name;
import java.util.concurrent.ThreadLocalRandom;
import static java.util.concurrent.TimeUnit.*;
public class Main {
public static void main(String[] args) throws Exception {
final MetricRegistry metrics = new MetricRegistry();
JmxReporter.forRegistry(metrics).build().start(); // starts reporting via JMX
final Channel<Object> ch = Channels.newChannel(0);
new Fiber<Void>(() -> {
Meter meter = metrics.meter(name(Main.class, "messages", "send", "rate"));
for (int i = 0; i < 100000; i++) {
Strand.sleep(ThreadLocalRandom.current().nextInt(50, 500)); // random sleep
meter.mark();
ch.send(i);
}
ch.close();
}).start();
new Fiber<Void>(() -> {
Counter counter = metrics.counter(name(Main.class, "messages", "received"));
Timer timer = metrics.timer(name(Main.class, "messages", "duration"));
Object x;
long lastReceived = System.nanoTime();
while ((x = ch.receive()) != null) {
final long now = System.nanoTime();
timer.update(now - lastReceived, NANOSECONDS);
lastReceived = now;
counter.inc();
double y = foo(x);
System.out.println("--> " + x + " " + y);
}
}).start().join();
}
static double foo(Object x) { // do crazy work
if (!(x instanceof Integer))
return 0.0;
double y = (Integer)x % 2723;
for(int i=0; i<10000; i++) {
String rstr = randomString('A', 'Z', 1000);
y *= rstr.matches("ABA") ? 0.5 : 2.0;
y = Math.sqrt(y);
}
return y;
}
public static String randomString(char from, char to, int length) {
return ThreadLocalRandom.current().ints(from, to + 1).limit(length)
.mapToObj(x -> Character.toString((char)x)).collect(Collectors.joining());
}
}
foo
方法进行了一些没有意义的计算,不用管它。当运行应用(gradle run
)时,你会注意到 Quasar
发出了警告,警告说有一个 fiber
占用了过多的 CPU
时间。为了弄清楚发生了什么,我们开始进行性能分析:
我们使用的分析器能够统计非常精确的信息,同时具有非常低的开销。该工具包括两个组件:第一个是 Java Flight Recorder 已经嵌入到 HotSpotVM 中。它能记录 JVM 中发生的事件,可以和 jcmd
配合使用,在这部分我们通过第二个工具来控制它。第二个工具是 JMC
(Java Mission Control),也在 JDK 中。它的作用等同于 VisualVM ,只是它比较难用。在这里我们用 JMC 来控制 Java Flight Recorder ,分析记录的信息(我希望 Oracle 能把这部分功能移到 VisualVM 中)。
Flight Recorder 在默认已经加入到应用中,只是不会记录任何信息也不会影响性能。先停止应用,然后把这行代码加到 build.gradle
中的 run
:
jvmArgs "-XX:+UnlockCommercialFeatures", "-XX:+FlightRecorder"
UnlockCommercialFeatures
标志是必须的,因为 Flight Recorder 是商业版的功能,不过可以在开发中自由使用。现在,我们重新启动应用。
在另一个终端中,我们使用 jmc
打开 Mission Control 。在左边的面板中,右击 jmodern.Main
,选择 Start Flight Recording…
。在引导窗口中选择 Event settings
下拉框,点击 Profiling - on server
,然后 Next >
,注意不是 Finish
。
接下来,选择 Heap Statistics
和 Allocation Profiling
,点击 Finish
:
JMC 会等 Flight Recorder 记录结束后,打开记录文件进行分析,在那时你可以关掉你的应用。
在 Code
部分的 Hot Methods
选项卡中,可以看出 randomString
是罪魁祸首,它占用了程序执行时间的 90%:
在 Memory
部分的 Garbage Collection
选项卡中,展示了在记录期间堆的使用情况:
在 GC 时间选项卡中,显示了GC的回收情况:
也可以查看内存分配的情况:
应用堆的内容:
Java Flight Recorder
还有一个不被支持的API,能记录应用事件。
高级话题:使用Byteman进行性能分析和调试
像第一部分一样,我们用高级话题来结束本期话题。首先讨论的是用 Byteman 进行性能分析和调试。我在第一部分提到, JVM 最强大的特性之一就是在运行时动态加载代码(这个特性远超本地原生应用加载动态链接库)。不只这个,JVM 还给了我们来回变换运行时代码的能力。
JBoss 开发的 Byteman 工具能充分利用 JVM 的这个特性。 Byteman 能让我们在运行应用时注入跟踪、调试和性能测试相关代码。这个话题之所以是一个高级话题,是因为当前 Byteman 只支持 Java7 ,对 Java8 的支持还不可靠,需要打补丁才能工作。这个项目当前开发活跃,但是正在落后。因此在这里使用一些 Byteman 非常基础的代码。
这是主类:
package jmodern;
import java.util.concurrent.ThreadLocalRandom;
public class Main {
public static void main(String[] args) throws Exception {
for (int i = 0;; i++) {
System.out.println("Calling foo");
foo(i);
}
}
private static String foo(int x) throws InterruptedException {
long pause = ThreadLocalRandom.current().nextInt(50, 500);
Thread.sleep(pause);
return "aaa" + pause;
}
}
foo
模拟调用服务器操作,这些操作要花费一定时间进行。
接下来,把下面的代码合并到构建文件中:
configurations {
byteman
}
dependencies {
byteman "org.jboss.byteman:byteman:2.1.4.1"
}
run {
jvmArgs "-javaagent:${configurations.byteman.iterator().next()}=listener:true,port:9977"
// remove the quasar agent
}
想在 capsule 中试一试 Byteman 使用,在构建文件中改一下 Java-Agents
属性:
'Java-Agents' : "${getDependencies(configurations.byteman).iterator().next()}=listener:true,port:9977",
现在,从这里下载 Byteman ,因为需要使用 Byteman 中的命令行工具,解压文件,设置环境变量 BYTEMAN_HOME
指向 Byteman 的目录。
启动应用gradle run
。打印结果如下:
Calling foo
Calling foo
Calling foo
Calling foo
Calling foo
我们想知道每次调用 foo
需要多长有时间,但是我们没有测量并记录这个信息。现在使用 Byteman
在运行时插入相关日志记录信息。
打开编辑器,在项目目录中创建文件 jmodern.btm
:
RULE trace foo entry
CLASS jmodern.Main
METHOD foo
AT ENTRY
IF true
DO createTimer("timer")
ENDRULE
RULE trace foo exit
CLASS jmodern.Main
METHOD foo
AT EXIT
IF true
DO traceln("::::::: foo(" + $1 + ") -> " + $! + " : " + resetTimer("timer") + "ms")
ENDRULE
上面列的是 Byteman rules
,就是当前我们想应用在程序上的 rules
。我们在另一个终端中运行命令:
$BYTEMAN_HOME/bin/bmsubmit.sh -p 9977 jmodern.btm
之后,运行中的应用打印信息:
Calling foo
::::::: foo(152) -> aaa217 : 217ms
Calling foo
::::::: foo(153) -> aaa281 : 281ms
Calling foo
::::::: foo(154) -> aaa282 : 283ms
Calling foo
::::::: foo(155) -> aaa166 : 166ms
Calling foo
::::::: foo(156) -> aaa160 : 161ms
查看哪个 rules
正在使用:
$BYTEMAN_HOME/bin/bmsubmit.sh -p 9977
卸载 Byteman
脚本:
$BYTEMAN_HOME/bin/bmsubmit.sh -p 9977 -u
运行该命令之后,注入的日志代码就被移出。
Byteman 是在 JVM 灵活代码变换的基础上创建的一个相当强大的工具。你可以使用这个工具来检查变量和日志事件,插入延迟代码等操作,甚至还可以轻松设置一些自定义的 Byteman 行为。更多的信息,参考Byteman documentation。
高级话题:使用JMH进行基准测试
当代硬件构架和编译技术的进步使考察代码性能的唯一方法就是基准测试。一方面,由于现代 CPU 和编译器非常聪明(可以看这里),它能为代码(可以是 c,甚至是汇编)自动地创建一个理论上非常高效的运行环境,就像 90 年代末一些游戏程序员做的那些非常不可思议的事一样。另一方面,正是因为聪明的 CPU 和编译器,让微基准测试非常困难,因为这样的话,代码的执行速度非常依赖具体的执行环境(如:代码速度受 CPU 缓存状态的影响,而 CPU 缓存状态又受其它线程操作的影响)。而对一个 Java 进行微基准测试又会更加的困难,因为 JVM 有 JIT ,而 JIT 是一个以性能优化为导向的编绎器,它能在运行时影响代码执行的上下文环境。因此在 JVM 中,同一段代码在微基准测试和实际程序中执行时间可能不一样,有时可能快,有时也可能慢。
JMH 是由 Oracle 创建的 Java 基准测试工具。你可以相信由 JMH 测试出来的数据(可以看看这个由 JMH 主要作者Aleksey Shipilev的演讲,幻灯片)。 Google 也做了一个基准测试的工具叫 Caliper
,但是这个工具很不成熟,有时还会有错误的结果。不要使用它。
我们马上来使用一下 JMH ,但是在这之前首先有一个忠告:过早优化是万恶之源。在基测试中,两种算法或者数据结构中,一种比另一种快 100 倍,而这个算法只占你应用运行时间的 1% ,这样测试是没有意义的。因为就算你把这个算法改进的非常快行但也只能加快你的应用 2% 时间。基准测试只能是已经对应用进行了性能测试后,用来发现哪一个小部分改变能得到最大的加速成果。
增加依赖:
testCompile 'org.openjdk.jmh:jmh-core:0.8'
testCompile 'org.openjdk.jmh:jmh-generator-annprocess:0.8'
然后增加bench
任务:
task bench(type: JavaExec, dependsOn: [classes, testClasses]) {
classpath = sourceSets.test.runtimeClasspath // we'll put jmodern.Benchamrk in the test directory
main = "jmodern.Benchmark";
}
最后,把测试代码放到 src/test/java/jmodern/Benchmark.java
文件中。我之前提到过 90 年代的游戏程序员,是为了说明古老的技术现在仍然有用,这里我们测试一个开平方根的计算,使用 fast inverse square root algorithm(平方根倒数速算法,这是 90 年代的程序):
package jmodern;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.profile.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.parameters.TimeValue;
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class Benchmark {
public static void main(String[] args) throws Exception {
new Runner(new OptionsBuilder()
.include(Benchmark.class.getName() + ".*")
.forks(1)
.warmupTime(TimeValue.seconds(5))
.warmupIterations(3)
.measurementTime(TimeValue.seconds(5))
.measurementIterations(5)
.build()).run();
}
private double x = 2.0; // prevent constant folding
@GenerateMicroBenchmark
public double standardInvSqrt() {
return 1.0/Math.sqrt(x);
}
@GenerateMicroBenchmark
public double fastInvSqrt() {
return invSqrt(x);
}
static double invSqrt(double x) {
double xhalf = 0.5d * x;
long i = Double.doubleToLongBits(x);
i = 0x5fe6ec85e7de30daL - (i >> 1);
x = Double.longBitsToDouble(i);
x = x * (1.5d - xhalf * x * x);
return x;
}
}
随便说一下,像第一部分中讨论的 Checker 一样, JMH 使用使用注解处理器。但是不同 Checker , JMH 做的不错,你能在所有的 IDE 中使用它。在下面的图中,我们可以看到, NetBeans 中,一但忘加 @State
注解, IDE 就会报错:
写入命令 gradle bench
,运行基准测试。会得到以下结果:
Benchmark Mode Samples Mean Mean error Units
j.Benchmark.fastInvSqrt avgt 10 2.708 0.019 ns/op
j.Benchmark.standardInvSqrt avgt 10 12.824 0.065 ns/op
很漂亮吧,但是你得知道 fast-inv-sqrt
结果是一个粗略近似值, 只在需要大量开平方的地方适用(如图形计算中)。
在下面的例子中, JMH 用来报到 GC 使用的时间和方法栈的调用时间:
package jmodern;
import java.util.*;
import java.util.concurrent.*;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.profile.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.parameters.TimeValue;
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class Benchmark {
public static void main(String[] args) throws Exception {
new Runner(new OptionsBuilder()
.include(Benchmark.class.getName() + ".*")
.forks(2)
.warmupTime(TimeValue.seconds(5))
.warmupIterations(3)
.measurementTime(TimeValue.seconds(5))
.measurementIterations(5)
.addProfiler(GCProfiler.class) // report GC time
.addProfiler(StackProfiler.class) // report method stack execution profile
.build()).run();
}
@GenerateMicroBenchmark
public Object arrayList() {
return add(new ArrayList<>());
}
@GenerateMicroBenchmark
public Object linkedList() {
return add(new LinkedList<>());
}
static Object add(List<Integer> list) {
for (int i = 0; i < 4000; i++)
list.add(i);
return list;
}
}
这是 JMH 的打印出来的信息:
Iteration 3: 33783.296 ns/op
GC | wall time = 5.000 secs, GC time = 0.048 secs, GC% = 0.96%, GC count = +97
|
Stack | 96.9% RUNNABLE jmodern.generated.Benchmark_arrayList.arrayList_AverageTime_measurementLoop
| 1.8% RUNNABLE java.lang.Integer.valueOf
| 1.3% RUNNABLE java.util.Arrays.copyOf
| 0.0% (other)
|
JMH 是一个功能非常丰富的框架。不幸的是,在文档方面有些薄弱,不过有一个相当好代码示例教程,用来展示 Java 中微基测试的陷阱。你也可以读读这篇介绍 JMH 的入门文章。
目前为止我们已经学了什么?
在这篇文章中,我们讨论了在 JVM 管理、监控和性能测试方面最好的几个工具。 JVM 除了很好的性能外,它还非常深思熟虑地提供了能深度洞察它运行状态的能力,这就是我不会用其它的技术来取代 JVM 做为重要的、长时间运行的服务器端应用平台的主要原因。
此外,我们还见识到了当使用 Byteman 等工具修改运行时代码时, JVM 是多么强大。
我们还介绍了 Capsule ,一个轻量级的、单文件、无状态、不用安装的部署工具。另外,通过一个公开或者组织内部的 Maven 仓库,它还支持整个Java应用自动升级,或者还是仅仅升级一个依赖库。
在第三部分中,我们将讨论如何使用 Dropwizard , Comsat , Web Actors ,和 DI 来写一个轻量级、可扩展的http服务。
水平有限,如果看不懂请直接看英文版。