前一篇文章Java中的纤程库 – Quasar中我做了简单的介绍,现在进一步介绍这个纤程库。

Quasar还没有得到广泛的应用,搜寻整个github也就pinterest/quasar-thrift这么一个像样的使用Quasar的库,并且官方的文档也很简陋,很多地方并没有详细的介绍,和Maven的集成也不是很好。这些都限制了Quasar的进一步发展。

但是,作为目前最好用的Java coroutine的实现,它在某些情况下的性能还是表现相当出色的,希望这个项目能够得到更大的支持和快速发展。

因为Quasar文档的缺乏,所以使用起来需要不断的摸索和在论坛上搜索答案,本文将一些记录了我在Quasar使用过程中的一些探索。

1 Thread vs Quasar

虽然Java的线程的API封装的很好,使用起来非常的方便,但是使用起来也得小心。首先线程需要耗费资源,所以单个的机器上创建上万个线程很困难,其次线程之间的切换也需要耗费CPU,在线程非常多的情况下导致很多CPU资源耗费在线程切换上,通过提高线程数来提高系统的性能有时候适得其反。你可以看到现在一些优秀的框架如Netty都不会创建很多的线程,默认2倍的CPU core的线程数就已经应付的很好了,比如node.js可以使用单一的进程/线程应付高并发。

纤程使用的资源更少,它主要保存栈信息,所以一个系统中可以创建上万的纤程Fiber,而实际的纤程调度器只需要几个Java线程即可。

我们看一个性能的比较,直观的感受一下Quasar带来的吞吐率的提高。

下面这个例子中方法m1调用m2,m2调用m3,但是m2会暂停1秒钟,用来模拟实际产品中的阻塞,m3执行了一个简单的计算。
通过线程和纤程两种方式我们看看系统的吞吐率(throughput)和延迟(latency)。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class Helloworld {
     @Suspendable
     static void m1() throws InterruptedException, SuspendExecution {
         String m = "m1" ;
         //System.out.println("m1 begin");
         m = m2();
         //System.out.println("m1 end");
         //System.out.println(m);
     }
     static String m2() throws SuspendExecution, InterruptedException {
         String m = m3();
         Strand.sleep( 1000 );
         return m;
     }
     //or define in META-INF/suspendables
     @Suspendable
     static String m3() {
         List l = Stream.of( 1 , 2 , 3 ).filter(i -> i% 2 == 0 ).collect(Collectors.toList());
         return l.toString();
     }
     static public void main(String[] args) throws ExecutionException, InterruptedException {
         int count = 10000 ;
         testThreadpool(count);
         testFiber(count);
     }
     static void testThreadpool( int count) throws InterruptedException {
         final CountDownLatch latch = new CountDownLatch(count);
         ExecutorService es = Executors.newFixedThreadPool( 200 );
         LongAdder latency = new LongAdder();
         long t = System.currentTimeMillis();
         for ( int i = 0 ; i< count; i++) {
             es.submit(() -> {
                 long start = System.currentTimeMillis();
                 try {
                     m1();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 } catch (SuspendExecution suspendExecution) {
                     suspendExecution.printStackTrace();
                 }
                 start = System.currentTimeMillis() - start;
                 latency.add(start);
                 latch.countDown();
             });
         }
         latch.await();
         t = System.currentTimeMillis() - t;
         long l = latency.longValue() / count;
         System.out.println( "thread pool took: " + t + ", latency: " + l + " ms" );
         es.shutdownNow();
     }
     static void testFiber( int count) throws InterruptedException {
         final CountDownLatch latch = new CountDownLatch(count);
         LongAdder latency = new LongAdder();
         long t = System.currentTimeMillis();
         for ( int i = 0 ; i< count; i++) {
             new Fiber<Void>( "Caller" , new SuspendableRunnable() {
                 @Override
                 public void run() throws SuspendExecution, InterruptedException {
                     long start = System.currentTimeMillis();
                     m1();
                     start = System.currentTimeMillis() - start;
                     latency.add(start);
                     latch.countDown();
                 }
             }).start();
         }
         latch.await();
         t = System.currentTimeMillis() - t;
         long l = latency.longValue() / count;
         System.out.println( "fiber took: " + t  + ", latency: " + l + " ms" );
     }
}

运行这个程序(需要某种instrument, agent或者AOT或者其它,在下面会介绍),输出结果为:

1
2
thread pool took: 50341, latency: 1005 ms
fiber took: 1158, latency: 1000 ms

如果使用线程,执行完1万个操作需要50秒,平均延迟为1秒左右(我们故意让延迟至少1秒),线程池数量为200。(其实总时间50秒可以计算出来)
但是如果使用纤程,执行完1万个操作仅需要1.158秒,平均延迟时间为1秒,线程数量为CPU core数(缺省使用ForkJoinPool)。

可以看到,通过使用纤程,尽受限于系统的业务逻辑,我们没有办法提升业务的处理时间, 但是我们确可以极大的提高系统的吞吐率,如上面的简单的例子将10000个操作的处理时间从50秒提高到1秒,非凡的成就。

如果我们将方法m2中的Strand.sleep(1000);注释掉,这样这个例子中就没有什么阻塞了,我们看看在这种纯计算的情况下两者的表现:

1
2
thread pool took: 114, latency: 0 ms
fiber took: 180, latency: 0 ms

可以看到,纤程非但没有提升性能,反而会带来性能的下降。对于这种纯计算没有阻塞的case,Quasar并不适合。

正如官方所说:

Fibers are not meant to replace threads in all circumstances. A fiber should be used when its body (the code it executes) blocks very often waiting on other fibers (e.g. waiting for messages sent by other fibers on a channel, or waiting for the value of a dataflow-variable). For long-running computations that rarely block, traditional threads are preferable. Fortunately, as we shall see, fibers and threads interoperate very well.

2 Suspendable方法

Fiber中的run方法,如SuspendableRunnable 和 SuspendableCallable声明了SuspendExecution异常。这并不是一个真的异常,而是fiber内部工作的机制。任何运行在fiber中的可能阻塞的方法,如果声明了这个异常,就被叫做 suspendable 方法。 如果你的方法调用了一个suspendable方法,那么你的方法也是suspendable方法,所以也需要声明抛出SuspendExecution异常。

有时候不能在某个方法上声明抛出SuspendExecution异常,比如你实现某个接口,你不能更改接口的方法声明,你不得不使用其它的方法来指定suspendable方法。方法之一就是使用@Suspendable注解,在你需要指定的suspendable方法上加上这个注解就可以告诉Quasar这个方法是suspendable方法。

另一个情况就是对于第三的库,你不可能更改它们的代码,如果想指定这些库的某些方法是suspendable方法,比如java.net.URL.openStream()Ljava/io/InputStream;, 就需要另外一种解决办法,也就是在META-INF/suspendablesMETA-INF/suspendable-supers定义。
文件中每个方法占一行,具体(concrete)的suspendable方法应该写在META-INF/suspendables中,non-suspendable方法,但是有suspendable override的类、接口写在META-INF/suspendable-supers中(可以是具体类单不能是final, 接口和抽象类也可以)。
每一行应该是方法的签名的全称“full.class.name.methodName” 以及*通配符。
使用`SuspendablesScanner`可以自动增加你的方法到这些文件中,待会介绍它。

java.lang包下的方法不能标记为suspendable,其它的JDK方法则可以显示地在文件META-INF/suspendablesMETA-INF/suspendable-supers中标记为suspendable,并且设置环境变量co.paralleluniverse.fibers.allowJdkInstrumentation为true,但是很少这样使用。

还有一些特殊的情况也会被认为是suspendable的。

反射调用总是被看作是suspendable的。

Java 8 lambda也总是被看作suspendable的。

构造函数/类初始化器不能被标记为suspendable

缺省情况下synchronized和blocking thread 调用不能运行在Fiber中。这是因为它们会阻塞Fiber使用的线程,导致系统处理变慢,但是如果你非要在Fiber中使用它们,可以可以将allowMonitorsallowBlocking传给instrumentation Ant task,或者将bm传给Quasar Java agent。

3 Maven配置

Quasar依赖字节码的instrumentation, instrumentation用来修改字节码。 Quasar可以在运行时或者编译时修改字节码,下面介绍这几种实现。

1、Quasar Java Agent
Quasar java agent可以在运行时动态修改字节码,将下面一行加搭配java命令行中即可,注意把path-to-quasar-jar.jar替换成你实际的quasar java的地址。

1
-javaagent:path-to-quasar-jar.jar

如果你使用maven的exec task,你可以使用maven-dependency-plugin为依赖设置properties,然后在插件exec-maven-plugin中引用quasar库即可。

详细配置可以参考Specifying the Java Agent with Maven:

Quasar对gradle的支持比较好,你可以方便的使用gradle配置

这是首选的一种方式,因为在某些情况下,比如你使用第三方的库,如comsat,它们只能使用这种方式配置。

2、AOT(Ahead-of-Time)
另外一种是在编译时的时候完成instrumentation。

它是通过一个Ant Task来完成的,所以对于Maven管理的项目来说,配置起来有些麻烦。

这个Ant Task是co.paralleluniverse.fibers.instrument.InstrumentationTask,包含在quasar-core.jar中。它接受一组(fileset)classes进行instrument,但并不是传给它的所有classes都需要classes进行instrument,只有suspendable方法才有可能被instrument。它还会进行优化,有些suspendable方法可能不需要instrument。

在Maven中配置起来有些复杂,如下面所示:

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
32
33
34
35
36
< plugin >
     < groupId >org.apache.maven.plugins</ groupId >
     < artifactId >maven-antrun-plugin</ artifactId >
     < executions >
         < execution >
             < id >instrument-classes</ id >
             < phase >compile</ phase >
             < configuration >
                 < tasks >
                     < property name = "ant_classpath" refid = "maven.dependency.classpath" />
                     < taskdef name = "instrumentationTask"
                              classname = "co.paralleluniverse.fibers.instrument.InstrumentationTask"
                              classpath = "${co.paralleluniverse:quasar-core:jar:jdk8}" />
                     < instrumentationTask allowMonitors = "true" allowBlocking = "true" check = "true" verbose = "true" debug = "true" >
                         < fileset dir = "${project.build.directory}/classes/" includes = "**/*" />
                     </ instrumentationTask >
                 </ tasks >
             </ configuration >
             < goals >
                 < goal >run</ goal >
             </ goals >
         </ execution >
     </ executions >
</ plugin >
< plugin >
     < artifactId >maven-dependency-plugin</ artifactId >
     < version >2.5.1</ version >
     < executions >
         < execution >
             < id >getClasspathFilenames</ id >
             < goals >
                 < goal >properties</ goal >
             </ goals >
         </ execution >
     </ executions >
</ plugin >

Quasar官方并没有提供一个maven插件,好心的社区倒是提供了一个quasar-maven-plugin。所以你可以不用上面的写法,而是用下面简单的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
< plugin >
     < groupId >com.vlkan</ groupId >
     < artifactId >quasar-maven-plugin</ artifactId >
     < version >0.7.3</ version >
     < configuration >
         < check >true</ check >
         < debug >true</ debug >
         < verbose >true</ verbose >
     </ configuration >
     < executions >
         < execution >
             < phase >compile</ phase >
             < goals >
                 < goal >instrument</ goal >
             </ goals >
         </ execution >
     </ executions >
</ plugin >

3、在Web容器中
如果你使用web容器使用基于Quasar的库comsat等,比如Tomcat,则比较棘手。因为你不太像将Quasar java agent直接加到tomcat的启动脚本中,这样会instrument所有的应用,导致很多的警告。

Comsat提供了Tomcat和Jetty的解决方案。

Tomcat
对于tomcat,你可以把comsat-tomcat-loader-0.7.0-jdk8.jar或者comsat-tomcat-loader-0.7.0.jar加入到tomcat的common/lib或者lib中,然后在你的web应用META-INF/context.xml中加入:

1
< Loader loaderClass = "co.paralleluniverse.comsat.tomcat.QuasarWebAppClassLoader" />

Jetty
如果使用Jetty,则把comsat-jetty-loader-0.7.0-jdk8.jar或者comsat-jetty-loader-0.7.0.jar加入到Jetty的lib中,然后在你的context.xml中加入<Set name="classLoader">:

1
2
3
4
5
6
7
8
9
10
11
< Configure id = "ctx" class = "org.eclipse.jetty.webapp.WebAppContext" >
     < Set name = "war" >./build/wars/dep.war</ Set >
     <!--use custom classloader in order to instrument classes by quasar-->
     < Set name = "classLoader" >
         < New class = "co.paralleluniverse.comsat.jetty.QuasarWebAppClassLoader" >
             < Arg >
                 < Ref id = "ctx" />
             </ Arg >
         </ New >
     </ Set >
</ Configure >

总之,通过实现一个定制的ClassLoader实现instrumentation。

4 Auto Suspendables Detection

quasar提供了一个ant task,可以实现自动侦测suspendable方法,并可以把它们写入到`META-INF/suspendablesMETA-INF/suspendable-supers`。

但是官方并没有详细的介绍,而且也没有相应的maven插件可以使用。

我们可以看看在gradle如何使用的,我们可以把侦测结果复制到maven中使用:

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
apply plugin: 'java'
apply plugin: 'maven'
group = 'com.colobu.fiber'
version = '1.0'
description = """"""
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
      maven { url "http://repo.maven.apache.org/maven2" }
}
dependencies {
     compile group: 'co.paralleluniverse', name: 'quasar-core', version:'0.7.5', classifier:'jdk8'
     compile group: 'co.paralleluniverse', name: 'comsat-httpclient', version:'0.7.0'
     testCompile group: 'junit', name: 'junit', version:'4.12'
}
classes {
     doFirst {
         ant.taskdef(name: 'scanSuspendables',
                 classname: 'co.paralleluniverse.fibers.instrument.SuspendablesScanner',
                 classpath: "build/classes/main:build/resources/main:${configurations.runtime.asPath}")
         ant.scanSuspendables(auto: true,
                 suspendablesFile: "$sourceSets.main.output.resourcesDir/META-INF/suspendables",
                 supersFile: "$sourceSets.main.output.resourcesDir/META-INF/suspendable-supers",
                 append: true) {
             fileset(dir: sourceSets.main.output.classesDir)
         }
     }
}

我们可以看一下官方的库comsat的一些`META-INF/suspendables`例子:

1、comsat-okhttp
/META-INF/suspendables:

1
com.squareup.okhttp.apache.OkApacheClient.execute

2、comsat-httpclient
/META-INF/suspendables

1
2
org.apache.http.impl.client.CloseableHttpClient.doExecute
org.apache.http.impl.client.CloseableHttpClient.execute

5 故障检测

当前quasar依赖字节码的instrumentation,所以suspendable方法必须在运行之前进行标记。

Quasar开发组和OpenJDK协作,将在JDK9中移除这个限制,将会有效地自动地实现instrumentation。

如果你忘记将一个方法标记为suspendable (throws SuspendExecution、@Suspendable或者META-INF/suspendables/META-INF/suspendable-supers),你可能会遇到一些奇怪的错误。

环境变量co.paralleluniverse.fibers.verifyInstrumentation设为true可以检查未标记的方法。但是在生产环境中不要设置它。

UnableToInstrumentException异常表明quasar不能instrument一些方法如synchronized或者阻塞的线程调用。verbose(v), debug(d) 和 check(c)可以打印出详细信息。

更多的调试可以参考:troubleshooting

6 其它

Fiber可以序列化。

Fiber也可以打印它的堆栈进行调试。

Fiber也有Actor和Channel的实现,并且可以运行在集群上。

前一篇文章Java中的纤程库 – Quasar中我做了简单的介绍,现在进一步介绍这个纤程库。

Quasar还没有得到广泛的应用,搜寻整个github也就pinterest/quasar-thrift这么一个像样的使用Quasar的库,并且官方的文档也很简陋,很多地方并没有详细的介绍,和Maven的集成也不是很好。这些都限制了Quasar的进一步发展。

但是,作为目前最好用的Java coroutine的实现,它在某些情况下的性能还是表现相当出色的,希望这个项目能够得到更大的支持和快速发展。

因为Quasar文档的缺乏,所以使用起来需要不断的摸索和在论坛上搜索答案,本文将一些记录了我在Quasar使用过程中的一些探索。

1 Thread vs Quasar

虽然Java的线程的API封装的很好,使用起来非常的方便,但是使用起来也得小心。首先线程需要耗费资源,所以单个的机器上创建上万个线程很困难,其次线程之间的切换也需要耗费CPU,在线程非常多的情况下导致很多CPU资源耗费在线程切换上,通过提高线程数来提高系统的性能有时候适得其反。你可以看到现在一些优秀的框架如Netty都不会创建很多的线程,默认2倍的CPU core的线程数就已经应付的很好了,比如node.js可以使用单一的进程/线程应付高并发。

纤程使用的资源更少,它主要保存栈信息,所以一个系统中可以创建上万的纤程Fiber,而实际的纤程调度器只需要几个Java线程即可。

我们看一个性能的比较,直观的感受一下Quasar带来的吞吐率的提高。

下面这个例子中方法m1调用m2,m2调用m3,但是m2会暂停1秒钟,用来模拟实际产品中的阻塞,m3执行了一个简单的计算。
通过线程和纤程两种方式我们看看系统的吞吐率(throughput)和延迟(latency)。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class Helloworld {
     @Suspendable
     static void m1() throws InterruptedException, SuspendExecution {
         String m = "m1" ;
         //System.out.println("m1 begin");
         m = m2();
         //System.out.println("m1 end");
         //System.out.println(m);
     }
     static String m2() throws SuspendExecution, InterruptedException {
         String m = m3();
         Strand.sleep( 1000 );
         return m;
     }
     //or define in META-INF/suspendables
     @Suspendable
     static String m3() {
         List l = Stream.of( 1 , 2 , 3 ).filter(i -> i% 2 == 0 ).collect(Collectors.toList());
         return l.toString();
     }
     static public void main(String[] args) throws ExecutionException, InterruptedException {
         int count = 10000 ;
         testThreadpool(count);
         testFiber(count);
     }
     static void testThreadpool( int count) throws InterruptedException {
         final CountDownLatch latch = new CountDownLatch(count);
         ExecutorService es = Executors.newFixedThreadPool( 200 );
         LongAdder latency = new LongAdder();
         long t = System.currentTimeMillis();
         for ( int i = 0 ; i< count; i++) {
             es.submit(() -> {
                 long start = System.currentTimeMillis();
                 try {
                     m1();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 } catch (SuspendExecution suspendExecution) {
                     suspendExecution.printStackTrace();
                 }
                 start = System.currentTimeMillis() - start;
                 latency.add(start);
                 latch.countDown();
             });
         }
         latch.await();
         t = System.currentTimeMillis() - t;
         long l = latency.longValue() / count;
         System.out.println( "thread pool took: " + t + ", latency: " + l + " ms" );
         es.shutdownNow();
     }
     static void testFiber( int count) throws InterruptedException {
         final CountDownLatch latch = new CountDownLatch(count);
         LongAdder latency = new LongAdder();
         long t = System.currentTimeMillis();
         for ( int i = 0 ; i< count; i++) {
             new Fiber<Void>( "Caller" , new SuspendableRunnable() {
                 @Override
                 public void run() throws SuspendExecution, InterruptedException {
                     long start = System.currentTimeMillis();
                     m1();
                     start = System.currentTimeMillis() - start;
                     latency.add(start);
                     latch.countDown();
                 }
             }).start();
         }
         latch.await();
         t = System.currentTimeMillis() - t;
         long l = latency.longValue() / count;
         System.out.println( "fiber took: " + t  + ", latency: " + l + " ms" );
     }
}

运行这个程序(需要某种instrument, agent或者AOT或者其它,在下面会介绍),输出结果为:

1
2
thread pool took: 50341, latency: 1005 ms
fiber took: 1158, latency: 1000 ms

如果使用线程,执行完1万个操作需要50秒,平均延迟为1秒左右(我们故意让延迟至少1秒),线程池数量为200。(其实总时间50秒可以计算出来)
但是如果使用纤程,执行完1万个操作仅需要1.158秒,平均延迟时间为1秒,线程数量为CPU core数(缺省使用ForkJoinPool)。

可以看到,通过使用纤程,尽受限于系统的业务逻辑,我们没有办法提升业务的处理时间, 但是我们确可以极大的提高系统的吞吐率,如上面的简单的例子将10000个操作的处理时间从50秒提高到1秒,非凡的成就。

如果我们将方法m2中的Strand.sleep(1000);注释掉,这样这个例子中就没有什么阻塞了,我们看看在这种纯计算的情况下两者的表现:

1
2
thread pool took: 114, latency: 0 ms
fiber took: 180, latency: 0 ms

可以看到,纤程非但没有提升性能,反而会带来性能的下降。对于这种纯计算没有阻塞的case,Quasar并不适合。

正如官方所说:

Fibers are not meant to replace threads in all circumstances. A fiber should be used when its body (the code it executes) blocks very often waiting on other fibers (e.g. waiting for messages sent by other fibers on a channel, or waiting for the value of a dataflow-variable). For long-running computations that rarely block, traditional threads are preferable. Fortunately, as we shall see, fibers and threads interoperate very well.

2 Suspendable方法

Fiber中的run方法,如SuspendableRunnable 和 SuspendableCallable声明了SuspendExecution异常。这并不是一个真的异常,而是fiber内部工作的机制。任何运行在fiber中的可能阻塞的方法,如果声明了这个异常,就被叫做 suspendable 方法。 如果你的方法调用了一个suspendable方法,那么你的方法也是suspendable方法,所以也需要声明抛出SuspendExecution异常。

有时候不能在某个方法上声明抛出SuspendExecution异常,比如你实现某个接口,你不能更改接口的方法声明,你不得不使用其它的方法来指定suspendable方法。方法之一就是使用@Suspendable注解,在你需要指定的suspendable方法上加上这个注解就可以告诉Quasar这个方法是suspendable方法。

另一个情况就是对于第三的库,你不可能更改它们的代码,如果想指定这些库的某些方法是suspendable方法,比如java.net.URL.openStream()Ljava/io/InputStream;, 就需要另外一种解决办法,也就是在META-INF/suspendablesMETA-INF/suspendable-supers定义。
文件中每个方法占一行,具体(concrete)的suspendable方法应该写在META-INF/suspendables中,non-suspendable方法,但是有suspendable override的类、接口写在META-INF/suspendable-supers中(可以是具体类单不能是final, 接口和抽象类也可以)。
每一行应该是方法的签名的全称“full.class.name.methodName” 以及*通配符。
使用`SuspendablesScanner`可以自动增加你的方法到这些文件中,待会介绍它。

java.lang包下的方法不能标记为suspendable,其它的JDK方法则可以显示地在文件META-INF/suspendablesMETA-INF/suspendable-supers中标记为suspendable,并且设置环境变量co.paralleluniverse.fibers.allowJdkInstrumentation为true,但是很少这样使用。

还有一些特殊的情况也会被认为是suspendable的。

反射调用总是被看作是suspendable的。

Java 8 lambda也总是被看作suspendable的。

构造函数/类初始化器不能被标记为suspendable

缺省情况下synchronized和blocking thread 调用不能运行在Fiber中。这是因为它们会阻塞Fiber使用的线程,导致系统处理变慢,但是如果你非要在Fiber中使用它们,可以可以将allowMonitorsallowBlocking传给instrumentation Ant task,或者将bm传给Quasar Java agent。

3 Maven配置

Quasar依赖字节码的instrumentation, instrumentation用来修改字节码。 Quasar可以在运行时或者编译时修改字节码,下面介绍这几种实现。

1、Quasar Java Agent
Quasar java agent可以在运行时动态修改字节码,将下面一行加搭配java命令行中即可,注意把path-to-quasar-jar.jar替换成你实际的quasar java的地址。

1
-javaagent:path-to-quasar-jar.jar

如果你使用maven的exec task,你可以使用maven-dependency-plugin为依赖设置properties,然后在插件exec-maven-plugin中引用quasar库即可。

详细配置可以参考Specifying the Java Agent with Maven:

Quasar对gradle的支持比较好,你可以方便的使用gradle配置

这是首选的一种方式,因为在某些情况下,比如你使用第三方的库,如comsat,它们只能使用这种方式配置。

2、AOT(Ahead-of-Time)
另外一种是在编译时的时候完成instrumentation。

它是通过一个Ant Task来完成的,所以对于Maven管理的项目来说,配置起来有些麻烦。

这个Ant Task是co.paralleluniverse.fibers.instrument.InstrumentationTask,包含在quasar-core.jar中。它接受一组(fileset)classes进行instrument,但并不是传给它的所有classes都需要classes进行instrument,只有suspendable方法才有可能被instrument。它还会进行优化,有些suspendable方法可能不需要instrument。

在Maven中配置起来有些复杂,如下面所示:

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
32
33
34
35
36
< plugin >
     < groupId >org.apache.maven.plugins</ groupId >
     < artifactId >maven-antrun-plugin</ artifactId >
     < executions >
         < execution >
             < id >instrument-classes</ id >
             < phase >compile</ phase >
             < configuration >
                 < tasks >
                     < property name = "ant_classpath" refid = "maven.dependency.classpath" />
                     < taskdef name = "instrumentationTask"
                              classname = "co.paralleluniverse.fibers.instrument.InstrumentationTask"
                              classpath = "${co.paralleluniverse:quasar-core:jar:jdk8}" />
                     < instrumentationTask allowMonitors = "true" allowBlocking = "true" check = "true" verbose = "true" debug = "true" >
                         < fileset dir = "${project.build.directory}/classes/" includes = "**/*" />
                     </ instrumentationTask >
                 </ tasks >
             </ configuration >
             < goals >
                 < goal >run</ goal >
             </ goals >
         </ execution >
     </ executions >
</ plugin >
< plugin >
     < artifactId >maven-dependency-plugin</ artifactId >
     < version >2.5.1</ version >
     < executions >
         < execution >
             < id >getClasspathFilenames</ id >
             < goals >
                 < goal >properties</ goal >
             </ goals >
         </ execution >
     </ executions >
</ plugin >

Quasar官方并没有提供一个maven插件,好心的社区倒是提供了一个quasar-maven-plugin。所以你可以不用上面的写法,而是用下面简单的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
< plugin >
     < groupId >com.vlkan</ groupId >
     < artifactId >quasar-maven-plugin</ artifactId >
     < version >0.7.3</ version >
     < configuration >
         < check >true</ check >
         < debug >true</ debug >
         < verbose >true</ verbose >
     </ configuration >
     < executions >
         < execution >
             < phase >compile</ phase >
             < goals >
                 < goal >instrument</ goal >
             </ goals >
         </ execution >
     </ executions >
</ plugin >

3、在Web容器中
如果你使用web容器使用基于Quasar的库comsat等,比如Tomcat,则比较棘手。因为你不太像将Quasar java agent直接加到tomcat的启动脚本中,这样会instrument所有的应用,导致很多的警告。

Comsat提供了Tomcat和Jetty的解决方案。

Tomcat
对于tomcat,你可以把comsat-tomcat-loader-0.7.0-jdk8.jar或者comsat-tomcat-loader-0.7.0.jar加入到tomcat的common/lib或者lib中,然后在你的web应用META-INF/context.xml中加入:

1
< Loader loaderClass = "co.paralleluniverse.comsat.tomcat.QuasarWebAppClassLoader" />

Jetty
如果使用Jetty,则把comsat-jetty-loader-0.7.0-jdk8.jar或者comsat-jetty-loader-0.7.0.jar加入到Jetty的lib中,然后在你的context.xml中加入<Set name="classLoader">:

1
2
3
4
5
6
7
8
9
10
11
< Configure id = "ctx" class = "org.eclipse.jetty.webapp.WebAppContext" >
     < Set name = "war" >./build/wars/dep.war</ Set >
     <!--use custom classloader in order to instrument classes by quasar-->
     < Set name = "classLoader" >
         < New class = "co.paralleluniverse.comsat.jetty.QuasarWebAppClassLoader" >
             < Arg >
                 < Ref id = "ctx" />
             </ Arg >
         </ New >
     </ Set >
</ Configure >

总之,通过实现一个定制的ClassLoader实现instrumentation。

4 Auto Suspendables Detection

quasar提供了一个ant task,可以实现自动侦测suspendable方法,并可以把它们写入到`META-INF/suspendablesMETA-INF/suspendable-supers`。

但是官方并没有详细的介绍,而且也没有相应的maven插件可以使用。

我们可以看看在gradle如何使用的,我们可以把侦测结果复制到maven中使用:

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
apply plugin: 'java'
apply plugin: 'maven'
group = 'com.colobu.fiber'
version = '1.0'
description = """"""
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
      maven { url "http://repo.maven.apache.org/maven2" }
}
dependencies {
     compile group: 'co.paralleluniverse', name: 'quasar-core', version:'0.7.5', classifier:'jdk8'
     compile group: 'co.paralleluniverse', name: 'comsat-httpclient', version:'0.7.0'
     testCompile group: 'junit', name: 'junit', version:'4.12'
}
classes {
     doFirst {
         ant.taskdef(name: 'scanSuspendables',
                 classname: 'co.paralleluniverse.fibers.instrument.SuspendablesScanner',
                 classpath: "build/classes/main:build/resources/main:${configurations.runtime.asPath}")
         ant.scanSuspendables(auto: true,
                 suspendablesFile: "$sourceSets.main.output.resourcesDir/META-INF/suspendables",
                 supersFile: "$sourceSets.main.output.resourcesDir/META-INF/suspendable-supers",
                 append: true) {
             fileset(dir: sourceSets.main.output.classesDir)
         }
     }
}

我们可以看一下官方的库comsat的一些`META-INF/suspendables`例子:

1、comsat-okhttp
/META-INF/suspendables:

1
com.squareup.okhttp.apache.OkApacheClient.execute

2、comsat-httpclient
/META-INF/suspendables

1
2
org.apache.http.impl.client.CloseableHttpClient.doExecute
org.apache.http.impl.client.CloseableHttpClient.execute

5 故障检测

当前quasar依赖字节码的instrumentation,所以suspendable方法必须在运行之前进行标记。

Quasar开发组和OpenJDK协作,将在JDK9中移除这个限制,将会有效地自动地实现instrumentation。

如果你忘记将一个方法标记为suspendable (throws SuspendExecution、@Suspendable或者META-INF/suspendables/META-INF/suspendable-supers),你可能会遇到一些奇怪的错误。

环境变量co.paralleluniverse.fibers.verifyInstrumentation设为true可以检查未标记的方法。但是在生产环境中不要设置它。

UnableToInstrumentException异常表明quasar不能instrument一些方法如synchronized或者阻塞的线程调用。verbose(v), debug(d) 和 check(c)可以打印出详细信息。

更多的调试可以参考:troubleshooting

6 其它

Fiber可以序列化。

Fiber也可以打印它的堆栈进行调试。

Fiber也有Actor和Channel的实现,并且可以运行在集群上。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐